[PE-242, 243] refactor: editor file handling, image upload status (#6442)
* refactor: editor file handling * refactor: asset store * refactor: space app file handlers * fix: separate webhook connection params * chore: handle undefined status * chore: add type to upload status * chore: added transition for upload status update
This commit is contained in:
parent
b7198234de
commit
214692f5b2
53 changed files with 602 additions and 315 deletions
|
|
@ -6,12 +6,15 @@ import { Controller, useForm } from "react-hook-form"; // services
|
|||
import { usePopper } from "react-popper";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// plane editor
|
||||
import { EditorReadOnlyRefApi } from "@plane/editor";
|
||||
// ui
|
||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
|
||||
// services
|
||||
import { AIService } from "@/services/ai.service";
|
||||
const aiService = new AIService();
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
|
|
@ -22,6 +25,7 @@ type Props = {
|
|||
prompt?: string;
|
||||
button: JSX.Element;
|
||||
className?: string;
|
||||
workspaceId: string;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
|
@ -31,8 +35,6 @@ type FormData = {
|
|||
task: string;
|
||||
};
|
||||
|
||||
const aiService = new AIService();
|
||||
|
||||
export const GptAssistantPopover: React.FC<Props> = (props) => {
|
||||
const {
|
||||
isOpen,
|
||||
|
|
@ -43,6 +45,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
|||
prompt,
|
||||
button,
|
||||
className = "",
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
} = props;
|
||||
|
|
@ -51,7 +54,8 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
|||
const [invalidResponse, setInvalidResponse] = useState(false);
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
const editorRef = useRef<any>(null);
|
||||
// refs
|
||||
const editorRef = useRef<EditorReadOnlyRefApi>(null);
|
||||
const responseRef = useRef<any>(null);
|
||||
// popper
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
|
|
@ -218,6 +222,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
|||
initialValue={prompt}
|
||||
containerClassName="-m-3"
|
||||
ref={editorRef}
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
|
@ -230,6 +235,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
|||
id="ai-assistant-response"
|
||||
initialValue={`<p>${response}</p>`}
|
||||
ref={responseRef}
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,20 +2,18 @@ import React, { useState } from "react";
|
|||
// plane constants
|
||||
import { EIssueCommentAccessSpecifier } from "@plane/constants";
|
||||
// plane editor
|
||||
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor";
|
||||
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
import { isCommentEmpty } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useEditorMention } from "@/hooks/use-editor-mention";
|
||||
import { useEditorConfig, useEditorMention } from "@/hooks/editor";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
|
@ -31,7 +29,7 @@ interface LiteTextEditorWrapperProps
|
|||
showSubmitButton?: boolean;
|
||||
isSubmitting?: boolean;
|
||||
showToolbarInitially?: boolean;
|
||||
uploadFile: (file: File) => Promise<string>;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
issue_id?: string;
|
||||
}
|
||||
|
||||
|
|
@ -66,8 +64,8 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
|||
issue_id: issue_id,
|
||||
}),
|
||||
});
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
// editor config
|
||||
const { getEditorFileHandlers } = useEditorConfig();
|
||||
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
|
||||
return !!ref && typeof ref === "object" && "current" in ref;
|
||||
}
|
||||
|
|
@ -85,7 +83,6 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
|||
ref={ref}
|
||||
disabledExtensions={disabledExtensions}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
maxFileSize,
|
||||
projectId,
|
||||
uploadFile,
|
||||
workspaceId,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditor, LiteTextReadOnlyEditorWi
|
|||
import { EditorMentionsRoot } from "@/components/editor";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useEditorConfig } from "@/hooks/editor";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
|
||||
|
|
@ -13,14 +14,17 @@ type LiteTextReadOnlyEditorWrapperProps = Omit<
|
|||
ILiteTextReadOnlyEditor,
|
||||
"disabledExtensions" | "fileHandler" | "mentionHandler"
|
||||
> & {
|
||||
workspaceId: string;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
|
||||
({ workspaceSlug, projectId, ...props }, ref) => {
|
||||
({ workspaceId, workspaceSlug, projectId, ...props }, ref) => {
|
||||
// editor flaggings
|
||||
const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
|
||||
// editor config
|
||||
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
|
||||
|
||||
return (
|
||||
<LiteTextReadOnlyEditorWithRef
|
||||
|
|
@ -28,6 +32,7 @@ export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Lit
|
|||
disabledExtensions={disabledExtensions}
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
projectId,
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
})}
|
||||
mentionHandler={{
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
import React, { forwardRef } from "react";
|
||||
// editor
|
||||
import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef } from "@plane/editor";
|
||||
import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef, TFileHandler } from "@plane/editor";
|
||||
// plane types
|
||||
import { TSearchEntityRequestPayload, TSearchResponse } from "@plane/types";
|
||||
// components
|
||||
import { EditorMentionsRoot } from "@/components/editor";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useEditorMention } from "@/hooks/use-editor-mention";
|
||||
import { useEditorConfig, useEditorMention } from "@/hooks/editor";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
|
||||
interface RichTextEditorWrapperProps
|
||||
extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
|
||||
|
|
@ -20,7 +18,7 @@ interface RichTextEditorWrapperProps
|
|||
workspaceSlug: string;
|
||||
workspaceId: string;
|
||||
projectId?: string;
|
||||
uploadFile: (file: File) => Promise<string>;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
}
|
||||
|
||||
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
|
||||
|
|
@ -32,15 +30,14 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
|||
const { fetchMentions } = useEditorMention({
|
||||
searchEntity: async (payload) => await searchMentionCallback(payload),
|
||||
});
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
// editor config
|
||||
const { getEditorFileHandlers } = useEditorConfig();
|
||||
|
||||
return (
|
||||
<RichTextEditorWithRef
|
||||
ref={ref}
|
||||
disabledExtensions={disabledExtensions}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
maxFileSize,
|
||||
projectId,
|
||||
uploadFile,
|
||||
workspaceId,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor, RichTextReadOnlyEditorWi
|
|||
import { EditorMentionsRoot } from "@/components/editor";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useEditorConfig } from "@/hooks/editor";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
|
||||
|
|
@ -13,14 +14,17 @@ type RichTextReadOnlyEditorWrapperProps = Omit<
|
|||
IRichTextReadOnlyEditor,
|
||||
"disabledExtensions" | "fileHandler" | "mentionHandler"
|
||||
> & {
|
||||
workspaceId: string;
|
||||
workspaceSlug: string;
|
||||
projectId?: string;
|
||||
};
|
||||
|
||||
export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
|
||||
({ workspaceSlug, projectId, ...props }, ref) => {
|
||||
({ workspaceId, workspaceSlug, projectId, ...props }, ref) => {
|
||||
// editor flaggings
|
||||
const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
|
||||
// editor config
|
||||
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
|
||||
|
||||
return (
|
||||
<RichTextReadOnlyEditorWithRef
|
||||
|
|
@ -28,6 +32,7 @@ export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Ric
|
|||
disabledExtensions={disabledExtensions}
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
projectId,
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
})}
|
||||
mentionHandler={{
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ import React, { useState } from "react";
|
|||
// plane constants
|
||||
import { EIssueCommentAccessSpecifier } from "@plane/constants";
|
||||
// plane editor
|
||||
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor";
|
||||
// plane types
|
||||
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
|
||||
// components
|
||||
import { TSticky } from "@plane/types";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useEditorConfig } from "@/hooks/editor";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
import { StickyEditorToolbar } from "./toolbar";
|
||||
|
||||
interface StickyEditorWrapperProps
|
||||
|
|
@ -25,7 +25,7 @@ interface StickyEditorWrapperProps
|
|||
isSubmitting?: boolean;
|
||||
showToolbarInitially?: boolean;
|
||||
showToolbar?: boolean;
|
||||
uploadFile: (file: File) => Promise<string>;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
parentClassName?: string;
|
||||
handleColorChange: (data: Partial<TSticky>) => Promise<void>;
|
||||
handleDelete: () => void;
|
||||
|
|
@ -49,8 +49,8 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
|
|||
const [isFocused, setIsFocused] = useState(showToolbarInitially);
|
||||
// editor flaggings
|
||||
const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
// editor config
|
||||
const { getEditorFileHandlers } = useEditorConfig();
|
||||
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
|
||||
return !!ref && typeof ref === "object" && "current" in ref;
|
||||
}
|
||||
|
|
@ -67,7 +67,6 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
|
|||
ref={ref}
|
||||
disabledExtensions={[...disabledExtensions, "enter-key"]}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
maxFileSize,
|
||||
projectId,
|
||||
uploadFile,
|
||||
workspaceId,
|
||||
|
|
|
|||
|
|
@ -19,13 +19,10 @@ import { RichTextEditor } from "@/components/editor/rich-text-editor/rich-text-e
|
|||
import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper";
|
||||
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
||||
// hooks
|
||||
import { useProjectInbox } from "@/hooks/store";
|
||||
import { useEditorAsset, useProjectInbox } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
const fileService = new FileService();
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
type TInboxIssueDescription = {
|
||||
|
|
@ -53,11 +50,10 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
|
|||
onEnterKeyPress,
|
||||
onAssetUpload,
|
||||
} = props;
|
||||
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
|
||||
// hooks
|
||||
// store hooks
|
||||
const { uploadEditorAsset } = useEditorAsset();
|
||||
const { loader } = useProjectInbox();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
|
|
@ -90,17 +86,18 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
|
|||
containerClassName={containerClassName}
|
||||
onEnterKeyPress={onEnterKeyPress}
|
||||
tabIndex={getIndex("description_html")}
|
||||
uploadFile={async (file) => {
|
||||
uploadFile={async (blockId, file) => {
|
||||
try {
|
||||
const { asset_id } = await fileService.uploadProjectAsset(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
{
|
||||
const { asset_id } = await uploadEditorAsset({
|
||||
blockId,
|
||||
data: {
|
||||
entity_identifier: data.id ?? "",
|
||||
entity_type: EFileAssetType.ISSUE_DESCRIPTION,
|
||||
},
|
||||
file
|
||||
);
|
||||
file,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
onAssetUpload?.(asset_id);
|
||||
return asset_id;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -17,13 +17,10 @@ import { TIssueOperations } from "@/components/issues/issue-detail";
|
|||
// helpers
|
||||
import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
import { useEditorAsset, useWorkspace } from "@/hooks/store";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
const workspaceService = new WorkspaceService();
|
||||
const fileService = new FileService();
|
||||
|
||||
export type IssueDescriptionInputProps = {
|
||||
containerClassName?: string;
|
||||
|
|
@ -51,6 +48,14 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
|||
setIsSubmitting,
|
||||
placeholder,
|
||||
} = props;
|
||||
// states
|
||||
const [localIssueDescription, setLocalIssueDescription] = useState({
|
||||
id: issueId,
|
||||
description_html: initialValue,
|
||||
});
|
||||
// store hooks
|
||||
const { uploadEditorAsset } = useEditorAsset();
|
||||
// form info
|
||||
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -61,11 +66,6 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
|||
},
|
||||
});
|
||||
|
||||
const [localIssueDescription, setLocalIssueDescription] = useState({
|
||||
id: issueId,
|
||||
description_html: initialValue,
|
||||
});
|
||||
|
||||
const handleDescriptionFormSubmit = useCallback(
|
||||
async (formData: Partial<TIssue>) => {
|
||||
await issueOperations.update(workspaceSlug, projectId, issueId, {
|
||||
|
|
@ -136,17 +136,18 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
|||
})
|
||||
}
|
||||
containerClassName={containerClassName}
|
||||
uploadFile={async (file) => {
|
||||
uploadFile={async (blockId, file) => {
|
||||
try {
|
||||
const { asset_id } = await fileService.uploadProjectAsset(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
{
|
||||
const { asset_id } = await uploadEditorAsset({
|
||||
blockId,
|
||||
data: {
|
||||
entity_identifier: issueId,
|
||||
entity_type: EFileAssetType.ISSUE_DESCRIPTION,
|
||||
},
|
||||
file
|
||||
);
|
||||
file,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
return asset_id;
|
||||
} catch (error) {
|
||||
console.log("Error in uploading work item asset:", error);
|
||||
|
|
@ -159,6 +160,7 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
|||
id={issueId}
|
||||
initialValue={localIssueDescription.description_html ?? ""}
|
||||
containerClassName={containerClassName}
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -170,8 +170,8 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
|||
}
|
||||
}}
|
||||
showSubmitButton={false}
|
||||
uploadFile={async (file) => {
|
||||
const { asset_id } = await activityOperations.uploadCommentAsset(file, comment.id);
|
||||
uploadFile={async (blockId, file) => {
|
||||
const { asset_id } = await activityOperations.uploadCommentAsset(blockId, file, comment.id);
|
||||
return asset_id;
|
||||
}}
|
||||
/>
|
||||
|
|
@ -215,6 +215,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
|||
ref={showEditorRef}
|
||||
id={comment.id}
|
||||
initialValue={comment.comment_html ?? ""}
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -110,8 +110,8 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
|||
handleAccessChange={onAccessChange}
|
||||
showAccessSpecifier={showAccessSpecifier}
|
||||
isSubmitting={isSubmitting}
|
||||
uploadFile={async (file) => {
|
||||
const { asset_id } = await activityOperations.uploadCommentAsset(file);
|
||||
uploadFile={async (blockId, file) => {
|
||||
const { asset_id } = await activityOperations.uploadCommentAsset(blockId, file);
|
||||
setUploadedAssetIds((prev) => [...prev, asset_id]);
|
||||
return asset_id;
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { FC, useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane package imports
|
||||
import { E_SORT_ORDER, TActivityFilters, defaultActivityFilters,EUserPermissions } from "@plane/constants";
|
||||
import { E_SORT_ORDER, TActivityFilters, defaultActivityFilters, EUserPermissions } from "@plane/constants";
|
||||
import { useLocalStorage } from "@plane/hooks";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
|
|
@ -16,12 +16,9 @@ import { IssueCommentCreate } from "@/components/issues";
|
|||
import { ActivitySortRoot, IssueActivityCommentRoot } from "@/components/issues/issue-detail";
|
||||
// constants
|
||||
// hooks
|
||||
import { useIssueDetail, useProject, useUser, useUserPermissions } from "@/hooks/store";
|
||||
import { useEditorAsset, useIssueDetail, useProject, useUser, useUserPermissions } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { ActivityFilterRoot, IssueActivityWorklogCreateButton } from "@/plane-web/components/issues/worklog";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
const fileService = new FileService();
|
||||
|
||||
type TIssueActivity = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -35,7 +32,7 @@ export type TActivityOperations = {
|
|||
createComment: (data: Partial<TIssueComment>) => Promise<TIssueComment>;
|
||||
updateComment: (commentId: string, data: Partial<TIssueComment>) => Promise<void>;
|
||||
removeComment: (commentId: string) => Promise<void>;
|
||||
uploadCommentAsset: (file: File, commentId?: string) => Promise<TFileSignedURLResponse>;
|
||||
uploadCommentAsset: (blockId: string, file: File, commentId?: string) => Promise<TFileSignedURLResponse>;
|
||||
};
|
||||
|
||||
export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
|
|
@ -48,6 +45,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||
defaultActivityFilters
|
||||
);
|
||||
const { setValue: setSortOrder, storedValue: sortOrder } = useLocalStorage("activity_sort_order", E_SORT_ORDER.ASC);
|
||||
// store hooks
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
createComment,
|
||||
|
|
@ -57,7 +55,8 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||
const { projectPermissionsByWorkspaceSlugAndProjectId } = useUserPermissions();
|
||||
const { getProjectById } = useProject();
|
||||
const { data: currentUser } = useUser();
|
||||
//derived values
|
||||
const { uploadEditorAsset } = useEditorAsset();
|
||||
// derived values
|
||||
const issue = issueId ? getIssueById(issueId) : undefined;
|
||||
const currentUserProjectRole = projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
|
||||
const isAdmin = (currentUserProjectRole ?? EUserPermissions.GUEST) === EUserPermissions.ADMIN;
|
||||
|
|
@ -94,7 +93,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||
message: t("issue.comments.create.success"),
|
||||
});
|
||||
return comment;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setToast({
|
||||
title: t("common.error.label"),
|
||||
type: TOAST_TYPE.ERROR,
|
||||
|
|
@ -111,7 +110,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||
type: TOAST_TYPE.SUCCESS,
|
||||
message: t("issue.comments.update.success"),
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setToast({
|
||||
title: t("common.error.label"),
|
||||
type: TOAST_TYPE.ERROR,
|
||||
|
|
@ -128,7 +127,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||
type: TOAST_TYPE.SUCCESS,
|
||||
message: t("issue.comments.remove.success"),
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setToast({
|
||||
title: t("common.error.label"),
|
||||
type: TOAST_TYPE.ERROR,
|
||||
|
|
@ -136,18 +135,19 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||
});
|
||||
}
|
||||
},
|
||||
uploadCommentAsset: async (file, commentId) => {
|
||||
uploadCommentAsset: async (blockId, file, commentId) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId) throw new Error("Missing fields");
|
||||
const res = await fileService.uploadProjectAsset(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
{
|
||||
const res = await uploadEditorAsset({
|
||||
blockId,
|
||||
data: {
|
||||
entity_identifier: commentId ?? "",
|
||||
entity_type: EFileAssetType.COMMENT_DESCRIPTION,
|
||||
},
|
||||
file
|
||||
);
|
||||
file,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.log("Error in uploading comment asset:", error);
|
||||
|
|
@ -155,7 +155,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||
}
|
||||
},
|
||||
}),
|
||||
[workspaceSlug, projectId, issueId, createComment, updateComment, removeComment]
|
||||
[workspaceSlug, projectId, issueId, createComment, updateComment, uploadEditorAsset, removeComment]
|
||||
);
|
||||
|
||||
const project = getProjectById(projectId);
|
||||
|
|
|
|||
|
|
@ -22,14 +22,15 @@ import { RichTextEditor } from "@/components/editor";
|
|||
import { getDescriptionPlaceholderI18n } from "@/helpers/issue.helper";
|
||||
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
||||
// hooks
|
||||
import { useInstance, useWorkspace } from "@/hooks/store";
|
||||
import { useEditorAsset, useInstance, useWorkspace } from "@/hooks/store";
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { AIService } from "@/services/ai.service";
|
||||
import { FileService } from "@/services/file.service";
|
||||
const workspaceService = new WorkspaceService();
|
||||
const aiService = new AIService();
|
||||
|
||||
type TIssueDescriptionEditorProps = {
|
||||
control: Control<TIssue>;
|
||||
|
|
@ -50,11 +51,6 @@ type TIssueDescriptionEditorProps = {
|
|||
onClose: () => void;
|
||||
};
|
||||
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
const aiService = new AIService();
|
||||
const fileService = new FileService();
|
||||
|
||||
export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = observer((props) => {
|
||||
const {
|
||||
control,
|
||||
|
|
@ -80,8 +76,10 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
|
|||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||
// store hooks
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString())?.id as string;
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString())?.id ?? "";
|
||||
const { config } = useInstance();
|
||||
const { uploadEditorAsset } = useEditorAsset();
|
||||
// platform
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
|
||||
|
|
@ -202,19 +200,20 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
|
|||
})
|
||||
}
|
||||
containerClassName="pt-3 min-h-[120px]"
|
||||
uploadFile={async (file) => {
|
||||
uploadFile={async (blockId, file) => {
|
||||
try {
|
||||
const { asset_id } = await fileService.uploadProjectAsset(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
{
|
||||
const { asset_id } = await uploadEditorAsset({
|
||||
blockId,
|
||||
data: {
|
||||
entity_identifier: issueId ?? "",
|
||||
entity_type: isDraft
|
||||
? EFileAssetType.DRAFT_ISSUE_DESCRIPTION
|
||||
: EFileAssetType.ISSUE_DESCRIPTION,
|
||||
},
|
||||
file
|
||||
);
|
||||
file,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
});
|
||||
onAssetUpload(asset_id);
|
||||
return asset_id;
|
||||
} catch (error) {
|
||||
|
|
@ -268,6 +267,7 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
|
|||
AI
|
||||
</button>
|
||||
}
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import { PageContentBrowser, PageContentLoader, PageEditorTitle } from "@/compon
|
|||
import { cn, LIVE_BASE_PATH, LIVE_BASE_URL } from "@/helpers/common.helper";
|
||||
import { generateRandomColor } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { useEditorMention } from "@/hooks/use-editor-mention";
|
||||
import { useEditorMention } from "@/hooks/editor";
|
||||
import { useUser, useWorkspace } from "@/hooks/store";
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
// plane web components
|
||||
import { EditorAIMenu } from "@/plane-web/components/pages";
|
||||
|
|
@ -34,7 +34,6 @@ import { TPageInstance } from "@/store/pages/base-page";
|
|||
|
||||
export type TEditorBodyConfig = {
|
||||
fileHandler: TFileHandler;
|
||||
webhookConnectionParams: TWebhookConnectionQueryParams;
|
||||
};
|
||||
|
||||
export type TEditorBodyHandlers = {
|
||||
|
|
@ -50,6 +49,7 @@ type Props = {
|
|||
handlers: TEditorBodyHandlers;
|
||||
page: TPageInstance;
|
||||
sidePeekVisible: boolean;
|
||||
webhookConnectionParams: TWebhookConnectionQueryParams;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
|
|
@ -62,12 +62,15 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||
handlers,
|
||||
page,
|
||||
sidePeekVisible,
|
||||
webhookConnectionParams,
|
||||
workspaceSlug,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const { id: pageId, name: pageTitle, isContentEditable, updateTitle } = page;
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? "";
|
||||
// issue-embed
|
||||
const { issueEmbedProps } = useIssueEmbed({
|
||||
fetchEmbedSuggestions: handlers.fetchEntity,
|
||||
|
|
@ -96,10 +99,11 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||
editorRef={editorRef}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
/>
|
||||
),
|
||||
[editorRef, workspaceSlug]
|
||||
[editorRef, workspaceId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleServerConnect = useCallback(() => {
|
||||
|
|
@ -129,13 +133,13 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||
// Construct realtime config
|
||||
return {
|
||||
url: WS_LIVE_URL.toString(),
|
||||
queryParams: config.webhookConnectionParams,
|
||||
queryParams: webhookConnectionParams,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating realtime config", error);
|
||||
return undefined;
|
||||
}
|
||||
}, [config.webhookConnectionParams]);
|
||||
}, [webhookConnectionParams]);
|
||||
|
||||
const userConfig = useMemo(
|
||||
() => ({
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useSearchParams } from "next/navigation";
|
|||
// editor
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
// types
|
||||
import { TDocumentPayload, TPage, TPageVersion } from "@plane/types";
|
||||
import { TDocumentPayload, TPage, TPageVersion, TWebhookConnectionQueryParams } from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
PageEditorHeaderRoot,
|
||||
|
|
@ -36,11 +36,12 @@ type TPageRootProps = {
|
|||
config: TPageRootConfig;
|
||||
handlers: TPageRootHandlers;
|
||||
page: TPageInstance;
|
||||
webhookConnectionParams: TWebhookConnectionQueryParams;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const PageRoot = observer((props: TPageRootProps) => {
|
||||
const { config, handlers, page, workspaceSlug } = props;
|
||||
const { config, handlers, page, webhookConnectionParams, workspaceSlug } = props;
|
||||
// states
|
||||
const [editorReady, setEditorReady] = useState(false);
|
||||
const [hasConnectionFailed, setHasConnectionFailed] = useState(false);
|
||||
|
|
@ -116,6 +117,7 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
|||
handlers={handlers}
|
||||
page={page}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
webhookConnectionParams={webhookConnectionParams}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import { TPageVersion } from "@plane/types";
|
|||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { EditorMentionsRoot } from "@/components/editor";
|
||||
// helpers
|
||||
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useEditorConfig } from "@/hooks/editor";
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
|
|
@ -27,8 +27,14 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
|
|||
const { activeVersion, currentVersionDescription, isCurrentVersionActive, versionDetails } = props;
|
||||
// params
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const workspaceDetails = getWorkspaceBySlug(workspaceSlug?.toString() ?? "");
|
||||
// editor flaggings
|
||||
const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? "");
|
||||
// editor config
|
||||
const { getReadOnlyEditorFileHandlers } = useEditorConfig();
|
||||
// issue-embed
|
||||
const { issueEmbedProps } = useIssueEmbed({
|
||||
projectId: projectId?.toString() ?? "",
|
||||
|
|
@ -97,6 +103,7 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
|
|||
editorClassName="pl-10"
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
projectId: projectId?.toString() ?? "",
|
||||
workspaceId: workspaceDetails?.id ?? "",
|
||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||
})}
|
||||
mentionHandler={{
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { ActivitySettingsLoader } from "@/components/ui";
|
|||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { useUser, useWorkspace } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
activity: IUserActivityResponse | undefined;
|
||||
|
|
@ -27,6 +27,9 @@ export const ActivityList: React.FC<Props> = observer((props) => {
|
|||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString() ?? "")?.id ?? "";
|
||||
|
||||
// TODO: refactor this component
|
||||
return (
|
||||
|
|
@ -79,6 +82,7 @@ export const ActivityList: React.FC<Props> = observer((props) => {
|
|||
: (activityItem.old_value?.toString() as string)
|
||||
}
|
||||
containerClassName="text-xs bg-custom-background-100"
|
||||
workspaceId={workspaceId}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
projectId={activityItem.project}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -103,7 +103,8 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
|
|||
activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value
|
||||
}
|
||||
containerClassName="text-xs bg-custom-background-100"
|
||||
workspaceSlug={activityItem?.workspace_detail?.slug.toString() ?? ""}
|
||||
workspaceId={activityItem?.workspace_detail?.id?.toString() ?? ""}
|
||||
workspaceSlug={activityItem?.workspace_detail?.slug?.toString() ?? ""}
|
||||
projectId={activityItem.project ?? ""}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
2
web/core/hooks/editor/index.ts
Normal file
2
web/core/hooks/editor/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./use-editor-config";
|
||||
export * from "./use-editor-mention";
|
||||
96
web/core/hooks/editor/use-editor-config.ts
Normal file
96
web/core/hooks/editor/use-editor-config.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { useCallback } from "react";
|
||||
// plane editor
|
||||
import { TFileHandler, TReadOnlyFileHandler } from "@plane/editor";
|
||||
// helpers
|
||||
import { getEditorAssetSrc } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useEditorAsset } from "@/hooks/store";
|
||||
// plane web hooks
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
const fileService = new FileService();
|
||||
|
||||
type TArgs = {
|
||||
projectId?: string;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
workspaceId: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const useEditorConfig = () => {
|
||||
// store hooks
|
||||
const { assetsUploadPercentage } = useEditorAsset();
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
|
||||
const getReadOnlyEditorFileHandlers = useCallback(
|
||||
(args: Pick<TArgs, "projectId" | "workspaceId" | "workspaceSlug">): TReadOnlyFileHandler => {
|
||||
const { projectId, workspaceId, workspaceSlug } = args;
|
||||
|
||||
return {
|
||||
getAssetSrc: async (path) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
return path;
|
||||
} else {
|
||||
return (
|
||||
getEditorAssetSrc({
|
||||
assetId: path,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
}) ?? ""
|
||||
);
|
||||
}
|
||||
},
|
||||
restore: async (src: string) => {
|
||||
if (src?.startsWith("http")) {
|
||||
await fileService.restoreOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.restoreNewAsset(workspaceSlug, src);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getEditorFileHandlers = useCallback(
|
||||
(args: TArgs): TFileHandler => {
|
||||
const { projectId, uploadFile, workspaceId, workspaceSlug } = args;
|
||||
|
||||
return {
|
||||
...getReadOnlyEditorFileHandlers({
|
||||
projectId,
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
}),
|
||||
assetsUploadStatus: assetsUploadPercentage,
|
||||
upload: uploadFile,
|
||||
delete: async (src: string) => {
|
||||
if (src?.startsWith("http")) {
|
||||
await fileService.deleteOldWorkspaceAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.deleteNewAsset(
|
||||
getEditorAssetSrc({
|
||||
assetId: src,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
}) ?? ""
|
||||
);
|
||||
}
|
||||
},
|
||||
cancel: fileService.cancelUpload,
|
||||
validation: {
|
||||
maxFileSize,
|
||||
},
|
||||
};
|
||||
},
|
||||
[assetsUploadPercentage, getReadOnlyEditorFileHandlers, maxFileSize]
|
||||
);
|
||||
|
||||
return {
|
||||
getEditorFileHandlers,
|
||||
getReadOnlyEditorFileHandlers,
|
||||
};
|
||||
};
|
||||
|
|
@ -7,6 +7,7 @@ export * from "./use-command-palette";
|
|||
export * from "./use-cycle";
|
||||
export * from "./use-cycle-filter";
|
||||
export * from "./use-dashboard";
|
||||
export * from "./use-editor-asset";
|
||||
export * from "./use-event-tracker";
|
||||
export * from "./use-global-view";
|
||||
export * from "./use-inbox-issues";
|
||||
|
|
|
|||
10
web/core/hooks/store/use-editor-asset.ts
Normal file
10
web/core/hooks/store/use-editor-asset.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import { IEditorAssetStore } from "@/store/editor/asset.store";
|
||||
|
||||
export const useEditorAsset = (): IEditorAssetStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useEditorAsset must be used within StoreProvider");
|
||||
return context.editorAssetStore;
|
||||
};
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { AxiosRequestConfig } from "axios";
|
||||
// plane types
|
||||
import { TFileEntityInfo, TFileSignedURLResponse } from "@plane/types";
|
||||
// helpers
|
||||
|
|
@ -64,7 +65,8 @@ export class FileService extends APIService {
|
|||
async uploadWorkspaceAsset(
|
||||
workspaceSlug: string,
|
||||
data: TFileEntityInfo,
|
||||
file: File
|
||||
file: File,
|
||||
uploadProgressHandler?: AxiosRequestConfig["onUploadProgress"]
|
||||
): Promise<TFileSignedURLResponse> {
|
||||
const fileMetaData = getFileMetaDataForUpload(file);
|
||||
return this.post(`/api/assets/v2/workspaces/${workspaceSlug}/`, {
|
||||
|
|
@ -74,7 +76,11 @@ export class FileService extends APIService {
|
|||
.then(async (response) => {
|
||||
const signedURLResponse: TFileSignedURLResponse = response?.data;
|
||||
const fileUploadPayload = generateFileUploadPayload(signedURLResponse, file);
|
||||
await this.fileUploadService.uploadFile(signedURLResponse.upload_data.url, fileUploadPayload);
|
||||
await this.fileUploadService.uploadFile(
|
||||
signedURLResponse.upload_data.url,
|
||||
fileUploadPayload,
|
||||
uploadProgressHandler
|
||||
);
|
||||
await this.updateWorkspaceAssetUploadStatus(workspaceSlug.toString(), signedURLResponse.asset_id);
|
||||
return signedURLResponse;
|
||||
})
|
||||
|
|
@ -122,7 +128,8 @@ export class FileService extends APIService {
|
|||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: TFileEntityInfo,
|
||||
file: File
|
||||
file: File,
|
||||
uploadProgressHandler?: AxiosRequestConfig["onUploadProgress"]
|
||||
): Promise<TFileSignedURLResponse> {
|
||||
const fileMetaData = getFileMetaDataForUpload(file);
|
||||
return this.post(`/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/`, {
|
||||
|
|
@ -132,7 +139,11 @@ export class FileService extends APIService {
|
|||
.then(async (response) => {
|
||||
const signedURLResponse: TFileSignedURLResponse = response?.data;
|
||||
const fileUploadPayload = generateFileUploadPayload(signedURLResponse, file);
|
||||
await this.fileUploadService.uploadFile(signedURLResponse.upload_data.url, fileUploadPayload);
|
||||
await this.fileUploadService.uploadFile(
|
||||
signedURLResponse.upload_data.url,
|
||||
fileUploadPayload,
|
||||
uploadProgressHandler
|
||||
);
|
||||
await this.updateProjectAssetUploadStatus(workspaceSlug, projectId, signedURLResponse.asset_id);
|
||||
return signedURLResponse;
|
||||
})
|
||||
|
|
|
|||
121
web/core/store/editor/asset.store.ts
Normal file
121
web/core/store/editor/asset.store.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import debounce from "lodash/debounce";
|
||||
import set from "lodash/set";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// plane types
|
||||
import { TFileEntityInfo, TFileSignedURLResponse } from "@plane/types";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
import { TAttachmentUploadStatus } from "../issue/issue-details/attachment.store";
|
||||
|
||||
export interface IEditorAssetStore {
|
||||
// computed
|
||||
assetsUploadPercentage: Record<string, number>;
|
||||
// helper methods
|
||||
getAssetUploadStatusByEditorBlockId: (blockId: string) => TAttachmentUploadStatus | undefined;
|
||||
// actions
|
||||
uploadEditorAsset: ({
|
||||
blockId,
|
||||
data,
|
||||
file,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
}: {
|
||||
blockId: string;
|
||||
data: TFileEntityInfo;
|
||||
file: File;
|
||||
projectId?: string;
|
||||
workspaceSlug: string;
|
||||
}) => Promise<TFileSignedURLResponse>;
|
||||
}
|
||||
|
||||
export class EditorAssetStore implements IEditorAssetStore {
|
||||
// observables
|
||||
assetsUploadStatus: Record<string, TAttachmentUploadStatus> = {};
|
||||
// services
|
||||
fileService: FileService;
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
assetsUploadStatus: observable,
|
||||
// computed
|
||||
assetsUploadPercentage: computed,
|
||||
// actions
|
||||
uploadEditorAsset: action,
|
||||
});
|
||||
// services
|
||||
this.fileService = new FileService();
|
||||
}
|
||||
|
||||
get assetsUploadPercentage() {
|
||||
const assetsStatus = this.assetsUploadStatus;
|
||||
const assetsPercentage: Record<string, number> = {};
|
||||
Object.keys(assetsStatus).forEach((blockId) => {
|
||||
const asset = assetsStatus[blockId];
|
||||
if (asset) assetsPercentage[blockId] = asset.progress;
|
||||
});
|
||||
return assetsPercentage;
|
||||
}
|
||||
|
||||
// helper methods
|
||||
getAssetUploadStatusByEditorBlockId: IEditorAssetStore["getAssetUploadStatusByEditorBlockId"] = computedFn(
|
||||
(blockId) => {
|
||||
const blockDetails = this.assetsUploadStatus[blockId];
|
||||
if (!blockDetails) return undefined;
|
||||
return blockDetails;
|
||||
}
|
||||
);
|
||||
|
||||
// actions
|
||||
private debouncedUpdateProgress = debounce((blockId: string, progress: number) => {
|
||||
runInAction(() => {
|
||||
set(this.assetsUploadStatus, [blockId, "progress"], progress);
|
||||
});
|
||||
}, 16);
|
||||
|
||||
uploadEditorAsset: IEditorAssetStore["uploadEditorAsset"] = async (args) => {
|
||||
const { blockId, data, file, projectId, workspaceSlug } = args;
|
||||
const tempId = uuidv4();
|
||||
|
||||
try {
|
||||
// update attachment upload status
|
||||
runInAction(() => {
|
||||
set(this.assetsUploadStatus, [blockId], {
|
||||
id: tempId,
|
||||
name: file.name,
|
||||
progress: 0,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
});
|
||||
});
|
||||
if (projectId) {
|
||||
const response = await this.fileService.uploadProjectAsset(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
data,
|
||||
file,
|
||||
(progressEvent) => {
|
||||
const progressPercentage = Math.round((progressEvent.progress ?? 0) * 100);
|
||||
this.debouncedUpdateProgress(blockId, progressPercentage);
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} else {
|
||||
const response = await this.fileService.uploadWorkspaceAsset(workspaceSlug, data, file, (progressEvent) => {
|
||||
const progressPercentage = Math.round((progressEvent.progress ?? 0) * 100);
|
||||
this.debouncedUpdateProgress(blockId, progressPercentage);
|
||||
});
|
||||
return response;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in uploading page asset:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
runInAction(() => {
|
||||
delete this.assetsUploadStatus[blockId];
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
|
|||
return response;
|
||||
};
|
||||
|
||||
debouncedUpdateProgress = debounce((issueId: string, tempId: string, progress: number) => {
|
||||
private debouncedUpdateProgress = debounce((issueId: string, tempId: string, progress: number) => {
|
||||
runInAction(() => {
|
||||
set(this.attachmentsUploadStatusMap, [issueId, tempId, "progress"], progress);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { enableStaticRendering } from "mobx-react";
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
// plane web store
|
||||
import { CommandPaletteStore, ICommandPaletteStore } from "@/plane-web/store/command-palette.store";
|
||||
import { RootStore } from "@/plane-web/store/root.store";
|
||||
|
|
@ -8,6 +7,7 @@ import { IStateStore, StateStore } from "@/plane-web/store/state.store";
|
|||
import { CycleStore, ICycleStore } from "./cycle.store";
|
||||
import { CycleFilterStore, ICycleFilterStore } from "./cycle_filter.store";
|
||||
import { DashboardStore, IDashboardStore } from "./dashboard.store";
|
||||
import { EditorAssetStore, IEditorAssetStore } from "./editor/asset.store";
|
||||
import { IProjectEstimateStore, ProjectEstimateStore } from "./estimates/project-estimate.store";
|
||||
import { EventTrackerStore, IEventTrackerStore } from "./event-tracker.store";
|
||||
import { FavoriteStore, IFavoriteStore } from "./favorite.store";
|
||||
|
|
@ -61,6 +61,7 @@ export class CoreRootStore {
|
|||
favorite: IFavoriteStore;
|
||||
transient: ITransientStore;
|
||||
stickyStore: IStickyStore;
|
||||
editorAssetStore: IEditorAssetStore;
|
||||
|
||||
constructor() {
|
||||
this.router = new RouterStore();
|
||||
|
|
@ -90,6 +91,7 @@ export class CoreRootStore {
|
|||
this.favorite = new FavoriteStore(this);
|
||||
this.transient = new TransientStore();
|
||||
this.stickyStore = new StickyStore();
|
||||
this.editorAssetStore = new EditorAssetStore();
|
||||
}
|
||||
|
||||
resetOnSignOut() {
|
||||
|
|
@ -122,5 +124,6 @@ export class CoreRootStore {
|
|||
this.favorite = new FavoriteStore(this);
|
||||
this.transient = new TransientStore();
|
||||
this.stickyStore = new StickyStore();
|
||||
this.editorAssetStore = new EditorAssetStore();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue