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