From 6c38bed4dddd636120c92938b8548b991062ff6b Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 12 Sep 2025 16:29:33 +0530 Subject: [PATCH 1/4] chore: common description input component --- .../editor/rich-text/description-input.tsx | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 apps/web/core/components/editor/rich-text/description-input.tsx diff --git a/apps/web/core/components/editor/rich-text/description-input.tsx b/apps/web/core/components/editor/rich-text/description-input.tsx new file mode 100644 index 00000000000..e0f21d5636e --- /dev/null +++ b/apps/web/core/components/editor/rich-text/description-input.tsx @@ -0,0 +1,202 @@ +"use client"; + +import { useCallback, useEffect, useState, useRef } from "react"; +import debounce from "lodash/debounce"; +import { observer } from "mobx-react"; +import { Controller, useForm } from "react-hook-form"; +// plane imports +import type { EditorRefApi, TExtensions } from "@plane/editor"; +import { useTranslation } from "@plane/i18n"; +import { EFileAssetType, type TNameDescriptionLoader } from "@plane/types"; +import { Loader } from "@plane/ui"; +import { getDescriptionPlaceholderI18n } from "@plane/utils"; +// components +import { RichTextEditor } from "@/components/editor/rich-text"; +// hooks +import { useEditorAsset } from "@/hooks/store/use-editor-asset"; +import { useWorkspace } from "@/hooks/store/use-workspace"; +// plane web services +import { WorkspaceService } from "@/plane-web/services"; +const workspaceService = new WorkspaceService(); + +type TFormData = { + id: string; + description_html: string; +}; + +type Props = { + containerClassName?: string; + disabled?: boolean; + disabledExtensions?: TExtensions[]; + editorRef?: React.RefObject; + entityId: string; + fileAssetType: EFileAssetType; + initialValue: string | undefined; + onSubmit: (value: string) => Promise; + placeholder?: string | ((isFocused: boolean, value: string) => string); + projectId?: string; + setIsSubmitting: (initialValue: TNameDescriptionLoader) => void; + swrDescription?: string | null | undefined; + workspaceSlug: string; +}; + +/** + * @description DescriptionInput component for rich text editor with autosave functionality using debounce + * The component also makes an API call to save the description on unmount + */ +export const DescriptionInput: React.FC = observer((props) => { + const { + containerClassName, + disabled, + disabledExtensions, + editorRef, + entityId, + fileAssetType, + initialValue, + onSubmit, + placeholder, + projectId, + setIsSubmitting, + swrDescription, + workspaceSlug, + } = props; + // states + const [localDescription, setLocalDescription] = useState({ + id: entityId, + description_html: initialValue, + }); + // ref to track if there are unsaved changes + const hasUnsavedChanges = useRef(false); + // store hooks + const { getWorkspaceBySlug } = useWorkspace(); + const { uploadEditorAsset } = useEditorAsset(); + // derived values + const workspaceDetails = getWorkspaceBySlug(workspaceSlug); + // translation + const { t } = useTranslation(); + // form info + const { handleSubmit, reset, control } = useForm({ + defaultValues: { + id: entityId, + description_html: initialValue || "", + }, + }); + + // submit handler + const handleDescriptionFormSubmit = useCallback( + async (formData: TFormData) => { + await onSubmit(formData.description_html ?? "

"); + }, + [onSubmit] + ); + + // reset form values + useEffect(() => { + if (!entityId) return; + reset({ + id: entityId, + description_html: initialValue?.trim() === "" ? "

" : initialValue, + }); + setLocalDescription({ + id: entityId, + description_html: initialValue?.trim() === "" ? "

