mirror of
https://github.com/docmost/docmost.git
synced 2026-06-10 10:13:01 +08:00
support fixed toolbar in templates editor
This commit is contained in:
@@ -32,6 +32,12 @@ import {
|
||||
} from "../queries/template-query";
|
||||
import { useGetSpacesQuery } from "@/features/space/queries/space-query";
|
||||
import useUserRole from "@/hooks/use-user-role";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { userAtom } from "@/features/user/atoms/current-user-atom";
|
||||
import { FixedToolbar } from "@/features/editor/components/fixed-toolbar/fixed-toolbar";
|
||||
import { EditorLinkMenu } from "@/features/editor/components/link/link-menu";
|
||||
import { EditorBubbleMenu } from "@/features/editor/components/bubble-menu/bubble-menu";
|
||||
import { EditorAiMenu } from "@/ee/ai/components/editor/ai-menu/ai-menu";
|
||||
|
||||
import classes from "./template-editor.module.css";
|
||||
|
||||
@@ -39,6 +45,9 @@ export default function TemplateEditor() {
|
||||
const { t } = useTranslation();
|
||||
const { templateId } = useParams<{ templateId: string }>();
|
||||
const { isAdmin: isWorkspaceAdmin } = useUserRole();
|
||||
const user = useAtomValue(userAtom);
|
||||
const editorToolbarEnabled =
|
||||
user?.settings?.preferences?.editorToolbar ?? false;
|
||||
|
||||
const { data: existingTemplate } = useGetTemplateByIdQuery(templateId || "");
|
||||
const { data: spaces } = useGetSpacesQuery({ limit: 100 });
|
||||
@@ -238,6 +247,10 @@ export default function TemplateEditor() {
|
||||
</title>
|
||||
</Helmet>
|
||||
|
||||
{editorToolbarEnabled && editor && (
|
||||
<FixedToolbar editor={editor} templateMode />
|
||||
)}
|
||||
|
||||
<div className={classes.header}>
|
||||
<Container size={900} h="100%" px={0}>
|
||||
<Group justify="space-between" h="100%" wrap="nowrap">
|
||||
@@ -379,6 +392,13 @@ export default function TemplateEditor() {
|
||||
)}
|
||||
</div>
|
||||
<EditorContent editor={editor} />
|
||||
{editor && (
|
||||
<>
|
||||
<EditorAiMenu editor={editor} />
|
||||
<EditorBubbleMenu editor={editor} templateMode />
|
||||
<EditorLinkMenu editor={editor} />
|
||||
</>
|
||||
)}
|
||||
<div style={{ paddingBottom: "20vh" }} />
|
||||
</Container>
|
||||
</>
|
||||
|
||||
@@ -38,9 +38,11 @@ export interface BubbleMenuItem {
|
||||
|
||||
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children" | "editor"> & {
|
||||
editor: Editor | null;
|
||||
templateMode?: boolean;
|
||||
};
|
||||
|
||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
const { templateMode = false } = props;
|
||||
const { t } = useTranslation();
|
||||
const [showAiMenu, setShowAiMenu] = useAtom(showAiMenuAtom);
|
||||
const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
||||
@@ -232,8 +234,6 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
))}
|
||||
</ActionIcon.Group>
|
||||
|
||||
<LinkSelector />
|
||||
|
||||
<ColorSelector
|
||||
editor={props.editor}
|
||||
isOpen={isColorSelectorOpen}
|
||||
@@ -246,18 +246,22 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<Tooltip label={t(commentItem.name)} withArrow withinPortal={false}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
radius="6px"
|
||||
aria-label={t(commentItem.name)}
|
||||
style={{ border: "none" }}
|
||||
onClick={commentItem.command}
|
||||
>
|
||||
<IconMessage size={16} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<LinkSelector />
|
||||
|
||||
{!templateMode && (
|
||||
<Tooltip label={t(commentItem.name)} withArrow withinPortal={false}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
radius="6px"
|
||||
aria-label={t(commentItem.name)}
|
||||
style={{ border: "none" }}
|
||||
onClick={commentItem.command}
|
||||
>
|
||||
<IconMessage size={16} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</BubbleMenu>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { FC } from "react";
|
||||
import { useAtomValue } from "jotai";
|
||||
import type { Editor } from "@tiptap/react";
|
||||
import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms";
|
||||
import { useToolbarState } from "./use-toolbar-state";
|
||||
import { BlockTypeGroup } from "./groups/block-type-group";
|
||||
import { InlineMarksGroup } from "./groups/inline-marks-group";
|
||||
import { ColorGroup } from "./groups/color-group";
|
||||
import { ListsGroup } from "./groups/lists-group";
|
||||
import { LinkGroup } from "./groups/link-group";
|
||||
import { AlignmentGroup } from "./groups/alignment-group";
|
||||
import { MediaGroup } from "./groups/media-group";
|
||||
import { QuickInsertsGroup } from "./groups/quick-inserts-group";
|
||||
@@ -16,8 +16,17 @@ import { AskAiGroup } from "./groups/ask-ai-group";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom";
|
||||
import classes from "./fixed-toolbar.module.css";
|
||||
|
||||
export const FixedToolbar: FC = () => {
|
||||
const editor = useAtomValue(pageEditorAtom);
|
||||
type FixedToolbarProps = {
|
||||
editor?: Editor | null;
|
||||
templateMode?: boolean;
|
||||
};
|
||||
|
||||
export const FixedToolbar: FC<FixedToolbarProps> = ({
|
||||
editor: editorProp,
|
||||
templateMode = false,
|
||||
}) => {
|
||||
const editorFromAtom = useAtomValue(pageEditorAtom);
|
||||
const editor = editorProp ?? editorFromAtom;
|
||||
const state = useToolbarState(editor);
|
||||
const workspace = useAtomValue(workspaceAtom);
|
||||
const isGenerativeAiEnabled = workspace?.settings?.ai?.generative === true;
|
||||
@@ -48,14 +57,12 @@ export const FixedToolbar: FC = () => {
|
||||
<div className={classes.divider} />
|
||||
<ListsGroup editor={editor} state={state} />
|
||||
<div className={classes.divider} />
|
||||
<LinkGroup />
|
||||
<div className={classes.divider} />
|
||||
<AlignmentGroup editor={editor} />
|
||||
<div className={classes.divider} />
|
||||
<MediaGroup editor={editor} />
|
||||
<MediaGroup editor={editor} templateMode={templateMode} />
|
||||
<div className={classes.divider} />
|
||||
<QuickInsertsGroup editor={editor} />
|
||||
<MoreInsertsGroup editor={editor} />
|
||||
<MoreInsertsGroup editor={editor} templateMode={templateMode} />
|
||||
<div className={classes.divider} />
|
||||
<HistoryGroup editor={editor} state={state} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector";
|
||||
|
||||
export const LinkGroup: FC = () => {
|
||||
return <LinkSelector />;
|
||||
};
|
||||
@@ -17,6 +17,7 @@ import { uploadPdfAction } from "@/features/editor/components/pdf/upload-pdf-act
|
||||
|
||||
interface Props {
|
||||
editor: Editor;
|
||||
templateMode?: boolean;
|
||||
}
|
||||
|
||||
type UploadFn = (
|
||||
@@ -60,7 +61,7 @@ function pickFile(
|
||||
input.click();
|
||||
}
|
||||
|
||||
export const MediaGroup: FC<Props> = ({ editor }) => {
|
||||
export const MediaGroup: FC<Props> = ({ editor, templateMode }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -78,24 +79,30 @@ export const MediaGroup: FC<Props> = ({ editor }) => {
|
||||
</Tooltip>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
leftSection={<IconPhoto size={16} />}
|
||||
onClick={() => pickFile(editor, "image/*", true, uploadImageAction)}
|
||||
>
|
||||
{t("Image")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<IconMovie size={16} />}
|
||||
onClick={() => pickFile(editor, "video/*", true, uploadVideoAction)}
|
||||
>
|
||||
{t("Video")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<IconMusic size={16} />}
|
||||
onClick={() => pickFile(editor, "audio/*", true, uploadAudioAction)}
|
||||
>
|
||||
{t("Audio")}
|
||||
</Menu.Item>
|
||||
{!templateMode && (
|
||||
<Menu.Item
|
||||
leftSection={<IconPhoto size={16} />}
|
||||
onClick={() => pickFile(editor, "image/*", true, uploadImageAction)}
|
||||
>
|
||||
{t("Image")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{!templateMode && (
|
||||
<Menu.Item
|
||||
leftSection={<IconMovie size={16} />}
|
||||
onClick={() => pickFile(editor, "video/*", true, uploadVideoAction)}
|
||||
>
|
||||
{t("Video")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{!templateMode && (
|
||||
<Menu.Item
|
||||
leftSection={<IconMusic size={16} />}
|
||||
onClick={() => pickFile(editor, "audio/*", true, uploadAudioAction)}
|
||||
>
|
||||
{t("Audio")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item
|
||||
leftSection={<IconFileTypePdf size={16} />}
|
||||
onClick={() =>
|
||||
@@ -104,14 +111,16 @@ export const MediaGroup: FC<Props> = ({ editor }) => {
|
||||
>
|
||||
PDF
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<IconPaperclip size={16} />}
|
||||
onClick={() =>
|
||||
pickFile(editor, "", true, uploadAttachmentAction, true)
|
||||
}
|
||||
>
|
||||
{t("File attachment")}
|
||||
</Menu.Item>
|
||||
{!templateMode && (
|
||||
<Menu.Item
|
||||
leftSection={<IconPaperclip size={16} />}
|
||||
onClick={() =>
|
||||
pickFile(editor, "", true, uploadAttachmentAction, true)
|
||||
}
|
||||
>
|
||||
{t("File attachment")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
+28
-21
@@ -32,9 +32,10 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
editor: Editor;
|
||||
templateMode?: boolean;
|
||||
}
|
||||
|
||||
export const MoreInsertsGroup: FC<Props> = ({ editor }) => {
|
||||
export const MoreInsertsGroup: FC<Props> = ({ editor, templateMode }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const setEmbed = (provider: string) =>
|
||||
@@ -91,14 +92,16 @@ export const MoreInsertsGroup: FC<Props> = ({ editor }) => {
|
||||
>
|
||||
{t("Subpages")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<IconRotate2 size={16} />}
|
||||
onClick={() =>
|
||||
editor.chain().focus().insertTransclusionSource().run()
|
||||
}
|
||||
>
|
||||
{t("Synced block")}
|
||||
</Menu.Item>
|
||||
{!templateMode && (
|
||||
<Menu.Item
|
||||
leftSection={<IconRotate2 size={16} />}
|
||||
onClick={() =>
|
||||
editor.chain().focus().insertTransclusionSource().run()
|
||||
}
|
||||
>
|
||||
{t("Synced block")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
<Menu.Divider />
|
||||
<Menu.Label>{t("Diagrams")}</Menu.Label>
|
||||
@@ -115,18 +118,22 @@ export const MoreInsertsGroup: FC<Props> = ({ editor }) => {
|
||||
>
|
||||
{t("Mermaid diagram")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<IconDrawio size={16} />}
|
||||
onClick={() => editor.chain().focus().setDrawio().run()}
|
||||
>
|
||||
Draw.io
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<IconExcalidraw size={16} />}
|
||||
onClick={() => editor.chain().focus().setExcalidraw().run()}
|
||||
>
|
||||
Excalidraw
|
||||
</Menu.Item>
|
||||
{!templateMode && (
|
||||
<Menu.Item
|
||||
leftSection={<IconDrawio size={16} />}
|
||||
onClick={() => editor.chain().focus().setDrawio().run()}
|
||||
>
|
||||
Draw.io
|
||||
</Menu.Item>
|
||||
)}
|
||||
{!templateMode && (
|
||||
<Menu.Item
|
||||
leftSection={<IconExcalidraw size={16} />}
|
||||
onClick={() => editor.chain().focus().setExcalidraw().run()}
|
||||
>
|
||||
Excalidraw
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
<Menu.Divider />
|
||||
<Menu.Label>{t("Embeds")}</Menu.Label>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { StarterKit } from "@tiptap/starter-kit";
|
||||
import { Code } from "@tiptap/extension-code";
|
||||
import { TextAlign } from "@tiptap/extension-text-align";
|
||||
import { TaskList, TaskItem } from "@tiptap/extension-list";
|
||||
import { Placeholder, CharacterCount } from "@tiptap/extensions";
|
||||
import { Placeholder, CharacterCount, UndoRedo } from "@tiptap/extensions";
|
||||
import { Superscript } from "@tiptap/extension-superscript";
|
||||
import SubScript from "@tiptap/extension-subscript";
|
||||
import { Typography } from "@tiptap/extension-typography";
|
||||
@@ -437,6 +437,7 @@ const TemplateSlashCommand = Command.configure({
|
||||
export const templateExtensions = [
|
||||
...mainExtensions.filter((ext: any) => ext !== SlashCommand),
|
||||
TemplateSlashCommand,
|
||||
UndoRedo,
|
||||
] as any;
|
||||
|
||||
export const collabExtensions: CollabExtensions = (provider, user) => [
|
||||
|
||||
Reference in New Issue
Block a user