support fixed toolbar in templates editor

This commit is contained in:
Philipinho
2026-05-27 18:08:44 +01:00
parent cb3b409a5a
commit 5a6b9503a7
7 changed files with 118 additions and 76 deletions
@@ -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>
); );
@@ -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) => [