From eed2ca77ef2a00a46aaff806606d4bbafc18695b Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:07:27 +0530 Subject: [PATCH 1/7] fix: added workspaceslug in renderChildren of project settings (#5951) * fix: added workspaceslug in renderChildren of project settings * fix: updated apis * fix: types * fix: added editor * fix: handled avatar for intake --- packages/types/src/inbox.d.ts | 3 +- .../components/editor/rich-text-editor.tsx | 41 +++++++++++++++++++ space/core/store/issue-detail.store.ts | 19 +++++++++ space/core/types/intake.d.ts | 6 +++ .../constants/project/settings/features.tsx | 3 +- .../inbox/sidebar/inbox-list-item.tsx | 9 +++- .../project/settings/features-list.tsx | 2 +- .../services/inbox/inbox-issue.service.ts | 8 ++-- web/core/store/inbox/project-inbox.store.ts | 15 ++++--- 9 files changed, 90 insertions(+), 16 deletions(-) create mode 100644 space/core/components/editor/rich-text-editor.tsx create mode 100644 space/core/types/intake.d.ts diff --git a/packages/types/src/inbox.d.ts b/packages/types/src/inbox.d.ts index bb60b35a21..5ae6c160e8 100644 --- a/packages/types/src/inbox.d.ts +++ b/packages/types/src/inbox.d.ts @@ -100,7 +100,8 @@ export type TInboxIssueWithPagination = TInboxIssuePaginationInfo & { export type TInboxForm = { anchor: string; id: string; - is_disabled: boolean; + is_in_app_enabled: boolean; + is_form_enabled: boolean; }; export type TInboxIssueForm = { diff --git a/space/core/components/editor/rich-text-editor.tsx b/space/core/components/editor/rich-text-editor.tsx new file mode 100644 index 0000000000..af4cf97136 --- /dev/null +++ b/space/core/components/editor/rich-text-editor.tsx @@ -0,0 +1,41 @@ +import React, { forwardRef } from "react"; +// editor +import { EditorRefApi, IMentionHighlight, IRichTextEditor, RichTextEditorWithRef } from "@plane/editor"; +// types +// helpers +import { cn } from "@/helpers/common.helper"; +import { getEditorFileHandlers } from "@/helpers/editor.helper"; + +interface RichTextEditorWrapperProps extends Omit { + uploadFile: (file: File) => Promise; +} + +export const RichTextEditor = forwardRef((props, ref) => { + const { containerClassName, uploadFile, ...rest } = props; + // store hooks + + // use-mention + + // file size + + return ( + { + throw new Error("Function not implemented."); + }, + suggestions: undefined, + }} + ref={ref} + fileHandler={getEditorFileHandlers({ + uploadFile, + workspaceId: "", + anchor: "", + })} + {...rest} + containerClassName={cn("relative pl-3 pb-3", containerClassName)} + /> + ); +}); + +RichTextEditor.displayName = "RichTextEditor"; diff --git a/space/core/store/issue-detail.store.ts b/space/core/store/issue-detail.store.ts index ee8a3031ed..31058f790f 100644 --- a/space/core/store/issue-detail.store.ts +++ b/space/core/store/issue-detail.store.ts @@ -36,6 +36,7 @@ export interface IIssueDetailStore { updateIssueComment: (anchor: string, issueID: string, commentID: string, data: any) => Promise; deleteIssueComment: (anchor: string, issueID: string, commentID: string) => void; uploadCommentAsset: (file: File, anchor: string, commentID?: string) => Promise; + uploadIssueAsset: (file: File, anchor: string, commentID?: string) => Promise; addCommentReaction: (anchor: string, issueID: string, commentID: string, reactionHex: string) => void; removeCommentReaction: (anchor: string, issueID: string, commentID: string, reactionHex: string) => void; // reaction actions @@ -79,6 +80,7 @@ export class IssueDetailStore implements IIssueDetailStore { updateIssueComment: action, deleteIssueComment: action, uploadCommentAsset: action, + uploadIssueAsset: action, addCommentReaction: action, removeCommentReaction: action, // reaction actions @@ -245,6 +247,23 @@ export class IssueDetailStore implements IIssueDetailStore { } }; + uploadIssueAsset = async (file: File, anchor: string, commentID?: string) => { + try { + const res = await this.fileService.uploadAsset( + anchor, + { + entity_identifier: commentID ?? "", + entity_type: EFileAssetType.ISSUE_ATTACHMENT, + }, + file + ); + return res; + } catch (error) { + console.log("Error in uploading comment asset:", error); + throw new Error("Asset upload failed. Please try again later."); + } + }; + addCommentReaction = async (anchor: string, issueID: string, commentID: string, reactionHex: string) => { const newReaction = { id: uuidv4(), diff --git a/space/core/types/intake.d.ts b/space/core/types/intake.d.ts new file mode 100644 index 0000000000..9cf2934f63 --- /dev/null +++ b/space/core/types/intake.d.ts @@ -0,0 +1,6 @@ +export type TIntakeIssueForm = { + name: string; + email: string; + username: string; + description_html: string; +}; diff --git a/web/ce/constants/project/settings/features.tsx b/web/ce/constants/project/settings/features.tsx index b9b39700d6..b3601e27b1 100644 --- a/web/ce/constants/project/settings/features.tsx +++ b/web/ce/constants/project/settings/features.tsx @@ -13,7 +13,8 @@ export type TProperties = { renderChildren?: ( currentProjectDetails: IProject, isAdmin: boolean, - handleSubmit: (featureKey: string, featureProperty: string) => Promise + handleSubmit: (featureKey: string, featureProperty: string) => Promise, + workspaceSlug: string ) => ReactNode; }; export type TFeatureList = { diff --git a/web/core/components/inbox/sidebar/inbox-list-item.tsx b/web/core/components/inbox/sidebar/inbox-list-item.tsx index c1574fe29e..6762c25280 100644 --- a/web/core/components/inbox/sidebar/inbox-list-item.tsx +++ b/web/core/components/inbox/sidebar/inbox-list-item.tsx @@ -4,7 +4,7 @@ import { FC, MouseEvent } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; -import { Tooltip, PriorityIcon, Row } from "@plane/ui"; +import { Tooltip, PriorityIcon, Row, Avatar } from "@plane/ui"; // components import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; import { InboxIssueStatus } from "@/components/inbox"; @@ -12,6 +12,7 @@ import { InboxIssueStatus } from "@/components/inbox"; import { cn } from "@/helpers/common.helper"; import { renderFormattedDate } from "@/helpers/date-time.helper"; // hooks +import { getFileURL } from "@/helpers/file.helper"; import { useLabel, useMember, useProjectInbox } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -116,7 +117,11 @@ export const InboxIssueListItem: FC = observer((props) )} {/* created by */} - {createdByDetails && } + {createdByDetails && createdByDetails.email?.includes("intake@plane.so") ? ( + + ) : createdByDetails ? ( + + ) : null} diff --git a/web/core/components/project/settings/features-list.tsx b/web/core/components/project/settings/features-list.tsx index f1f17f2d59..7bd0a15e6d 100644 --- a/web/core/components/project/settings/features-list.tsx +++ b/web/core/components/project/settings/features-list.tsx @@ -102,7 +102,7 @@ export const ProjectFeaturesList: FC = observer((props) => {
{currentProjectDetails?.[featureItem.property as keyof IProject] && featureItem.renderChildren && - featureItem.renderChildren(currentProjectDetails, isAdmin, handleSubmit)} + featureItem.renderChildren(currentProjectDetails, isAdmin, handleSubmit, workspaceSlug)}
); diff --git a/web/core/services/inbox/inbox-issue.service.ts b/web/core/services/inbox/inbox-issue.service.ts index 61aaeb8490..b1bf74999f 100644 --- a/web/core/services/inbox/inbox-issue.service.ts +++ b/web/core/services/inbox/inbox-issue.service.ts @@ -77,17 +77,15 @@ export class InboxIssueService extends APIService { } async retrievePublishForm(workspaceSlug: string, projectId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake/`) + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/intake-settings/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; }); } - async updatePublishForm(workspaceSlug: string, projectId: string, is_disabled: boolean): Promise { - return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake/`, { - is_disabled, - }) + async updatePublishForm(workspaceSlug: string, projectId: string, data: Partial): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/intake-settings/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/web/core/store/inbox/project-inbox.store.ts b/web/core/store/inbox/project-inbox.store.ts index f6953debf8..9960faa154 100644 --- a/web/core/store/inbox/project-inbox.store.ts +++ b/web/core/store/inbox/project-inbox.store.ts @@ -71,7 +71,7 @@ export interface IProjectInboxStore { fetchInboxPaginationIssues: (workspaceSlug: string, projectId: string) => Promise; fetchInboxIssueById: (workspaceSlug: string, projectId: string, inboxIssueId: string) => Promise; fetchIntakeForms: (workspaceSlug: string, projectId: string) => Promise; - toggleIntakeForms: (workspaceSlug: string, projectId: string, isDisabled: boolean) => Promise; + toggleIntakeForms: (workspaceSlug: string, projectId: string, data: Partial) => Promise; regenerateIntakeForms: (workspaceSlug: string, projectId: string) => Promise; createInboxIssue: ( workspaceSlug: string, @@ -329,20 +329,23 @@ export class ProjectInboxStore implements IProjectInboxStore { } }; - toggleIntakeForms = async (workspaceSlug: string, projectId: string, isDisabled: boolean) => { + toggleIntakeForms = async (workspaceSlug: string, projectId: string, data: Partial) => { + const initialData = this.intakeForms[projectId]; try { runInAction(() => { - set(this.intakeForms, projectId, { ...this.intakeForms[projectId], is_disabled: isDisabled }); + set(this.intakeForms, projectId, { ...this.intakeForms[projectId], ...data }); + }); + const result = await this.inboxIssueService.updatePublishForm(workspaceSlug, projectId, data); + runInAction(() => { + set(this.intakeForms, projectId, { ...this.intakeForms[projectId], anchor: result?.anchor }); }); - await this.inboxIssueService.updatePublishForm(workspaceSlug, projectId, isDisabled); } catch { console.error("Error fetching the publish forms"); runInAction(() => { - set(this.intakeForms, projectId, { ...this.intakeForms[projectId], is_disabled: !isDisabled }); + set(this.intakeForms, projectId, initialData); }); } }; - regenerateIntakeForms = async (workspaceSlug: string, projectId: string) => { try { const form = await this.inboxIssueService.regeneratePublishForm(workspaceSlug, projectId); From ea8583b2d46b3003543deb16521a1e4578a0e1cd Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:04:03 +0530 Subject: [PATCH 2/7] chore: code refactor (#5952) * chore: code refactor * chore: code refactor --- web/ce/components/de-dupe/de-dupe-button.tsx | 15 ++++++++++++++ web/ce/components/de-dupe/index.ts | 1 + .../inbox/modals/create-modal/create-root.tsx | 20 +++++++------------ .../components/issues/issue-modal/form.tsx | 20 +++++++------------ web/ee/components/de-dupe/index.ts | 1 + 5 files changed, 31 insertions(+), 26 deletions(-) create mode 100644 web/ce/components/de-dupe/de-dupe-button.tsx create mode 100644 web/ee/components/de-dupe/index.ts diff --git a/web/ce/components/de-dupe/de-dupe-button.tsx b/web/ce/components/de-dupe/de-dupe-button.tsx new file mode 100644 index 0000000000..eaa4e3b7c8 --- /dev/null +++ b/web/ce/components/de-dupe/de-dupe-button.tsx @@ -0,0 +1,15 @@ +"use client"; +import React, { FC } from "react"; +// local components + +type TDeDupeButtonRoot = { + workspaceSlug: string; + isDuplicateModalOpen: boolean; + handleOnClick: () => void; + label: string; +}; + +export const DeDupeButtonRoot: FC = (props) => { + const { workspaceSlug, isDuplicateModalOpen, label, handleOnClick } = props; + return <>; +}; diff --git a/web/ce/components/de-dupe/index.ts b/web/ce/components/de-dupe/index.ts index 83622b978c..91856db18e 100644 --- a/web/ce/components/de-dupe/index.ts +++ b/web/ce/components/de-dupe/index.ts @@ -1,3 +1,4 @@ +export * from "./de-dupe-button"; export * from "./duplicate-modal"; export * from "./duplicate-popover"; export * from "./issue-block"; diff --git a/web/core/components/inbox/modals/create-modal/create-root.tsx b/web/core/components/inbox/modals/create-modal/create-root.tsx index 4a318d1b2a..edd17060cd 100644 --- a/web/core/components/inbox/modals/create-modal/create-root.tsx +++ b/web/core/components/inbox/modals/create-modal/create-root.tsx @@ -22,7 +22,7 @@ import { useAppRouter } from "@/hooks/use-app-router"; import useKeypress from "@/hooks/use-keypress"; import { usePlatformOS } from "@/hooks/use-platform-os"; // services -import { DeDupeIssueButtonLabel, DuplicateModalRoot } from "@/plane-web/components/de-dupe"; +import { DeDupeButtonRoot, DuplicateModalRoot } from "@/plane-web/components/de-dupe"; import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues"; import { FileService } from "@/services/file.service"; @@ -210,18 +210,12 @@ export const InboxIssueCreateRoot: FC = observer((props)

Create intake issue

{duplicateIssues?.length > 0 && ( - + 1 ? "s" : ""} found!`} + handleOnClick={() => handleDuplicateIssueModal(!isDuplicateModalOpen)} + /> )}
diff --git a/web/core/components/issues/issue-modal/form.tsx b/web/core/components/issues/issue-modal/form.tsx index f9dd9d06dd..b99b517495 100644 --- a/web/core/components/issues/issue-modal/form.tsx +++ b/web/core/components/issues/issue-modal/form.tsx @@ -31,7 +31,7 @@ import { useIssueDetail, useProject, useProjectState, useWorkspaceDraftIssues } import { usePlatformOS } from "@/hooks/use-platform-os"; import { useProjectIssueProperties } from "@/hooks/use-project-issue-properties"; // plane web components -import { DeDupeIssueButtonLabel, DuplicateModalRoot } from "@/plane-web/components/de-dupe"; +import { DeDupeButtonRoot, DuplicateModalRoot } from "@/plane-web/components/de-dupe"; import { IssueAdditionalProperties, IssueTypeSelect } from "@/plane-web/components/issues/issue-modal"; import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues"; @@ -350,18 +350,12 @@ export const IssueFormRoot: FC = observer((props) => { )}
{duplicateIssues.length > 0 && ( - + 1 ? "s" : ""} found!`} + handleOnClick={() => handleDuplicateIssueModal(!isDuplicateModalOpen)} + /> )} {watch("parent_id") && selectedParentIssue && ( diff --git a/web/ee/components/de-dupe/index.ts b/web/ee/components/de-dupe/index.ts new file mode 100644 index 0000000000..1c66dae21d --- /dev/null +++ b/web/ee/components/de-dupe/index.ts @@ -0,0 +1 @@ +export * from "ce/components/de-dupe"; From bb311b750fc71895d7159d3f9cc19e2e021c2718 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:45:53 +0530 Subject: [PATCH 3/7] fix: wrong token being passed in the read-only editor (#5954) * fix: wrong token * chore: update useMemo dependencies --- .../src/core/hooks/use-read-only-collaborative-editor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts index 9fa73c3ecb..d408192297 100644 --- a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts @@ -32,7 +32,7 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit new HocuspocusProvider({ url: realtimeConfig.url, name: id, - token: user.id, + token: JSON.stringify(user), parameters: realtimeConfig.queryParams, onAuthenticationFailed: () => { serverHandler?.onServerError?.(); @@ -47,7 +47,7 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit }, onSynced: () => setHasServerSynced(true), }), - [id, realtimeConfig, user.id] + [id, realtimeConfig, user] ); // destroy and disconnect connection on unmount useEffect( From b4de055463e8328fcc5124a7c43a29ff2c553204 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:46:34 +0530 Subject: [PATCH 4/7] [PULSE-42] feat: text alignment for all editors (#5847) * feat: text alignment for editors * fix: text alignment types * fix: build errors * fix: build error * fix: toolbar movement post alignment selection * fix: callout type * fix: image node types * chore: add ts error warning --- packages/editor/package.json | 1 + .../menus/bubble-menu/alignment-selector.tsx | 91 +++++++++++++ .../menus/bubble-menu/color-selector.tsx | 8 +- .../menus/bubble-menu/node-selector.tsx | 6 +- .../components/menus/bubble-menu/root.tsx | 18 ++- .../src/core/components/menus/menu-items.ts | 98 +++++++------- .../src/core/extensions/core-without-props.ts | 2 + .../editor/src/core/extensions/extensions.tsx | 2 + packages/editor/src/core/extensions/index.ts | 4 +- .../core/extensions/read-only-extensions.tsx | 2 + .../slash-commands/command-menu.tsx | 4 +- .../core/extensions/slash-commands/root.tsx | 4 +- .../editor/src/core/extensions/text-align.ts | 8 ++ .../src/core/helpers/editor-commands.ts | 6 +- packages/editor/src/core/hooks/use-editor.ts | 16 +-- packages/editor/src/core/types/editor.ts | 77 ++++++++--- .../core/types/slash-commands-suggestion.ts | 30 +---- .../components/editor/lite-text-editor.tsx | 20 +-- space/core/components/editor/toolbar.tsx | 95 ++++++------- .../peek-overview/comment/add-comment.tsx | 2 +- space/core/constants/editor.ts | 126 +++++++++++++---- .../lite-text-editor/lite-text-editor.tsx | 23 ++-- .../editor/lite-text-editor/toolbar.tsx | 95 ++++++------- .../pages/editor/header/color-dropdown.tsx | 12 +- .../pages/editor/header/toolbar.tsx | 33 +++-- web/core/constants/editor.ts | 127 ++++++++++++++---- yarn.lock | 55 ++++---- 27 files changed, 641 insertions(+), 324 deletions(-) create mode 100644 packages/editor/src/core/components/menus/bubble-menu/alignment-selector.tsx create mode 100644 packages/editor/src/core/extensions/text-align.ts diff --git a/packages/editor/package.json b/packages/editor/package.json index c52c630c8c..067be0145d 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -48,6 +48,7 @@ "@tiptap/extension-placeholder": "^2.3.0", "@tiptap/extension-task-item": "^2.1.13", "@tiptap/extension-task-list": "^2.1.13", + "@tiptap/extension-text-align": "^2.8.0", "@tiptap/extension-text-style": "^2.7.1", "@tiptap/extension-underline": "^2.1.13", "@tiptap/pm": "^2.1.13", diff --git a/packages/editor/src/core/components/menus/bubble-menu/alignment-selector.tsx b/packages/editor/src/core/components/menus/bubble-menu/alignment-selector.tsx new file mode 100644 index 0000000000..e3ccc6cf62 --- /dev/null +++ b/packages/editor/src/core/components/menus/bubble-menu/alignment-selector.tsx @@ -0,0 +1,91 @@ +import { Editor } from "@tiptap/core"; +import { AlignCenter, AlignLeft, AlignRight, LucideIcon } from "lucide-react"; +// components +import { TextAlignItem } from "@/components/menus"; +// helpers +import { cn } from "@/helpers/common"; +// types +import { TEditorCommands } from "@/types"; + +type Props = { + editor: Editor; + onClose: () => void; +}; + +export const TextAlignmentSelector: React.FC = (props) => { + const { editor, onClose } = props; + + const menuItem = TextAlignItem(editor); + + const textAlignmentOptions: { + itemKey: TEditorCommands; + renderKey: string; + icon: LucideIcon; + command: () => void; + isActive: () => boolean; + }[] = [ + { + itemKey: "text-align", + renderKey: "text-align-left", + icon: AlignLeft, + command: () => + menuItem.command({ + alignment: "left", + }), + isActive: () => + menuItem.isActive({ + alignment: "left", + }), + }, + { + itemKey: "text-align", + renderKey: "text-align-center", + icon: AlignCenter, + command: () => + menuItem.command({ + alignment: "center", + }), + isActive: () => + menuItem.isActive({ + alignment: "center", + }), + }, + { + itemKey: "text-align", + renderKey: "text-align-right", + icon: AlignRight, + command: () => + menuItem.command({ + alignment: "right", + }), + isActive: () => + menuItem.isActive({ + alignment: "right", + }), + }, + ]; + + return ( +
+ {textAlignmentOptions.map((item) => ( + + ))} +
+ ); +}; diff --git a/packages/editor/src/core/components/menus/bubble-menu/color-selector.tsx b/packages/editor/src/core/components/menus/bubble-menu/color-selector.tsx index b48070775e..bc7f5a56f1 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/color-selector.tsx +++ b/packages/editor/src/core/components/menus/bubble-menu/color-selector.tsx @@ -16,8 +16,8 @@ type Props = { export const BubbleMenuColorSelector: FC = (props) => { const { editor, isOpen, setIsOpen } = props; - const activeTextColor = COLORS_LIST.find((c) => TextColorItem(editor).isActive(c.key)); - const activeBackgroundColor = COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive(c.key)); + const activeTextColor = COLORS_LIST.find((c) => TextColorItem(editor).isActive({ color: c.key })); + const activeBackgroundColor = COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive({ color: c.key })); return (
@@ -64,7 +64,7 @@ export const BubbleMenuColorSelector: FC = (props) => { style={{ backgroundColor: color.textColor, }} - onClick={() => TextColorItem(editor).command(color.key)} + onClick={() => TextColorItem(editor).command({ color: color.key })} /> ))}
- {items.map((item) => ( + {basicFormattingOptions.map((item) => ( ))}
+ { + const editor = props.editor as Editor; + if (!editor) return; + const pos = editor.state.selection.to; + editor.commands.setTextSelection(pos ?? 0); + }} + /> )} diff --git a/packages/editor/src/core/components/menus/menu-items.ts b/packages/editor/src/core/components/menus/menu-items.ts index 0ece455ed2..5e987fca6d 100644 --- a/packages/editor/src/core/components/menus/menu-items.ts +++ b/packages/editor/src/core/components/menus/menu-items.ts @@ -1,4 +1,3 @@ -import { Selection } from "@tiptap/pm/state"; import { Editor } from "@tiptap/react"; import { BoldIcon, @@ -22,6 +21,7 @@ import { LucideIcon, MinusSquare, Palette, + AlignCenter, } from "lucide-react"; // helpers import { @@ -29,6 +29,7 @@ import { insertImage, insertTableCommand, setText, + setTextAlign, toggleBackgroundColor, toggleBlockquote, toggleBold, @@ -48,24 +49,20 @@ import { toggleUnderline, } from "@/helpers/editor-commands"; // types -import { TColorEditorCommands, TNonColorEditorCommands } from "@/types"; +import { TCommandWithProps, TEditorCommands } from "@/types"; -export type EditorMenuItem = { +type isActiveFunction = (params?: TCommandWithProps) => boolean; +type commandFunction = (params?: TCommandWithProps) => void; + +export type EditorMenuItem = { + key: T; name: string; - command: (...args: any) => void; + command: commandFunction; icon: LucideIcon; -} & ( - | { - key: TNonColorEditorCommands; - isActive: () => boolean; - } - | { - key: TColorEditorCommands; - isActive: (color: string | undefined) => boolean; - } -); + isActive: isActiveFunction; +}; -export const TextItem = (editor: Editor): EditorMenuItem => ({ +export const TextItem = (editor: Editor): EditorMenuItem<"text"> => ({ key: "text", name: "Text", isActive: () => editor.isActive("paragraph"), @@ -73,7 +70,7 @@ export const TextItem = (editor: Editor): EditorMenuItem => ({ icon: CaseSensitive, }); -export const HeadingOneItem = (editor: Editor): EditorMenuItem => ({ +export const HeadingOneItem = (editor: Editor): EditorMenuItem<"h1"> => ({ key: "h1", name: "Heading 1", isActive: () => editor.isActive("heading", { level: 1 }), @@ -81,7 +78,7 @@ export const HeadingOneItem = (editor: Editor): EditorMenuItem => ({ icon: Heading1, }); -export const HeadingTwoItem = (editor: Editor): EditorMenuItem => ({ +export const HeadingTwoItem = (editor: Editor): EditorMenuItem<"h2"> => ({ key: "h2", name: "Heading 2", isActive: () => editor.isActive("heading", { level: 2 }), @@ -89,7 +86,7 @@ export const HeadingTwoItem = (editor: Editor): EditorMenuItem => ({ icon: Heading2, }); -export const HeadingThreeItem = (editor: Editor): EditorMenuItem => ({ +export const HeadingThreeItem = (editor: Editor): EditorMenuItem<"h3"> => ({ key: "h3", name: "Heading 3", isActive: () => editor.isActive("heading", { level: 3 }), @@ -97,7 +94,7 @@ export const HeadingThreeItem = (editor: Editor): EditorMenuItem => ({ icon: Heading3, }); -export const HeadingFourItem = (editor: Editor): EditorMenuItem => ({ +export const HeadingFourItem = (editor: Editor): EditorMenuItem<"h4"> => ({ key: "h4", name: "Heading 4", isActive: () => editor.isActive("heading", { level: 4 }), @@ -105,7 +102,7 @@ export const HeadingFourItem = (editor: Editor): EditorMenuItem => ({ icon: Heading4, }); -export const HeadingFiveItem = (editor: Editor): EditorMenuItem => ({ +export const HeadingFiveItem = (editor: Editor): EditorMenuItem<"h5"> => ({ key: "h5", name: "Heading 5", isActive: () => editor.isActive("heading", { level: 5 }), @@ -113,7 +110,7 @@ export const HeadingFiveItem = (editor: Editor): EditorMenuItem => ({ icon: Heading5, }); -export const HeadingSixItem = (editor: Editor): EditorMenuItem => ({ +export const HeadingSixItem = (editor: Editor): EditorMenuItem<"h6"> => ({ key: "h6", name: "Heading 6", isActive: () => editor.isActive("heading", { level: 6 }), @@ -121,7 +118,7 @@ export const HeadingSixItem = (editor: Editor): EditorMenuItem => ({ icon: Heading6, }); -export const BoldItem = (editor: Editor): EditorMenuItem => ({ +export const BoldItem = (editor: Editor): EditorMenuItem<"bold"> => ({ key: "bold", name: "Bold", isActive: () => editor?.isActive("bold"), @@ -129,7 +126,7 @@ export const BoldItem = (editor: Editor): EditorMenuItem => ({ icon: BoldIcon, }); -export const ItalicItem = (editor: Editor): EditorMenuItem => ({ +export const ItalicItem = (editor: Editor): EditorMenuItem<"italic"> => ({ key: "italic", name: "Italic", isActive: () => editor?.isActive("italic"), @@ -137,7 +134,7 @@ export const ItalicItem = (editor: Editor): EditorMenuItem => ({ icon: ItalicIcon, }); -export const UnderLineItem = (editor: Editor): EditorMenuItem => ({ +export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({ key: "underline", name: "Underline", isActive: () => editor?.isActive("underline"), @@ -145,7 +142,7 @@ export const UnderLineItem = (editor: Editor): EditorMenuItem => ({ icon: UnderlineIcon, }); -export const StrikeThroughItem = (editor: Editor): EditorMenuItem => ({ +export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough"> => ({ key: "strikethrough", name: "Strikethrough", isActive: () => editor?.isActive("strike"), @@ -153,7 +150,7 @@ export const StrikeThroughItem = (editor: Editor): EditorMenuItem => ({ icon: StrikethroughIcon, }); -export const BulletListItem = (editor: Editor): EditorMenuItem => ({ +export const BulletListItem = (editor: Editor): EditorMenuItem<"bulleted-list"> => ({ key: "bulleted-list", name: "Bulleted list", isActive: () => editor?.isActive("bulletList"), @@ -161,7 +158,7 @@ export const BulletListItem = (editor: Editor): EditorMenuItem => ({ icon: ListIcon, }); -export const NumberedListItem = (editor: Editor): EditorMenuItem => ({ +export const NumberedListItem = (editor: Editor): EditorMenuItem<"numbered-list"> => ({ key: "numbered-list", name: "Numbered list", isActive: () => editor?.isActive("orderedList"), @@ -169,7 +166,7 @@ export const NumberedListItem = (editor: Editor): EditorMenuItem => ({ icon: ListOrderedIcon, }); -export const TodoListItem = (editor: Editor): EditorMenuItem => ({ +export const TodoListItem = (editor: Editor): EditorMenuItem<"to-do-list"> => ({ key: "to-do-list", name: "To-do list", isActive: () => editor.isActive("taskItem"), @@ -177,7 +174,7 @@ export const TodoListItem = (editor: Editor): EditorMenuItem => ({ icon: CheckSquare, }); -export const QuoteItem = (editor: Editor): EditorMenuItem => ({ +export const QuoteItem = (editor: Editor): EditorMenuItem<"quote"> => ({ key: "quote", name: "Quote", isActive: () => editor?.isActive("blockquote"), @@ -185,7 +182,7 @@ export const QuoteItem = (editor: Editor): EditorMenuItem => ({ icon: TextQuote, }); -export const CodeItem = (editor: Editor): EditorMenuItem => ({ +export const CodeItem = (editor: Editor): EditorMenuItem<"code"> => ({ key: "code", name: "Code", isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"), @@ -193,7 +190,7 @@ export const CodeItem = (editor: Editor): EditorMenuItem => ({ icon: CodeIcon, }); -export const TableItem = (editor: Editor): EditorMenuItem => ({ +export const TableItem = (editor: Editor): EditorMenuItem<"table"> => ({ key: "table", name: "Table", isActive: () => editor?.isActive("table"), @@ -201,14 +198,14 @@ export const TableItem = (editor: Editor): EditorMenuItem => ({ icon: TableIcon, }); -export const ImageItem = (editor: Editor) => - ({ - key: "image", - name: "Image", - isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"), - command: (savedSelection: Selection | null) => insertImage({ editor, event: "insert", pos: savedSelection?.from }), - icon: ImageIcon, - }) as const; +export const ImageItem = (editor: Editor): EditorMenuItem<"image"> => ({ + key: "image", + name: "Image", + isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"), + command: ({ savedSelection }) => + insertImage({ editor, event: "insert", pos: savedSelection?.from ?? editor.state.selection.from }), + icon: ImageIcon, +}); export const HorizontalRuleItem = (editor: Editor) => ({ @@ -219,23 +216,31 @@ export const HorizontalRuleItem = (editor: Editor) => icon: MinusSquare, }) as const; -export const TextColorItem = (editor: Editor): EditorMenuItem => ({ +export const TextColorItem = (editor: Editor): EditorMenuItem<"text-color"> => ({ key: "text-color", name: "Color", - isActive: (color) => editor.isActive("customColor", { color }), - command: (color: string) => toggleTextColor(color, editor), + isActive: ({ color }) => editor.isActive("customColor", { color }), + command: ({ color }) => toggleTextColor(color, editor), icon: Palette, }); -export const BackgroundColorItem = (editor: Editor): EditorMenuItem => ({ +export const BackgroundColorItem = (editor: Editor): EditorMenuItem<"background-color"> => ({ key: "background-color", name: "Background color", - isActive: (color) => editor.isActive("customColor", { backgroundColor: color }), - command: (color: string) => toggleBackgroundColor(color, editor), + isActive: ({ color }) => editor.isActive("customColor", { backgroundColor: color }), + command: ({ color }) => toggleBackgroundColor(color, editor), icon: Palette, }); -export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem[] => { +export const TextAlignItem = (editor: Editor): EditorMenuItem<"text-align"> => ({ + key: "text-align", + name: "Text align", + isActive: ({ alignment }) => editor.isActive({ textAlign: alignment }), + command: ({ alignment }) => setTextAlign(alignment, editor), + icon: AlignCenter, +}); + +export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem[] => { if (!editor) return []; return [ @@ -260,5 +265,6 @@ export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem[] => { HorizontalRuleItem(editor), TextColorItem(editor), BackgroundColorItem(editor), + TextAlignItem(editor), ]; }; diff --git a/packages/editor/src/core/extensions/core-without-props.ts b/packages/editor/src/core/extensions/core-without-props.ts index 39a243f88a..abceea4ff5 100644 --- a/packages/editor/src/core/extensions/core-without-props.ts +++ b/packages/editor/src/core/extensions/core-without-props.ts @@ -16,6 +16,7 @@ import { IssueWidgetWithoutProps } from "./issue-embed/issue-embed-without-props import { CustomMentionWithoutProps } from "./mentions/mentions-without-props"; import { CustomQuoteExtension } from "./quote"; import { TableHeader, TableCell, TableRow, Table } from "./table"; +import { CustomTextAlignExtension } from "./text-align"; import { CustomCalloutExtensionConfig } from "./callout/extension-config"; import { CustomColorExtension } from "./custom-color"; @@ -85,6 +86,7 @@ export const CoreEditorExtensionsWithoutProps = [ TableCell, TableRow, CustomMentionWithoutProps(), + CustomTextAlignExtension, CustomCalloutExtensionConfig, CustomColorExtension, ]; diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index 24a372b4aa..3907e3b9d2 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -19,6 +19,7 @@ import { CustomLinkExtension, CustomMention, CustomQuoteExtension, + CustomTextAlignExtension, CustomTypographyExtension, DropHandlerExtension, ImageExtension, @@ -158,6 +159,7 @@ export const CoreEditorExtensions = (args: TArguments) => { includeChildren: true, }), CharacterCount, + CustomTextAlignExtension, CustomCalloutExtension, CustomColorExtension, ]; diff --git a/packages/editor/src/core/extensions/index.ts b/packages/editor/src/core/extensions/index.ts index dbd529d8c1..d1fa0ce6db 100644 --- a/packages/editor/src/core/extensions/index.ts +++ b/packages/editor/src/core/extensions/index.ts @@ -16,10 +16,10 @@ export * from "./custom-color"; export * from "./drop"; export * from "./enter-key-extension"; export * from "./extensions"; +export * from "./headers"; export * from "./horizontal-rule"; export * from "./keymap"; export * from "./quote"; export * from "./read-only-extensions"; export * from "./side-menu"; -export * from "./slash-commands"; -export * from "./headers"; +export * from "./text-align"; diff --git a/packages/editor/src/core/extensions/read-only-extensions.tsx b/packages/editor/src/core/extensions/read-only-extensions.tsx index 096cefbae6..31ae1d90a1 100644 --- a/packages/editor/src/core/extensions/read-only-extensions.tsx +++ b/packages/editor/src/core/extensions/read-only-extensions.tsx @@ -21,6 +21,7 @@ import { CustomMention, HeadingListExtension, CustomReadOnlyImageExtension, + CustomTextAlignExtension, CustomCalloutReadOnlyExtension, CustomColorExtension, } from "@/extensions"; @@ -125,6 +126,7 @@ export const CoreReadOnlyEditorExtensions = (props: Props) => { CharacterCount, CustomColorExtension, HeadingListExtension, + CustomTextAlignExtension, CustomCalloutReadOnlyExtension, ]; }; diff --git a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx index c6363bc51c..d6148b69ae 100644 --- a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx +++ b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx @@ -3,12 +3,12 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react import { TSlashCommandSection } from "./command-items-list"; import { CommandMenuItem } from "./command-menu-item"; -type Props = { +export type SlashCommandsMenuProps = { items: TSlashCommandSection[]; command: any; }; -export const SlashCommandsMenu = (props: Props) => { +export const SlashCommandsMenu = (props: SlashCommandsMenuProps) => { const { items: sections, command } = props; // states const [selectedIndex, setSelectedIndex] = useState({ diff --git a/packages/editor/src/core/extensions/slash-commands/root.tsx b/packages/editor/src/core/extensions/slash-commands/root.tsx index df70820dcf..a99cbc5f90 100644 --- a/packages/editor/src/core/extensions/slash-commands/root.tsx +++ b/packages/editor/src/core/extensions/slash-commands/root.tsx @@ -6,7 +6,7 @@ import tippy from "tippy.js"; import { ISlashCommandItem } from "@/types"; // components import { getSlashCommandFilteredSections } from "./command-items-list"; -import { SlashCommandsMenu } from "./command-menu"; +import { SlashCommandsMenu, SlashCommandsMenuProps } from "./command-menu"; export type SlashCommandOptions = { suggestion: Omit; @@ -55,7 +55,7 @@ interface CommandListInstance { } const renderItems = () => { - let component: ReactRenderer | null = null; + let component: ReactRenderer | null = null; let popup: any | null = null; return { onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { diff --git a/packages/editor/src/core/extensions/text-align.ts b/packages/editor/src/core/extensions/text-align.ts new file mode 100644 index 0000000000..bfe62f6c04 --- /dev/null +++ b/packages/editor/src/core/extensions/text-align.ts @@ -0,0 +1,8 @@ +import TextAlign from "@tiptap/extension-text-align"; + +export type TTextAlign = "left" | "center" | "right"; + +export const CustomTextAlignExtension = TextAlign.configure({ + alignments: ["left", "center", "right"], + types: ["heading", "paragraph"], +}); diff --git a/packages/editor/src/core/helpers/editor-commands.ts b/packages/editor/src/core/helpers/editor-commands.ts index efa19e3876..ec593d5367 100644 --- a/packages/editor/src/core/helpers/editor-commands.ts +++ b/packages/editor/src/core/helpers/editor-commands.ts @@ -181,10 +181,14 @@ export const toggleBackgroundColor = (color: string | undefined, editor: Editor, } }; +export const setTextAlign = (alignment: string, editor: Editor) => { + editor.chain().focus().setTextAlign(alignment).run(); +}; + export const insertHorizontalRule = (editor: Editor, range?: Range) => { if (range) editor.chain().focus().deleteRange(range).setHorizontalRule().run(); else editor.chain().focus().setHorizontalRule().run(); -} +}; export const insertCallout = (editor: Editor, range?: Range) => { if (range) editor.chain().focus().deleteRange(range).insertCallout().run(); else editor.chain().focus().insertCallout().run(); diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 67d7799660..eef72797ce 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -6,7 +6,7 @@ import { EditorProps } from "@tiptap/pm/view"; import { useEditor as useTiptapEditor, Editor } from "@tiptap/react"; import * as Y from "yjs"; // components -import { getEditorMenuItems } from "@/components/menus"; +import { EditorMenuItem, getEditorMenuItems } from "@/components/menus"; // extensions import { CoreEditorExtensions } from "@/extensions"; // helpers @@ -155,11 +155,11 @@ export const useEditor = (props: CustomEditorProps) => { const item = getEditorMenuItem(itemKey); if (item) { if (item.key === "image") { - item.command(savedSelectionRef.current); - } else if (itemKey === "text-color" || itemKey === "background-color") { - item.command(props.color); + (item as EditorMenuItem<"image">).command({ + savedSelection: savedSelectionRef.current, + }); } else { - item.command(); + item.command(props); } } else { console.warn(`No command found for item: ${itemKey}`); @@ -173,11 +173,7 @@ export const useEditor = (props: CustomEditorProps) => { const item = getEditorMenuItem(itemKey); if (!item) return false; - if (itemKey === "text-color" || itemKey === "background-color") { - return item.isActive(props.color); - } else { - return item.isActive(""); - } + return item.isActive(props); }, onHeadingChange: (callback: (headings: IMarking[]) => void) => { // Subscribe to update event emitted from headers extension diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index 97f22e7723..53aae1f265 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -1,4 +1,5 @@ import { JSONContent } from "@tiptap/core"; +import { Selection } from "@tiptap/pm/state"; // helpers import { IMarking } from "@/helpers/scroll-to-node"; // types @@ -6,14 +7,64 @@ import { IMentionHighlight, IMentionSuggestion, TAIHandler, - TColorEditorCommands, TDisplayConfig, TEmbedConfig, TExtensions, TFileHandler, - TNonColorEditorCommands, TServerHandler, } from "@/types"; +import { TTextAlign } from "@/extensions"; + +export type TEditorCommands = + | "text" + | "h1" + | "h2" + | "h3" + | "h4" + | "h5" + | "h6" + | "bold" + | "italic" + | "underline" + | "strikethrough" + | "bulleted-list" + | "numbered-list" + | "to-do-list" + | "quote" + | "code" + | "table" + | "image" + | "divider" + | "issue-embed" + | "text-color" + | "background-color" + | "text-align" + | "callout"; + +export type TCommandExtraProps = { + image: { + savedSelection: Selection | null; + }; + "text-color": { + color: string | undefined; + }; + "background-color": { + color: string | undefined; + }; + "text-align": { + alignment: TTextAlign; + }; +}; + +// Create a utility type that maps a command to its extra props or an empty object if none are defined +export type TCommandWithProps = T extends keyof TCommandExtraProps + ? TCommandExtraProps[T] // If the command has extra props, include them + : object; // Otherwise, just return the command type with no extra props + +type TCommandWithPropsWithItemKey = T extends keyof TCommandExtraProps + ? { itemKey: T } & TCommandExtraProps[T] + : { itemKey: T }; + // editor refs export type EditorReadOnlyRefApi = { getMarkDown: () => string; @@ -39,26 +90,8 @@ export interface EditorRefApi extends EditorReadOnlyRefApi { scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void; getCurrentCursorPosition: () => number | undefined; setEditorValueAtCursorPosition: (content: string) => void; - executeMenuItemCommand: ( - props: - | { - itemKey: TNonColorEditorCommands; - } - | { - itemKey: TColorEditorCommands; - color: string | undefined; - } - ) => void; - isMenuItemActive: ( - props: - | { - itemKey: TNonColorEditorCommands; - } - | { - itemKey: TColorEditorCommands; - color: string | undefined; - } - ) => boolean; + executeMenuItemCommand: (props: TCommandWithPropsWithItemKey) => void; + isMenuItemActive: (props: TCommandWithPropsWithItemKey) => boolean; onStateChange: (callback: () => void) => () => void; setFocusAtPosition: (position: number) => void; isEditorReadyToDiscard: () => boolean; diff --git a/packages/editor/src/core/types/slash-commands-suggestion.ts b/packages/editor/src/core/types/slash-commands-suggestion.ts index 122231111e..91c93203af 100644 --- a/packages/editor/src/core/types/slash-commands-suggestion.ts +++ b/packages/editor/src/core/types/slash-commands-suggestion.ts @@ -1,33 +1,7 @@ import { CSSProperties } from "react"; import { Editor, Range } from "@tiptap/core"; - -export type TEditorCommands = - | "text" - | "h1" - | "h2" - | "h3" - | "h4" - | "h5" - | "h6" - | "bold" - | "italic" - | "underline" - | "strikethrough" - | "bulleted-list" - | "numbered-list" - | "to-do-list" - | "quote" - | "code" - | "table" - | "image" - | "divider" - | "issue-embed" - | "text-color" - | "background-color" - | "callout"; - -export type TColorEditorCommands = Extract; -export type TNonColorEditorCommands = Exclude; +// types +import { TEditorCommands } from "@/types"; export type CommandProps = { editor: Editor; diff --git a/space/core/components/editor/lite-text-editor.tsx b/space/core/components/editor/lite-text-editor.tsx index 4cd6d82e87..0e3f34293b 100644 --- a/space/core/components/editor/lite-text-editor.tsx +++ b/space/core/components/editor/lite-text-editor.tsx @@ -1,6 +1,6 @@ import React from "react"; // editor -import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TNonColorEditorCommands } from "@plane/editor"; +import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor"; // components import { IssueCommentToolbar } from "@/components/editor"; // helpers @@ -30,11 +30,12 @@ export const LiteTextEditor = React.forwardRef(ref: React.ForwardedRef): ref is React.MutableRefObject { return !!ref && typeof ref === "object" && "current" in ref; } + // derived values const isEmpty = isCommentEmpty(props.initialValue); + const editorRef = isMutableRefObject(ref) ? ref.current : null; return (
@@ -54,18 +55,19 @@ export const LiteTextEditor = React.forwardRef { - if (isMutableRefObject(ref)) { - ref.current?.executeMenuItemCommand({ - itemKey: key as TNonColorEditorCommands, - }); - } + executeCommand={(item) => { + // TODO: update this while toolbar homogenization + // @ts-expect-error type mismatch here + editorRef?.executeMenuItemCommand({ + itemKey: item.itemKey, + ...item.extraProps, + }); }} isSubmitting={isSubmitting} showSubmitButton={showSubmitButton} handleSubmit={(e) => rest.onEnterKeyPress?.(e)} isCommentEmpty={isEmpty} - editorRef={isMutableRefObject(ref) ? ref : null} + editorRef={editorRef} />
); diff --git a/space/core/components/editor/toolbar.tsx b/space/core/components/editor/toolbar.tsx index beccc8cb76..4593aaf653 100644 --- a/space/core/components/editor/toolbar.tsx +++ b/space/core/components/editor/toolbar.tsx @@ -2,21 +2,21 @@ import React, { useEffect, useState, useCallback } from "react"; // editor -import { EditorRefApi, TEditorCommands, TNonColorEditorCommands } from "@plane/editor"; +import { EditorRefApi } from "@plane/editor"; // ui import { Button, Tooltip } from "@plane/ui"; // constants -import { TOOLBAR_ITEMS } from "@/constants/editor"; +import { TOOLBAR_ITEMS, ToolbarMenuItem } from "@/constants/editor"; // helpers import { cn } from "@/helpers/common.helper"; type Props = { - executeCommand: (commandKey: TEditorCommands) => void; + executeCommand: (item: ToolbarMenuItem) => void; handleSubmit: (event: React.MouseEvent) => void; isCommentEmpty: boolean; isSubmitting: boolean; showSubmitButton: boolean; - editorRef: React.MutableRefObject | null; + editorRef: EditorRefApi | null; }; const toolbarItems = TOOLBAR_ITEMS.lite; @@ -28,24 +28,25 @@ export const IssueCommentToolbar: React.FC = (props) => { // Function to update active states const updateActiveStates = useCallback(() => { - if (editorRef?.current) { - const newActiveStates: Record = {}; - Object.values(toolbarItems) - .flat() - .forEach((item) => { - // Assert that editorRef.current is not null - newActiveStates[item.key] = (editorRef.current as EditorRefApi).isMenuItemActive({ - itemKey: item.key as TNonColorEditorCommands, - }); + if (!editorRef) return; + const newActiveStates: Record = {}; + Object.values(toolbarItems) + .flat() + .forEach((item) => { + // TODO: update this while toolbar homogenization + // @ts-expect-error type mismatch here + newActiveStates[item.renderKey] = editorRef.isMenuItemActive({ + itemKey: item.itemKey, + ...item.extraProps, }); - setActiveStates(newActiveStates); - } + }); + setActiveStates(newActiveStates); }, [editorRef]); // useEffect to call updateActiveStates when isActive prop changes useEffect(() => { - if (!editorRef?.current) return; - const unsubscribe = editorRef.current.onStateChange(updateActiveStates); + if (!editorRef) return; + const unsubscribe = editorRef.onStateChange(updateActiveStates); updateActiveStates(); return () => unsubscribe(); }, [editorRef, updateActiveStates]); @@ -61,35 +62,39 @@ export const IssueCommentToolbar: React.FC = (props) => { "pl-0": index === 0, })} > - {toolbarItems[key].map((item) => ( - - {item.name} - {item.shortcut && {item.shortcut.join(" + ")}} -

- } - > - -
- ))} + + + ); + })} ))} diff --git a/space/core/components/issues/peek-overview/comment/add-comment.tsx b/space/core/components/issues/peek-overview/comment/add-comment.tsx index f6c8a452b5..3623f39864 100644 --- a/space/core/components/issues/peek-overview/comment/add-comment.tsx +++ b/space/core/components/issues/peek-overview/comment/add-comment.tsx @@ -91,7 +91,7 @@ export const AddComment: React.FC = observer((props) => { } onChange={(comment_json, comment_html) => onChange(comment_html)} isSubmitting={isSubmitting} - placeholder="Add Comment..." + placeholder="Add comment..." uploadFile={async (file) => { const { asset_id } = await uploadCommentAsset(file, anchor); setUploadAssetIds((prev) => [...prev, asset_id]); diff --git a/space/core/constants/editor.ts b/space/core/constants/editor.ts index 2c6ac2bbb7..6089c56046 100644 --- a/space/core/constants/editor.ts +++ b/space/core/constants/editor.ts @@ -1,5 +1,9 @@ import { + AlignCenter, + AlignLeft, + AlignRight, Bold, + CaseSensitive, Code2, Heading1, Heading2, @@ -19,30 +23,99 @@ import { Underline, } from "lucide-react"; // editor -import { TEditorCommands } from "@plane/editor"; +import { TCommandExtraProps, TEditorCommands } from "@plane/editor"; type TEditorTypes = "lite" | "document"; -export type ToolbarMenuItem = { - key: TEditorCommands; +// Utility type to enforce the necessary extra props or make extraProps optional +type ExtraPropsForCommand = T extends keyof TCommandExtraProps + ? TCommandExtraProps[T] + : object; // Default to empty object for commands without extra props + +export type ToolbarMenuItem = { + itemKey: T; + renderKey: string; name: string; icon: LucideIcon; shortcut?: string[]; editors: TEditorTypes[]; + extraProps?: ExtraPropsForCommand; }; -export const BASIC_MARK_ITEMS: ToolbarMenuItem[] = [ - { key: "h1", name: "Heading 1", icon: Heading1, editors: ["document"] }, - { key: "h2", name: "Heading 2", icon: Heading2, editors: ["document"] }, - { key: "h3", name: "Heading 3", icon: Heading3, editors: ["document"] }, - { key: "h4", name: "Heading 4", icon: Heading4, editors: ["document"] }, - { key: "h5", name: "Heading 5", icon: Heading5, editors: ["document"] }, - { key: "h6", name: "Heading 6", icon: Heading6, editors: ["document"] }, - { key: "bold", name: "Bold", icon: Bold, shortcut: ["Cmd", "B"], editors: ["lite", "document"] }, - { key: "italic", name: "Italic", icon: Italic, shortcut: ["Cmd", "I"], editors: ["lite", "document"] }, - { key: "underline", name: "Underline", icon: Underline, shortcut: ["Cmd", "U"], editors: ["lite", "document"] }, +export const TYPOGRAPHY_ITEMS: ToolbarMenuItem<"text" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6">[] = [ + { itemKey: "text", renderKey: "text", name: "Text", icon: CaseSensitive, editors: ["document"] }, + { itemKey: "h1", renderKey: "h1", name: "Heading 1", icon: Heading1, editors: ["document"] }, + { itemKey: "h2", renderKey: "h2", name: "Heading 2", icon: Heading2, editors: ["document"] }, + { itemKey: "h3", renderKey: "h3", name: "Heading 3", icon: Heading3, editors: ["document"] }, + { itemKey: "h4", renderKey: "h4", name: "Heading 4", icon: Heading4, editors: ["document"] }, + { itemKey: "h5", renderKey: "h5", name: "Heading 5", icon: Heading5, editors: ["document"] }, + { itemKey: "h6", renderKey: "h6", name: "Heading 6", icon: Heading6, editors: ["document"] }, +]; + +export const TEXT_ALIGNMENT_ITEMS: ToolbarMenuItem<"text-align">[] = [ { - key: "strikethrough", + itemKey: "text-align", + renderKey: "text-align-left", + name: "Left align", + icon: AlignLeft, + shortcut: ["Cmd", "Shift", "L"], + editors: ["lite", "document"], + extraProps: { + alignment: "left", + }, + }, + { + itemKey: "text-align", + renderKey: "text-align-center", + name: "Center align", + icon: AlignCenter, + shortcut: ["Cmd", "Shift", "E"], + editors: ["lite", "document"], + extraProps: { + alignment: "center", + }, + }, + { + itemKey: "text-align", + renderKey: "text-align-right", + name: "Right align", + icon: AlignRight, + shortcut: ["Cmd", "Shift", "R"], + editors: ["lite", "document"], + extraProps: { + alignment: "right", + }, + }, +]; + +const BASIC_MARK_ITEMS: ToolbarMenuItem<"bold" | "italic" | "underline" | "strikethrough">[] = [ + { + itemKey: "bold", + renderKey: "bold", + name: "Bold", + icon: Bold, + shortcut: ["Cmd", "B"], + editors: ["lite", "document"], + }, + { + itemKey: "italic", + renderKey: "italic", + name: "Italic", + icon: Italic, + shortcut: ["Cmd", "I"], + editors: ["lite", "document"], + }, + { + itemKey: "underline", + renderKey: "underline", + name: "Underline", + icon: Underline, + shortcut: ["Cmd", "U"], + editors: ["lite", "document"], + }, + { + itemKey: "strikethrough", + renderKey: "strikethrough", name: "Strikethrough", icon: Strikethrough, shortcut: ["Cmd", "Shift", "S"], @@ -50,23 +123,26 @@ export const BASIC_MARK_ITEMS: ToolbarMenuItem[] = [ }, ]; -export const LIST_ITEMS: ToolbarMenuItem[] = [ +const LIST_ITEMS: ToolbarMenuItem<"bulleted-list" | "numbered-list" | "to-do-list">[] = [ { - key: "bulleted-list", + itemKey: "bulleted-list", + renderKey: "bulleted-list", name: "Bulleted list", icon: List, shortcut: ["Cmd", "Shift", "7"], editors: ["lite", "document"], }, { - key: "numbered-list", + itemKey: "numbered-list", + renderKey: "numbered-list", name: "Numbered list", icon: ListOrdered, shortcut: ["Cmd", "Shift", "8"], editors: ["lite", "document"], }, { - key: "to-do-list", + itemKey: "to-do-list", + renderKey: "to-do-list", name: "To-do list", icon: ListTodo, shortcut: ["Cmd", "Shift", "9"], @@ -74,14 +150,14 @@ export const LIST_ITEMS: ToolbarMenuItem[] = [ }, ]; -export const USER_ACTION_ITEMS: ToolbarMenuItem[] = [ - { key: "quote", name: "Quote", icon: TextQuote, editors: ["lite", "document"] }, - { key: "code", name: "Code", icon: Code2, editors: ["lite", "document"] }, +export const USER_ACTION_ITEMS: ToolbarMenuItem<"quote" | "code">[] = [ + { itemKey: "quote", renderKey: "quote", name: "Quote", icon: TextQuote, editors: ["lite", "document"] }, + { itemKey: "code", renderKey: "code", name: "Code", icon: Code2, editors: ["lite", "document"] }, ]; -export const COMPLEX_ITEMS: ToolbarMenuItem[] = [ - { key: "table", name: "Table", icon: Table, editors: ["document"] }, - { key: "image", name: "Image", icon: Image, editors: ["lite", "document"] }, +export const COMPLEX_ITEMS: ToolbarMenuItem<"table" | "image">[] = [ + { itemKey: "table", renderKey: "table", name: "Table", icon: Table, editors: ["document"] }, + { itemKey: "image", renderKey: "image", name: "Image", icon: Image, editors: ["lite", "document"] }, ]; export const TOOLBAR_ITEMS: { @@ -91,12 +167,14 @@ export const TOOLBAR_ITEMS: { } = { lite: { basic: BASIC_MARK_ITEMS.filter((item) => item.editors.includes("lite")), + alignment: TEXT_ALIGNMENT_ITEMS.filter((item) => item.editors.includes("lite")), list: LIST_ITEMS.filter((item) => item.editors.includes("lite")), userAction: USER_ACTION_ITEMS.filter((item) => item.editors.includes("lite")), complex: COMPLEX_ITEMS.filter((item) => item.editors.includes("lite")), }, document: { basic: BASIC_MARK_ITEMS.filter((item) => item.editors.includes("document")), + alignment: TEXT_ALIGNMENT_ITEMS.filter((item) => item.editors.includes("document")), list: LIST_ITEMS.filter((item) => item.editors.includes("document")), userAction: USER_ACTION_ITEMS.filter((item) => item.editors.includes("document")), complex: COMPLEX_ITEMS.filter((item) => item.editors.includes("document")), diff --git a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx index 3e64e83a33..0822f1a97d 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx @@ -1,6 +1,6 @@ import React from "react"; // editor -import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TNonColorEditorCommands } from "@plane/editor"; +import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor"; // types import { IUserLite } from "@plane/types"; // components @@ -61,12 +61,12 @@ export const LiteTextEditor = React.forwardRef(ref: React.ForwardedRef): ref is React.MutableRefObject { return !!ref && typeof ref === "object" && "current" in ref; } + // derived values + const isEmpty = isCommentEmpty(props.initialValue); + const editorRef = isMutableRefObject(ref) ? ref.current : null; return (
@@ -89,19 +89,20 @@ export const LiteTextEditor = React.forwardRef { - if (isMutableRefObject(ref)) { - ref.current?.executeMenuItemCommand({ - itemKey: key as TNonColorEditorCommands, - }); - } + executeCommand={(item) => { + // TODO: update this while toolbar homogenization + // @ts-expect-error type mismatch here + editorRef?.executeMenuItemCommand({ + itemKey: item.itemKey, + ...item.extraProps, + }); }} handleAccessChange={handleAccessChange} handleSubmit={(e) => rest.onEnterKeyPress?.(e)} isCommentEmpty={isEmpty} isSubmitting={isSubmitting} showAccessSpecifier={showAccessSpecifier} - editorRef={isMutableRefObject(ref) ? ref : null} + editorRef={editorRef} showSubmitButton={showSubmitButton} />
diff --git a/web/core/components/editor/lite-text-editor/toolbar.tsx b/web/core/components/editor/lite-text-editor/toolbar.tsx index ecf8c3283c..1951a3170e 100644 --- a/web/core/components/editor/lite-text-editor/toolbar.tsx +++ b/web/core/components/editor/lite-text-editor/toolbar.tsx @@ -3,25 +3,25 @@ import React, { useEffect, useState, useCallback } from "react"; import { Globe2, Lock, LucideIcon } from "lucide-react"; // editor -import { EditorRefApi, TEditorCommands, TNonColorEditorCommands } from "@plane/editor"; +import { EditorRefApi } from "@plane/editor"; // ui import { Button, Tooltip } from "@plane/ui"; // constants -import { TOOLBAR_ITEMS } from "@/constants/editor"; +import { TOOLBAR_ITEMS, ToolbarMenuItem } from "@/constants/editor"; import { EIssueCommentAccessSpecifier } from "@/constants/issue"; // helpers import { cn } from "@/helpers/common.helper"; type Props = { accessSpecifier?: EIssueCommentAccessSpecifier; - executeCommand: (commandKey: TEditorCommands) => void; + executeCommand: (item: ToolbarMenuItem) => void; handleAccessChange?: (accessKey: EIssueCommentAccessSpecifier) => void; handleSubmit: (event: React.MouseEvent) => void; isCommentEmpty: boolean; isSubmitting: boolean; showAccessSpecifier: boolean; showSubmitButton: boolean; - editorRef: React.MutableRefObject | null; + editorRef: EditorRefApi | null; }; type TCommentAccessType = { @@ -63,24 +63,25 @@ export const IssueCommentToolbar: React.FC = (props) => { // Function to update active states const updateActiveStates = useCallback(() => { - if (editorRef?.current) { - const newActiveStates: Record = {}; - Object.values(toolbarItems) - .flat() - .forEach((item) => { - // Assert that editorRef.current is not null - newActiveStates[item.key] = (editorRef.current as EditorRefApi).isMenuItemActive({ - itemKey: item.key as TNonColorEditorCommands, - }); + if (!editorRef) return; + const newActiveStates: Record = {}; + Object.values(toolbarItems) + .flat() + .forEach((item) => { + // TODO: update this while toolbar homogenization + // @ts-expect-error type mismatch here + newActiveStates[item.renderKey] = editorRef.isMenuItemActive({ + itemKey: item.itemKey, + ...item.extraProps, }); - setActiveStates(newActiveStates); - } + }); + setActiveStates(newActiveStates); }, [editorRef]); // useEffect to call updateActiveStates when isActive prop changes useEffect(() => { - if (!editorRef?.current) return; - const unsubscribe = editorRef.current.onStateChange(updateActiveStates); + if (!editorRef) return; + const unsubscribe = editorRef.onStateChange(updateActiveStates); updateActiveStates(); return () => unsubscribe(); }, [editorRef, updateActiveStates]); @@ -122,35 +123,39 @@ export const IssueCommentToolbar: React.FC = (props) => { "pl-0": index === 0, })} > - {toolbarItems[key].map((item) => ( - - {item.name} - {item.shortcut && {item.shortcut.join(" + ")}} -

- } - > - -
- ))} + + + ); + })} ))} diff --git a/web/core/components/pages/editor/header/color-dropdown.tsx b/web/core/components/pages/editor/header/color-dropdown.tsx index 68de2dc36b..2809336c17 100644 --- a/web/core/components/pages/editor/header/color-dropdown.tsx +++ b/web/core/components/pages/editor/header/color-dropdown.tsx @@ -4,13 +4,19 @@ import { memo } from "react"; import { ALargeSmall, Ban } from "lucide-react"; import { Popover } from "@headlessui/react"; // plane editor -import { COLORS_LIST, TColorEditorCommands } from "@plane/editor"; +import { COLORS_LIST, TEditorCommands } from "@plane/editor"; // helpers import { cn } from "@/helpers/common.helper"; type Props = { - handleColorSelect: (key: TColorEditorCommands, color: string | undefined) => void; - isColorActive: (key: TColorEditorCommands, color: string | undefined) => boolean; + handleColorSelect: ( + key: Extract, + color: string | undefined + ) => void; + isColorActive: ( + key: Extract, + color: string | undefined + ) => boolean; }; export const ColorDropdown: React.FC = memo((props) => { diff --git a/web/core/components/pages/editor/header/toolbar.tsx b/web/core/components/pages/editor/header/toolbar.tsx index 447616b532..6e4ffdd5f8 100644 --- a/web/core/components/pages/editor/header/toolbar.tsx +++ b/web/core/components/pages/editor/header/toolbar.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState, useCallback } from "react"; import { Check, ChevronDown } from "lucide-react"; // editor -import { EditorRefApi, TNonColorEditorCommands } from "@plane/editor"; +import { EditorRefApi } from "@plane/editor"; // ui import { CustomMenu, Tooltip } from "@plane/ui"; // components @@ -36,11 +36,13 @@ const ToolbarButton: React.FC = React.memo((props) => { } >