style: lite text editor editor toolbar (#2601)

* style: comment editor toolbar

* style: updated icon styling
This commit is contained in:
Aaryan Khandelwal 2023-11-02 16:26:57 +05:30 committed by GitHub
parent 5b808571e5
commit c394a4f64e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 182 additions and 142 deletions

View File

@ -7,7 +7,11 @@ interface EditorContainerProps {
children: ReactNode; children: ReactNode;
} }
export const EditorContainer = ({ editor, editorClassNames, children }: EditorContainerProps) => ( export const EditorContainer = ({
editor,
editorClassNames,
children,
}: EditorContainerProps) => (
<div <div
id="editor-container" id="editor-container"
onClick={() => { onClick={() => {

View File

@ -1,14 +1,14 @@
"use client" "use client";
import * as React from 'react'; import * as React from "react";
import { Extension } from "@tiptap/react"; import { Extension } from "@tiptap/react";
import { UploadImage } from '../types/upload-image'; import { UploadImage } from "../types/upload-image";
import { DeleteImage } from '../types/delete-image'; import { DeleteImage } from "../types/delete-image";
import { getEditorClassNames } from '../lib/utils'; import { getEditorClassNames } from "../lib/utils";
import { EditorProps } from '@tiptap/pm/view'; import { EditorProps } from "@tiptap/pm/view";
import { useEditor } from './hooks/useEditor'; import { useEditor } from "./hooks/useEditor";
import { EditorContainer } from '../ui/components/editor-container'; import { EditorContainer } from "../ui/components/editor-container";
import { EditorContentWrapper } from '../ui/components/editor-content'; import { EditorContentWrapper } from "../ui/components/editor-content";
import { IMentionSuggestion } from '../types/mention-suggestion'; import { IMentionSuggestion } from "../types/mention-suggestion";
interface ICoreEditor { interface ICoreEditor {
value: string; value: string;
@ -19,7 +19,9 @@ interface ICoreEditor {
customClassName?: string; customClassName?: string;
editorContentCustomClassNames?: string; editorContentCustomClassNames?: string;
onChange?: (json: any, html: string) => void; onChange?: (json: any, html: string) => void;
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
setShouldShowAlert?: (showAlert: boolean) => void; setShouldShowAlert?: (showAlert: boolean) => void;
editable?: boolean; editable?: boolean;
forwardedRef?: any; forwardedRef?: any;
@ -72,22 +74,29 @@ const CoreEditor = ({
forwardedRef, forwardedRef,
}); });
const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName }); const editorClassNames = getEditorClassNames({
noBorder,
borderOnFocus,
customClassName,
});
if (!editor) return null; if (!editor) return null;
return ( return (
<EditorContainer editor={editor} editorClassNames={editorClassNames}> <EditorContainer editor={editor} editorClassNames={editorClassNames}>
<div className="flex flex-col"> <div className="flex flex-col">
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} /> <EditorContentWrapper
editor={editor}
editorContentCustomClassNames={editorContentCustomClassNames}
/>
</div> </div>
</EditorContainer> </EditorContainer>
); );
}; };
const CoreEditorWithRef = React.forwardRef<EditorHandle, ICoreEditor>((props, ref) => ( const CoreEditorWithRef = React.forwardRef<EditorHandle, ICoreEditor>(
<CoreEditor {...props} forwardedRef={ref} /> (props, ref) => <CoreEditor {...props} forwardedRef={ref} />,
)); );
CoreEditorWithRef.displayName = "CoreEditorWithRef"; CoreEditorWithRef.displayName = "CoreEditorWithRef";

View File

@ -18,9 +18,9 @@ export type IMentionSuggestion = {
title: string; title: string;
subtitle: string; subtitle: string;
redirect_uri: string; redirect_uri: string;
} };
export type IMentionHighlight = string export type IMentionHighlight = string;
interface ILiteTextEditor { interface ILiteTextEditor {
value: string; value: string;
@ -32,7 +32,7 @@ interface ILiteTextEditor {
editorContentCustomClassNames?: string; editorContentCustomClassNames?: string;
onChange?: (json: any, html: string) => void; onChange?: (json: any, html: string) => void;
setIsSubmitting?: ( setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved" isSubmitting: "submitting" | "submitted" | "saved",
) => void; ) => void;
setShouldShowAlert?: (showAlert: boolean) => void; setShouldShowAlert?: (showAlert: boolean) => void;
forwardedRef?: any; forwardedRef?: any;
@ -50,6 +50,7 @@ interface ILiteTextEditor {
onEnterKeyPress?: (e?: any) => void; onEnterKeyPress?: (e?: any) => void;
mentionHighlights?: string[]; mentionHighlights?: string[];
mentionSuggestions?: IMentionSuggestion[]; mentionSuggestions?: IMentionSuggestion[];
submitButton?: React.ReactNode;
} }
interface LiteTextEditorProps extends ILiteTextEditor { interface LiteTextEditorProps extends ILiteTextEditor {
@ -61,7 +62,8 @@ interface EditorHandle {
setEditorValue: (content: string) => void; setEditorValue: (content: string) => void;
} }
const LiteTextEditor = ({ const LiteTextEditor = (props: LiteTextEditorProps) => {
const {
onChange, onChange,
debouncedUpdatesEnabled, debouncedUpdatesEnabled,
setIsSubmitting, setIsSubmitting,
@ -77,8 +79,10 @@ const LiteTextEditor = ({
commentAccessSpecifier, commentAccessSpecifier,
onEnterKeyPress, onEnterKeyPress,
mentionHighlights, mentionHighlights,
mentionSuggestions mentionSuggestions,
}: LiteTextEditorProps) => { submitButton,
} = props;
const editor = useEditor({ const editor = useEditor({
onChange, onChange,
debouncedUpdatesEnabled, debouncedUpdatesEnabled,
@ -90,7 +94,7 @@ const LiteTextEditor = ({
forwardedRef, forwardedRef,
extensions: LiteTextEditorExtensions(onEnterKeyPress), extensions: LiteTextEditorExtensions(onEnterKeyPress),
mentionHighlights, mentionHighlights,
mentionSuggestions mentionSuggestions,
}); });
const editorClassNames = getEditorClassNames({ const editorClassNames = getEditorClassNames({
@ -114,6 +118,7 @@ const LiteTextEditor = ({
uploadFile={uploadFile} uploadFile={uploadFile}
setIsSubmitting={setIsSubmitting} setIsSubmitting={setIsSubmitting}
commentAccessSpecifier={commentAccessSpecifier} commentAccessSpecifier={commentAccessSpecifier}
submitButton={submitButton}
/> />
</div> </div>
</div> </div>
@ -122,7 +127,7 @@ const LiteTextEditor = ({
}; };
const LiteTextEditorWithRef = React.forwardRef<EditorHandle, ILiteTextEditor>( const LiteTextEditorWithRef = React.forwardRef<EditorHandle, ILiteTextEditor>(
(props, ref) => <LiteTextEditor {...props} forwardedRef={ref} /> (props, ref) => <LiteTextEditor {...props} forwardedRef={ref} />,
); );
LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef"; LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef";

View File

@ -1,5 +1,5 @@
import { Editor } from "@tiptap/react"; import { Editor } from "@tiptap/react";
import { BoldIcon, LucideIcon } from "lucide-react"; import { BoldIcon } from "lucide-react";
import { import {
BoldItem, BoldItem,
@ -14,7 +14,6 @@ import {
TableItem, TableItem,
UnderLineItem, UnderLineItem,
} from "@plane/editor-core"; } from "@plane/editor-core";
import { Icon } from "./icon";
import { Tooltip } from "../../tooltip"; import { Tooltip } from "../../tooltip";
import { UploadImage } from "../.."; import { UploadImage } from "../..";
@ -41,8 +40,9 @@ type EditorBubbleMenuProps = {
}; };
uploadFile: UploadImage; uploadFile: UploadImage;
setIsSubmitting?: ( setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved" isSubmitting: "submitting" | "submitted" | "saved",
) => void; ) => void;
submitButton: React.ReactNode;
}; };
export const FixedMenu = (props: EditorBubbleMenuProps) => { export const FixedMenu = (props: EditorBubbleMenuProps) => {
@ -72,117 +72,133 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
props.commentAccessSpecifier?.onAccessChange(accessKey); props.commentAccessSpecifier?.onAccessChange(accessKey);
}; };
console.log(complexItems);
return ( return (
<div className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"> <div className="flex items-stretch gap-1.5 w-full h-9">
{props.commentAccessSpecifier && ( {props.commentAccessSpecifier && (
<div className="flex border border-custom-border-300 mt-0 divide-x divide-custom-border-300 rounded overflow-hidden"> <div className="flex-shrink-0 flex items-stretch gap-0.5 border border-custom-border-200 rounded p-1">
{props?.commentAccessSpecifier.commentAccess?.map((access) => ( {props?.commentAccessSpecifier.commentAccess?.map((access) => (
<Tooltip key={access.key} tooltipContent={access.label}> <Tooltip key={access.key} tooltipContent={access.label}>
<button <button
type="button" type="button"
onClick={() => handleAccessChange(access.key)} onClick={() => handleAccessChange(access.key)}
className={`grid place-basicMarkItems-center p-1 hover:bg-custom-background-80 ${ className={`aspect-square grid place-items-center p-1 rounded-sm hover:bg-custom-background-90 ${
props.commentAccessSpecifier?.accessValue === access.key props.commentAccessSpecifier?.accessValue === access.key
? "bg-custom-background-80" ? "bg-custom-background-90"
: "" : ""
}`} }`}
> >
<access.icon <access.icon
className={`w-4 h-4 ${ className={`w-3.5 h-3.5 ${
props.commentAccessSpecifier?.accessValue === access.key props.commentAccessSpecifier?.accessValue === access.key
? "!text-custom-text-100" ? "text-custom-text-100"
: "!text-custom-text-400" : "text-custom-text-400"
}`} }`}
strokeWidth={2}
/> />
</button> </button>
</Tooltip> </Tooltip>
))} ))}
</div> </div>
)} )}
<div className="flex"> <div className="flex items-stretch justify-between gap-2 w-full border border-custom-border-200 bg-custom-background-90 rounded p-1">
<div className="flex items-stretch">
<div className="flex items-stretch gap-0.5 pr-2.5 border-r border-custom-border-200">
{basicMarkItems.map((item, index) => ( {basicMarkItems.map((item, index) => (
<button <button
key={index} key={index}
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors", "p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center",
{ {
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(), "text-custom-text-100 bg-custom-background-80":
} item.isActive(),
},
)} )}
> >
<item.icon <item.icon
className={cn("h-4 w-4", { className={cn("h-3.5 w-3.5", {
"text-custom-text-100": item.isActive(), "text-custom-text-100": item.isActive(),
})} })}
strokeWidth={2.5}
/> />
</button> </button>
))} ))}
</div> </div>
<div className="flex"> <div className="flex items-stretch gap-0.5 px-2.5 border-r border-custom-border-200">
{listItems.map((item, index) => ( {listItems.map((item, index) => (
<button <button
key={index} key={index}
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors", "p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center",
{ {
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(), "text-custom-text-100 bg-custom-background-80":
} item.isActive(),
},
)} )}
> >
<item.icon <item.icon
className={cn("h-4 w-4", { className={cn("h-3.5 w-3.5", {
"text-custom-text-100": item.isActive(), "text-custom-text-100": item.isActive(),
})} })}
strokeWidth={2.5}
/> />
</button> </button>
))} ))}
</div> </div>
<div className="flex"> <div className="flex items-stretch gap-0.5 px-2.5 border-r border-custom-border-200">
{userActionItems.map((item, index) => ( {userActionItems.map((item, index) => (
<button <button
key={index} key={index}
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors", "p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center",
{ {
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(), "text-custom-text-100 bg-custom-background-80":
} item.isActive(),
},
)} )}
> >
<item.icon <item.icon
className={cn("h-4 w-4", { className={cn("h-3.5 w-3.5", {
"text-custom-text-100": item.isActive(), "text-custom-text-100": item.isActive(),
})} })}
strokeWidth={2.5}
/> />
</button> </button>
))} ))}
</div> </div>
<div className="flex"> <div className="flex items-stretch gap-0.5 pl-2.5">
{complexItems.map((item, index) => ( {complexItems.map((item, index) => (
<button <button
key={index} key={index}
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors", "p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center",
{ {
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(), "text-custom-text-100 bg-custom-background-80":
} item.isActive(),
},
)} )}
> >
<item.icon <item.icon
className={cn("h-4 w-4", { className={cn("h-3.5 w-3.5", {
"text-custom-text-100": item.isActive(), "text-custom-text-100": item.isActive(),
})} })}
strokeWidth={2.5}
/> />
</button> </button>
))} ))}
</div> </div>
</div> </div>
{props.submitButton}
</div>
</div>
); );
}; };

