Revert "[WEB-1435] dev: conflict free issue descriptions (#5912)" (#6000)

This reverts commit e9680cab74.
This commit is contained in:
Aaryan Khandelwal 2024-11-15 17:13:31 +05:30 committed by GitHub
parent e9680cab74
commit 9408e92e44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 361 additions and 1469 deletions

View file

@ -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";
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
// services
import { AIService } from "@/services/ai.service";

View file

@ -1,63 +0,0 @@
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";

View file

@ -1,63 +0,0 @@
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";

View file

@ -1,4 +1,2 @@
export * from "./collaborative-editor";
export * from "./collaborative-read-only-editor";
export * from "./editor";
export * from "./read-only-editor";
export * from "./rich-text-editor";
export * from "./rich-text-read-only-editor";

View file

@ -3,8 +3,10 @@
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 { TOAST_TYPE, setToast } from "@plane/ui";
import { Loader, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { InboxIssueContentProperties } from "@/components/inbox/content";
import {
@ -20,12 +22,11 @@ import { ISSUE_ARCHIVED, ISSUE_DELETED } from "@/constants/event-tracker";
// helpers
import { getTextContent } from "@/helpers/editor.helper";
// hooks
import { useEventTracker, useIssueDetail, useProject, useUser } from "@/hooks/store";
import { useEventTracker, useIssueDetail, useProject, useProjectInbox, 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 = {
@ -44,6 +45,7 @@ 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();
@ -58,7 +60,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
}
}, [isSubmitting, setShowAlert, setIsSubmitting]);
// derived values
// dervied values
const issue = inboxIssue.issue;
const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined;
@ -73,8 +75,12 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
const issueOperations: TIssueOperations = useMemo(
() => ({
fetch: async () => {},
remove: async (_workspaceSlug, _projectId, _issueId) => {
// 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) => {
try {
await removeIssue(workspaceSlug, projectId, _issueId);
setToast({
@ -101,7 +107,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
});
}
},
update: async (_workspaceSlug, _projectId, _issueId, data) => {
update: async (_workspaceSlug: string, _projectId: string, _issueId: string, data: Partial<TIssue>) => {
try {
await inboxIssue.updateIssue(data);
captureIssueEvent({
@ -113,7 +119,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
},
path: pathname,
});
} catch {
} catch (error) {
setToast({
title: "Issue update failed",
type: TOAST_TYPE.ERROR,
@ -130,14 +136,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
});
}
},
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) => {
archive: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
await archiveIssue(workspaceSlug, projectId, issueId);
captureIssueEvent({
@ -155,7 +154,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
}
},
}),
[archiveIssue, captureIssueEvent, inboxIssue, pathname, projectId, removeIssue, workspaceSlug]
[inboxIssue]
);
if (!issue?.project_id || !issue?.id) return <></>;
@ -185,20 +184,21 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
containerClassName="-ml-3"
/>
{issue.description_binary !== undefined && (
{loader === "issue-loading" ? (
<Loader className="min-h-[6rem] rounded-md border border-custom-border-200">
<Loader.Item width="100%" height="140px" />
</Loader>
) : (
<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, projectId, issue.id ?? "", data)
}
issueId={issue.id}
projectId={issue.project_id}
setIsSubmitting={(value) => setIsSubmitting(value)}
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)}
containerClassName="-ml-3 border-none"
/>
)}

View file

@ -10,7 +10,7 @@ import { EFileAssetType } from "@plane/types/src/enums";
// ui
import { Loader } from "@plane/ui";
// components
import { RichTextEditor } from "@/components/editor";
import { RichTextEditor } from "@/components/editor/rich-text-editor/rich-text-editor";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers

View file

