Compare commits

...

5 Commits

Author SHA1 Message Date
Aaryan Khandelwal d74a23c778 refactor: additional options push logic 2024-11-28 13:22:29 +05:30
Aaryan Khandelwal a248807b1d fix: product updates modal 2024-11-27 14:21:34 +05:30
Aaryan Khandelwal a39161dcb2 chore: core file changes 2024-11-27 14:14:02 +05:30
Aaryan Khandelwal c8c9d13e93 chore: core file changes 2024-11-27 14:08:58 +05:30
Aaryan Khandelwal 84bf457980 fix: editor code split 2024-11-27 14:08:27 +05:30
48 changed files with 742 additions and 61 deletions
@@ -15,6 +15,7 @@ import { EditorReadOnlyRefApi, ICollaborativeDocumentReadOnlyEditor } from "@/ty
const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOnlyEditor) => {
const {
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
embedHandler,
@@ -37,6 +38,7 @@ const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOn
}
const { editor, hasServerConnectionFailed, hasServerSynced } = useReadOnlyCollaborativeEditor({
disabledExtensions,
editorClassName,
extensions,
fileHandler,
@@ -12,9 +12,10 @@ import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// plane web types
import { TReadOnlyEmbedConfig } from "@/plane-editor/types";
// types
import { EditorReadOnlyRefApi, IMentionHighlight, TDisplayConfig, TFileHandler } from "@/types";
import { EditorReadOnlyRefApi, IMentionHighlight, TDisplayConfig, TExtensions, TFileHandler } from "@/types";
interface IDocumentReadOnlyEditor {
disabledExtensions: TExtensions[];
id: string;
initialValue: string;
containerClassName: string;
@@ -33,6 +34,7 @@ interface IDocumentReadOnlyEditor {
const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
const {
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
embedHandler,
@@ -53,6 +55,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
}
const editor = useReadOnlyEditor({
disabledExtensions,
editorClassName,
extensions,
fileHandler,
@@ -19,6 +19,7 @@ export const EditorWrapper: React.FC<Props> = (props) => {
const {
children,
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
extensions,
@@ -37,6 +38,7 @@ export const EditorWrapper: React.FC<Props> = (props) => {
} = props;
const editor = useEditor({
disabledExtensions,
editorClassName,
enableHistory: true,
extensions,
@@ -12,6 +12,7 @@ import { IReadOnlyEditorProps } from "@/types";
export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
const {
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
fileHandler,
@@ -22,6 +23,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
} = props;
const editor = useReadOnlyEditor({
disabledExtensions,
editorClassName,
fileHandler,
forwardedRef,
@@ -8,12 +8,7 @@ import { SideMenuExtension, SlashCommands } from "@/extensions";
import { EditorRefApi, IRichTextEditor } from "@/types";
const RichTextEditor = (props: IRichTextEditor) => {
const {
disabledExtensions,
dragDropEnabled,
bubbleMenuEnabled = true,
extensions: externalExtensions = [],
} = props;
const { disabledExtensions, dragDropEnabled, bubbleMenuEnabled = true, extensions: externalExtensions = [] } = props;
const getExtensions = useCallback(() => {
const extensions = [
@@ -24,7 +19,11 @@ const RichTextEditor = (props: IRichTextEditor) => {
}),
];
if (!disabledExtensions?.includes("slash-commands")) {
extensions.push(SlashCommands());
extensions.push(
SlashCommands({
disabledExtensions,
})
);
}
return extensions;
@@ -32,11 +32,12 @@ import {
// helpers
import { isValidHttpUrl } from "@/helpers/common";
// types
import { IMentionHighlight, IMentionSuggestion, TFileHandler } from "@/types";
import { IMentionHighlight, IMentionSuggestion, TExtensions, TFileHandler } from "@/types";
// plane editor extensions
import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions";
type TArguments = {
disabledExtensions: TExtensions[];
enableHistory: boolean;
fileHandler: TFileHandler;
mentionConfig: {
@@ -48,7 +49,7 @@ type TArguments = {
};
export const CoreEditorExtensions = (args: TArguments): Extensions => {
const { enableHistory, fileHandler, mentionConfig, placeholder, tabIndex } = args;
const { disabledExtensions, enableHistory, fileHandler, mentionConfig, placeholder, tabIndex } = args;
return [
StarterKit.configure({
@@ -163,6 +164,8 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
CharacterCount,
CustomTextAlignExtension,
CustomColorExtension,
...CoreEditorAdditionalExtensions(),
...CoreEditorAdditionalExtensions({
disabledExtensions,
}),
];
};
@@ -28,11 +28,12 @@ import {
// helpers
import { isValidHttpUrl } from "@/helpers/common";
// types
import { IMentionHighlight, TFileHandler } from "@/types";
import { IMentionHighlight, TExtensions, TFileHandler } from "@/types";
// plane editor extensions
import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions";
type Props = {
disabledExtensions: TExtensions[];
fileHandler: Pick<TFileHandler, "getAssetSrc">;
mentionConfig: {
mentionHighlights?: () => Promise<IMentionHighlight[]>;
@@ -40,7 +41,7 @@ type Props = {
};
export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
const { fileHandler, mentionConfig } = props;
const { disabledExtensions, fileHandler, mentionConfig } = props;
return [
StarterKit.configure({
@@ -129,6 +130,8 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
CustomColorExtension,
HeadingListExtension,
CustomTextAlignExtension,
...CoreReadOnlyEditorAdditionalExtensions(),
...CoreReadOnlyEditorAdditionalExtensions({
disabledExtensions,
}),
];
};
@@ -37,7 +37,10 @@ import {
setText,
} from "@/helpers/editor-commands";
// types
import { CommandProps, ISlashCommandItem, TSlashCommandSectionKeys } from "@/types";
import { CommandProps, ISlashCommandItem, TExtensions, TSlashCommandSectionKeys } from "@/types";
// plane editor extensions
import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions";
// local types
import { TSlashCommandAdditionalOption } from "./root";
export type TSlashCommandSection = {
@@ -46,9 +49,15 @@ export type TSlashCommandSection = {
items: ISlashCommandItem[];
};
type TArgs = {
additionalOptions?: TSlashCommandAdditionalOption[];
disabledExtensions: TExtensions[];
};
export const getSlashCommandFilteredSections =
(additionalOptions?: TSlashCommandAdditionalOption[]) =>
(args: TArgs) =>
({ query }: { query: string }): TSlashCommandSection[] => {
const { additionalOptions, disabledExtensions } = args;
const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [
{
key: "general",
@@ -269,15 +278,16 @@ export const getSlashCommandFilteredSections =
},
];
additionalOptions?.forEach((item) => {
[
...(additionalOptions ?? []),
...coreEditorAdditionalSlashCommandOptions({
disabledExtensions,
}),
]?.forEach((item) => {
const sectionToPushTo = SLASH_COMMAND_SECTIONS.find((s) => s.key === item.section) ?? SLASH_COMMAND_SECTIONS[0];
const itemIndexToPushAfter = sectionToPushTo.items.findIndex((i) => i.commandKey === item.pushAfter);
if (itemIndexToPushAfter !== undefined) {
const resolvedIndex =
itemIndexToPushAfter + 1 < sectionToPushTo.items.length
? itemIndexToPushAfter + 1
: sectionToPushTo.items.length - 1;
sectionToPushTo.items.splice(resolvedIndex, 0, item);
if (itemIndexToPushAfter !== -1) {
sectionToPushTo.items.splice(itemIndexToPushAfter + 1, 0, item);
} else {
sectionToPushTo.items.push(item);
}
@@ -3,7 +3,7 @@ import { ReactRenderer } from "@tiptap/react";
import Suggestion, { SuggestionOptions } from "@tiptap/suggestion";
import tippy from "tippy.js";
// types
import { ISlashCommandItem, TEditorCommands, TSlashCommandSectionKeys } from "@/types";
import { ISlashCommandItem, TEditorCommands, TExtensions, TSlashCommandSectionKeys } from "@/types";
// components
import { getSlashCommandFilteredSections } from "./command-items-list";
import { SlashCommandsMenu, SlashCommandsMenuProps } from "./command-menu";
@@ -107,10 +107,15 @@ const renderItems = () => {
};
};
export const SlashCommands = (additionalOptions?: TSlashCommandAdditionalOption[]) =>
type TExtensionProps = {
additionalOptions?: TSlashCommandAdditionalOption[];
disabledExtensions: TExtensions[];
};
export const SlashCommands = (props: TExtensionProps) =>
Command.configure({
suggestion: {
items: getSlashCommandFilteredSections(additionalOptions),
items: getSlashCommandFilteredSections(props),
render: renderItems,
},
});
@@ -75,6 +75,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
}, [provider, id]);
const editor = useEditor({
disabledExtensions,
id,
onTransaction,
editorProps,
+11 -1
View File
@@ -16,12 +16,20 @@ import { IMarking, scrollSummary, scrollToNodeViaDOMCoordinates } from "@/helper
// props
import { CoreEditorProps } from "@/props";
// types
import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TEditorCommands, TFileHandler } from "@/types";
import {
EditorRefApi,
IMentionHighlight,
IMentionSuggestion,
TEditorCommands,
TExtensions,
TFileHandler,
} from "@/types";
export interface CustomEditorProps {
editorClassName: string;
editorProps?: EditorProps;
enableHistory: boolean;
disabledExtensions: TExtensions[];
extensions?: any;
fileHandler: TFileHandler;
forwardedRef?: MutableRefObject<EditorRefApi | null>;
@@ -45,6 +53,7 @@ export interface CustomEditorProps {
export const useEditor = (props: CustomEditorProps) => {
const {
disabledExtensions,
editorClassName,
editorProps = {},
enableHistory,
@@ -79,6 +88,7 @@ export const useEditor = (props: CustomEditorProps) => {
},
extensions: [
...CoreEditorExtensions({
disabledExtensions,
enableHistory,
fileHandler,
mentionConfig: {
@@ -11,6 +11,7 @@ import { TReadOnlyCollaborativeEditorProps } from "@/types";
export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEditorProps) => {
const {
disabledExtensions,
editorClassName,
editorProps = {},
extensions,
@@ -66,6 +67,7 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit
}, [provider, id]);
const editor = useReadOnlyEditor({
disabledExtensions,
editorProps,
editorClassName,
extensions: [
@@ -11,14 +11,15 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
// props
import { CoreReadOnlyEditorProps } from "@/props";
// types
import { EditorReadOnlyRefApi, IMentionHighlight, TFileHandler } from "@/types";
import { EditorReadOnlyRefApi, IMentionHighlight, TExtensions, TFileHandler } from "@/types";
interface CustomReadOnlyEditorProps {
initialValue?: string;
disabledExtensions: TExtensions[];
editorClassName: string;
forwardedRef?: MutableRefObject<EditorReadOnlyRefApi | null>;
extensions?: any;
editorProps?: EditorProps;
extensions?: any;
forwardedRef?: MutableRefObject<EditorReadOnlyRefApi | null>;
initialValue?: string;
fileHandler: Pick<TFileHandler, "getAssetSrc">;
handleEditorReady?: (value: boolean) => void;
mentionHandler: {
@@ -29,6 +30,7 @@ interface CustomReadOnlyEditorProps {
export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
const {
disabledExtensions,
initialValue,
editorClassName,
forwardedRef,
@@ -54,6 +56,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
},
extensions: [
...CoreReadOnlyEditorExtensions({
disabledExtensions,
mentionConfig: {
mentionHighlights: mentionHandler.highlights,
},
@@ -20,7 +20,7 @@ export type TServerHandler = {
};
type TCollaborativeEditorHookProps = {
disabledExtensions?: TExtensions[];
disabledExtensions: TExtensions[];
editorClassName: string;
editorProps?: EditorProps;
extensions?: Extensions;
+2 -1
View File
@@ -106,7 +106,7 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
export interface IEditorProps {
containerClassName?: string;
displayConfig?: TDisplayConfig;
disabledExtensions?: TExtensions[];
disabledExtensions: TExtensions[];
editorClassName?: string;
fileHandler: TFileHandler;
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
@@ -148,6 +148,7 @@ export interface ICollaborativeDocumentEditor
// read only editor props
export interface IReadOnlyEditorProps {
containerClassName?: string;
disabledExtensions: TExtensions[];
displayConfig?: TDisplayConfig;
editorClassName?: string;
fileHandler: Pick<TFileHandler, "getAssetSrc">;
+1 -1
View File
@@ -1 +1 @@
export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed" | "slash-commands"| "enter-key";
export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed" | "slash-commands" | "enter-key" | "callout";
@@ -0,0 +1,56 @@
import React, { useState } from "react";
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
// constants
import { COLORS_LIST } from "@/constants/common";
// local components
import { CalloutBlockColorSelector } from "./color-selector";
import { CalloutBlockLogoSelector } from "./logo-selector";
// types
import { EAttributeNames, TCalloutBlockAttributes } from "./types";
// utils
import { updateStoredBackgroundColor } from "./utils";
type Props = NodeViewProps & {
node: NodeViewProps["node"] & {
attrs: TCalloutBlockAttributes;
};
updateAttributes: (attrs: Partial<TCalloutBlockAttributes>) => void;
};
export const CustomCalloutBlock: React.FC<Props> = (props) => {
const { editor, node, updateAttributes } = props;
// states
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
// derived values
const activeBackgroundColor = COLORS_LIST.find((c) => node.attrs["data-background"] === c.key)?.backgroundColor;
return (
<NodeViewWrapper
className="editor-callout-component group/callout-node relative bg-custom-background-90 rounded-lg text-custom-text-100 p-4 my-2 flex items-start gap-4 transition-colors duration-500 break-words"
style={{
backgroundColor: activeBackgroundColor,
}}
>
<CalloutBlockLogoSelector
blockAttributes={node.attrs}
disabled={!editor.isEditable}
isOpen={isEmojiPickerOpen}
handleOpen={(val) => setIsEmojiPickerOpen(val)}
updateAttributes={updateAttributes}
/>
<CalloutBlockColorSelector
disabled={!editor.isEditable}
isOpen={isColorPickerOpen}
toggleDropdown={() => setIsColorPickerOpen((prev) => !prev)}
onSelect={(val) => {
updateAttributes({
[EAttributeNames.BACKGROUND]: val,
});
updateStoredBackgroundColor(val);
}}
/>
<NodeViewContent as="div" className="w-full break-words" />
</NodeViewWrapper>
);
};
@@ -0,0 +1,75 @@
import { Ban, ChevronDown } from "lucide-react";
// constants
import { COLORS_LIST } from "@/constants/common";
// helpers
import { cn } from "@/helpers/common";
type Props = {
disabled: boolean;
isOpen: boolean;
onSelect: (color: string | null) => void;
toggleDropdown: () => void;
};
export const CalloutBlockColorSelector: React.FC<Props> = (props) => {
const { disabled, isOpen, onSelect, toggleDropdown } = props;
const handleColorSelect = (val: string | null) => {
onSelect(val);
toggleDropdown();
};
return (
<div
className={cn("opacity-0 pointer-events-none absolute top-2 right-2 z-10 transition-opacity", {
"group-hover/callout-node:opacity-100 group-hover/callout-node:pointer-events-auto": !disabled,
"opacity-100 pointer-events-auto": isOpen,
})}
contentEditable={false}
>
<div className="relative">
<button
type="button"
onClick={(e) => {
toggleDropdown();
e.stopPropagation();
}}
className={cn(
"flex items-center gap-1 h-full whitespace-nowrap py-1 px-2.5 text-sm font-medium text-custom-text-300 hover:bg-white/10 active:bg-custom-background-80 rounded transition-colors",
{
"bg-white/10": isOpen,
}
)}
disabled={disabled}
>
<span>Color</span>
<ChevronDown className="flex-shrink-0 size-3" />
</button>
{isOpen && (
<section className="absolute top-full right-0 z-10 mt-1 rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 p-2 shadow-custom-shadow-rg animate-in fade-in slide-in-from-top-1">
<div className="flex items-center gap-2">
{COLORS_LIST.map((color) => (
<button
key={color.key}
type="button"
className="flex-shrink-0 size-6 rounded border-[0.5px] border-custom-border-400 hover:opacity-60 transition-opacity"
style={{
backgroundColor: color.backgroundColor,
}}
onClick={() => handleColorSelect(color.key)}
/>
))}
<button
type="button"
className="flex-shrink-0 size-6 grid place-items-center rounded text-custom-text-300 border-[0.5px] border-custom-border-400 hover:bg-custom-background-80 transition-colors"
onClick={() => handleColorSelect(null)}
>
<Ban className="size-4" />
</button>
</div>
</section>
)}
</div>
</div>
);
};
@@ -0,0 +1,72 @@
import { Node, mergeAttributes } from "@tiptap/core";
import { Node as NodeType } from "@tiptap/pm/model";
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
// types
import { EAttributeNames, TCalloutBlockAttributes } from "./types";
// utils
import { DEFAULT_CALLOUT_BLOCK_ATTRIBUTES } from "./utils";
// Extend Tiptap's Commands interface
declare module "@tiptap/core" {
interface Commands<ReturnType> {
calloutComponent: {
insertCallout: () => ReturnType;
};
}
}
export const CustomCalloutExtensionConfig = Node.create({
name: "calloutComponent",
group: "block",
content: "block+",
addAttributes() {
const attributes = {
// Reduce instead of map to accumulate the attributes directly into an object
...Object.values(EAttributeNames).reduce((acc, value) => {
acc[value] = {
default: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[value],
};
return acc;
}, {}),
};
return attributes;
},
addStorage() {
return {
markdown: {
serialize(state: MarkdownSerializerState, node: NodeType) {
const attrs = node.attrs as TCalloutBlockAttributes;
const logoInUse = attrs["data-logo-in-use"];
// add callout logo
if (logoInUse === "emoji") {
state.write(
`> <img src="${attrs["data-emoji-url"]}" alt="${attrs["data-emoji-unicode"]}" width="30px" />\n`
);
} else {
state.write(`> <icon>${attrs["data-icon-name"]} icon</icon>\n`);
}
// add an empty line after the logo
state.write("> \n");
// add '> ' before each line of the callout content
state.wrapBlock("> ", null, node, () => state.renderContent(node));
state.closeBlock(node);
},
},
};
},
parseHTML() {
return [
{
tag: `div[${EAttributeNames.BLOCK_TYPE}="${DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[EAttributeNames.BLOCK_TYPE]}"]`,
},
];
},
// Render HTML for the callout node
renderHTML({ HTMLAttributes }) {
return ["div", mergeAttributes(HTMLAttributes), 0];
},
});
@@ -0,0 +1,68 @@
import { findParentNodeClosestToPos, Predicate, ReactNodeViewRenderer } from "@tiptap/react";
// helpers
import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary";
// components
import { CustomCalloutBlock } from "./block";
// config
import { CustomCalloutExtensionConfig } from "./extension-config";
// utils
import { getStoredBackgroundColor, getStoredLogo } from "./utils";
export const CustomCalloutExtension = CustomCalloutExtensionConfig.extend({
selectable: true,
draggable: true,
addCommands() {
return {
insertCallout:
() =>
({ commands }) => {
// get stored logo values and background color from the local storage
const storedLogoValues = getStoredLogo();
const storedBackgroundValue = getStoredBackgroundColor();
return commands.insertContent({
type: this.name,
content: [
{
type: "paragraph",
},
],
attrs: {
...storedLogoValues,
"data-background": storedBackgroundValue,
},
});
},
};
},
addKeyboardShortcuts() {
return {
Backspace: ({ editor }) => {
const { $from, empty } = editor.state.selection;
try {
const isParentNodeCallout: Predicate = (node) => node.type === this.type;
const parentNodeDetails = findParentNodeClosestToPos($from, isParentNodeCallout);
// Check if selection is empty and at the beginning of the callout
if (empty && parentNodeDetails) {
const isCursorAtCalloutBeginning = $from.pos === parentNodeDetails.start + 1;
if (parentNodeDetails.node.content.size > 2 && isCursorAtCalloutBeginning) {
editor.commands.setTextSelection(parentNodeDetails.pos - 1);
return true;
}
}
} catch (error) {
console.error("Error in performing backspace action on callout", error);
}
return false; // Allow the default behavior if conditions are not met
},
ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name),
ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name),
};
},
addNodeView() {
return ReactNodeViewRenderer(CustomCalloutBlock);
},
});
@@ -0,0 +1,2 @@
export * from "./extension";
export * from "./read-only-extension";
@@ -0,0 +1,97 @@
// plane helpers
import { convertHexEmojiToDecimal } from "@plane/helpers";
// plane ui
import { EmojiIconPicker, EmojiIconPickerTypes, Logo, TEmojiLogoProps } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common";
// types
import { TCalloutBlockAttributes } from "./types";
// utils
import { DEFAULT_CALLOUT_BLOCK_ATTRIBUTES, updateStoredLogo } from "./utils";
type Props = {
blockAttributes: TCalloutBlockAttributes;
disabled: boolean;
handleOpen: (val: boolean) => void;
isOpen: boolean;
updateAttributes: (attrs: Partial<TCalloutBlockAttributes>) => void;
};
export const CalloutBlockLogoSelector: React.FC<Props> = (props) => {
const { blockAttributes, disabled, handleOpen, isOpen, updateAttributes } = props;
const logoValue: TEmojiLogoProps = {
in_use: blockAttributes["data-logo-in-use"],
icon: {
color: blockAttributes["data-icon-color"],
name: blockAttributes["data-icon-name"],
},
emoji: {
value: blockAttributes["data-emoji-unicode"]?.toString(),
url: blockAttributes["data-emoji-url"],
},
};
return (
<div contentEditable={false}>
<EmojiIconPicker
closeOnSelect={false}
isOpen={isOpen}
handleToggle={handleOpen}
className="flex-shrink-0 grid place-items-center"
buttonClassName={cn("flex-shrink-0 size-8 grid place-items-center rounded-lg", {
"hover:bg-white/10": !disabled,
})}
label={<Logo logo={logoValue} size={18} type="lucide" />}
onChange={(val) => {
// construct the new logo value based on the type of value
let newLogoValue: Partial<TCalloutBlockAttributes> = {};
let newLogoValueToStoreInLocalStorage: TEmojiLogoProps = {
in_use: "emoji",
emoji: {
value: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-unicode"],
url: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-url"],
},
};
if (val.type === "emoji") {
newLogoValue = {
"data-emoji-unicode": convertHexEmojiToDecimal(val.value.unified),
"data-emoji-url": val.value.imageUrl,
};
newLogoValueToStoreInLocalStorage = {
in_use: "emoji",
emoji: {
value: convertHexEmojiToDecimal(val.value.unified),
url: val.value.imageUrl,
},
};
} else if (val.type === "icon") {
newLogoValue = {
"data-icon-name": val.value.name,
"data-icon-color": val.value.color,
};
newLogoValueToStoreInLocalStorage = {
in_use: "icon",
icon: {
name: val.value.name,
color: val.value.color,
},
};
}
// update node attributes
updateAttributes({
"data-logo-in-use": val.type,
...newLogoValue,
});
// update stored logo in local storage
updateStoredLogo(newLogoValueToStoreInLocalStorage);
handleOpen(false);
}}
defaultIconColor={logoValue?.in_use && logoValue.in_use === "icon" ? logoValue?.icon?.color : undefined}
defaultOpen={logoValue.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON}
disabled={disabled}
searchDisabled
/>
</div>
);
};
@@ -0,0 +1,14 @@
import { ReactNodeViewRenderer } from "@tiptap/react";
// components
import { CustomCalloutBlock } from "./block";
// config
import { CustomCalloutExtensionConfig } from "./extension-config";
export const CustomCalloutReadOnlyExtension = CustomCalloutExtensionConfig.extend({
selectable: false,
draggable: false,
addNodeView() {
return ReactNodeViewRenderer(CustomCalloutBlock);
},
});
@@ -0,0 +1,26 @@
export enum EAttributeNames {
ICON_COLOR = "data-icon-color",
ICON_NAME = "data-icon-name",
EMOJI_UNICODE = "data-emoji-unicode",
EMOJI_URL = "data-emoji-url",
LOGO_IN_USE = "data-logo-in-use",
BACKGROUND = "data-background",
BLOCK_TYPE = "data-block-type",
}
export type TCalloutBlockIconAttributes = {
[EAttributeNames.ICON_COLOR]: string | undefined;
[EAttributeNames.ICON_NAME]: string | undefined;
};
export type TCalloutBlockEmojiAttributes = {
[EAttributeNames.EMOJI_UNICODE]: string | undefined;
[EAttributeNames.EMOJI_URL]: string | undefined;
};
export type TCalloutBlockAttributes = {
[EAttributeNames.LOGO_IN_USE]: "emoji" | "icon";
[EAttributeNames.BACKGROUND]: string;
[EAttributeNames.BLOCK_TYPE]: "callout-component";
} & TCalloutBlockIconAttributes &
TCalloutBlockEmojiAttributes;
@@ -0,0 +1,85 @@
// plane helpers
import { sanitizeHTML } from "@plane/helpers";
// plane ui
import { TEmojiLogoProps } from "@plane/ui";
// types
import {
EAttributeNames,
TCalloutBlockAttributes,
TCalloutBlockEmojiAttributes,
TCalloutBlockIconAttributes,
} from "./types";
export const DEFAULT_CALLOUT_BLOCK_ATTRIBUTES: TCalloutBlockAttributes = {
"data-logo-in-use": "emoji",
"data-icon-color": null,
"data-icon-name": null,
"data-emoji-unicode": "128161",
"data-emoji-url": "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png",
"data-background": null,
"data-block-type": "callout-component",
};
type TStoredLogoValue = Pick<TCalloutBlockAttributes, EAttributeNames.LOGO_IN_USE> &
(TCalloutBlockEmojiAttributes | TCalloutBlockIconAttributes);
// function to get the stored logo from local storage
export const getStoredLogo = (): TStoredLogoValue => {
const fallBackValues: TStoredLogoValue = {
"data-logo-in-use": "emoji",
"data-emoji-unicode": DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-unicode"],
"data-emoji-url": DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-url"],
};
if (typeof window !== "undefined") {
const storedData = sanitizeHTML(localStorage.getItem("editor-calloutComponent-logo"));
if (storedData) {
let parsedData: TEmojiLogoProps;
try {
parsedData = JSON.parse(storedData);
} catch (error) {
console.error(`Error parsing stored callout logo, stored value- ${storedData}`, error);
localStorage.removeItem("editor-calloutComponent-logo");
return fallBackValues;
}
if (parsedData.in_use === "emoji" && parsedData.emoji?.value) {
return {
"data-logo-in-use": "emoji",
"data-emoji-unicode": parsedData.emoji.value || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-unicode"],
"data-emoji-url": parsedData.emoji.url || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-url"],
};
}
if (parsedData.in_use === "icon" && parsedData.icon?.name) {
return {
"data-logo-in-use": "icon",
"data-icon-name": parsedData.icon.name || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-icon-name"],
"data-icon-color": parsedData.icon.color || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-icon-color"],
};
}
}
}
// fallback values
return fallBackValues;
};
// function to update the stored logo on local storage
export const updateStoredLogo = (value: TEmojiLogoProps): void => {
if (typeof window === "undefined") return;
localStorage.setItem("editor-calloutComponent-logo", JSON.stringify(value));
};
// function to get the stored background color from local storage
export const getStoredBackgroundColor = (): string | null => {
if (typeof window !== "undefined") {
return sanitizeHTML(localStorage.getItem("editor-calloutComponent-background"));
}
return null;
};
// function to update the stored background color on local storage
export const updateStoredBackgroundColor = (value: string | null): void => {
if (typeof window === "undefined") return;
if (value === null) {
localStorage.removeItem("editor-calloutComponent-background");
return;
} else {
localStorage.setItem("editor-calloutComponent-background", value);
}
};
@@ -0,0 +1,16 @@
import { Extensions } from "@tiptap/core";
// types
import { TExtensions } from "@/types";
// extensions
import { CustomCalloutExtension } from "src/ee/extensions/callout";
type Props = {
disabledExtensions: TExtensions[];
};
export const CoreEditorAdditionalExtensions = (props: Props): Extensions => {
const { disabledExtensions } = props;
const extensions: Extensions = [];
if (!disabledExtensions.includes("callout")) extensions.push(CustomCalloutExtension);
return extensions;
};
@@ -0,0 +1,2 @@
export * from "./extensions";
export * from "./read-only-extensions";
@@ -0,0 +1,16 @@
import { Extensions } from "@tiptap/core";
// types
import { TExtensions } from "@/types";
// extensions
import { CustomCalloutReadOnlyExtension } from "src/ee/extensions/callout";
type Props = {
disabledExtensions: TExtensions[];
};
export const CoreReadOnlyEditorAdditionalExtensions = (props: Props): Extensions => {
const { disabledExtensions } = props;
const extensions: Extensions = [];
if (!disabledExtensions.includes("callout")) extensions.push(CustomCalloutReadOnlyExtension);
return extensions;
};
@@ -0,0 +1,5 @@
import { Extensions } from "@tiptap/core";
// extensions
import { CustomCalloutExtensionConfig } from "src/ee/extensions/callout/extension-config";
export const CoreEditorAdditionalExtensionsWithoutProps: Extensions = [CustomCalloutExtensionConfig];
@@ -3,7 +3,7 @@ import { Extensions } from "@tiptap/core";
// ui
import { LayersIcon } from "@plane/ui";
// extensions
import { SlashCommands } from "@/extensions";
import { SlashCommands, TSlashCommandAdditionalOption } from "@/extensions";
// plane editor extensions
import { IssueEmbedSuggestions, IssueListRenderer } from "@/plane-editor/extensions";
// plane editor types
@@ -27,25 +27,30 @@ export const DocumentEditorAdditionalExtensions = (props: Props) => {
const isCollaborationCursorDisabled = !!disabledExtensions?.includes("collaboration-cursor");
const extensions: Extensions = [];
const additionalSlashCommandOptions: TSlashCommandAdditionalOption[] = [];
if (!isIssueEmbedDisabled) {
const slashCommandAdditionalOptions: ISlashCommandItem[] = [
{
commandKey: "issue-embed",
key: "issue-embed",
title: "Issue embed",
description: "Embed an issue from the project.",
searchTerms: ["issue", "link", "embed"],
icon: <LayersIcon className="size-3.5" />,
command: ({ editor, range }) => {
editor.chain().focus().insertContentAt(range, "<p>#issue_</p>").run();
},
const issueEmbedOption: TSlashCommandAdditionalOption = {
commandKey: "issue-embed",
key: "issue-embed",
title: "Issue embed",
description: "Embed an issue from the project.",
searchTerms: ["issue", "link", "embed"],
icon: <LayersIcon className="size-3.5" />,
command: ({ editor, range }) => {
editor.chain().focus().insertContentAt(range, "<p>#issue_</p>").run();
},
];
extensions.push(SlashCommands(slashCommandAdditionalOptions));
} else {
extensions.push(SlashCommands());
section: "general",
pushAfter: "callout",
};
additionalSlashCommandOptions.push(issueEmbedOption);
}
extensions.push(
SlashCommands({
additionalOptions: additionalSlashCommandOptions,
disabledExtensions,
})
);
if (issueEmbedConfig && !isIssueEmbedDisabled) {
extensions.push(
@@ -1,2 +1,4 @@
export * from "./core";
export * from "./issue-suggestions";
export * from "./document-extensions";
export * from "./slash-commands";
@@ -0,0 +1,31 @@
import { MessageSquareText } from "lucide-react";
// extensions
import { TSlashCommandAdditionalOption } from "@/extensions";
// types
import { TExtensions } from "@/types";
// helpers
import { insertCallout } from "src/ee/helpers/editor-commands";
type Props = {
disabledExtensions: TExtensions[];
};
export const coreEditorAdditionalSlashCommandOptions = (props: Props): TSlashCommandAdditionalOption[] => {
const { disabledExtensions } = props;
const options: TSlashCommandAdditionalOption[] = [];
if (!disabledExtensions.includes("callout")) {
options.push({
commandKey: "callout",
key: "callout",
title: "Callout",
description: "Insert callout",
searchTerms: ["callout", "comment", "message", "info", "alert"],
icon: <MessageSquareText className="size-3.5" />,
command: ({ editor, range }) => insertCallout(editor, range),
section: "general",
pushAfter: "image",
});
}
return options;
};
@@ -0,0 +1,6 @@
import { Editor, Range } from "@tiptap/core";
export const insertCallout = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).insertCallout().run();
else editor.chain().focus().insertCallout().run();
};
+1
View File
@@ -0,0 +1 @@
export type TEditorAdditionalCommands = "callout";
+1
View File
@@ -1 +1,2 @@
export * from "./editor";
export * from "./issue-embed";
@@ -10,7 +10,8 @@ import { isCommentEmpty } from "@/helpers/string.helper";
// hooks
import { useMention } from "@/hooks/use-mention";
interface LiteTextEditorWrapperProps extends Omit<ILiteTextEditor, "fileHandler" | "mentionHandler"> {
interface LiteTextEditorWrapperProps
extends Omit<ILiteTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
anchor: string;
workspaceId: string;
isSubmitting?: boolean;
@@ -41,6 +42,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
<div className="border border-custom-border-200 rounded p-3 space-y-3">
<LiteTextEditorWithRef
ref={ref}
disabledExtensions={[]}
fileHandler={getEditorFileHandlers({
anchor,
uploadFile,
@@ -7,7 +7,10 @@ import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
// hooks
import { useMention } from "@/hooks/use-mention";
type LiteTextReadOnlyEditorWrapperProps = Omit<ILiteTextReadOnlyEditor, "fileHandler" | "mentionHandler"> & {
type LiteTextReadOnlyEditorWrapperProps = Omit<
ILiteTextReadOnlyEditor,
"disabledExtensions" | "fileHandler" | "mentionHandler"
> & {
anchor: string;
};
@@ -18,6 +21,7 @@ export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Lit
return (
<LiteTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={[]}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
})}
@@ -4,7 +4,8 @@ import { EditorRefApi, IMentionHighlight, IRichTextEditor, RichTextEditorWithRef
// helpers
import { getEditorFileHandlers } from "@/helpers/editor.helper";
interface RichTextEditorWrapperProps extends Omit<IRichTextEditor, "fileHandler" | "mentionHandler"> {
interface RichTextEditorWrapperProps
extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
anchor: string;
uploadFile: (file: File) => Promise<string>;
workspaceId: string;
@@ -15,6 +16,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
return (
<RichTextEditorWithRef
disabledExtensions={[]}
mentionHandler={{
highlights: function (): Promise<IMentionHighlight[]> {
throw new Error("Function not implemented.");
@@ -7,7 +7,10 @@ import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
// hooks
import { useMention } from "@/hooks/use-mention";
type RichTextReadOnlyEditorWrapperProps = Omit<IRichTextReadOnlyEditor, "fileHandler" | "mentionHandler"> & {
type RichTextReadOnlyEditorWrapperProps = Omit<
IRichTextReadOnlyEditor,
"disabledExtensions" | "fileHandler" | "mentionHandler"
> & {
anchor: string;
};
@@ -18,6 +21,7 @@ export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Ric
return (
<RichTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={[]}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
})}
@@ -14,9 +14,11 @@ import { isCommentEmpty } from "@/helpers/string.helper";
// hooks
import { useMember, useMention, useUser } from "@/hooks/store";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { useFileSize } from "@/plane-web/hooks/use-file-size";
interface LiteTextEditorWrapperProps extends Omit<ILiteTextEditor, "fileHandler" | "mentionHandler"> {
interface LiteTextEditorWrapperProps
extends Omit<ILiteTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
workspaceSlug: string;
workspaceId: string;
projectId: string;
@@ -49,6 +51,8 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
getUserDetails,
project: { getProjectMemberIds },
} = useMember();
// editor flaggings
const { liteTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
// derived values
const projectMemberIds = getProjectMemberIds(projectId);
const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
@@ -72,6 +76,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
<div className="border border-custom-border-200 rounded p-3 space-y-3">
<LiteTextEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions}
fileHandler={getEditorFileHandlers({
maxFileSize,
projectId,
@@ -6,8 +6,13 @@ import { cn } from "@/helpers/common.helper";
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
// hooks
import { useMention, useUser } from "@/hooks/store";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
type LiteTextReadOnlyEditorWrapperProps = Omit<ILiteTextReadOnlyEditor, "fileHandler" | "mentionHandler"> & {
type LiteTextReadOnlyEditorWrapperProps = Omit<
ILiteTextReadOnlyEditor,
"disabledExtensions" | "fileHandler" | "mentionHandler"
> & {
workspaceSlug: string;
projectId: string;
};
@@ -19,10 +24,13 @@ export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Lit
const { mentionHighlights } = useMention({
user: currentUser,
});
// editor flaggings
const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
return (
<LiteTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions}
fileHandler={getReadOnlyEditorFileHandlers({
projectId,
workspaceSlug,
@@ -9,9 +9,11 @@ import { getEditorFileHandlers } from "@/helpers/editor.helper";
// hooks
import { useMember, useMention, useUser } from "@/hooks/store";
// 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, "fileHandler" | "mentionHandler"> {
interface RichTextEditorWrapperProps
extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
workspaceSlug: string;
workspaceId: string;
projectId: string;
@@ -26,6 +28,8 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
getUserDetails,
project: { getProjectMemberIds },
} = useMember();
// editor flaggings
const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
// derived values
const projectMemberIds = getProjectMemberIds(projectId);
const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
@@ -42,6 +46,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
return (
<RichTextEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions}
fileHandler={getEditorFileHandlers({
maxFileSize,
projectId,
@@ -6,8 +6,13 @@ import { cn } from "@/helpers/common.helper";
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
// hooks
import { useMention } from "@/hooks/store";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
type RichTextReadOnlyEditorWrapperProps = Omit<IRichTextReadOnlyEditor, "fileHandler" | "mentionHandler"> & {
type RichTextReadOnlyEditorWrapperProps = Omit<
IRichTextReadOnlyEditor,
"disabledExtensions" | "fileHandler" | "mentionHandler"
> & {
workspaceSlug: string;
projectId?: string;
};
@@ -15,10 +20,13 @@ type RichTextReadOnlyEditorWrapperProps = Omit<IRichTextReadOnlyEditor, "fileHan
export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
({ workspaceSlug, projectId, ...props }, ref) => {
const { mentionHighlights } = useMention({});
// editor flaggings
const { richTextEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
return (
<RichTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions}
fileHandler={getReadOnlyEditorFileHandlers({
projectId,
workspaceSlug,
@@ -59,6 +59,7 @@ export const ProductUpdatesModal: FC<ProductUpdatesModalProps> = observer((props
{data?.id && (
<DocumentReadOnlyEditorWithRef
ref={editorRef}
disabledExtensions={[]}
id={data.id}
initialValue={data.description_html ?? "<p></p>"}
containerClassName="p-0 border-none"
@@ -84,7 +84,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
user: currentUser ?? undefined,
});
// editor flaggings
const { documentEditor } = useEditorFlagging(workspaceSlug?.toString());
const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
// page filters
const { fontSize, fontStyle, isFullWidth } = usePageFilters();
// issue-embed
@@ -224,7 +224,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
realtimeConfig={realtimeConfig}
serverHandler={serverHandler}
user={userConfig}
disabledExtensions={documentEditor}
disabledExtensions={disabledExtensions}
aiHandler={{
menu: getAIMenu,
}}
@@ -233,6 +233,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
<CollaborativeDocumentReadOnlyEditorWithRef
id={pageId}
ref={readOnlyEditorRef}
disabledExtensions={disabledExtensions}
fileHandler={getReadOnlyEditorFileHandlers({
projectId: projectId?.toString() ?? "",
workspaceSlug: workspaceSlug?.toString() ?? "",
@@ -12,6 +12,7 @@ import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
import { useMember, useMention, useUser } from "@/hooks/store";
import { usePageFilters } from "@/hooks/use-page-filters";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed";
export type TVersionEditorProps = {
@@ -31,6 +32,8 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
getUserDetails,
project: { getProjectMemberIds },
} = useMember();
// editor flaggings
const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
// derived values
const projectMemberIds = projectId ? getProjectMemberIds(projectId.toString()) : [];
const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
@@ -101,6 +104,7 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
id={activeVersion ?? ""}
initialValue={description ?? "<p></p>"}
containerClassName="p-0 pb-64 border-none"
disabledExtensions={disabledExtensions}
displayConfig={displayConfig}
editorClassName="pl-10"
fileHandler={getReadOnlyEditorFileHandlers({
+1
View File
@@ -7,6 +7,7 @@ export enum E_FEATURE_FLAGS {
BULK_OPS_ADVANCED = "BULK_OPS_ADVANCED",
COLLABORATION_CURSOR = "COLLABORATION_CURSOR",
EDITOR_AI_OPS = "EDITOR_AI_OPS",
EDITOR_CALLOUT_COMPONENT = "EDITOR_CALLOUT_COMPONENT",
ESTIMATE_WITH_TIME = "ESTIMATE_WITH_TIME",
ISSUE_TYPE_DISPLAY = "ISSUE_TYPE_DISPLAY",
ISSUE_TYPE_SETTINGS = "ISSUE_TYPE_SETTINGS",
+11 -1
View File
@@ -10,19 +10,29 @@ export const useEditorFlagging = (
workspaceSlug: string
): {
documentEditor: TExtensions[];
liteTextEditor: TExtensions[];
richTextEditor: TExtensions[];
} => {
const isIssueEmbedEnabled = useFlag(workspaceSlug, "PAGE_ISSUE_EMBEDS");
const isEditorAIOpsEnabled = useFlag(workspaceSlug, "EDITOR_AI_OPS");
const isCollaborationCursorEnabled = useFlag(workspaceSlug, "COLLABORATION_CURSOR");
const isCalloutComponentEnabled = useFlag(workspaceSlug, "EDITOR_CALLOUT_COMPONENT");
// extensions disabled in the document editor
const documentEditor: TExtensions[] = [];
const liteTextEditor: TExtensions[] = [];
const richTextEditor: TExtensions[] = [];
if (!isIssueEmbedEnabled) documentEditor.push("issue-embed");
if (!isEditorAIOpsEnabled) documentEditor.push("ai");
if (!isCollaborationCursorEnabled) documentEditor.push("collaboration-cursor");
if (!isCalloutComponentEnabled) {
documentEditor.push("callout");
liteTextEditor.push("callout");
richTextEditor.push("callout");
}
return {
documentEditor,
richTextEditor: [],
liteTextEditor,
richTextEditor,
};
};