View File

@ -49,7 +49,7 @@ export const IssueCommentCard: React.FC<IIssueCommentCard> = (props) => {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const editorSuggestions = useEditorSuggestions(workspaceSlug, projectId) const editorSuggestions = useEditorSuggestions(workspaceSlug, projectId);
const { const {
formState: { isSubmitting }, formState: { isSubmitting },

View File

@ -54,7 +54,7 @@ export const IssueCommentEditor: React.FC<IIssueCommentEditor> = (props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const editorSuggestions = useEditorSuggestions(workspaceSlug as string | undefined, projectId as string | undefined) const editorSuggestions = useEditorSuggestions(workspaceSlug as string | undefined, projectId as string | undefined);
const { const {
control, control,
@ -75,7 +75,7 @@ export const IssueCommentEditor: React.FC<IIssueCommentEditor> = (props) => {
return ( return (
<form onSubmit={handleSubmit(handleAddComment)}> <form onSubmit={handleSubmit(handleAddComment)}>
<div className="space-y-2"> <div className="space-y-2">
<div className="relative"> <div className="relative h-full">
{showAccessSpecifier && ( {showAccessSpecifier && (
<div className="absolute bottom-2 left-3 z-[1]"> <div className="absolute bottom-2 left-3 z-[1]">
<Controller <Controller
@ -119,22 +119,28 @@ export const IssueCommentEditor: React.FC<IIssueCommentEditor> = (props) => {
deleteFile={fileService.deleteImage} deleteFile={fileService.deleteImage}
ref={editorRef} ref={editorRef}
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue} value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
customClassName="p-3 min-h-[100px] shadow-sm" customClassName="p-2 h-full"
debouncedUpdatesEnabled={false} debouncedUpdatesEnabled={false}
mentionSuggestions={editorSuggestions.mentionSuggestions} mentionSuggestions={editorSuggestions.mentionSuggestions}
mentionHighlights={editorSuggestions.mentionHighlights} mentionHighlights={editorSuggestions.mentionHighlights}
onChange={(comment_json: Object, comment_html: string) => onCommentChange(comment_html)} onChange={(comment_json: Object, comment_html: string) => onCommentChange(comment_html)}
commentAccessSpecifier={{ accessValue, onAccessChange, showAccessSpecifier, commentAccess }} commentAccessSpecifier={{ accessValue, onAccessChange, showAccessSpecifier, commentAccess }}
submitButton={
<Button
variant="primary"
type="submit"
className="!px-2.5 !py-1.5 !text-xs"
disabled={isSubmitting || disabled}
>
{isSubmitting ? "Adding..." : "Comment"}
</Button>
}
/> />
)} )}
/> />
)} )}
/> />
</div> </div>
<Button variant="neutral-primary" type="submit" disabled={isSubmitting || disabled}>
{isSubmitting ? "Adding..." : "Comment"}
</Button>
</div> </div>
</form> </form>
); );