" : initialValue, + }); + // Reset unsaved changes flag when form is reset + hasUnsavedChanges.current = false; + }, [entityId, initialValue, reset]); + + // ADDING handleDescriptionFormSubmit TO DEPENDENCY ARRAY PRODUCES ADVERSE EFFECTS + // TODO: Verify the exhaustive-deps warning + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedFormSave = useCallback( + debounce(async () => { + handleSubmit(handleDescriptionFormSubmit)().finally(() => { + setIsSubmitting("submitted"); + hasUnsavedChanges.current = false; + }); + }, 1500), + [entityId, handleSubmit] + ); + + // Save on unmount if there are unsaved changes + useEffect( + () => () => { + debouncedFormSave.cancel(); + + if (hasUnsavedChanges.current) { + handleSubmit(handleDescriptionFormSubmit)() + .catch((error) => { + console.error("Failed to save description on unmount:", error); + }) + .finally(() => { + setIsSubmitting("submitted"); + hasUnsavedChanges.current = false; + }); + } + }, + // since we don't want to save on unmount if there are no unsaved changes, no deps are needed + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + return ( + <> + {localDescription.description_html ? ( + ( +

"} + value={swrDescription ?? null} + workspaceSlug={workspaceSlug} + workspaceId={workspaceDetails?.id ?? ""} + projectId={projectId} + dragDropEnabled + onChange={(_description, description_html) => { + setIsSubmitting("submitting"); + onChange(description_html); + hasUnsavedChanges.current = true; + debouncedFormSave(); + }} + placeholder={placeholder ?? ((isFocused, value) => t(getDescriptionPlaceholderI18n(isFocused, value)))} + searchMentionCallback={async (payload) => + await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", { + ...payload, + project_id: projectId, + }) + } + containerClassName={containerClassName} + uploadFile={async (blockId, file) => { + try { + const { asset_id } = await uploadEditorAsset({ + blockId, + data: { + entity_identifier: entityId, + entity_type: fileAssetType, + }, + file, + projectId, + workspaceSlug, + }); + return asset_id; + } catch (error) { + console.log("Error in uploading asset:", error); + throw new Error("Asset upload failed. Please try again later."); + } + }} + /> + )} + /> + ) : ( + + + + )} + + ); +}); From 31f3a8116a2309cb33f0cae3725298992dbf9c34 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 12 Sep 2025 16:54:15 +0530 Subject: [PATCH 2/4] chore: replace existing description input components --- .../rich-text/description-input/index.ts | 1 + .../rich-text/description-input/loader.tsx | 38 ++++ .../root.tsx} | 47 ++++- .../components/inbox/content/issue-root.tsx | 37 ++-- .../components/issues/description-input.tsx | 197 ------------------ .../issues/issue-detail/main-content.tsx | 24 ++- .../issues/peek-overview/issue-detail.tsx | 26 ++- 7 files changed, 134 insertions(+), 236 deletions(-) create mode 100644 apps/web/core/components/editor/rich-text/description-input/index.ts create mode 100644 apps/web/core/components/editor/rich-text/description-input/loader.tsx rename apps/web/core/components/editor/rich-text/{description-input.tsx => description-input/root.tsx} (81%) delete mode 100644 apps/web/core/components/issues/description-input.tsx diff --git a/apps/web/core/components/editor/rich-text/description-input/index.ts b/apps/web/core/components/editor/rich-text/description-input/index.ts new file mode 100644 index 00000000000..1efe34c51ec --- /dev/null +++ b/apps/web/core/components/editor/rich-text/description-input/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/apps/web/core/components/editor/rich-text/description-input/loader.tsx b/apps/web/core/components/editor/rich-text/description-input/loader.tsx new file mode 100644 index 00000000000..6d094b645b4 --- /dev/null +++ b/apps/web/core/components/editor/rich-text/description-input/loader.tsx @@ -0,0 +1,38 @@ +// plane imports +import { Loader } from "@plane/ui"; +import { cn } from "@plane/utils"; + +type Props = { + className?: string; +}; + +export const DescriptionInputLoader: React.FC = (props) => { + const { className } = props; + + return ( + + +
+ + +
+
+ + +
+ +
+ +
+
+ + +
+
+ ); +}; diff --git a/apps/web/core/components/editor/rich-text/description-input.tsx b/apps/web/core/components/editor/rich-text/description-input/root.tsx similarity index 81% rename from apps/web/core/components/editor/rich-text/description-input.tsx rename to apps/web/core/components/editor/rich-text/description-input/root.tsx index e0f21d5636e..8f7d5868f6e 100644 --- a/apps/web/core/components/editor/rich-text/description-input.tsx +++ b/apps/web/core/components/editor/rich-text/description-input/root.tsx @@ -8,7 +8,6 @@ import { Controller, useForm } from "react-hook-form"; import type { EditorRefApi, TExtensions } from "@plane/editor"; import { useTranslation } from "@plane/i18n"; import { EFileAssetType, type TNameDescriptionLoader } from "@plane/types"; -import { Loader } from "@plane/ui"; import { getDescriptionPlaceholderI18n } from "@plane/utils"; // components import { RichTextEditor } from "@/components/editor/rich-text"; @@ -17,6 +16,9 @@ import { useEditorAsset } from "@/hooks/store/use-editor-asset"; import { useWorkspace } from "@/hooks/store/use-workspace"; // plane web services import { WorkspaceService } from "@/plane-web/services"; +// local imports +import { DescriptionInputLoader } from "./loader"; +// services init const workspaceService = new WorkspaceService(); type TFormData = { @@ -25,18 +27,57 @@ type TFormData = { }; type Props = { + /** + * @description Container class name, this will be used to add custom styles to the editor container + */ containerClassName?: string; + /** + * @description Disabled, this will be used to disable the editor + */ disabled?: boolean; + /** + * @description Disabled extensions, this will be used to disable the extensions in the editor + */ disabledExtensions?: TExtensions[]; + /** + * @description Editor ref, this will be used to imperatively attach editor related helper functions + */ editorRef?: React.RefObject; + /** + * @description Entity ID, this will be used for file uploads and as the unique identifier for the entity + */ entityId: string; + /** + * @description File asset type, this will be used to upload the file to the editor + */ fileAssetType: EFileAssetType; + /** + * @description Initial value, pass the actual description to initialize the editor + */ initialValue: string | undefined; + /** + * @description Submit handler, the actual function which will be called when the form is submitted + */ onSubmit: (value: string) => Promise; + /** + * @description Placeholder, if not provided, the placeholder will be the default placeholder + */ placeholder?: string | ((isFocused: boolean, value: string) => string); + /** + * @description projectId, if not provided, the entity will be considered as a workspace entity + */ projectId?: string; + /** + * @description Set is submitting, use it to set the loading state of the form + */ setIsSubmitting: (initialValue: TNameDescriptionLoader) => void; + /** + * @description SWR description, use it only if you want to sync changes in realtime(pseudo realtime) + */ swrDescription?: string | null | undefined; + /** + * @description Workspace slug, this will be used to get the workspace details + */ workspaceSlug: string; }; @@ -193,9 +234,7 @@ export const DescriptionInput: React.FC = observer((props) => { )} /> ) : ( - - - + )} ); diff --git a/apps/web/core/components/inbox/content/issue-root.tsx b/apps/web/core/components/inbox/content/issue-root.tsx index 390aed13a7d..bb3f133e037 100644 --- a/apps/web/core/components/inbox/content/issue-root.tsx +++ b/apps/web/core/components/inbox/content/issue-root.tsx @@ -5,20 +5,21 @@ import { observer } from "mobx-react"; // plane imports import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants"; import type { EditorRefApi } from "@plane/editor"; -import { EInboxIssueSource, TIssue, TNameDescriptionLoader } from "@plane/types"; -import { Loader, TOAST_TYPE, setToast } from "@plane/ui"; +import { EFileAssetType, EInboxIssueSource, TIssue, TNameDescriptionLoader } from "@plane/types"; +import { TOAST_TYPE, setToast } from "@plane/ui"; import { getTextContent } from "@plane/utils"; // components import { DescriptionVersionsRoot } from "@/components/core/description-versions"; +import { DescriptionInput } from "@/components/editor/rich-text/description-input"; +import { DescriptionInputLoader } from "@/components/editor/rich-text/description-input/loader"; import { IssueAttachmentRoot } from "@/components/issues/attachment"; -import { IssueDescriptionInput } from "@/components/issues/description-input"; import type { TIssueOperations } from "@/components/issues/issue-detail"; import { IssueActivity } from "@/components/issues/issue-detail/issue-activity"; import { IssueReaction } from "@/components/issues/issue-detail/reactions"; import { IssueTitleInput } from "@/components/issues/title-input"; // helpers -// hooks import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; +// hooks import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { useMember } from "@/hooks/store/use-member"; import { useProject } from "@/hooks/store/use-project"; @@ -149,7 +150,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => { payload: { id: issueId }, }); } catch (error) { - console.log("Error in archiving issue:", error); + console.error("Error in archiving issue:", error); captureError({ eventName: WORK_ITEM_TRACKER_EVENTS.archive, payload: { id: issueId }, @@ -189,21 +190,25 @@ export const InboxIssueMainContent: React.FC = observer((props) => { /> {loader === "issue-loading" ? ( - - - + ) : ( -

