mirror of
https://github.com/docmost/docmost.git
synced 2026-07-25 02:44:42 +10:00
bug fixes (#2250)
* util * fix page position collation * support fixed toolbar in templates editor * date localization * fix clipped emoji in templates editor * fix page updated time object * fix flickers * fix: remove redundant breadcrumb from destination modal
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
+30
-23
@@ -32,16 +32,17 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
editor: Editor;
|
||||
templateMode?: boolean;
|
||||
}
|
||||
|
||||
export const MoreInsertsGroup: FC<Props> = ({ editor }) => {
|
||||
const { t } = useTranslation();
|
||||
export const MoreInsertsGroup: FC<Props> = ({ editor, templateMode }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const setEmbed = (provider: string) =>
|
||||
editor.chain().focus().setEmbed({ provider }).run();
|
||||
|
||||
const insertDate = () => {
|
||||
const currentDate = new Date().toLocaleDateString("en-US", {
|
||||
const currentDate = new Date().toLocaleDateString(i18n.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
@@ -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>
|
||||
|
||||
@@ -43,6 +43,7 @@ import IconMermaid from "@/components/icons/icon-mermaid";
|
||||
import IconDrawio from "@/components/icons/icon-drawio";
|
||||
import { IconColumns4 } from "@/components/icons/icon-columns-4";
|
||||
import { IconColumns5 } from "@/components/icons/icon-columns-5";
|
||||
import i18n from "@/i18n.ts";
|
||||
import {
|
||||
AirtableIcon,
|
||||
FigmaIcon,
|
||||
@@ -459,7 +460,7 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
||||
searchTerms: ["date", "today"],
|
||||
icon: IconCalendar,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
const currentDate = new Date().toLocaleDateString("en-US", {
|
||||
const currentDate = new Date().toLocaleDateString(i18n.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
|
||||
@@ -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) => [
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
WebSocketStatus,
|
||||
HocuspocusProviderWebsocket,
|
||||
onSyncedParameters,
|
||||
onStatelessParameters,
|
||||
} from "@hocuspocus/provider";
|
||||
import {
|
||||
Editor,
|
||||
@@ -145,6 +146,24 @@ export default function PageEditor({
|
||||
const onSyncedHandler = (event: onSyncedParameters) => {
|
||||
setIsRemoteSynced(event.state);
|
||||
};
|
||||
const onStatelessHandler = ({ payload }: onStatelessParameters) => {
|
||||
try {
|
||||
const message = JSON.parse(payload);
|
||||
if (message?.type !== "page.updated" || !message.updatedAt) return;
|
||||
const pageData = queryClient.getQueryData<IPage>(["pages", slugId]);
|
||||
if (pageData) {
|
||||
queryClient.setQueryData(["pages", slugId], {
|
||||
...pageData,
|
||||
updatedAt: message.updatedAt,
|
||||
...(message.lastUpdatedBy && {
|
||||
lastUpdatedBy: message.lastUpdatedBy,
|
||||
}),
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// ignore unrelated stateless messages
|
||||
}
|
||||
};
|
||||
const onAuthenticationFailedHandler = () => {
|
||||
const payload = jwtDecode(collabQuery?.token);
|
||||
const now = Date.now().valueOf() / 1000;
|
||||
@@ -169,6 +188,7 @@ export default function PageEditor({
|
||||
onAuthenticationFailed: onAuthenticationFailedHandler,
|
||||
onStatus: onStatusHandler,
|
||||
onSynced: onSyncedHandler,
|
||||
onStateless: onStatelessHandler,
|
||||
});
|
||||
|
||||
local.on("synced", onLocalSyncedHandler);
|
||||
@@ -318,7 +338,6 @@ export default function PageEditor({
|
||||
queryClient.setQueryData(["pages", slugId], {
|
||||
...pageData,
|
||||
content: newContent,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
Reference in New Issue
Block a user