mirror of
https://github.com/makeplane/plane.git
synced 2026-04-29 12:39:35 -05:00
[WIKI-538] chore: common description component (#7785)
* chore: common description input component * chore: replace existing description input components * fix: await for update calls * refactor: handle fallback values for description states and form data * fix: import statements * chore: add workspaceDetails check
This commit is contained in:
committed by
GitHub
parent
fd542a85fa
commit
12a050e7d3
@@ -1,18 +1,25 @@
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { ArrowUpToLine, Building, CreditCard, Users, Webhook } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
// plane imports
|
||||
import {
|
||||
EUserPermissionsLevel,
|
||||
EUserPermissions,
|
||||
GROUPED_WORKSPACE_SETTINGS,
|
||||
WORKSPACE_SETTINGS_CATEGORIES,
|
||||
EUserPermissions,
|
||||
WORKSPACE_SETTINGS_CATEGORY,
|
||||
} from "@plane/constants";
|
||||
import type { WORKSPACE_SETTINGS } from "@plane/constants";
|
||||
import type { ISvgIcons } from "@plane/propel/icons";
|
||||
import type { EUserWorkspaceRoles } from "@plane/types";
|
||||
// components
|
||||
import { SettingsSidebar } from "@/components/settings/sidebar";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
// plane web imports
|
||||
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
|
||||
|
||||
export const WORKSPACE_SETTINGS_ICONS = {
|
||||
export const WORKSPACE_SETTINGS_ICONS: Record<keyof typeof WORKSPACE_SETTINGS, LucideIcon | React.FC<ISvgIcons>> = {
|
||||
general: Building,
|
||||
members: Users,
|
||||
export: ArrowUpToLine,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,33 @@
|
||||
// plane imports
|
||||
import { Loader } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const DescriptionInputLoader: React.FC<Props> = (props) => {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<Loader className={cn("space-y-2", className)}>
|
||||
<Loader.Item width="100%" height="26px" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="26px" height="26px" />
|
||||
<Loader.Item width="400px" height="26px" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="26px" height="26px" />
|
||||
<Loader.Item width="400px" height="26px" />
|
||||
</div>
|
||||
<Loader.Item width="80%" height="26px" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader.Item width="50%" height="26px" />
|
||||
</div>
|
||||
<div className="border-0.5 absolute bottom-2 right-3.5 z-10 flex items-center gap-2">
|
||||
<Loader.Item width="100px" height="26px" />
|
||||
<Loader.Item width="50px" height="26px" />
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,245 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState, useRef } from "react";
|
||||
import { debounce } from "lodash-es";
|
||||
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 type { EFileAssetType, TNameDescriptionLoader } from "@plane/types";
|
||||
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";
|
||||
// local imports
|
||||
import { DescriptionInputLoader } from "./loader";
|
||||
// services init
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
type TFormData = {
|
||||
id: string;
|
||||
description_html: string;
|
||||
};
|
||||
|
||||
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<EditorRefApi>;
|
||||
/**
|
||||
* @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<void>;
|
||||
/**
|
||||
* @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;
|
||||
};
|
||||
|
||||
/**
|
||||
* @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<Props> = observer((props) => {
|
||||
const {
|
||||
containerClassName,
|
||||
disabled,
|
||||
disabledExtensions,
|
||||
editorRef,
|
||||
entityId,
|
||||
fileAssetType,
|
||||
initialValue,
|
||||
onSubmit,
|
||||
placeholder,
|
||||
projectId,
|
||||
setIsSubmitting,
|
||||
swrDescription,
|
||||
workspaceSlug,
|
||||
} = props;
|
||||
// states
|
||||
const [localDescription, setLocalDescription] = useState<TFormData>({
|
||||
id: entityId,
|
||||
description_html: initialValue?.trim() ?? "",
|
||||
});
|
||||
// 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<TFormData>({
|
||||
defaultValues: {
|
||||
id: entityId,
|
||||
description_html: initialValue?.trim() ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
// 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() === "" ? "<p></p>" : (initialValue ?? "<p></p>"),
|
||||
});
|
||||
setLocalDescription({
|
||||
id: entityId,
|
||||
description_html: initialValue?.trim() === "" ? "<p></p>" : (initialValue ?? "<p></p>"),
|
||||
});
|
||||
// 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)()
|
||||
.catch((error) => console.error(`Failed to save description for ${entityId}:`, error))
|
||||
.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
|
||||
[]
|
||||
);
|
||||
|
||||
if (!workspaceDetails) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{localDescription.description_html ? (
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { onChange } }) => (
|
||||
<RichTextEditor
|
||||
editable={!disabled}
|
||||
ref={editorRef}
|
||||
id={entityId}
|
||||
disabledExtensions={disabledExtensions}
|
||||
initialValue={localDescription.description_html ?? "<p></p>"}
|
||||
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.");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<DescriptionInputLoader />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -8,20 +8,20 @@ import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { TIssue, TNameDescriptionLoader } from "@plane/types";
|
||||
import { EInboxIssueSource } from "@plane/types";
|
||||
import { Loader } from "@plane/ui";
|
||||
import { EFileAssetType, EInboxIssueSource } 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";
|
||||
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";
|
||||
@@ -152,7 +152,7 @@ export const InboxIssueMainContent: React.FC<Props> = 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 },
|
||||
@@ -192,21 +192,25 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
|
||||
{loader === "issue-loading" ? (
|
||||
<Loader className="min-h-[6rem] rounded-md border border-custom-border-200">
|
||||
<Loader.Item width="100%" height="140px" />
|
||||
</Loader>
|
||||
<DescriptionInputLoader />
|
||||
) : (
|
||||
<IssueDescriptionInput
|
||||
editorRef={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
swrIssueDescription={issue.description_html ?? "<p></p>"}
|
||||
initialValue={issue.description_html ?? "<p></p>"}
|
||||
disabled={!isEditable}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
<DescriptionInput
|
||||
containerClassName="-ml-3 border-none"
|
||||
disabled={!isEditable}
|
||||
editorRef={editorRef}
|
||||
entityId={issue.id}
|
||||
fileAssetType={EFileAssetType.ISSUE_DESCRIPTION}
|
||||
initialValue={issue.description_html ?? "<p></p>"}
|
||||
onSubmit={async (value) => {
|
||||
if (!issue.id || !issue.project_id) return;
|
||||
await issueOperations.update(workspaceSlug, issue.project_id, issue.id, {
|
||||
description_html: value,
|
||||
});
|
||||
}}
|
||||
projectId={issue.project_id}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
swrDescription={issue.description_html ?? "<p></p>"}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { debounce } from "lodash-es";
|
||||
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 type { TIssue, TNameDescriptionLoader } from "@plane/types";
|
||||
import { EFileAssetType } from "@plane/types";
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { getDescriptionPlaceholderI18n } from "@plane/utils";
|
||||
import { RichTextEditor } from "@/components/editor/rich-text";
|
||||
import type { 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<EditorRefApi>;
|
||||
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<IssueDescriptionInputProps> = 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<TIssue>({
|
||||
defaultValues: {
|
||||
description_html: initialValue,
|
||||
},
|
||||
});
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleDescriptionFormSubmit = useCallback(
|
||||
async (formData: Partial<TIssue>) => {
|
||||
await issueOperations.update(workspaceSlug, projectId, issueId, {
|
||||
description_html: formData.description_html ?? "<p></p>",
|
||||
});
|
||||
},
|
||||
[workspaceSlug, projectId, issueId, issueOperations]
|
||||
);
|
||||
|
||||
// reset form values
|
||||
useEffect(() => {
|
||||
if (!issueId) return;
|
||||
reset({
|
||||
id: issueId,
|
||||
description_html: initialValue === "" ? "<p></p>" : initialValue,
|
||||
});
|
||||
setLocalIssueDescription({
|
||||
id: issueId,
|
||||
description_html: initialValue === "" ? "<p></p>" : 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 ? (
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { onChange } }) => (
|
||||
<RichTextEditor
|
||||
editable={!disabled}
|
||||
id={issueId}
|
||||
initialValue={localIssueDescription.description_html ?? "<p></p>"}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Loader>
|
||||
<Loader.Item height="150px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -5,10 +5,11 @@ import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import type { TNameDescriptionLoader } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
import { EFileAssetType, EIssueServiceType } 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";
|
||||
@@ -23,7 +24,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";
|
||||
@@ -128,16 +128,22 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||
containerClassName="-ml-3"
|
||||
/>
|
||||
|
||||
<IssueDescriptionInput
|
||||
editorRef={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
initialValue={issue.description_html}
|
||||
disabled={isArchived || !isEditable}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
<DescriptionInput
|
||||
containerClassName="-ml-3 border-none"
|
||||
disabled={isArchived || !isEditable}
|
||||
editorRef={editorRef}
|
||||
entityId={issue.id}
|
||||
fileAssetType={EFileAssetType.ISSUE_DESCRIPTION}
|
||||
initialValue={issue.description_html}
|
||||
onSubmit={async (value) => {
|
||||
if (!issue.id || !issue.project_id) return;
|
||||
await issueOperations.update(workspaceSlug, issue.project_id, issue.id, {
|
||||
description_html: value,
|
||||
});
|
||||
}}
|
||||
projectId={issue.project_id}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
"use-client";
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import { EFileAssetType } from "@plane/types";
|
||||
import type { TNameDescriptionLoader } from "@plane/types";
|
||||
// components
|
||||
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 +25,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";
|
||||
@@ -125,16 +127,22 @@ export const PeekOverviewIssueDetails: FC<Props> = observer((props) => {
|
||||
containerClassName="-ml-3"
|
||||
/>
|
||||
|
||||
<IssueDescriptionInput
|
||||
editorRef={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
initialValue={issueDescription}
|
||||
disabled={disabled || isArchived}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
<DescriptionInput
|
||||
containerClassName="-ml-3 border-none"
|
||||
disabled={disabled || isArchived}
|
||||
editorRef={editorRef}
|
||||
entityId={issue.id}
|
||||
fileAssetType={EFileAssetType.ISSUE_DESCRIPTION}
|
||||
initialValue={issueDescription}
|
||||
onSubmit={async (value) => {
|
||||
if (!issue.id || !issue.project_id) return;
|
||||
await issueOperations.update(workspaceSlug, issue.project_id, issue.id, {
|
||||
description_html: value,
|
||||
});
|
||||
}}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
projectId={issue.project_id}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
|
||||
Reference in New Issue
Block a user