@ -1,144 +1,157 @@
"use client";
import { FC, useCallback, useRef } from "react";
import { FC, useCallback, useEffect, useState } from "react";
import debounce from "lodash/debounce";
import { observer } from "mobx-react";
// plane editor
import { convertBinaryDataToBase64String, EditorRefApi } from "@plane/editor";
import { Controller, useForm } from "react-hook-form";
// types
import { TIssue } from "@plane/types";
import { EFileAssetType } from "@plane/types/src/enums";
// plane ui
// ui
import { Loader } from "@plane/ui";
// components
import { CollaborativeRichTextEditor, CollaborativeRichTextReadOnlyEditor } from "@/components/editor";
import { RichTextEditor, RichTextReadOnlyEditor } from "@/components/editor";
import { TIssueOperations } from "@/components/issues/issue-detail";
// 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;
projectId: string;
issueId: string;
initialValue: string | undefined;
disabled?: boolean;
issueOperations: TIssueOperations;
placeholder?: string | ((isFocused: boolean, value: string) => string);
setIsSubmitting: (initialValue: "submitting" | "submitted" | "saved") => void;
swrIssueDescription?: string | null | undefined;
};
export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((props) => {
const {
containerClassName,
descriptionBinary: savedDescriptionBinary,
descriptionHTML,
disabled,
issueId,
placeholder,
projectId,
setIsSubmitting,
updateDescription,
workspaceSlug,
projectId,
issueId,
disabled,
swrIssueDescription,
initialValue,
issueOperations,
setIsSubmitting,
placeholder,
} = 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 { handleSubmit, reset, control } = useForm<TIssue>({
defaultValues: {
description_html: initialValue,
},
});
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),
[]
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]
);
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>
);
const { getWorkspaceBySlug } = useWorkspace();
// computed values
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id as string;
// 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"));
}, 1500),
[handleSubmit, issueId]
);
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}
{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}
/>
)
}
/>
) : (
<CollaborativeRichTextReadOnlyEditor
containerClassName={containerClassName}
descriptionBinary={savedDescriptionBinary}
descriptionHTML={descriptionHTML}
id={issueId}
projectId={projectId}
workspaceSlug={workspaceSlug}
/>
<Loader>
<Loader.Item height="150px" />
</Loader>
)}
</>
);

View file