"} + entityId={issue.id} + fileAssetType={EFileAssetType.ISSUE_DESCRIPTION} initialValue={issue.description_html ?? "

"} - disabled={!isEditable} - issueOperations={issueOperations} + onSubmit={async (value) => { + if (!issue.id || !issue.project_id) return; + issueOperations.update(workspaceSlug, issue.project_id, issue.id, { + description_html: value, + }); + }} + projectId={issue.project_id} setIsSubmitting={(value) => setIsSubmitting(value)} - containerClassName="-ml-3 border-none" + swrDescription={issue.description_html ?? "

"} + workspaceSlug={workspaceSlug} /> )} diff --git a/apps/web/core/components/issues/description-input.tsx b/apps/web/core/components/issues/description-input.tsx deleted file mode 100644 index c320daf279a..00000000000 --- a/apps/web/core/components/issues/description-input.tsx +++ /dev/null @@ -1,197 +0,0 @@ -"use client"; - -import { FC, useCallback, useEffect, useRef, useState } from "react"; -import debounce from "lodash/debounce"; -import { observer } from "mobx-react"; -import { Controller, useForm } from "react-hook-form"; -// plane imports -import type { EditorRefApi } from "@plane/editor"; -import { useTranslation } from "@plane/i18n"; -import { EFileAssetType, TIssue, TNameDescriptionLoader } from "@plane/types"; -import { Loader } from "@plane/ui"; -// components -import { getDescriptionPlaceholderI18n } from "@plane/utils"; -import { RichTextEditor } from "@/components/editor/rich-text"; -import { TIssueOperations } from "@/components/issues/issue-detail"; -// helpers -// hooks -import { useEditorAsset } from "@/hooks/store/use-editor-asset"; -import { useWorkspace } from "@/hooks/store/use-workspace"; -// plane web services -import { WorkspaceService } from "@/plane-web/services"; -const workspaceService = new WorkspaceService(); - -export type IssueDescriptionInputProps = { - containerClassName?: string; - editorRef?: React.RefObject; - workspaceSlug: string; - projectId: string; - issueId: string; - initialValue: string | undefined; - disabled?: boolean; - issueOperations: TIssueOperations; - placeholder?: string | ((isFocused: boolean, value: string) => string); - setIsSubmitting: (initialValue: TNameDescriptionLoader) => void; - swrIssueDescription?: string | null | undefined; -}; - -export const IssueDescriptionInput: FC = observer((props) => { - const { - containerClassName, - editorRef, - workspaceSlug, - projectId, - issueId, - disabled, - swrIssueDescription, - initialValue, - issueOperations, - setIsSubmitting, - placeholder, - } = props; - // states - const [localIssueDescription, setLocalIssueDescription] = useState({ - id: issueId, - description_html: initialValue, - }); - // ref to track if there are unsaved changes - const hasUnsavedChanges = useRef(false); - // store hooks - const { uploadEditorAsset } = useEditorAsset(); - const { getWorkspaceBySlug } = useWorkspace(); - // derived values - const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id?.toString(); - // form info - const { handleSubmit, reset, control } = useForm({ - defaultValues: { - description_html: initialValue, - }, - }); - // i18n - const { t } = useTranslation(); - - const handleDescriptionFormSubmit = useCallback( - async (formData: Partial) => { - await issueOperations.update(workspaceSlug, projectId, issueId, { - description_html: formData.description_html ?? "

