feat: init
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
17
apps/space/core/components/editor/embeds/mentions/root.tsx
Normal file
17
apps/space/core/components/editor/embeds/mentions/root.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
// plane editor
|
||||
import type { TMentionComponentProps } from "@plane/editor";
|
||||
// plane web components
|
||||
import { EditorAdditionalMentionsRoot } from "@/plane-web/components/editor";
|
||||
// local components
|
||||
import { EditorUserMention } from "./user";
|
||||
|
||||
export const EditorMentionsRoot: React.FC<TMentionComponentProps> = (props) => {
|
||||
const { entity_identifier, entity_name } = props;
|
||||
|
||||
switch (entity_name) {
|
||||
case "user_mention":
|
||||
return <EditorUserMention id={entity_identifier} />;
|
||||
default:
|
||||
return <EditorAdditionalMentionsRoot {...props} />;
|
||||
}
|
||||
};
|
||||
40
apps/space/core/components/editor/embeds/mentions/user.tsx
Normal file
40
apps/space/core/components/editor/embeds/mentions/user.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
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: React.FC<Props> = observer((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 px-1 py-0.5 rounded bg-custom-background-80 text-custom-text-300 no-underline">
|
||||
@deactivated user
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"not-prose inline px-1 py-0.5 rounded bg-custom-primary-100/20 text-custom-primary-100 no-underline",
|
||||
{
|
||||
"bg-yellow-500/20 text-yellow-500": id === currentUser?.id,
|
||||
}
|
||||
)}
|
||||
>
|
||||
@{userDetails?.member__display_name}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
90
apps/space/core/components/editor/lite-text-editor.tsx
Normal file
90
apps/space/core/components/editor/lite-text-editor.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
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";
|
||||
import { useEditorFlagging } from "@/plane-web/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"
|
||||
> & {
|
||||
anchor: string;
|
||||
isSubmitting?: boolean;
|
||||
showSubmitButton?: boolean;
|
||||
workspaceId: string;
|
||||
} & (
|
||||
| {
|
||||
editable: false;
|
||||
}
|
||||
| {
|
||||
editable: true;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
}
|
||||
);
|
||||
|
||||
export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapperProps>((props, ref) => {
|
||||
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);
|
||||
|
||||
return (
|
||||
<div className="border border-custom-border-200 rounded p-3 space-y-3">
|
||||
<LiteTextEditorWithRef
|
||||
ref={ref}
|
||||
disabledExtensions={[...liteTextEditorExtensions.disabled, ...additionalDisabledExtensions]}
|
||||
flaggedExtensions={liteTextEditorExtensions.flagged}
|
||||
editable={editable}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
anchor,
|
||||
uploadFile: editable ? props.uploadFile : async () => "",
|
||||
workspaceId,
|
||||
})}
|
||||
mentionHandler={{
|
||||
renderComponent: (props) => <EditorMentionsRoot {...props} />,
|
||||
}}
|
||||
extendedEditorProps={{}}
|
||||
{...rest}
|
||||
// overriding the containerClassName to add relative class passed
|
||||
containerClassName={cn(containerClassName, "relative")}
|
||||
/>
|
||||
<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";
|
||||
69
apps/space/core/components/editor/rich-text-editor.tsx
Normal file
69
apps/space/core/components/editor/rich-text-editor.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { 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";
|
||||
// plane web imports
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
// local imports
|
||||
import { EditorMentionsRoot } from "./embeds/mentions";
|
||||
|
||||
type RichTextEditorWrapperProps = MakeOptional<
|
||||
Omit<IRichTextEditorProps, "editable" | "fileHandler" | "mentionHandler" | "extendedEditorProps">,
|
||||
"disabledExtensions" | "flaggedExtensions"
|
||||
> & {
|
||||
anchor: string;
|
||||
workspaceId: string;
|
||||
} & (
|
||||
| {
|
||||
editable: false;
|
||||
}
|
||||
| {
|
||||
editable: true;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
}
|
||||
);
|
||||
|
||||
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
|
||||
const {
|
||||
anchor,
|
||||
containerClassName,
|
||||
editable,
|
||||
workspaceId,
|
||||
disabledExtensions: additionalDisabledExtensions = [],
|
||||
...rest
|
||||
} = props;
|
||||
const { getMemberById } = useMember();
|
||||
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,
|
||||
})}
|
||||
flaggedExtensions={richTextEditorExtensions.flagged}
|
||||
extendedEditorProps={{}}
|
||||
{...rest}
|
||||
containerClassName={containerClassName}
|
||||
editorClassName="min-h-[100px] py-2 overflow-hidden"
|
||||
displayConfig={{ fontSize: "large-font" }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
RichTextEditor.displayName = "RichTextEditor";
|
||||
116
apps/space/core/components/editor/toolbar.tsx
Normal file
116
apps/space/core/components/editor/toolbar.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
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 const IssueCommentToolbar: React.FC<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 bg-custom-background-90 overflow-x-scroll">
|
||||
<div className="flex w-full items-stretch justify-between gap-2 rounded border-[0.5px] border-custom-border-200 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-custom-border-200 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-xs">
|
||||
<span className="font-medium">{item.name}</span>
|
||||
{item.shortcut && <kbd className="text-custom-text-400">{item.shortcut.join(" + ")}</kbd>}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => executeCommand(item)}
|
||||
className={cn(
|
||||
"grid place-items-center aspect-square rounded-sm p-0.5 text-custom-text-400 hover:bg-custom-background-80",
|
||||
{
|
||||
"bg-custom-background-80 text-custom-text-100": isItemActive,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={cn("h-3.5 w-3.5", {
|
||||
"text-custom-text-100": isItemActive,
|
||||
})}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{showSubmitButton && (
|
||||
<div className="sticky right-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
className="px-2.5 py-1.5 text-xs"
|
||||
onClick={handleSubmit}
|
||||
disabled={isCommentEmpty}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Comment
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user