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]
);