", - }); - }, - [workspaceSlug, projectId, issueId, issueOperations] - ); - - // reset form values - useEffect(() => { - if (!issueId) return; - reset({ - id: issueId, - description_html: initialValue === "" ? "

" : initialValue, - }); - setLocalIssueDescription({ - id: issueId, - description_html: initialValue === "" ? "

" : initialValue, - }); - // Reset unsaved changes flag when form is reset - hasUnsavedChanges.current = false; - }, [initialValue, issueId, reset]); - - // ADDING handleDescriptionFormSubmit TO DEPENDENCY ARRAY PRODUCES ADVERSE EFFECTS - // TODO: Verify the exhaustive-deps warning - // eslint-disable-next-line react-hooks/exhaustive-deps - const debouncedFormSave = useCallback( - debounce(async () => { - handleSubmit(handleDescriptionFormSubmit)().finally(() => { - setIsSubmitting("submitted"); - hasUnsavedChanges.current = false; - }); - }, 1500), - [handleSubmit, issueId] - ); - - // Save on unmount if there are unsaved changes - useEffect( - () => () => { - debouncedFormSave.cancel(); - - if (hasUnsavedChanges.current) { - handleSubmit(handleDescriptionFormSubmit)() - .catch((error) => { - console.error("Failed to save description on unmount:", error); - }) - .finally(() => { - setIsSubmitting("submitted"); - hasUnsavedChanges.current = false; - }); - } - }, - // since we don't want to save on unmount if there are no unsaved changes, no deps are needed - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - if (!workspaceId) return null; - - return ( - <> - {localIssueDescription.description_html ? ( - ( -

