base
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { TCallbackMentionComponentProps } from "@plane/editor";
|
||||
// local components
|
||||
import { EditorUserMention } from "./user";
|
||||
|
||||
export function EditorMentionsRoot(props: TCallbackMentionComponentProps) {
|
||||
const { entity_identifier, entity_name } = props;
|
||||
|
||||
switch (entity_name) {
|
||||
case "user_mention":
|
||||
return <EditorUserMention id={entity_identifier} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useUser } from "@/hooks/store/use-user";
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const EditorUserMention = observer(function EditorUserMention(props: Props) {
|
||||
const { id } = props;
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { getMemberById } = useMember();
|
||||
// derived values
|
||||
const userDetails = getMemberById(id);
|
||||
|
||||
if (!userDetails) {
|
||||
return (
|
||||
<div className="not-prose inline rounded-sm bg-layer-1 px-1 py-0.5 text-tertiary no-underline">
|
||||
@deactivated user
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("not-prose inline rounded-sm bg-accent-primary/20 px-1 py-0.5 text-accent-primary no-underline", {
|
||||
"bg-yellow-500/20 text-yellow-500": id === currentUser?.id,
|
||||
})}
|
||||
>
|
||||
@{userDetails?.member__display_name}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
// plane imports
|
||||
import { LiteTextEditorWithRef } from "@plane/editor";
|
||||
import type { EditorRefApi, ILiteTextEditorProps, TFileHandler } from "@plane/editor";
|
||||
import type { MakeOptional } from "@plane/types";
|
||||
import { cn, isCommentEmpty } from "@plane/utils";
|
||||
// helpers
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
|
||||
// plane web imports
|
||||
import { useEditorFlagging } from "@/hooks/use-editor-flagging";
|
||||
// local imports
|
||||
import { EditorMentionsRoot } from "./embeds/mentions";
|
||||
import { IssueCommentToolbar } from "./toolbar";
|
||||
|
||||
type LiteTextEditorWrapperProps = MakeOptional<
|
||||
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler" | "extendedEditorProps">,
|
||||
"disabledExtensions" | "flaggedExtensions" | "getEditorMetaData"
|
||||
> & {
|
||||
anchor: string;
|
||||
isSubmitting?: boolean;
|
||||
showSubmitButton?: boolean;
|
||||
workspaceId: string;
|
||||
} & (
|
||||
| {
|
||||
editable: false;
|
||||
}
|
||||
| {
|
||||
editable: true;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
}
|
||||
);
|
||||
|
||||
export const LiteTextEditor = React.forwardRef(function LiteTextEditor(
|
||||
props: LiteTextEditorWrapperProps,
|
||||
ref: React.ForwardedRef<EditorRefApi>
|
||||
) {
|
||||
const {
|
||||
anchor,
|
||||
containerClassName,
|
||||
disabledExtensions: additionalDisabledExtensions = [],
|
||||
editable,
|
||||
isSubmitting = false,
|
||||
showSubmitButton = true,
|
||||
workspaceId,
|
||||
...rest
|
||||
} = props;
|
||||
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
|
||||
return !!ref && typeof ref === "object" && "current" in ref;
|
||||
}
|
||||
// derived values
|
||||
const isEmpty = isCommentEmpty(props.initialValue);
|
||||
const editorRef = isMutableRefObject<EditorRefApi>(ref) ? ref.current : null;
|
||||
const { liteText: liteTextEditorExtensions } = useEditorFlagging(anchor);
|
||||
// parse content
|
||||
const { getEditorMetaData } = useParseEditorContent({
|
||||
anchor,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-3 rounded-sm border border-subtle p-3">
|
||||
<LiteTextEditorWithRef
|
||||
ref={ref}
|
||||
disabledExtensions={[...liteTextEditorExtensions.disabled, ...additionalDisabledExtensions]}
|
||||
flaggedExtensions={liteTextEditorExtensions.flagged}
|
||||
editable={editable}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
anchor,
|
||||
uploadFile: editable ? props.uploadFile : async () => "",
|
||||
workspaceId,
|
||||
})}
|
||||
getEditorMetaData={getEditorMetaData}
|
||||
mentionHandler={{
|
||||
renderComponent: (props) => <EditorMentionsRoot {...props} />,
|
||||
}}
|
||||
extendedEditorProps={{}}
|
||||
{...rest}
|
||||
// overriding the containerClassName to add relative class passed
|
||||
containerClassName={cn(containerClassName, "relative")}
|
||||
/>
|
||||
{editable && (
|
||||
<IssueCommentToolbar
|
||||
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={editorRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
LiteTextEditor.displayName = "LiteTextEditor";
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { forwardRef } from "react";
|
||||
// plane imports
|
||||
import { RichTextEditorWithRef } from "@plane/editor";
|
||||
import type { EditorRefApi, IRichTextEditorProps, TFileHandler } from "@plane/editor";
|
||||
import type { MakeOptional } from "@plane/types";
|
||||
// helpers
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
|
||||
// plane web imports
|
||||
import { useEditorFlagging } from "@/hooks/use-editor-flagging";
|
||||
// local imports
|
||||
import { EditorMentionsRoot } from "./embeds/mentions";
|
||||
|
||||
type RichTextEditorWrapperProps = MakeOptional<
|
||||
Omit<IRichTextEditorProps, "editable" | "fileHandler" | "mentionHandler" | "extendedEditorProps">,
|
||||
"disabledExtensions" | "flaggedExtensions" | "getEditorMetaData"
|
||||
> & {
|
||||
anchor: string;
|
||||
workspaceId: string;
|
||||
} & (
|
||||
| {
|
||||
editable: false;
|
||||
}
|
||||
| {
|
||||
editable: true;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
}
|
||||
);
|
||||
|
||||
export const RichTextEditor = forwardRef(function RichTextEditor(
|
||||
props: RichTextEditorWrapperProps,
|
||||
ref: React.ForwardedRef<EditorRefApi>
|
||||
) {
|
||||
const {
|
||||
anchor,
|
||||
containerClassName,
|
||||
editable,
|
||||
workspaceId,
|
||||
disabledExtensions: additionalDisabledExtensions = [],
|
||||
...rest
|
||||
} = props;
|
||||
// store hooks
|
||||
const { getMemberById } = useMember();
|
||||
// parse content
|
||||
const { getEditorMetaData } = useParseEditorContent({
|
||||
anchor,
|
||||
});
|
||||
// editor flaggings
|
||||
const { richText: richTextEditorExtensions } = useEditorFlagging(anchor);
|
||||
|
||||
return (
|
||||
<RichTextEditorWithRef
|
||||
mentionHandler={{
|
||||
renderComponent: (props) => <EditorMentionsRoot {...props} />,
|
||||
getMentionedEntityDetails: (id: string) => ({
|
||||
display_name: getMemberById(id)?.member__display_name ?? "",
|
||||
}),
|
||||
}}
|
||||
ref={ref}
|
||||
disabledExtensions={[...richTextEditorExtensions.disabled, ...additionalDisabledExtensions]}
|
||||
editable={editable}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
anchor,
|
||||
uploadFile: editable ? props.uploadFile : async () => "",
|
||||
workspaceId,
|
||||
})}
|
||||
getEditorMetaData={getEditorMetaData}
|
||||
flaggedExtensions={richTextEditorExtensions.flagged}
|
||||
extendedEditorProps={{}}
|
||||
{...rest}
|
||||
containerClassName={containerClassName}
|
||||
editorClassName="min-h-[100px] py-2 overflow-hidden"
|
||||
displayConfig={{ fontSize: "large-font" }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
RichTextEditor.displayName = "RichTextEditor";
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
// plane imports
|
||||
import { TOOLBAR_ITEMS } from "@plane/editor";
|
||||
import type { ToolbarMenuItem, EditorRefApi } from "@plane/editor";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
executeCommand: (item: ToolbarMenuItem) => void;
|
||||
handleSubmit: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
isCommentEmpty: boolean;
|
||||
isSubmitting: boolean;
|
||||
showSubmitButton: boolean;
|
||||
editorRef: EditorRefApi | null;
|
||||
};
|
||||
|
||||
const toolbarItems = TOOLBAR_ITEMS.lite;
|
||||
|
||||
export function IssueCommentToolbar(props: Props) {
|
||||
const { executeCommand, handleSubmit, isCommentEmpty, editorRef, isSubmitting, showSubmitButton } = props;
|
||||
// states
|
||||
const [activeStates, setActiveStates] = useState<Record<string, boolean>>({});
|
||||
|
||||
// Function to update active states
|
||||
const updateActiveStates = useCallback(() => {
|
||||
if (!editorRef) return;
|
||||
const newActiveStates: Record<string, boolean> = {};
|
||||
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);
|
||||
}, [editorRef]);
|
||||
|
||||
// useEffect to call updateActiveStates when isActive prop changes
|
||||
useEffect(() => {
|
||||
if (!editorRef) return;
|
||||
const unsubscribe = editorRef.onStateChange(updateActiveStates);
|
||||
updateActiveStates();
|
||||
return () => unsubscribe();
|
||||
}, [editorRef, updateActiveStates]);
|
||||
|
||||
return (
|
||||
<div className="flex h-9 w-full items-stretch gap-1.5 overflow-x-scroll">
|
||||
<div className="flex w-full items-stretch justify-between gap-2 rounded-sm border-[0.5px] border-subtle p-1">
|
||||
<div className="flex items-stretch">
|
||||
{Object.keys(toolbarItems).map((key, index) => (
|
||||
<div
|
||||
key={key}
|
||||
className={cn("flex items-stretch gap-0.5 border-r border-subtle px-2.5", {
|
||||
"pl-0": index === 0,
|
||||
})}
|
||||
>
|
||||
{toolbarItems[key].map((item) => {
|
||||
const isItemActive = activeStates[item.renderKey];
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={item.renderKey}
|
||||
tooltipContent={
|
||||
<p className="flex flex-col gap-1 text-center text-11">
|
||||
<span className="font-medium">{item.name}</span>
|
||||
{item.shortcut && <kbd className="text-placeholder">{item.shortcut.join(" + ")}</kbd>}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => executeCommand(item)}
|
||||
className={cn(
|
||||
"grid aspect-square place-items-center rounded-xs p-0.5 text-placeholder hover:bg-layer-transparent-hover",
|
||||
{
|
||||
"bg-layer-transparent-hover text-primary": isItemActive,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={cn("h-3.5 w-3.5", {
|
||||
"text-primary": isItemActive,
|
||||
})}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{showSubmitButton && (
|
||||
<div className="sticky right-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={isCommentEmpty}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Comment
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user