@ -22,7 +22,6 @@ 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";
@ -114,22 +113,16 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
containerClassName="-ml-3"
/>
{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}
/>
)}
<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"
/>
{currentUser && (
<IssueReaction

View file

@ -26,12 +26,6 @@ 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>;
@ -70,7 +64,6 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
issue: { getIssueById },
fetchIssue,
updateIssue,
updateIssueDescription,
removeIssue,
archiveIssue,
addCycleToIssue,
@ -125,13 +118,6 @@ 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);
@ -331,7 +317,6 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
is_archived,
fetchIssue,
updateIssue,
updateIssueDescription,
removeIssue,
archiveIssue,
removeArchivedIssue,

View file

@ -12,9 +12,8 @@ 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";
// plane web hooks
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
// local components
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
import { IssueDescriptionInput } from "../description-input";
import { IssueReaction } from "../issue-detail/reactions";
import { IssueTitleInput } from "../title-input";
@ -64,6 +63,13 @@ 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 && (
@ -99,22 +105,16 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
containerClassName="-ml-3"
/>
{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}
/>
)}
<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"
/>
{currentUser && (
<IssueReaction

View file

@ -39,7 +39,6 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
setPeekIssue,
issue: { fetchIssue, getIsFetchingIssueDetails },
fetchActivities,
updateIssueDescription,
} = useIssueDetail();
const { issues } = useIssuesStore();
@ -93,16 +92,6 @@ 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(() => {
@ -329,17 +318,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
}
},
}),
[
fetchIssue,
is_draft,
issues,
fetchActivities,
captureIssueEvent,
pathname,
removeRoutePeekId,
restoreIssue,
updateIssueDescription,
]
[fetchIssue, is_draft, issues, fetchActivities, captureIssueEvent, pathname, removeRoutePeekId, restoreIssue]
);
useEffect(() => {

View file

@ -50,9 +50,7 @@ export const PageRoot = observer((props: TPageRootProps) => {
usePageFallback({
editorRef,
fetchPageDescription: async () => {
if (!page.id) {
throw new Error("Required fields missing while fetching binary description");
}
if (!page.id) return;
return await projectPageService.fetchDescriptionBinary(workspaceSlug, projectId, page.id);
},
hasConnectionFailed,

View file

@ -8,7 +8,7 @@ import { IUserActivityResponse } from "@plane/types";
// components
import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core";
// editor
import { RichTextReadOnlyEditor } from "@/components/editor";
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
// ui
import { ActivitySettingsLoader } from "@/components/ui";
// helpers

View file

@ -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";
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
import { ActivitySettingsLoader } from "@/components/ui";
// constants
import { USER_ACTIVITY } from "@/constants/fetch-keys";

View file

@ -1,54 +0,0 @@
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,
};
};

View file

@ -1,6 +1,6 @@
import { useCallback, useEffect } from "react";
// plane editor
import { convertBinaryDataToBase64String, EditorRefApi } from "@plane/editor";
import { 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<ArrayBuffer>;
fetchPageDescription: () => Promise<any>;
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 = convertBinaryDataToBase64String(binary);
const encodedBinary = Buffer.from(binary).toString("base64");
await updatePageDescription({
description_binary: encodedBinary,

View file

@ -1,5 +1,5 @@
// types
import type { TInboxIssue, TIssue, TInboxIssueWithPagination, TInboxForm, TDocumentPayload } from "@plane/types";
import type { TInboxIssue, TIssue, TInboxIssueWithPagination, TInboxForm } from "@plane/types";
import { API_BASE_URL } from "@/helpers/common.helper";
import { APIService } from "@/services/api.service";
// helpers
@ -76,25 +76,6 @@ 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)

View file

@ -4,7 +4,6 @@ import isEmpty from "lodash/isEmpty";
import type {
IIssueDisplayProperties,
TBulkOperationsPayload,
TDocumentPayload,
TIssue,
TIssueActivity,
TIssueLink,
@ -389,19 +388,4 @@ 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;
});
}
}

View file

@ -4,10 +4,15 @@ 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[]> {
@ -128,7 +133,7 @@ export class ProjectPageService extends APIService {
});
}
async fetchDescriptionBinary(workspaceSlug: string, projectId: string, pageId: string): Promise<ArrayBuffer> {
async fetchDescriptionBinary(workspaceSlug: string, projectId: string, pageId: string): Promise<any> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, {
headers: {
"Content-Type": "application/octet-stream",

View file

@ -26,7 +26,6 @@ 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
}
@ -79,7 +78,6 @@ export class InboxIssueStore implements IInboxIssueStore {
updateInboxIssueDuplicateTo: action,
updateInboxIssueSnoozeTill: action,
updateIssue: action,
updateIssueDescription: action,
updateProjectIssue: action,
fetchIssueActivity: action,
});
@ -177,26 +175,6 @@ 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 {

View file

@ -7,7 +7,6 @@ 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 {
@ -16,15 +15,9 @@ 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>;
@ -51,21 +44,19 @@ 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: IIssueRootStore, rootIssueDetailStore: IIssueDetail) {
constructor(rootStore: IIssueDetail) {
makeObservable(this, {
fetchingIssueDetails: observable.ref,
localDBIssueDescription: observable.ref,
});
// root store
this.rootIssueStore = rootStore;
this.rootIssueDetailStore = rootIssueDetailStore;
this.rootIssueDetailStore = rootStore;
// services
this.issueService = new IssueService();
this.issueArchiveService = new IssueArchiveService();
@ -165,7 +156,6 @@ 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,
@ -204,20 +194,6 @@ 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);

View file

@ -192,7 +192,7 @@ export class IssueDetail implements IIssueDetail {
// store
this.rootIssueStore = rootStore;
this.issue = new IssueStore(rootStore, this);
this.issue = new IssueStore(this);
this.reaction = new IssueReactionStore(this);
this.attachment = new IssueAttachmentStore(rootStore);
this.activity = new IssueActivityStore(rootStore.rootStore as RootStore);
@ -257,12 +257,6 @@ 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) =>