* chore: new description binary endpoints * chore: conflict free issue description * chore: fix submitting status * chore: update yjs utils * chore: handle component re-mounting * chore: update buffer response type * chore: add try catch for issue description update * chore: update buffer response type * chore: description binary in retrieve * chore: update issue description hook * chore: decode description binary * chore: migrations fixes and cleanup * chore: migration fixes * fix: inbox issue description * chore: move update operations to the issue store * fix: merge conflicts * chore: reverted the commit * chore: removed the unwanted imports * chore: remove unnecessary props * chore: remove unused services * chore: update live server error handling --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
145 lines
4.8 KiB
TypeScript
145 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import { FC, useCallback, useRef } from "react";
|
|
import debounce from "lodash/debounce";
|
|
import { observer } from "mobx-react";
|
|
// plane editor
|
|
import { convertBinaryDataToBase64String, EditorRefApi } from "@plane/editor";
|
|
// types
|
|
import { EFileAssetType } from "@plane/types/src/enums";
|
|
// plane ui
|
|
import { Loader } from "@plane/ui";
|
|
// components
|
|
import { CollaborativeRichTextEditor, CollaborativeRichTextReadOnlyEditor } from "@/components/editor";
|
|
// helpers
|
|
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
|
// hooks
|
|
import { useWorkspace } from "@/hooks/store";
|
|
import { useIssueDescription } from "@/hooks/use-issue-description";
|
|
// services
|
|
import { FileService } from "@/services/file.service";
|
|
const fileService = new FileService();
|
|
|
|
export type IssueDescriptionInputProps = {
|
|
containerClassName?: string;
|
|
descriptionBinary: string | null;
|
|
descriptionHTML: string;
|
|
disabled?: boolean;
|
|
issueId: string;
|
|
key: string;
|
|
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
|
projectId: string;
|
|
setIsSubmitting: (initialValue: "submitting" | "submitted" | "saved") => void;
|
|
updateDescription: (data: string) => Promise<ArrayBuffer>;
|
|
workspaceSlug: string;
|
|
};
|
|
|
|
export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((props) => {
|
|
const {
|
|
containerClassName,
|
|
descriptionBinary: savedDescriptionBinary,
|
|
descriptionHTML,
|
|
disabled,
|
|
issueId,
|
|
placeholder,
|
|
projectId,
|
|
setIsSubmitting,
|
|
updateDescription,
|
|
workspaceSlug,
|
|
} = props;
|
|
// refs
|
|
const editorRef = useRef<EditorRefApi>(null);
|
|
// store hooks
|
|
const { getWorkspaceBySlug } = useWorkspace();
|
|
// derived values
|
|
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id?.toString() ?? "";
|
|
// use issue description
|
|
const { descriptionBinary, resolveConflictsAndUpdateDescription } = useIssueDescription({
|
|
descriptionBinary: savedDescriptionBinary,
|
|
descriptionHTML,
|
|
updateDescription,
|
|
});
|
|
|
|
const debouncedDescriptionSave = useCallback(
|
|
debounce(async (updatedDescription: Uint8Array) => {
|
|
const editor = editorRef.current;
|
|
if (!editor) return;
|
|
const encodedDescription = convertBinaryDataToBase64String(updatedDescription);
|
|
await resolveConflictsAndUpdateDescription(encodedDescription, editor);
|
|
setIsSubmitting("submitted");
|
|
}, 1500),
|
|
[]
|
|
);
|
|
|
|
if (!descriptionBinary)
|
|
return (
|
|
<Loader className="min-h-[120px] max-h-64 space-y-2 overflow-hidden rounded-md">
|
|
<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>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{!disabled ? (
|
|
<CollaborativeRichTextEditor
|
|
key={issueId}
|
|
containerClassName={containerClassName}
|
|
value={descriptionBinary}
|
|
onChange={(val) => {
|
|
setIsSubmitting("submitting");
|
|
debouncedDescriptionSave(val);
|
|
}}
|
|
dragDropEnabled
|
|
id={issueId}
|
|
placeholder={placeholder ? placeholder : (isFocused, value) => getDescriptionPlaceholder(isFocused, value)}
|
|
projectId={projectId}
|
|
ref={editorRef}
|
|
uploadFile={async (file) => {
|
|
try {
|
|
const { asset_id } = await fileService.uploadProjectAsset(
|
|
workspaceSlug,
|
|
projectId,
|
|
{
|
|
entity_identifier: issueId,
|
|
entity_type: EFileAssetType.ISSUE_DESCRIPTION,
|
|
},
|
|
file
|
|
);
|
|
return asset_id;
|
|
} catch (error) {
|
|
console.log("Error in uploading issue asset:", error);
|
|
throw new Error("Asset upload failed. Please try again later.");
|
|
}
|
|
}}
|
|
workspaceId={workspaceId}
|
|
workspaceSlug={workspaceSlug}
|
|
/>
|
|
) : (
|
|
<CollaborativeRichTextReadOnlyEditor
|
|
containerClassName={containerClassName}
|
|
descriptionBinary={savedDescriptionBinary}
|
|
descriptionHTML={descriptionHTML}
|
|
id={issueId}
|
|
projectId={projectId}
|
|
workspaceSlug={workspaceSlug}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
});
|