"} - value={swrIssueDescription ?? null} - workspaceSlug={workspaceSlug} - workspaceId={workspaceId} - projectId={projectId} - dragDropEnabled - onChange={(_description: object, description_html: string) => { - setIsSubmitting("submitting"); - onChange(description_html); - hasUnsavedChanges.current = true; - debouncedFormSave(); - }} - placeholder={ - placeholder - ? placeholder - : (isFocused, value) => t(`${getDescriptionPlaceholderI18n(isFocused, value)}`) - } - searchMentionCallback={async (payload) => - await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", { - ...payload, - project_id: projectId?.toString() ?? "", - issue_id: issueId?.toString(), - }) - } - containerClassName={containerClassName} - uploadFile={async (blockId, file) => { - try { - const { asset_id } = await uploadEditorAsset({ - blockId, - data: { - entity_identifier: issueId, - entity_type: EFileAssetType.ISSUE_DESCRIPTION, - }, - file, - projectId, - workspaceSlug, - }); - return asset_id; - } catch (error) { - console.log("Error in uploading work item asset:", error); - throw new Error("Asset upload failed. Please try again later."); - } - }} - ref={editorRef} - /> - )} - /> - ) : ( - - - - )} - - ); -}); diff --git a/apps/web/core/components/issues/issue-detail/main-content.tsx b/apps/web/core/components/issues/issue-detail/main-content.tsx index 8535cc4d8d7..1b0e2198115 100644 --- a/apps/web/core/components/issues/issue-detail/main-content.tsx +++ b/apps/web/core/components/issues/issue-detail/main-content.tsx @@ -4,10 +4,11 @@ import { useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; // plane imports import type { EditorRefApi } from "@plane/editor"; -import { EIssueServiceType, TNameDescriptionLoader } from "@plane/types"; +import { EFileAssetType, EIssueServiceType, TNameDescriptionLoader } from "@plane/types"; import { getTextContent } from "@plane/utils"; // components import { DescriptionVersionsRoot } from "@/components/core/description-versions"; +import { DescriptionInput } from "@/components/editor/rich-text/description-input"; // hooks import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { useMember } from "@/hooks/store/use-member"; @@ -22,7 +23,6 @@ import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-dup // services import { WorkItemVersionService } from "@/services/issue"; // local imports -import { IssueDescriptionInput } from "../description-input"; import { IssueDetailWidgets } from "../issue-detail-widgets"; import { NameDescriptionUpdateStatus } from "../issue-update-status"; import { PeekOverviewProperties } from "../peek-overview/properties"; @@ -127,16 +127,22 @@ export const IssueMainContent: React.FC = observer((props) => { containerClassName="-ml-3" /> - { + if (!issue.id || !issue.project_id) return; + issueOperations.update(workspaceSlug, issue.project_id, issue.id, { + description_html: value, + }); + }} + projectId={issue.project_id} setIsSubmitting={(value) => setIsSubmitting(value)} - containerClassName="-ml-3 border-none" + workspaceSlug={workspaceSlug} />
diff --git a/apps/web/core/components/issues/peek-overview/issue-detail.tsx b/apps/web/core/components/issues/peek-overview/issue-detail.tsx index 781aa623cf8..33421c9244f 100644 --- a/apps/web/core/components/issues/peek-overview/issue-detail.tsx +++ b/apps/web/core/components/issues/peek-overview/issue-detail.tsx @@ -3,10 +3,11 @@ import { FC, useEffect, useRef } from "react"; import { observer } from "mobx-react"; // plane imports import type { EditorRefApi } from "@plane/editor"; -import { TNameDescriptionLoader } from "@plane/types"; -// components +import { EFileAssetType, TNameDescriptionLoader } from "@plane/types"; import { getTextContent } from "@plane/utils"; +// components import { DescriptionVersionsRoot } from "@/components/core/description-versions"; +import { DescriptionInput } from "@/components/editor/rich-text/description-input"; // hooks import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { useMember } from "@/hooks/store/use-member"; @@ -21,7 +22,6 @@ import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-dup // services import { WorkItemVersionService } from "@/services/issue"; // local components -import { IssueDescriptionInput } from "../description-input"; import type { TIssueOperations } from "../issue-detail"; import { IssueParentDetail } from "../issue-detail/parent"; import { IssueReaction } from "../issue-detail/reactions"; @@ -124,16 +124,22 @@ export const PeekOverviewIssueDetails: FC = observer( containerClassName="-ml-3" /> - { + if (!issue.id || !issue.project_id) return; + issueOperations.update(workspaceSlug, issue.project_id, issue.id, { + description_html: value, + }); + }} setIsSubmitting={(value) => setIsSubmitting(value)} - containerClassName="-ml-3 border-none" + projectId={issue.project_id} + workspaceSlug={workspaceSlug} />
From 670e98615f7d1f5e43a350e42e7ffc2346f084bb Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 12 Sep 2025 17:22:08 +0530 Subject: [PATCH 3/4] fix: await for update calls --- .../editor/rich-text/description-input/loader.tsx | 7 +------ apps/web/core/components/inbox/content/issue-root.tsx | 2 +- .../core/components/issues/issue-detail/main-content.tsx | 2 +- .../core/components/issues/peek-overview/issue-detail.tsx | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/web/core/components/editor/rich-text/description-input/loader.tsx b/apps/web/core/components/editor/rich-text/description-input/loader.tsx index 6d094b645b4..47bd597e3e5 100644 --- a/apps/web/core/components/editor/rich-text/description-input/loader.tsx +++ b/apps/web/core/components/editor/rich-text/description-input/loader.tsx @@ -10,12 +10,7 @@ export const DescriptionInputLoader: React.FC = (props) => { const { className } = props; return ( - +
diff --git a/apps/web/core/components/inbox/content/issue-root.tsx b/apps/web/core/components/inbox/content/issue-root.tsx index bb3f133e037..91fbf90fb24 100644 --- a/apps/web/core/components/inbox/content/issue-root.tsx +++ b/apps/web/core/components/inbox/content/issue-root.tsx @@ -201,7 +201,7 @@ export const InboxIssueMainContent: React.FC = observer((props) => { initialValue={issue.description_html ?? "

"} onSubmit={async (value) => { if (!issue.id || !issue.project_id) return; - issueOperations.update(workspaceSlug, issue.project_id, issue.id, { + await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { description_html: value, }); }} diff --git a/apps/web/core/components/issues/issue-detail/main-content.tsx b/apps/web/core/components/issues/issue-detail/main-content.tsx index 1b0e2198115..e9c3d78e546 100644 --- a/apps/web/core/components/issues/issue-detail/main-content.tsx +++ b/apps/web/core/components/issues/issue-detail/main-content.tsx @@ -136,7 +136,7 @@ export const IssueMainContent: React.FC = observer((props) => { initialValue={issue.description_html} onSubmit={async (value) => { if (!issue.id || !issue.project_id) return; - issueOperations.update(workspaceSlug, issue.project_id, issue.id, { + await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { description_html: value, }); }} diff --git a/apps/web/core/components/issues/peek-overview/issue-detail.tsx b/apps/web/core/components/issues/peek-overview/issue-detail.tsx index 33421c9244f..ec7074c05c2 100644 --- a/apps/web/core/components/issues/peek-overview/issue-detail.tsx +++ b/apps/web/core/components/issues/peek-overview/issue-detail.tsx @@ -133,7 +133,7 @@ export const PeekOverviewIssueDetails: FC = observer( initialValue={issueDescription} onSubmit={async (value) => { if (!issue.id || !issue.project_id) return; - issueOperations.update(workspaceSlug, issue.project_id, issue.id, { + await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { description_html: value, }); }} From d678c007ef532e5a6366ab15c345823176bb0751 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Tue, 23 Sep 2025 18:14:20 +0530 Subject: [PATCH 4/4] refactor: handle fallback values for description states and form data --- .../rich-text/description-input/root.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/web/core/components/editor/rich-text/description-input/root.tsx b/apps/web/core/components/editor/rich-text/description-input/root.tsx index 8f7d5868f6e..8fecdd341c8 100644 --- a/apps/web/core/components/editor/rich-text/description-input/root.tsx +++ b/apps/web/core/components/editor/rich-text/description-input/root.tsx @@ -102,9 +102,9 @@ export const DescriptionInput: React.FC = observer((props) => { workspaceSlug, } = props; // states - const [localDescription, setLocalDescription] = useState({ + const [localDescription, setLocalDescription] = useState({ id: entityId, - description_html: initialValue, + description_html: initialValue?.trim() ?? "", }); // ref to track if there are unsaved changes const hasUnsavedChanges = useRef(false); @@ -119,14 +119,14 @@ export const DescriptionInput: React.FC = observer((props) => { const { handleSubmit, reset, control } = useForm({ defaultValues: { id: entityId, - description_html: initialValue || "", + description_html: initialValue?.trim() ?? "", }, }); // submit handler const handleDescriptionFormSubmit = useCallback( async (formData: TFormData) => { - await onSubmit(formData.description_html ?? "

"); + await onSubmit(formData.description_html); }, [onSubmit] ); @@ -136,11 +136,11 @@ export const DescriptionInput: React.FC = observer((props) => { if (!entityId) return; reset({ id: entityId, - description_html: initialValue?.trim() === "" ? "

" : initialValue, + description_html: initialValue?.trim() === "" ? "

" : (initialValue ?? "

"), }); setLocalDescription({ id: entityId, - description_html: initialValue?.trim() === "" ? "

" : initialValue, + description_html: initialValue?.trim() === "" ? "

" : (initialValue ?? "

"), }); // Reset unsaved changes flag when form is reset hasUnsavedChanges.current = false; @@ -151,10 +151,12 @@ export const DescriptionInput: React.FC = observer((props) => { // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedFormSave = useCallback( debounce(async () => { - handleSubmit(handleDescriptionFormSubmit)().finally(() => { - setIsSubmitting("submitted"); - hasUnsavedChanges.current = false; - }); + handleSubmit(handleDescriptionFormSubmit)() + .catch((error) => console.error(`Failed to save description for ${entityId}:`, error)) + .finally(() => { + setIsSubmitting("submitted"); + hasUnsavedChanges.current = false; + }); }, 1500), [entityId, handleSubmit] );