[WEB-1435] dev: conflict free issue descriptions (#5912)
* 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>
This commit is contained in:
parent
229610513a
commit
e9680cab74
65 changed files with 1466 additions and 358 deletions
|
|
@ -9,7 +9,7 @@ import { Popover, Transition } from "@headlessui/react";
|
|||
// ui
|
||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
|
||||
import { RichTextReadOnlyEditor } from "@/components/editor";
|
||||
// services
|
||||
import { AIService } from "@/services/ai.service";
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import React, { forwardRef } from "react";
|
||||
// editor
|
||||
import { CollaborativeRichTextEditorWithRef, EditorRefApi, ICollaborativeRichTextEditor } from "@plane/editor";
|
||||
// types
|
||||
import { IUserLite } from "@plane/types";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useMember, useMention, useUser } from "@/hooks/store";
|
||||
// plane web hooks
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
|
||||
interface Props extends Omit<ICollaborativeRichTextEditor, "fileHandler" | "mentionHandler"> {
|
||||
key: string;
|
||||
projectId: string;
|
||||
uploadFile: (file: File) => Promise<string>;
|
||||
workspaceId: string;
|
||||
workspaceSlug: string;
|
||||
}
|
||||
|
||||
export const CollaborativeRichTextEditor = forwardRef<EditorRefApi, Props>((props, ref) => {
|
||||
const { containerClassName, workspaceSlug, workspaceId, projectId, uploadFile, ...rest } = props;
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const {
|
||||
getUserDetails,
|
||||
project: { getProjectMemberIds },
|
||||
} = useMember();
|
||||
// derived values
|
||||
const projectMemberIds = getProjectMemberIds(projectId);
|
||||
const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
|
||||
// use-mention
|
||||
const { mentionHighlights, mentionSuggestions } = useMention({
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
members: projectMemberDetails,
|
||||
user: currentUser,
|
||||
});
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
|
||||
return (
|
||||
<CollaborativeRichTextEditorWithRef
|
||||
ref={ref}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
maxFileSize,
|
||||
projectId,
|
||||
uploadFile,
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
})}
|
||||
mentionHandler={{
|
||||
highlights: mentionHighlights,
|
||||
suggestions: mentionSuggestions,
|
||||
}}
|
||||
{...rest}
|
||||
containerClassName={cn("relative pl-3", containerClassName)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
CollaborativeRichTextEditor.displayName = "CollaborativeRichTextEditor";
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import React from "react";
|
||||
// editor
|
||||
import {
|
||||
CollaborativeRichTextReadOnlyEditorWithRef,
|
||||
EditorReadOnlyRefApi,
|
||||
ICollaborativeRichTextReadOnlyEditor,
|
||||
} from "@plane/editor";
|
||||
// plane ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useMention } from "@/hooks/store";
|
||||
import { useIssueDescription } from "@/hooks/use-issue-description";
|
||||
|
||||
type RichTextReadOnlyEditorWrapperProps = Omit<
|
||||
ICollaborativeRichTextReadOnlyEditor,
|
||||
"fileHandler" | "mentionHandler" | "value"
|
||||
> & {
|
||||
descriptionBinary: string | null;
|
||||
descriptionHTML: string;
|
||||
projectId?: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const CollaborativeRichTextReadOnlyEditor = React.forwardRef<
|
||||
EditorReadOnlyRefApi,
|
||||
RichTextReadOnlyEditorWrapperProps
|
||||
>(({ descriptionBinary: savedDescriptionBinary, descriptionHTML, projectId, workspaceSlug, ...props }, ref) => {
|
||||
const { mentionHighlights } = useMention({});
|
||||
|
||||
const { descriptionBinary } = useIssueDescription({
|
||||
descriptionBinary: savedDescriptionBinary,
|
||||
descriptionHTML,
|
||||
});
|
||||
|
||||
if (!descriptionBinary)
|
||||
return (
|
||||
<Loader>
|
||||
<Loader.Item height="150px" />
|
||||
</Loader>
|
||||
);
|
||||
|
||||
return (
|
||||
<CollaborativeRichTextReadOnlyEditorWithRef
|
||||
ref={ref}
|
||||
value={descriptionBinary}
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
})}
|
||||
mentionHandler={{
|
||||
highlights: mentionHighlights,
|
||||
}}
|
||||
{...props}
|
||||
// overriding the containerClassName to add relative class passed
|
||||
containerClassName={cn(props.containerClassName, "relative pl-3")}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
CollaborativeRichTextReadOnlyEditor.displayName = "CollaborativeRichTextReadOnlyEditor";
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
export * from "./rich-text-editor";
|
||||
export * from "./rich-text-read-only-editor";
|
||||
export * from "./collaborative-editor";
|
||||
export * from "./collaborative-read-only-editor";
|
||||
export * from "./editor";
|
||||
export * from "./read-only-editor";
|
||||
|
|
|
|||
|
|
@ -3,10 +3,8 @@
|
|||
import { Dispatch, SetStateAction, useEffect, useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
// plane types
|
||||
import { TIssue } from "@plane/types";
|
||||
// plane ui
|
||||
import { Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { InboxIssueContentProperties } from "@/components/inbox/content";
|
||||
import {
|
||||
|
|
@ -22,11 +20,12 @@ import { ISSUE_ARCHIVED, ISSUE_DELETED } from "@/constants/event-tracker";
|
|||
// helpers
|
||||
import { getTextContent } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useIssueDetail, useProject, useProjectInbox, useUser } from "@/hooks/store";
|
||||
import { useEventTracker, useIssueDetail, useProject, useUser } from "@/hooks/store";
|
||||
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||
// store types
|
||||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
// store
|
||||
import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -45,7 +44,6 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
const { data: currentUser } = useUser();
|
||||
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { loader } = useProjectInbox();
|
||||
const { getProjectById } = useProject();
|
||||
const { removeIssue, archiveIssue } = useIssueDetail();
|
||||
|
||||
|
|
@ -60,7 +58,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
}
|
||||
}, [isSubmitting, setShowAlert, setIsSubmitting]);
|
||||
|
||||
// dervied values
|
||||
// derived values
|
||||
const issue = inboxIssue.issue;
|
||||
const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined;
|
||||
|
||||
|
|
@ -75,12 +73,8 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
|
||||
const issueOperations: TIssueOperations = useMemo(
|
||||
() => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars, arrow-body-style
|
||||
fetch: async (_workspaceSlug: string, _projectId: string, _issueId: string) => {
|
||||
return;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars, arrow-body-style
|
||||
remove: async (_workspaceSlug: string, _projectId: string, _issueId: string) => {
|
||||
fetch: async () => {},
|
||||
remove: async (_workspaceSlug, _projectId, _issueId) => {
|
||||
try {
|
||||
await removeIssue(workspaceSlug, projectId, _issueId);
|
||||
setToast({
|
||||
|
|
@ -107,7 +101,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
});
|
||||
}
|
||||
},
|
||||
update: async (_workspaceSlug: string, _projectId: string, _issueId: string, data: Partial<TIssue>) => {
|
||||
update: async (_workspaceSlug, _projectId, _issueId, data) => {
|
||||
try {
|
||||
await inboxIssue.updateIssue(data);
|
||||
captureIssueEvent({
|
||||
|
|
@ -119,7 +113,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
},
|
||||
path: pathname,
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setToast({
|
||||
title: "Issue update failed",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
|
|
@ -136,7 +130,14 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
});
|
||||
}
|
||||
},
|
||||
archive: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
updateDescription: async (_workspaceSlug, _projectId, _issueId, descriptionBinary) => {
|
||||
try {
|
||||
return await inboxIssue.updateIssueDescription(descriptionBinary);
|
||||
} catch {
|
||||
throw new Error("Failed to update issue description");
|
||||
}
|
||||
},
|
||||
archive: async (workspaceSlug, projectId, issueId) => {
|
||||
try {
|
||||
await archiveIssue(workspaceSlug, projectId, issueId);
|
||||
captureIssueEvent({
|
||||
|
|
@ -154,7 +155,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
}
|
||||
},
|
||||
}),
|
||||
[inboxIssue]
|
||||
[archiveIssue, captureIssueEvent, inboxIssue, pathname, projectId, removeIssue, workspaceSlug]
|
||||
);
|
||||
|
||||
if (!issue?.project_id || !issue?.id) return <></>;
|
||||
|
|
@ -184,21 +185,20 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
|||
containerClassName="-ml-3"
|
||||
/>
|
||||
|
||||
{loader === "issue-loading" ? (
|
||||
<Loader className="min-h-[6rem] rounded-md border border-custom-border-200">
|
||||
<Loader.Item width="100%" height="140px" />
|
||||
</Loader>
|
||||
) : (
|
||||
{issue.description_binary !== undefined && (
|
||||
<IssueDescriptionInput
|
||||
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)}
|
||||
key={issue.id}
|
||||
containerClassName="-ml-3 border-none"
|
||||
descriptionBinary={issue.description_binary}
|
||||
descriptionHTML={issue.description_html ?? "<p></p>"}
|
||||
disabled={!isEditable}
|
||||
updateDescription={async (data) =>
|
||||
await issueOperations.updateDescription(workspaceSlug, projectId, issue.id ?? "", data)
|
||||
}
|
||||
issueId={issue.id}
|
||||
projectId={issue.project_id}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { EFileAssetType } from "@plane/types/src/enums";
|
|||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { RichTextEditor } from "@/components/editor/rich-text-editor/rich-text-editor";
|
||||
import { RichTextEditor } from "@/components/editor";
|
||||
// constants
|
||||
import { ETabIndices } from "@/constants/tab-indices";
|
||||
// helpers
|
||||
|
|
|
|||
|
|
@ -1,157 +1,144 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useCallback, useEffect, useState } from "react";
|
||||
import { FC, useCallback, useRef } from "react";
|
||||
import debounce from "lodash/debounce";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// plane editor
|
||||
import { convertBinaryDataToBase64String, EditorRefApi } from "@plane/editor";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
import { EFileAssetType } from "@plane/types/src/enums";
|
||||
// ui
|
||||
// plane ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { RichTextEditor, RichTextReadOnlyEditor } from "@/components/editor";
|
||||
import { TIssueOperations } from "@/components/issues/issue-detail";
|
||||
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;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
initialValue: string | undefined;
|
||||
descriptionBinary: string | null;
|
||||
descriptionHTML: string;
|
||||
disabled?: boolean;
|
||||
issueOperations: TIssueOperations;
|
||||
issueId: string;
|
||||
key: string;
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
projectId: string;
|
||||
setIsSubmitting: (initialValue: "submitting" | "submitted" | "saved") => void;
|
||||
swrIssueDescription?: string | null | undefined;
|
||||
updateDescription: (data: string) => Promise<ArrayBuffer>;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((props) => {
|
||||
const {
|
||||
containerClassName,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
descriptionBinary: savedDescriptionBinary,
|
||||
descriptionHTML,
|
||||
disabled,
|
||||
swrIssueDescription,
|
||||
initialValue,
|
||||
issueOperations,
|
||||
setIsSubmitting,
|
||||
issueId,
|
||||
placeholder,
|
||||
projectId,
|
||||
setIsSubmitting,
|
||||
updateDescription,
|
||||
workspaceSlug,
|
||||
} = props;
|
||||
|
||||
const { handleSubmit, reset, control } = useForm<TIssue>({
|
||||
defaultValues: {
|
||||
description_html: initialValue,
|
||||
},
|
||||
});
|
||||
|
||||
const [localIssueDescription, setLocalIssueDescription] = useState({
|
||||
id: issueId,
|
||||
description_html: initialValue,
|
||||
});
|
||||
|
||||
const handleDescriptionFormSubmit = useCallback(
|
||||
async (formData: Partial<TIssue>) => {
|
||||
await issueOperations.update(workspaceSlug, projectId, issueId, {
|
||||
description_html: formData.description_html ?? "<p></p>",
|
||||
});
|
||||
},
|
||||
[workspaceSlug, projectId, issueId, issueOperations]
|
||||
);
|
||||
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
// store hooks
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// computed values
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id as string;
|
||||
// derived values
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id?.toString() ?? "";
|
||||
// use issue description
|
||||
const { descriptionBinary, resolveConflictsAndUpdateDescription } = useIssueDescription({
|
||||
descriptionBinary: savedDescriptionBinary,
|
||||
descriptionHTML,
|
||||
updateDescription,
|
||||
});
|
||||
|
||||
// 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,
|
||||
});
|
||||
}, [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"));
|
||||
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),
|
||||
[handleSubmit, issueId]
|
||||
[]
|
||||
);
|
||||
|
||||
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 (
|
||||
<>
|
||||
{localIssueDescription.description_html ? (
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { onChange } }) =>
|
||||
!disabled ? (
|
||||
<RichTextEditor
|
||||
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);
|
||||
debouncedFormSave();
|
||||
}}
|
||||
placeholder={
|
||||
placeholder ? placeholder : (isFocused, value) => getDescriptionPlaceholder(isFocused, value)
|
||||
}
|
||||
containerClassName={containerClassName}
|
||||
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.");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<RichTextReadOnlyEditor
|
||||
id={issueId}
|
||||
initialValue={localIssueDescription.description_html ?? ""}
|
||||
containerClassName={containerClassName}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{!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}
|
||||
/>
|
||||
) : (
|
||||
<Loader>
|
||||
<Loader.Item height="150px" />
|
||||
</Loader>
|
||||
<CollaborativeRichTextReadOnlyEditor
|
||||
containerClassName={containerClassName}
|
||||
descriptionBinary={savedDescriptionBinary}
|
||||
descriptionHTML={descriptionHTML}
|
||||
id={issueId}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import useSize from "@/hooks/use-window-size";
|
|||
// plane web components
|
||||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
||||
import { IssueTypeSwitcher } from "@/plane-web/components/issues";
|
||||
// plane web hooks
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
// types
|
||||
import { TIssueOperations } from "./root";
|
||||
|
|
@ -113,16 +114,22 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||
containerClassName="-ml-3"
|
||||
/>
|
||||
|
||||
<IssueDescriptionInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
initialValue={issue.description_html}
|
||||
disabled={!isEditable}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
containerClassName="-ml-3 border-none"
|
||||
/>
|
||||
{issue.description_binary !== undefined && (
|
||||
<IssueDescriptionInput
|
||||
key={issue.id}
|
||||
containerClassName="-ml-3 border-none"
|
||||
descriptionBinary={issue.description_binary}
|
||||
descriptionHTML={issue.description_html ?? "<p></p>"}
|
||||
disabled={!isEditable}
|
||||
updateDescription={async (data) =>
|
||||
await issueOperations.updateDescription(workspaceSlug, issue.project_id ?? "", issue.id, data)
|
||||
}
|
||||
issueId={issue.id}
|
||||
projectId={issue.project_id}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentUser && (
|
||||
<IssueReaction
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ import { IssueDetailsSidebar } from "./sidebar";
|
|||
export type TIssueOperations = {
|
||||
fetch: (workspaceSlug: string, projectId: string, issueId: string, loader?: boolean) => Promise<void>;
|
||||
update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
updateDescription: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
descriptionBinary: string
|
||||
) => Promise<ArrayBuffer>;
|
||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
archive?: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
restore?: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
|
|
@ -64,6 +70,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
|||
issue: { getIssueById },
|
||||
fetchIssue,
|
||||
updateIssue,
|
||||
updateIssueDescription,
|
||||
removeIssue,
|
||||
archiveIssue,
|
||||
addCycleToIssue,
|
||||
|
|
@ -118,6 +125,13 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
|||
});
|
||||
}
|
||||
},
|
||||
updateDescription: async (workspaceSlug, projectId, issueId, descriptionBinary) => {
|
||||
try {
|
||||
return await updateIssueDescription(workspaceSlug, projectId, issueId, descriptionBinary);
|
||||
} catch {
|
||||
throw new Error("Failed to update issue description");
|
||||
}
|
||||
},
|
||||
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
if (is_archived) await removeArchivedIssue(workspaceSlug, projectId, issueId);
|
||||
|
|
@ -317,6 +331,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
|||
is_archived,
|
||||
fetchIssue,
|
||||
updateIssue,
|
||||
updateIssueDescription,
|
||||
removeIssue,
|
||||
archiveIssue,
|
||||
removeArchivedIssue,
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
|||
// plane web components
|
||||
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe";
|
||||
import { IssueTypeSwitcher } from "@/plane-web/components/issues";
|
||||
// local components
|
||||
// plane web hooks
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
// local components
|
||||
import { IssueDescriptionInput } from "../description-input";
|
||||
import { IssueReaction } from "../issue-detail/reactions";
|
||||
import { IssueTitleInput } from "../title-input";
|
||||
|
|
@ -63,13 +64,6 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
|||
|
||||
if (!issue || !issue.project_id) return <></>;
|
||||
|
||||
const issueDescription =
|
||||
issue.description_html !== undefined || issue.description_html !== null
|
||||
? issue.description_html != ""
|
||||
? issue.description_html
|
||||
: "<p></p>"
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{issue.parent_id && (
|
||||
|
|
@ -105,16 +99,22 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
|||
containerClassName="-ml-3"
|
||||
/>
|
||||
|
||||
<IssueDescriptionInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
initialValue={issueDescription}
|
||||
disabled={disabled}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
containerClassName="-ml-3 border-none"
|
||||
/>
|
||||
{issue.description_binary !== undefined && (
|
||||
<IssueDescriptionInput
|
||||
key={issue.id}
|
||||
containerClassName="-ml-3 border-none"
|
||||
descriptionBinary={issue.description_binary}
|
||||
descriptionHTML={issue.description_html ?? "<p></p>"}
|
||||
disabled={disabled}
|
||||
updateDescription={async (data) =>
|
||||
await issueOperations.updateDescription(workspaceSlug, issue.project_id ?? "", issue.id, data)
|
||||
}
|
||||
issueId={issue.id}
|
||||
projectId={issue.project_id}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentUser && (
|
||||
<IssueReaction
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||
setPeekIssue,
|
||||
issue: { fetchIssue, getIsFetchingIssueDetails },
|
||||
fetchActivities,
|
||||
updateIssueDescription,
|
||||
} = useIssueDetail();
|
||||
|
||||
const { issues } = useIssuesStore();
|
||||
|
|
@ -92,6 +93,16 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||
});
|
||||
}
|
||||
},
|
||||
updateDescription: async (workspaceSlug, projectId, issueId, descriptionBinary) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) {
|
||||
throw new Error("Required fields missing while updating binary description");
|
||||
}
|
||||
try {
|
||||
return await updateIssueDescription(workspaceSlug, projectId, issueId, descriptionBinary);
|
||||
} catch {
|
||||
throw new Error("Failed to update issue description");
|
||||
}
|
||||
},
|
||||
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
return issues?.removeIssue(workspaceSlug, projectId, issueId).then(() => {
|
||||
|
|
@ -318,7 +329,17 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||
}
|
||||
},
|
||||
}),
|
||||
[fetchIssue, is_draft, issues, fetchActivities, captureIssueEvent, pathname, removeRoutePeekId, restoreIssue]
|
||||
[
|
||||
fetchIssue,
|
||||
is_draft,
|
||||
issues,
|
||||
fetchActivities,
|
||||
captureIssueEvent,
|
||||
pathname,
|
||||
removeRoutePeekId,
|
||||
restoreIssue,
|
||||
updateIssueDescription,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,9 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
|||
usePageFallback({
|
||||
editorRef,
|
||||
fetchPageDescription: async () => {
|
||||
if (!page.id) return;
|
||||
if (!page.id) {
|
||||
throw new Error("Required fields missing while fetching binary description");
|
||||
}
|
||||
return await projectPageService.fetchDescriptionBinary(workspaceSlug, projectId, page.id);
|
||||
},
|
||||
hasConnectionFailed,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { IUserActivityResponse } from "@plane/types";
|
|||
// components
|
||||
import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core";
|
||||
// editor
|
||||
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
|
||||
import { RichTextReadOnlyEditor } from "@/components/editor";
|
||||
// ui
|
||||
import { ActivitySettingsLoader } from "@/components/ui";
|
||||
// helpers
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import useSWR from "swr";
|
|||
import { History, MessageSquare } from "lucide-react";
|
||||
// hooks
|
||||
import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core";
|
||||
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
|
||||
import { RichTextReadOnlyEditor } from "@/components/editor";
|
||||
import { ActivitySettingsLoader } from "@/components/ui";
|
||||
// constants
|
||||
import { USER_ACTIVITY } from "@/constants/fetch-keys";
|
||||
|
|
|
|||
54
web/core/hooks/use-issue-description.ts
Normal file
54
web/core/hooks/use-issue-description.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
// plane editor
|
||||
import {
|
||||
convertBase64StringToBinaryData,
|
||||
EditorRefApi,
|
||||
getBinaryDataFromRichTextEditorHTMLString,
|
||||
} from "@plane/editor";
|
||||
|
||||
type TArgs = {
|
||||
descriptionBinary: string | null;
|
||||
descriptionHTML: string | null;
|
||||
updateDescription?: (data: string) => Promise<ArrayBuffer>;
|
||||
};
|
||||
|
||||
export const useIssueDescription = (args: TArgs) => {
|
||||
const { descriptionBinary: savedDescriptionBinary, descriptionHTML, updateDescription } = args;
|
||||
// states
|
||||
const [descriptionBinary, setDescriptionBinary] = useState<Uint8Array | null>(null);
|
||||
// update description
|
||||
const resolveConflictsAndUpdateDescription = useCallback(
|
||||
async (encodedDescription: string, editorRef: EditorRefApi | null) => {
|
||||
if (!updateDescription) return;
|
||||
try {
|
||||
const conflictFreeEncodedDescription = await updateDescription(encodedDescription);
|
||||
const decodedDescription = conflictFreeEncodedDescription
|
||||
? new Uint8Array(conflictFreeEncodedDescription)
|
||||
: new Uint8Array();
|
||||
editorRef?.setProviderDocument(decodedDescription);
|
||||
} catch (error) {
|
||||
console.error("Error while updating description", error);
|
||||
}
|
||||
},
|
||||
[updateDescription]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (descriptionBinary) return;
|
||||
if (savedDescriptionBinary) {
|
||||
const savedDescriptionBuffer = convertBase64StringToBinaryData(savedDescriptionBinary);
|
||||
const decodedSavedDescription = savedDescriptionBuffer
|
||||
? new Uint8Array(savedDescriptionBuffer)
|
||||
: new Uint8Array();
|
||||
setDescriptionBinary(decodedSavedDescription);
|
||||
} else {
|
||||
const decodedDescriptionHTML = getBinaryDataFromRichTextEditorHTMLString(descriptionHTML ?? "<p></p>");
|
||||
setDescriptionBinary(decodedDescriptionHTML);
|
||||
}
|
||||
}, [descriptionBinary, descriptionHTML, savedDescriptionBinary]);
|
||||
|
||||
return {
|
||||
descriptionBinary,
|
||||
resolveConflictsAndUpdateDescription,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useEffect } from "react";
|
||||
// plane editor
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
import { convertBinaryDataToBase64String, EditorRefApi } from "@plane/editor";
|
||||
// plane types
|
||||
import { TDocumentPayload } from "@plane/types";
|
||||
// hooks
|
||||
|
|
@ -8,7 +8,7 @@ import useAutoSave from "@/hooks/use-auto-save";
|
|||
|
||||
type TArgs = {
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
fetchPageDescription: () => Promise<any>;
|
||||
fetchPageDescription: () => Promise<ArrayBuffer>;
|
||||
hasConnectionFailed: boolean;
|
||||
updatePageDescription: (data: TDocumentPayload) => Promise<void>;
|
||||
};
|
||||
|
|
@ -29,7 +29,7 @@ export const usePageFallback = (args: TArgs) => {
|
|||
editor.setProviderDocument(latestDecodedDescription);
|
||||
const { binary, html, json } = editor.getDocument();
|
||||
if (!binary || !json) return;
|
||||
const encodedBinary = Buffer.from(binary).toString("base64");
|
||||
const encodedBinary = convertBinaryDataToBase64String(binary);
|
||||
|
||||
await updatePageDescription({
|
||||
description_binary: encodedBinary,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// types
|
||||
import type { TInboxIssue, TIssue, TInboxIssueWithPagination, TInboxForm } from "@plane/types";
|
||||
import type { TInboxIssue, TIssue, TInboxIssueWithPagination, TInboxForm, TDocumentPayload } from "@plane/types";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { APIService } from "@/services/api.service";
|
||||
// helpers
|
||||
|
|
@ -76,6 +76,25 @@ export class InboxIssueService extends APIService {
|
|||
});
|
||||
}
|
||||
|
||||
async updateDescriptionBinary(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxIssueId: string,
|
||||
data: Pick<TDocumentPayload, "description_binary">
|
||||
): Promise<ArrayBuffer> {
|
||||
return this.post(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inbox-issues/${inboxIssueId}/description/`,
|
||||
data,
|
||||
{
|
||||
responseType: "arraybuffer",
|
||||
}
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async retrievePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxForm> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/intake-settings/`)
|
||||
.then((response) => response?.data)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import isEmpty from "lodash/isEmpty";
|
|||
import type {
|
||||
IIssueDisplayProperties,
|
||||
TBulkOperationsPayload,
|
||||
TDocumentPayload,
|
||||
TIssue,
|
||||
TIssueActivity,
|
||||
TIssueLink,
|
||||
|
|
@ -388,4 +389,19 @@ export class IssueService extends APIService {
|
|||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateDescriptionBinary(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: Pick<TDocumentPayload, "description_binary">
|
||||
): Promise<ArrayBuffer> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/description/`, data, {
|
||||
responseType: "arraybuffer",
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,10 @@ import { TDocumentPayload, TPage } from "@plane/types";
|
|||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
import { FileUploadService } from "@/services/file-upload.service";
|
||||
|
||||
export class ProjectPageService extends APIService {
|
||||
private fileUploadService: FileUploadService;
|
||||
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
// upload service
|
||||
this.fileUploadService = new FileUploadService();
|
||||
}
|
||||
|
||||
async fetchAll(workspaceSlug: string, projectId: string): Promise<TPage[]> {
|
||||
|
|
@ -133,7 +128,7 @@ export class ProjectPageService extends APIService {
|
|||
});
|
||||
}
|
||||
|
||||
async fetchDescriptionBinary(workspaceSlug: string, projectId: string, pageId: string): Promise<any> {
|
||||
async fetchDescriptionBinary(workspaceSlug: string, projectId: string, pageId: string): Promise<ArrayBuffer> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, {
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export interface IInboxIssueStore {
|
|||
updateInboxIssueDuplicateTo: (issueId: string) => Promise<void>; // connecting the inbox issue to the project existing issue
|
||||
updateInboxIssueSnoozeTill: (date: Date | undefined) => Promise<void>; // snooze the issue
|
||||
updateIssue: (issue: Partial<TIssue>) => Promise<void>; // updating the issue
|
||||
updateIssueDescription: (descriptionBinary: string) => Promise<ArrayBuffer>; // updating the local issue description
|
||||
updateProjectIssue: (issue: Partial<TIssue>) => Promise<void>; // updating the issue
|
||||
fetchIssueActivity: () => Promise<void>; // fetching the issue activity
|
||||
}
|
||||
|
|
@ -78,6 +79,7 @@ export class InboxIssueStore implements IInboxIssueStore {
|
|||
updateInboxIssueDuplicateTo: action,
|
||||
updateInboxIssueSnoozeTill: action,
|
||||
updateIssue: action,
|
||||
updateIssueDescription: action,
|
||||
updateProjectIssue: action,
|
||||
fetchIssueActivity: action,
|
||||
});
|
||||
|
|
@ -175,6 +177,26 @@ export class InboxIssueStore implements IInboxIssueStore {
|
|||
}
|
||||
};
|
||||
|
||||
updateIssueDescription = async (descriptionBinary: string): Promise<ArrayBuffer> => {
|
||||
try {
|
||||
if (!this.issue.id) throw new Error("Issue id is missing");
|
||||
const res = await this.inboxIssueService.updateDescriptionBinary(
|
||||
this.workspaceSlug,
|
||||
this.projectId,
|
||||
this.issue.id,
|
||||
{
|
||||
description_binary: descriptionBinary,
|
||||
}
|
||||
);
|
||||
set(this.issue, "description_binary", descriptionBinary);
|
||||
// fetching activity
|
||||
this.fetchIssueActivity();
|
||||
return res;
|
||||
} catch {
|
||||
throw new Error("Failed to update local issue description");
|
||||
}
|
||||
};
|
||||
|
||||
updateProjectIssue = async (issue: Partial<TIssue>) => {
|
||||
const inboxIssue = clone(this.issue);
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { persistence } from "@/local-db/storage.sqlite";
|
|||
// services
|
||||
import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue";
|
||||
// types
|
||||
import { IIssueRootStore } from "../root.store";
|
||||
import { IIssueDetail } from "./root.store";
|
||||
|
||||
export interface IIssueStoreActions {
|
||||
|
|
@ -15,9 +16,15 @@ export interface IIssueStoreActions {
|
|||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
issueStatus?: "DEFAULT" | "DRAFT",
|
||||
issueStatus?: "DEFAULT" | "DRAFT"
|
||||
) => Promise<TIssue>;
|
||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
updateIssueDescription: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
descriptionBinary: string
|
||||
) => Promise<ArrayBuffer>;
|
||||
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
addCycleToIssue: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||
|
|
@ -44,19 +51,21 @@ export class IssueStore implements IIssueStore {
|
|||
fetchingIssueDetails: string | undefined = undefined;
|
||||
localDBIssueDescription: string | undefined = undefined;
|
||||
// root store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
rootIssueDetailStore: IIssueDetail;
|
||||
// services
|
||||
issueService;
|
||||
issueArchiveService;
|
||||
issueDraftService;
|
||||
|
||||
constructor(rootStore: IIssueDetail) {
|
||||
constructor(rootStore: IIssueRootStore, rootIssueDetailStore: IIssueDetail) {
|
||||
makeObservable(this, {
|
||||
fetchingIssueDetails: observable.ref,
|
||||
localDBIssueDescription: observable.ref,
|
||||
});
|
||||
// root store
|
||||
this.rootIssueDetailStore = rootStore;
|
||||
this.rootIssueStore = rootStore;
|
||||
this.rootIssueDetailStore = rootIssueDetailStore;
|
||||
// services
|
||||
this.issueService = new IssueService();
|
||||
this.issueArchiveService = new IssueArchiveService();
|
||||
|
|
@ -156,6 +165,7 @@ export class IssueStore implements IIssueStore {
|
|||
id: issue?.id,
|
||||
sequence_id: issue?.sequence_id,
|
||||
name: issue?.name,
|
||||
description_binary: issue?.description_binary,
|
||||
description_html: issue?.description_html,
|
||||
sort_order: issue?.sort_order,
|
||||
state_id: issue?.state_id,
|
||||
|
|
@ -194,6 +204,20 @@ export class IssueStore implements IIssueStore {
|
|||
await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
};
|
||||
|
||||
updateIssueDescription = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
descriptionBinary: string
|
||||
): Promise<ArrayBuffer> => {
|
||||
const res = await this.issueService.updateDescriptionBinary(workspaceSlug, projectId, issueId, {
|
||||
description_binary: descriptionBinary,
|
||||
});
|
||||
this.rootIssueStore.issues.updateIssue(issueId, { description_binary: descriptionBinary });
|
||||
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
|
||||
return res;
|
||||
};
|
||||
|
||||
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
|
||||
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ export class IssueDetail implements IIssueDetail {
|
|||
|
||||
// store
|
||||
this.rootIssueStore = rootStore;
|
||||
this.issue = new IssueStore(this);
|
||||
this.issue = new IssueStore(rootStore, this);
|
||||
this.reaction = new IssueReactionStore(this);
|
||||
this.attachment = new IssueAttachmentStore(rootStore);
|
||||
this.activity = new IssueActivityStore(rootStore.rootStore as RootStore);
|
||||
|
|
@ -257,6 +257,12 @@ export class IssueDetail implements IIssueDetail {
|
|||
) => this.issue.fetchIssue(workspaceSlug, projectId, issueId, issueStatus);
|
||||
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) =>
|
||||
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
updateIssueDescription = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
descriptionBinary: string
|
||||
) => this.issue.updateIssueDescription(workspaceSlug, projectId, issueId, descriptionBinary);
|
||||
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.issue.removeIssue(workspaceSlug, projectId, issueId);
|
||||
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue