From d8771668571dcd309eb2fca64441d89e473ac390 Mon Sep 17 00:00:00 2001 From: pranav-afk Date: Mon, 13 Jul 2026 17:37:19 +0530 Subject: [PATCH] fix: validate quick-add title length and surface field errors Inline work item creation showed a generic toast when the title exceeded 255 characters, while the modal path already validated with a clear message. Add maxLength validation across quick-add layouts, extract API field errors in the toast, and roll back optimistic temp issues on failure. Fixes #9329 --- .../issue-layouts/quick-add/form/calendar.tsx | 6 +++ .../issue-layouts/quick-add/form/gantt.tsx | 4 ++ .../issue-layouts/quick-add/form/kanban.tsx | 4 ++ .../issue-layouts/quick-add/form/list.tsx | 4 ++ .../quick-add/form/spreadsheet.tsx | 4 ++ .../issues/issue-layouts/quick-add/root.tsx | 45 +++++++++++++----- .../store/issue/helpers/base-issues.store.ts | 47 +++++++++++-------- 7 files changed, 82 insertions(+), 32 deletions(-) diff --git a/apps/web/core/components/issues/issue-layouts/quick-add/form/calendar.tsx b/apps/web/core/components/issues/issue-layouts/quick-add/form/calendar.tsx index 9fd69a8b9fe..c19af680b26 100644 --- a/apps/web/core/components/issues/issue-layouts/quick-add/form/calendar.tsx +++ b/apps/web/core/components/issues/issue-layouts/quick-add/form/calendar.tsx @@ -5,10 +5,12 @@ */ import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; import type { TQuickAddIssueForm } from "../root"; export const CalendarQuickAddIssueForm = observer(function CalendarQuickAddIssueForm(props: TQuickAddIssueForm) { const { ref, isOpen, projectDetail, register, onSubmit, isEpic } = props; + const { t } = useTranslation(); return (
diff --git a/apps/web/core/components/issues/issue-layouts/quick-add/form/gantt.tsx b/apps/web/core/components/issues/issue-layouts/quick-add/form/gantt.tsx index 65d05c40311..6af7830f47e 100644 --- a/apps/web/core/components/issues/issue-layouts/quick-add/form/gantt.tsx +++ b/apps/web/core/components/issues/issue-layouts/quick-add/form/gantt.tsx @@ -27,6 +27,10 @@ export const GanttQuickAddIssueForm = observer(function GanttQuickAddIssueForm(p placeholder={isEpic ? t("epic.title.label") : t("issue.title.label")} {...register("name", { required: isEpic ? t("epic.title.required") : t("issue.title.required"), + maxLength: { + value: 255, + message: t("title_should_be_less_than_255_characters"), + }, })} className="w-full rounded-md bg-transparent px-2 py-3 text-13 leading-5 font-medium text-secondary outline-none" /> diff --git a/apps/web/core/components/issues/issue-layouts/quick-add/form/kanban.tsx b/apps/web/core/components/issues/issue-layouts/quick-add/form/kanban.tsx index f6a1aba136b..40b233b5332 100644 --- a/apps/web/core/components/issues/issue-layouts/quick-add/form/kanban.tsx +++ b/apps/web/core/components/issues/issue-layouts/quick-add/form/kanban.tsx @@ -21,6 +21,10 @@ export const KanbanQuickAddIssueForm = observer(function KanbanQuickAddIssueForm placeholder={isEpic ? t("epic.title.label") : t("issue.title.label")} {...register("name", { required: isEpic ? t("epic.title.required") : t("issue.title.required"), + maxLength: { + value: 255, + message: t("title_should_be_less_than_255_characters"), + }, })} className="w-full rounded-md bg-transparent px-2 py-1.5 pl-0 text-13 leading-5 font-medium text-secondary outline-none" /> diff --git a/apps/web/core/components/issues/issue-layouts/quick-add/form/list.tsx b/apps/web/core/components/issues/issue-layouts/quick-add/form/list.tsx index 7f9ff9b6d25..fc12615ef18 100644 --- a/apps/web/core/components/issues/issue-layouts/quick-add/form/list.tsx +++ b/apps/web/core/components/issues/issue-layouts/quick-add/form/list.tsx @@ -26,6 +26,10 @@ export const ListQuickAddIssueForm = observer(function ListQuickAddIssueForm(pro placeholder={isEpic ? t("epic.title.label") : t("issue.title.label")} {...register("name", { required: isEpic ? t("epic.title.required") : t("issue.title.required"), + maxLength: { + value: 255, + message: t("title_should_be_less_than_255_characters"), + }, })} className="w-full rounded-md bg-transparent px-2 py-3 text-13 leading-5 font-medium text-secondary outline-none" /> diff --git a/apps/web/core/components/issues/issue-layouts/quick-add/form/spreadsheet.tsx b/apps/web/core/components/issues/issue-layouts/quick-add/form/spreadsheet.tsx index ebf5978a2c6..8b7b23d2be8 100644 --- a/apps/web/core/components/issues/issue-layouts/quick-add/form/spreadsheet.tsx +++ b/apps/web/core/components/issues/issue-layouts/quick-add/form/spreadsheet.tsx @@ -25,6 +25,10 @@ export const SpreadsheetQuickAddIssueForm = observer(function SpreadsheetQuickAd placeholder={isEpic ? t("epic.title.label") : t("issue.title.label")} {...register("name", { required: isEpic ? t("epic.title.required") : t("issue.title.required"), + maxLength: { + value: 255, + message: t("title_should_be_less_than_255_characters"), + }, })} className="w-full rounded-md bg-transparent py-3 text-13 leading-5 text-secondary outline-none" /> diff --git a/apps/web/core/components/issues/issue-layouts/quick-add/root.tsx b/apps/web/core/components/issues/issue-layouts/quick-add/root.tsx index 4d0f784159d..abb7f031add 100644 --- a/apps/web/core/components/issues/issue-layouts/quick-add/root.tsx +++ b/apps/web/core/components/issues/issue-layouts/quick-add/root.tsx @@ -126,7 +126,21 @@ export const QuickAddIssueRoot = observer(function QuickAddIssueRoot(props: TQui }, error: { title: t("common.error.label"), - message: (err) => err?.message || t("common.error.message"), + message: (err) => { + // Issue service throws response.data (field errors), not Error instances + if (typeof err === "string") return err; + if (err?.message && typeof err.message === "string") return err.message; + if (err?.name) { + return Array.isArray(err.name) ? err.name[0] : err.name; + } + if (err && typeof err === "object") { + for (const value of Object.values(err)) { + if (typeof value === "string") return value; + if (Array.isArray(value) && typeof value[0] === "string") return value[0]; + } + } + return t("common.error.message"); + }, }, }); @@ -144,18 +158,23 @@ export const QuickAddIssueRoot = observer(function QuickAddIssueRoot(props: TQui )} > {isOpen ? ( - handleIsOpen(false)} - isEpic={isEpic} - /> + <> + handleIsOpen(false)} + isEpic={isEpic} + /> + {errors?.name?.message && ( +

{errors.name.message}

+ )} + ) : ( <> {QuickAddButton && handleIsOpen(true)} />} diff --git a/apps/web/core/store/issue/helpers/base-issues.store.ts b/apps/web/core/store/issue/helpers/base-issues.store.ts index 023f6590da8..f24c0bf7aae 100644 --- a/apps/web/core/store/issue/helpers/base-issues.store.ts +++ b/apps/web/core/store/issue/helpers/base-issues.store.ts @@ -645,26 +645,35 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore { async issueQuickAdd(workspaceSlug: string, projectId: string, data: TIssue) { // Add issue to store with a temporary Id this.addIssue(data); - // call Create issue method - const response = await this.createIssue(workspaceSlug, projectId, data); - runInAction(() => { - this.removeIssueFromList(data.id); - this.rootIssueStore.issues.removeIssue(data.id); - }); - const currentCycleId = data.cycle_id !== "" && data.cycle_id === "None" ? undefined : data.cycle_id; - const currentModuleIds = - data.module_ids && data.module_ids.length > 0 ? data.module_ids.filter((moduleId) => moduleId != "None") : []; - const promiseRequests = []; - if (currentCycleId) { - promiseRequests.push(this.addCycleToIssue(workspaceSlug, projectId, currentCycleId, response.id)); - } - if (currentModuleIds.length > 0) { - promiseRequests.push(this.changeModulesInIssue(workspaceSlug, projectId, response.id, currentModuleIds, [])); - } - if (promiseRequests && promiseRequests.length > 0) { - await Promise.all(promiseRequests); + try { + // call Create issue method + const response = await this.createIssue(workspaceSlug, projectId, data); + runInAction(() => { + this.removeIssueFromList(data.id); + this.rootIssueStore.issues.removeIssue(data.id); + }); + const currentCycleId = data.cycle_id !== "" && data.cycle_id === "None" ? undefined : data.cycle_id; + const currentModuleIds = + data.module_ids && data.module_ids.length > 0 ? data.module_ids.filter((moduleId) => moduleId != "None") : []; + const promiseRequests = []; + if (currentCycleId) { + promiseRequests.push(this.addCycleToIssue(workspaceSlug, projectId, currentCycleId, response.id)); + } + if (currentModuleIds.length > 0) { + promiseRequests.push(this.changeModulesInIssue(workspaceSlug, projectId, response.id, currentModuleIds, [])); + } + if (promiseRequests && promiseRequests.length > 0) { + await Promise.all(promiseRequests); + } + return response; + } catch (error) { + // Roll back optimistic temp issue so the list does not keep a failed entry + runInAction(() => { + this.removeIssueFromList(data.id); + this.rootIssueStore.issues.removeIssue(data.id); + }); + throw error; } - return response; } /**