forked from github/plane
173 lines
5.9 KiB
TypeScript
173 lines
5.9 KiB
TypeScript
|
import React, { useEffect, useState, useCallback } from "react";
|
||
|
import { Globe2, Lock, LucideIcon } from "lucide-react";
|
||
|
// editor
|
||
|
import { EditorMenuItemNames } from "@plane/document-editor";
|
||
|
import { EditorRefApi } from "@plane/lite-text-editor";
|
||
|
// ui
|
||
|
import { Button, Tooltip } from "@plane/ui";
|
||
|
// constants
|
||
|
import { TOOLBAR_ITEMS } from "@/constants/editor";
|
||
|
import { EIssueCommentAccessSpecifier } from "@/constants/issue";
|
||
|
// helpers
|
||
|
import { cn } from "@/helpers/common.helper";
|
||
|
|
||
|
type Props = {
|
||
|
accessSpecifier?: EIssueCommentAccessSpecifier;
|
||
|
executeCommand: (commandName: EditorMenuItemNames) => void;
|
||
|
handleAccessChange?: (accessKey: EIssueCommentAccessSpecifier) => void;
|
||
|
handleSubmit: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||
|
isCommentEmpty: boolean;
|
||
|
isSubmitting: boolean;
|
||
|
showAccessSpecifier: boolean;
|
||
|
showSubmitButton: boolean;
|
||
|
editorRef: React.MutableRefObject<EditorRefApi | null> | null;
|
||
|
};
|
||
|
|
||
|
type TCommentAccessType = {
|
||
|
icon: LucideIcon;
|
||
|
key: EIssueCommentAccessSpecifier;
|
||
|
label: "Private" | "Public";
|
||
|
};
|
||
|
|
||
|
const COMMENT_ACCESS_SPECIFIERS: TCommentAccessType[] = [
|
||
|
{
|
||
|
icon: Lock,
|
||
|
key: EIssueCommentAccessSpecifier.INTERNAL,
|
||
|
label: "Private",
|
||
|
},
|
||
|
{
|
||
|
icon: Globe2,
|
||
|
key: EIssueCommentAccessSpecifier.EXTERNAL,
|
||
|
label: "Public",
|
||
|
},
|
||
|
];
|
||
|
|
||
|
const toolbarItems = TOOLBAR_ITEMS.lite;
|
||
|
|
||
|
export const IssueCommentToolbar: React.FC<Props> = (props) => {
|
||
|
const {
|
||
|
accessSpecifier,
|
||
|
executeCommand,
|
||
|
handleAccessChange,
|
||
|
handleSubmit,
|
||
|
isCommentEmpty,
|
||
|
isSubmitting,
|
||
|
showAccessSpecifier,
|
||
|
showSubmitButton,
|
||
|
editorRef,
|
||
|
} = props;
|
||
|
|
||
|
// State to manage active states of toolbar items
|
||
|
const [activeStates, setActiveStates] = useState<Record<string, boolean>>({});
|
||
|
|
||
|
// Function to update active states
|
||
|
const updateActiveStates = useCallback(() => {
|
||
|
if (editorRef?.current) {
|
||
|
const newActiveStates: Record<string, boolean> = {};
|
||
|
Object.values(toolbarItems)
|
||
|
.flat()
|
||
|
.forEach((item) => {
|
||
|
// Assert that editorRef.current is not null
|
||
|
newActiveStates[item.key] = (editorRef.current as EditorRefApi).isMenuItemActive(item.key);
|
||
|
});
|
||
|
setActiveStates(newActiveStates);
|
||
|
}
|
||
|
}, [editorRef]);
|
||
|
|
||
|
// useEffect to call updateActiveStates when isActive prop changes
|
||
|
useEffect(() => {
|
||
|
if (!editorRef?.current) return;
|
||
|
const unsubscribe = editorRef.current.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">
|
||
|
{showAccessSpecifier && (
|
||
|
<div className="flex flex-shrink-0 items-stretch gap-0.5 rounded border-[0.5px] border-custom-border-200 p-1">
|
||
|
{COMMENT_ACCESS_SPECIFIERS.map((access) => {
|
||
|
const isAccessActive = accessSpecifier === access.key;
|
||
|
|
||
|
return (
|
||
|
<Tooltip key={access.key} tooltipContent={access.label}>
|
||
|
<button
|
||
|
type="button"
|
||
|
onClick={() => handleAccessChange?.(access.key)}
|
||
|
className={cn("grid place-items-center aspect-square rounded-sm p-1 hover:bg-custom-background-80", {
|
||
|
"bg-custom-background-80": isAccessActive,
|
||
|
})}
|
||
|
>
|
||
|
<access.icon
|
||
|
className={cn("h-3.5 w-3.5 text-custom-text-400", {
|
||
|
"text-custom-text-100": isAccessActive,
|
||
|
})}
|
||
|
strokeWidth={2}
|
||
|
/>
|
||
|
</button>
|
||
|
</Tooltip>
|
||
|
);
|
||
|
})}
|
||
|
</div>
|
||
|
)}
|
||
|
<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,
|
||
|
"pr-0": index === Object.keys(toolbarItems).length - 1,
|
||
|
})}
|
||
|
>
|
||
|
{toolbarItems[key].map((item) => (
|
||
|
<Tooltip
|
||
|
key={item.key}
|
||
|
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.key)}
|
||
|
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": activeStates[item.key],
|
||
|
}
|
||
|
)}
|
||
|
>
|
||
|
<item.icon
|
||
|
className={cn("h-3.5 w-3.5", {
|
||
|
"text-custom-text-100": activeStates[item.key],
|
||
|
})}
|
||
|
strokeWidth={2.5}
|
||
|
/>
|
||
|
</button>
|
||
|
</Tooltip>
|
||
|
))}
|
||
|
</div>
|
||
|
))}
|
||
|
</div>
|
||
|
{showSubmitButton && (
|
||
|
<div className="sticky right-1">
|
||
|
<Button
|
||
|
type="submit"
|
||
|
variant="primary"
|
||
|
className="px-2.5 py-1.5 text-xs"
|
||
|
onClick={handleSubmit}
|
||
|
disabled={isCommentEmpty}
|
||
|
loading={isSubmitting}
|
||
|
>
|
||
|
Comment
|
||
|
</Button>
|
||
|
</div>
|
||
|
)}
|
||
|
</div>
|
||
|
</div>
|
||
|
);
|
||
|
};
|