mirror of
https://github.com/docmost/docmost.git
synced 2025-11-18 04:01:12 +10:00
Support I18n (#243)
* feat: support i18n * feat: wip support i18n * feat: complete space translation * feat: complete page translation * feat: update space translation * feat: update workspace translation * feat: update group translation * feat: update workspace translation * feat: update page translation * feat: update user translation * chore: update pnpm-lock * feat: add query translation * refactor: merge to single file * chore: remove necessary code * feat: save language to BE * fix: only load current language * feat: save language to locale column * fix: cleanups * add language menu to preferences page * new translations * translate editor * Translate editor placeholders * translate space selection component --------- Co-authored-by: Philip Okugbe <phil@docmost.com> Co-authored-by: Philip Okugbe <16838612+Philipinho@users.noreply.github.com>
This commit is contained in:
@ -1,8 +1,9 @@
|
||||
import { handleAttachmentUpload } from "@docmost/editor-ext";
|
||||
import { uploadFile } from "@/features/page/services/page-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import {getFileUploadSizeLimit} from "@/lib/config.ts";
|
||||
import {formatBytes} from "@/lib";
|
||||
import { getFileUploadSizeLimit } from "@/lib/config.ts";
|
||||
import { formatBytes } from "@/lib";
|
||||
import i18n from "i18next";
|
||||
|
||||
export const uploadAttachmentAction = handleAttachmentUpload({
|
||||
onUpload: async (file: File, pageId: string): Promise<any> => {
|
||||
@ -23,7 +24,9 @@ export const uploadAttachmentAction = handleAttachmentUpload({
|
||||
if (file.size > getFileUploadSizeLimit()) {
|
||||
notifications.show({
|
||||
color: "red",
|
||||
message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`,
|
||||
message: i18n.t("File exceeds the {{limit}} attachment limit", {
|
||||
limit: formatBytes(getFileUploadSizeLimit()),
|
||||
}),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ import { useAtom } from "jotai";
|
||||
import { v7 as uuid7 } from "uuid";
|
||||
import { isCellSelection, isTextSelected } from "@docmost/editor-ext";
|
||||
import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface BubbleMenuItem {
|
||||
name: string;
|
||||
@ -39,6 +40,7 @@ type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children" | "editor"> & {
|
||||
};
|
||||
|
||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
||||
const [, setDraftCommentId] = useAtom(draftCommentIdAtom);
|
||||
const showCommentPopupRef = useRef(showCommentPopup);
|
||||
@ -49,31 +51,31 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
|
||||
const items: BubbleMenuItem[] = [
|
||||
{
|
||||
name: "bold",
|
||||
name: "Bold",
|
||||
isActive: () => props.editor.isActive("bold"),
|
||||
command: () => props.editor.chain().focus().toggleBold().run(),
|
||||
icon: IconBold,
|
||||
},
|
||||
{
|
||||
name: "italic",
|
||||
name: "Italic",
|
||||
isActive: () => props.editor.isActive("italic"),
|
||||
command: () => props.editor.chain().focus().toggleItalic().run(),
|
||||
icon: IconItalic,
|
||||
},
|
||||
{
|
||||
name: "underline",
|
||||
name: "Underline",
|
||||
isActive: () => props.editor.isActive("underline"),
|
||||
command: () => props.editor.chain().focus().toggleUnderline().run(),
|
||||
icon: IconUnderline,
|
||||
},
|
||||
{
|
||||
name: "strike",
|
||||
name: "Strike",
|
||||
isActive: () => props.editor.isActive("strike"),
|
||||
command: () => props.editor.chain().focus().toggleStrike().run(),
|
||||
icon: IconStrikethrough,
|
||||
},
|
||||
{
|
||||
name: "code",
|
||||
name: "Code",
|
||||
isActive: () => props.editor.isActive("code"),
|
||||
command: () => props.editor.chain().focus().toggleCode().run(),
|
||||
icon: IconCode,
|
||||
@ -81,7 +83,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
];
|
||||
|
||||
const commentItem: BubbleMenuItem = {
|
||||
name: "comment",
|
||||
name: "Comment",
|
||||
isActive: () => props.editor.isActive("comment"),
|
||||
command: () => {
|
||||
const commentId = uuid7();
|
||||
@ -138,13 +140,13 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
|
||||
<ActionIcon.Group>
|
||||
{items.map((item, index) => (
|
||||
<Tooltip key={index} label={item.name} withArrow>
|
||||
<Tooltip key={index} label={t(item.name)} withArrow>
|
||||
<ActionIcon
|
||||
key={index}
|
||||
variant="default"
|
||||
size="lg"
|
||||
radius="0"
|
||||
aria-label={item.name}
|
||||
aria-label={t(item.name)}
|
||||
className={clsx({ [classes.active]: item.isActive() })}
|
||||
style={{ border: "none" }}
|
||||
onClick={item.command}
|
||||
@ -175,7 +177,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
variant="default"
|
||||
size="lg"
|
||||
radius="0"
|
||||
aria-label={commentItem.name}
|
||||
aria-label={t(commentItem.name)}
|
||||
style={{ border: "none" }}
|
||||
onClick={commentItem.command}
|
||||
>
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface BubbleColorMenuItem {
|
||||
name: string;
|
||||
@ -106,6 +107,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const activeColorItem = TEXT_COLORS.find(({ color }) =>
|
||||
editor.isActive("textStyle", { color }),
|
||||
);
|
||||
@ -117,7 +119,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
return (
|
||||
<Popover width={200} opened={isOpen} withArrow>
|
||||
<Popover.Target>
|
||||
<Tooltip label="Text color" withArrow>
|
||||
<Tooltip label={t("Text color")} withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
@ -136,8 +138,8 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
<Popover.Dropdown>
|
||||
{/* make mah responsive */}
|
||||
<ScrollArea.Autosize type="scroll" mah="400">
|
||||
<Text span c="dimmed" inherit>
|
||||
COLOR
|
||||
<Text span c="dimmed" tt="uppercase" inherit>
|
||||
{t("Color")}
|
||||
</Text>
|
||||
|
||||
<Button.Group orientation="vertical">
|
||||
@ -155,7 +157,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
}
|
||||
onClick={() => {
|
||||
editor.commands.unsetColor();
|
||||
name !== "Default" &&
|
||||
name !== t("Default") &&
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
@ -165,7 +167,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
}}
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
{name}
|
||||
{t(name)}
|
||||
</Button>
|
||||
))}
|
||||
</Button.Group>
|
||||
|
||||
@ -3,6 +3,7 @@ import { IconLink } from "@tabler/icons-react";
|
||||
import { ActionIcon, Popover, Tooltip } from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface LinkSelectorProps {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
@ -15,6 +16,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const onLink = useCallback(
|
||||
(url: string) => {
|
||||
setIsOpen(false);
|
||||
@ -32,7 +34,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
||||
withArrow
|
||||
>
|
||||
<Popover.Target>
|
||||
<Tooltip label="Add link" withArrow>
|
||||
<Tooltip label={t("Add link")} withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
} from "@tabler/icons-react";
|
||||
import { Popover, Button, ScrollArea } from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface NodeSelectorProps {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
@ -33,6 +34,8 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const items: BubbleMenuItem[] = [
|
||||
{
|
||||
name: "Text",
|
||||
@ -114,7 +117,7 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
rightSection={<IconChevronDown size={16} />}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{activeItem?.name}
|
||||
{t(activeItem?.name)}
|
||||
</Button>
|
||||
</Popover.Target>
|
||||
|
||||
@ -137,7 +140,7 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
}}
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
{item.name}
|
||||
{t(item.name)}
|
||||
</Button>
|
||||
))}
|
||||
</Button.Group>
|
||||
|
||||
@ -17,8 +17,10 @@ import {
|
||||
IconInfoCircleFilled,
|
||||
} from "@tabler/icons-react";
|
||||
import { CalloutType } from "@docmost/editor-ext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldShow = useCallback(
|
||||
({ state }: ShouldShowProps) => {
|
||||
if (!state) {
|
||||
@ -71,11 +73,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
<ActionIcon.Group className="actionIconGroup">
|
||||
<Tooltip position="top" label="Info">
|
||||
<Tooltip position="top" label={t("Info")}>
|
||||
<ActionIcon
|
||||
onClick={() => setCalloutType("info")}
|
||||
size="lg"
|
||||
aria-label="Info"
|
||||
aria-label={t("Info")}
|
||||
variant={
|
||||
editor.isActive("callout", { type: "info" }) ? "light" : "default"
|
||||
}
|
||||
@ -84,11 +86,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Success">
|
||||
<Tooltip position="top" label={t("Success")}>
|
||||
<ActionIcon
|
||||
onClick={() => setCalloutType("success")}
|
||||
size="lg"
|
||||
aria-label="Success"
|
||||
aria-label={t("Success")}
|
||||
variant={
|
||||
editor.isActive("callout", { type: "success" })
|
||||
? "light"
|
||||
@ -99,11 +101,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Warning">
|
||||
<Tooltip position="top" label={t("Warning")}>
|
||||
<ActionIcon
|
||||
onClick={() => setCalloutType("warning")}
|
||||
size="lg"
|
||||
aria-label="Warning"
|
||||
aria-label={t("Warning")}
|
||||
variant={
|
||||
editor.isActive("callout", { type: "warning" })
|
||||
? "light"
|
||||
@ -114,11 +116,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Danger">
|
||||
<Tooltip position="top" label={t("Danger")}>
|
||||
<ActionIcon
|
||||
onClick={() => setCalloutType("danger")}
|
||||
size="lg"
|
||||
aria-label="Danger"
|
||||
aria-label={t("Danger")}
|
||||
variant={
|
||||
editor.isActive("callout", { type: "danger" })
|
||||
? "light"
|
||||
|
||||
@ -2,16 +2,17 @@ import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '@tiptap/react';
|
||||
import { ActionIcon, CopyButton, Group, Select, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { IconCheck, IconCopy } from '@tabler/icons-react';
|
||||
//import MermaidView from "@/features/editor/components/code-block/mermaid-view.tsx";
|
||||
import classes from './code-block.module.css';
|
||||
import React from 'react';
|
||||
import { Suspense } from 'react';
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const MermaidView = React.lazy(
|
||||
() => import('@/features/editor/components/code-block/mermaid-view.tsx')
|
||||
);
|
||||
|
||||
export default function CodeBlockView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, updateAttributes, extension, editor, getPos } = props;
|
||||
const { language } = node.attrs;
|
||||
const [languageValue, setLanguageValue] = useState<string | null>(
|
||||
@ -61,7 +62,7 @@ export default function CodeBlockView(props: NodeViewProps) {
|
||||
<CopyButton value={node?.textContent} timeout={2000}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip
|
||||
label={copied ? 'Copied' : 'Copy'}
|
||||
label={copied ? t('Copied') : t('Copy')}
|
||||
withArrow
|
||||
position="right"
|
||||
>
|
||||
|
||||
@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
|
||||
import mermaid from "mermaid";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import classes from "./code-block.module.css";
|
||||
import { t } from "i18next";
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
@ -29,11 +30,11 @@ export default function MermaidView({ props }: MermaidViewProps) {
|
||||
.catch((err) => {
|
||||
if (props.editor.isEditable) {
|
||||
setPreview(
|
||||
`<div class="${classes.error}">Mermaid diagram error: ${err}</div>`,
|
||||
`<div class="${classes.error}">${t("Mermaid diagram error:")} ${err}</div>`,
|
||||
);
|
||||
} else {
|
||||
setPreview(
|
||||
`<div class="${classes.error}">Invalid Mermaid Diagram</div>`,
|
||||
`<div class="${classes.error}">${t("Invalid Mermaid diagram")}</div>`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,25 +1,34 @@
|
||||
import { NodeViewProps, NodeViewWrapper } from '@tiptap/react';
|
||||
import { ActionIcon, Card, Image, Modal, Text, useComputedColorScheme } from '@mantine/core';
|
||||
import { useRef, useState } from 'react';
|
||||
import { uploadFile } from '@/features/page/services/page-service.ts';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { getDrawioUrl, getFileUrl } from '@/lib/config.ts';
|
||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Card,
|
||||
Image,
|
||||
Modal,
|
||||
Text,
|
||||
useComputedColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { useRef, useState } from "react";
|
||||
import { uploadFile } from "@/features/page/services/page-service.ts";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { getDrawioUrl, getFileUrl } from "@/lib/config.ts";
|
||||
import {
|
||||
DrawIoEmbed,
|
||||
DrawIoEmbedRef,
|
||||
EventExit,
|
||||
EventSave,
|
||||
} from 'react-drawio';
|
||||
import { IAttachment } from '@/lib/types';
|
||||
import { decodeBase64ToSvgString, svgStringToFile } from '@/lib/utils';
|
||||
import clsx from 'clsx';
|
||||
import { IconEdit } from '@tabler/icons-react';
|
||||
} from "react-drawio";
|
||||
import { IAttachment } from "@/lib/types";
|
||||
import { decodeBase64ToSvgString, svgStringToFile } from "@/lib/utils";
|
||||
import clsx from "clsx";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function DrawioView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, updateAttributes, editor, selected } = props;
|
||||
const { src, title, width, attachmentId } = node.attrs;
|
||||
const drawioRef = useRef<DrawIoEmbedRef>(null);
|
||||
const [initialXML, setInitialXML] = useState<string>('');
|
||||
const [initialXML, setInitialXML] = useState<string>("");
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const computedColorScheme = useComputedColorScheme();
|
||||
|
||||
@ -32,15 +41,15 @@ export default function DrawioView(props: NodeViewProps) {
|
||||
if (src) {
|
||||
const url = getFileUrl(src);
|
||||
const request = await fetch(url, {
|
||||
credentials: 'include',
|
||||
cache: 'no-store',
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
});
|
||||
const blob = await request.blob();
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(blob);
|
||||
reader.onloadend = () => {
|
||||
const base64data = (reader.result || '') as string;
|
||||
const base64data = (reader.result || "") as string;
|
||||
setInitialXML(base64data);
|
||||
};
|
||||
}
|
||||
@ -54,7 +63,7 @@ export default function DrawioView(props: NodeViewProps) {
|
||||
const handleSave = async (data: EventSave) => {
|
||||
const svgString = decodeBase64ToSvgString(data.xml);
|
||||
|
||||
const fileName = 'diagram.drawio.svg';
|
||||
const fileName = "diagram.drawio.svg";
|
||||
const drawioSVGFile = await svgStringToFile(svgString, fileName);
|
||||
|
||||
const pageId = editor.storage?.pageId;
|
||||
@ -81,15 +90,15 @@ export default function DrawioView(props: NodeViewProps) {
|
||||
<NodeViewWrapper>
|
||||
<Modal.Root opened={opened} onClose={close} fullScreen>
|
||||
<Modal.Overlay />
|
||||
<Modal.Content style={{ overflow: 'hidden' }}>
|
||||
<Modal.Content style={{ overflow: "hidden" }}>
|
||||
<Modal.Body>
|
||||
<div style={{ height: '100vh' }}>
|
||||
<div style={{ height: "100vh" }}>
|
||||
<DrawIoEmbed
|
||||
ref={drawioRef}
|
||||
xml={initialXML}
|
||||
baseUrl={getDrawioUrl()}
|
||||
urlParameters={{
|
||||
ui: computedColorScheme === 'light' ? 'kennedy' : 'dark',
|
||||
ui: computedColorScheme === "light" ? "kennedy" : "dark",
|
||||
spin: true,
|
||||
libraries: true,
|
||||
saveAndExit: true,
|
||||
@ -97,7 +106,7 @@ export default function DrawioView(props: NodeViewProps) {
|
||||
}}
|
||||
onSave={(data: EventSave) => {
|
||||
// If the save is triggered by another event, then do nothing
|
||||
if (data.parentEvent !== 'save') {
|
||||
if (data.parentEvent !== "save") {
|
||||
return;
|
||||
}
|
||||
handleSave(data);
|
||||
@ -116,7 +125,7 @@ export default function DrawioView(props: NodeViewProps) {
|
||||
</Modal.Root>
|
||||
|
||||
{src ? (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<Image
|
||||
onClick={(e) => e.detail === 2 && handleOpen()}
|
||||
radius="md"
|
||||
@ -125,8 +134,8 @@ export default function DrawioView(props: NodeViewProps) {
|
||||
src={getFileUrl(src)}
|
||||
alt={title}
|
||||
className={clsx(
|
||||
selected ? 'ProseMirror-selectednode' : '',
|
||||
'alignCenter'
|
||||
selected ? "ProseMirror-selectednode" : "",
|
||||
"alignCenter",
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -137,7 +146,7 @@ export default function DrawioView(props: NodeViewProps) {
|
||||
color="gray"
|
||||
mx="xs"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
}}
|
||||
@ -152,20 +161,20 @@ export default function DrawioView(props: NodeViewProps) {
|
||||
onClick={(e) => e.detail === 2 && handleOpen()}
|
||||
p="xs"
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
withBorder
|
||||
className={clsx(selected ? 'ProseMirror-selectednode' : '')}
|
||||
className={clsx(selected ? "ProseMirror-selectednode" : "")}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<ActionIcon variant="transparent" color="gray">
|
||||
<IconEdit size={18} />
|
||||
</ActionIcon>
|
||||
|
||||
<Text component="span" size="lg" c="dimmed">
|
||||
Double-click to edit drawio diagram
|
||||
{t("Double-click to edit Draw.io diagram")}
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -1,22 +1,37 @@
|
||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { useMemo } from "react";
|
||||
import clsx from "clsx";
|
||||
import { ActionIcon, AspectRatio, Button, Card, FocusTrap, Group, Popover, Text, TextInput } from "@mantine/core";
|
||||
import {
|
||||
ActionIcon,
|
||||
AspectRatio,
|
||||
Button,
|
||||
Card,
|
||||
FocusTrap,
|
||||
Group,
|
||||
Popover,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import { z } from "zod";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import {
|
||||
getEmbedProviderById,
|
||||
getEmbedUrlAndProvider
|
||||
getEmbedUrlAndProvider,
|
||||
} from "@/features/editor/components/embed/providers.ts";
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import i18n from "i18next";
|
||||
|
||||
const schema = z.object({
|
||||
url: z
|
||||
.string().trim().url({ message: 'please enter a valid url' }),
|
||||
.string()
|
||||
.trim()
|
||||
.url({ message: i18n.t("Please enter a valid url") }),
|
||||
});
|
||||
|
||||
export default function EmbedView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, selected, updateAttributes } = props;
|
||||
const { src, provider } = node.attrs;
|
||||
|
||||
@ -41,9 +56,9 @@ export default function EmbedView(props: NodeViewProps) {
|
||||
updateAttributes({ src: data.url });
|
||||
} else {
|
||||
notifications.show({
|
||||
message: `Invalid ${provider} embed link`,
|
||||
position: 'top-right',
|
||||
color: 'red'
|
||||
message: t("Invalid {{provider}} embed link", { provider: provider }),
|
||||
position: "top-right",
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -62,7 +77,6 @@ export default function EmbedView(props: NodeViewProps) {
|
||||
frameBorder="0"
|
||||
></iframe>
|
||||
</AspectRatio>
|
||||
|
||||
</>
|
||||
) : (
|
||||
<Popover width={300} position="bottom" withArrow shadow="md">
|
||||
@ -71,20 +85,22 @@ export default function EmbedView(props: NodeViewProps) {
|
||||
radius="md"
|
||||
p="xs"
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
withBorder
|
||||
className={clsx(selected ? 'ProseMirror-selectednode' : '')}
|
||||
className={clsx(selected ? "ProseMirror-selectednode" : "")}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<ActionIcon variant="transparent" color="gray">
|
||||
<IconEdit size={18} />
|
||||
</ActionIcon>
|
||||
|
||||
<Text component="span" size="lg" c="dimmed">
|
||||
Embed {getEmbedProviderById(provider).name}
|
||||
{t("Embed {{provider}}", {
|
||||
provider: getEmbedProviderById(provider).name,
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
@ -92,15 +108,18 @@ export default function EmbedView(props: NodeViewProps) {
|
||||
<Popover.Dropdown bg="var(--mantine-color-body)">
|
||||
<form onSubmit={embedForm.onSubmit(onSubmit)}>
|
||||
<FocusTrap active={true}>
|
||||
<TextInput placeholder={`Enter ${getEmbedProviderById(provider).name} link to embed`}
|
||||
key={embedForm.key('url')}
|
||||
{... embedForm.getInputProps('url')}
|
||||
data-autofocus
|
||||
<TextInput
|
||||
placeholder={t("Enter {{provider}} link to embed", {
|
||||
provider: getEmbedProviderById(provider).name,
|
||||
})}
|
||||
key={embedForm.key("url")}
|
||||
{...embedForm.getInputProps("url")}
|
||||
data-autofocus
|
||||
/>
|
||||
</FocusTrap>
|
||||
|
||||
<Group justify="center" mt="xs">
|
||||
<Button type="submit">Embed link</Button>
|
||||
<Button type="submit">{t("Embed link")}</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Popover.Dropdown>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { NodeViewProps, NodeViewWrapper } from '@tiptap/react';
|
||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
@ -7,27 +7,29 @@ import {
|
||||
Image,
|
||||
Text,
|
||||
useComputedColorScheme,
|
||||
} from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { uploadFile } from '@/features/page/services/page-service.ts';
|
||||
import { svgStringToFile } from '@/lib';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { getFileUrl } from '@/lib/config.ts';
|
||||
import { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types';
|
||||
import { IAttachment } from '@/lib/types';
|
||||
import ReactClearModal from 'react-clear-modal';
|
||||
import clsx from 'clsx';
|
||||
import { IconEdit } from '@tabler/icons-react';
|
||||
import { lazy } from 'react';
|
||||
import { Suspense } from 'react';
|
||||
} from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
import { uploadFile } from "@/features/page/services/page-service.ts";
|
||||
import { svgStringToFile } from "@/lib";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { getFileUrl } from "@/lib/config.ts";
|
||||
import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types/types";
|
||||
import { IAttachment } from "@/lib/types";
|
||||
import ReactClearModal from "react-clear-modal";
|
||||
import clsx from "clsx";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import { lazy } from "react";
|
||||
import { Suspense } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Excalidraw = lazy(() =>
|
||||
import('@excalidraw/excalidraw').then((module) => ({
|
||||
import("@excalidraw/excalidraw").then((module) => ({
|
||||
default: module.Excalidraw,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
|
||||
export default function ExcalidrawView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, updateAttributes, editor, selected } = props;
|
||||
const { src, title, width, attachmentId } = node.attrs;
|
||||
|
||||
@ -46,11 +48,11 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
||||
if (src) {
|
||||
const url = getFileUrl(src);
|
||||
const request = await fetch(url, {
|
||||
credentials: 'include',
|
||||
cache: 'no-store',
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
const { loadFromBlob } = await import('@excalidraw/excalidraw');
|
||||
const { loadFromBlob } = await import("@excalidraw/excalidraw");
|
||||
|
||||
const data = await loadFromBlob(await request.blob(), null, null);
|
||||
setExcalidrawData(data);
|
||||
@ -67,7 +69,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { exportToSvg } = await import('@excalidraw/excalidraw');
|
||||
const { exportToSvg } = await import("@excalidraw/excalidraw");
|
||||
|
||||
const svg = await exportToSvg({
|
||||
elements: excalidrawAPI?.getSceneElements(),
|
||||
@ -83,10 +85,10 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
||||
|
||||
svgString = svgString.replace(
|
||||
/https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g,
|
||||
'https://unpkg.com/@excalidraw/excalidraw@latest'
|
||||
"https://unpkg.com/@excalidraw/excalidraw@latest",
|
||||
);
|
||||
|
||||
const fileName = 'diagram.excalidraw.svg';
|
||||
const fileName = "diagram.excalidraw.svg";
|
||||
const excalidrawSvgFile = await svgStringToFile(svgString, fileName);
|
||||
|
||||
const pageId = editor.storage?.pageId;
|
||||
@ -112,7 +114,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
||||
<NodeViewWrapper>
|
||||
<ReactClearModal
|
||||
style={{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
padding: 0,
|
||||
zIndex: 200,
|
||||
}}
|
||||
@ -122,7 +124,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
||||
contentProps={{
|
||||
style: {
|
||||
padding: 0,
|
||||
width: '90vw',
|
||||
width: "90vw",
|
||||
},
|
||||
}}
|
||||
>
|
||||
@ -132,14 +134,14 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
||||
bg="var(--mantine-color-body)"
|
||||
p="xs"
|
||||
>
|
||||
<Button onClick={handleSave} size={'compact-sm'}>
|
||||
Save & Exit
|
||||
<Button onClick={handleSave} size={"compact-sm"}>
|
||||
{t("Save & Exit")}
|
||||
</Button>
|
||||
<Button onClick={close} color="red" size={'compact-sm'}>
|
||||
Exit
|
||||
<Button onClick={close} color="red" size={"compact-sm"}>
|
||||
{t("Exit")}
|
||||
</Button>
|
||||
</Group>
|
||||
<div style={{ height: '90vh' }}>
|
||||
<div style={{ height: "90vh" }}>
|
||||
<Suspense fallback={null}>
|
||||
<Excalidraw
|
||||
excalidrawAPI={(api) => setExcalidrawAPI(api)}
|
||||
@ -154,7 +156,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
||||
</ReactClearModal>
|
||||
|
||||
{src ? (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<Image
|
||||
onClick={(e) => e.detail === 2 && handleOpen()}
|
||||
radius="md"
|
||||
@ -163,8 +165,8 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
||||
src={getFileUrl(src)}
|
||||
alt={title}
|
||||
className={clsx(
|
||||
selected ? 'ProseMirror-selectednode' : '',
|
||||
'alignCenter'
|
||||
selected ? "ProseMirror-selectednode" : "",
|
||||
"alignCenter",
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -175,7 +177,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
||||
color="gray"
|
||||
mx="xs"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
}}
|
||||
@ -190,20 +192,20 @@ export default function ExcalidrawView(props: NodeViewProps) {
|
||||
onClick={(e) => e.detail === 2 && handleOpen()}
|
||||
p="xs"
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
withBorder
|
||||
className={clsx(selected ? 'ProseMirror-selectednode' : '')}
|
||||
className={clsx(selected ? "ProseMirror-selectednode" : "")}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<ActionIcon variant="transparent" color="gray">
|
||||
<IconEdit size={18} />
|
||||
</ActionIcon>
|
||||
|
||||
<Text component="span" size="lg" c="dimmed">
|
||||
Double-click to edit Excalidraw diagram
|
||||
{t("Double-click to edit Excalidraw diagram")}
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -17,8 +17,10 @@ import {
|
||||
IconLayoutAlignRight,
|
||||
} from "@tabler/icons-react";
|
||||
import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function ImageMenu({ editor }: EditorMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldShow = useCallback(
|
||||
({ state }: ShouldShowProps) => {
|
||||
if (!state) {
|
||||
@ -96,11 +98,11 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
<ActionIcon.Group className="actionIconGroup">
|
||||
<Tooltip position="top" label="Align image left">
|
||||
<Tooltip position="top" label={t("Align left")}>
|
||||
<ActionIcon
|
||||
onClick={alignImageLeft}
|
||||
size="lg"
|
||||
aria-label="Align image left"
|
||||
aria-label={t("Align left")}
|
||||
variant={
|
||||
editor.isActive("image", { align: "left" }) ? "light" : "default"
|
||||
}
|
||||
@ -109,11 +111,11 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Align image center">
|
||||
<Tooltip position="top" label={t("Align center")}>
|
||||
<ActionIcon
|
||||
onClick={alignImageCenter}
|
||||
size="lg"
|
||||
aria-label="Align image center"
|
||||
aria-label={t("Align center")}
|
||||
variant={
|
||||
editor.isActive("image", { align: "center" })
|
||||
? "light"
|
||||
@ -124,11 +126,11 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Align image right">
|
||||
<Tooltip position="top" label={t("Align right")}>
|
||||
<ActionIcon
|
||||
onClick={alignImageRight}
|
||||
size="lg"
|
||||
aria-label="Align image right"
|
||||
aria-label={t("Align right")}
|
||||
variant={
|
||||
editor.isActive("image", { align: "right" }) ? "light" : "default"
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { handleImageUpload } from "@docmost/editor-ext";
|
||||
import { uploadFile } from "@/features/page/services/page-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import {getFileUploadSizeLimit} from "@/lib/config.ts";
|
||||
import { getFileUploadSizeLimit } from "@/lib/config.ts";
|
||||
import { formatBytes } from "@/lib";
|
||||
import i18n from "i18next";
|
||||
|
||||
export const uploadImageAction = handleImageUpload({
|
||||
onUpload: async (file: File, pageId: string): Promise<any> => {
|
||||
@ -23,7 +24,9 @@ export const uploadImageAction = handleImageUpload({
|
||||
if (file.size > getFileUploadSizeLimit()) {
|
||||
notifications.show({
|
||||
color: "red",
|
||||
message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`,
|
||||
message: i18n.t("File exceeds the {{limit}} attachment limit", {
|
||||
limit: formatBytes(getFileUploadSizeLimit()),
|
||||
}),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -3,11 +3,13 @@ import { Button, Group, TextInput } from "@mantine/core";
|
||||
import { IconLink } from "@tabler/icons-react";
|
||||
import { useLinkEditorState } from "@/features/editor/components/link/use-link-editor-state.tsx";
|
||||
import { LinkEditorPanelProps } from "@/features/editor/components/link/types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const LinkEditorPanel = ({
|
||||
onSetLink,
|
||||
initialUrl,
|
||||
}: LinkEditorPanelProps) => {
|
||||
const { t } = useTranslation();
|
||||
const state = useLinkEditorState({
|
||||
onSetLink,
|
||||
initialUrl,
|
||||
@ -20,12 +22,12 @@ export const LinkEditorPanel = ({
|
||||
<TextInput
|
||||
leftSection={<IconLink size={16} />}
|
||||
variant="filled"
|
||||
placeholder="Paste link"
|
||||
placeholder={t("Paste link")}
|
||||
value={state.url}
|
||||
onChange={state.onChange}
|
||||
/>
|
||||
<Button p={"xs"} type="submit" disabled={!state.isValidUrl}>
|
||||
Save
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
Flex,
|
||||
} from "@mantine/core";
|
||||
import { IconLinkOff, IconPencil } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type LinkPreviewPanelProps = {
|
||||
url: string;
|
||||
@ -19,6 +20,8 @@ export const LinkPreviewPanel = ({
|
||||
onEdit,
|
||||
url,
|
||||
}: LinkPreviewPanelProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card withBorder radius="md" padding="xs" bg="var(--mantine-color-body)">
|
||||
@ -42,13 +45,13 @@ export const LinkPreviewPanel = ({
|
||||
<Flex align="center">
|
||||
<Divider mx={4} orientation="vertical" />
|
||||
|
||||
<Tooltip label="Edit link">
|
||||
<Tooltip label={t("Edit link")}>
|
||||
<ActionIcon onClick={onEdit} variant="subtle" color="gray">
|
||||
<IconPencil size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Remove link">
|
||||
<Tooltip label={t("Remove link")}>
|
||||
<ActionIcon onClick={onClear} variant="subtle" color="red">
|
||||
<IconLinkOff size={16} />
|
||||
</ActionIcon>
|
||||
|
||||
@ -8,8 +8,10 @@ import classes from "./math.module.css";
|
||||
import { v4 } from "uuid";
|
||||
import { IconTrashX } from "@tabler/icons-react";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function MathBlockView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, updateAttributes, editor, getPos } = props;
|
||||
const mathResultContainer = useRef<HTMLDivElement>(null);
|
||||
const mathPreviewContainer = useRef<HTMLDivElement>(null);
|
||||
@ -94,9 +96,9 @@ export default function MathBlockView(props: NodeViewProps) {
|
||||
></div>
|
||||
{((isEditing && !preview?.trim().length) ||
|
||||
(!isEditing && !node.attrs.text.trim().length)) && (
|
||||
<div>Empty equation</div>
|
||||
<div>{t("Empty equation")}</div>
|
||||
)}
|
||||
{error && <div>Invalid equation</div>}
|
||||
{error && <div>{t("Invalid equation")}</div>}
|
||||
</NodeViewWrapper>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
|
||||
@ -6,8 +6,10 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { Popover, Textarea } from "@mantine/core";
|
||||
import classes from "./math.module.css";
|
||||
import { v4 } from "uuid";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function MathInlineView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, updateAttributes, editor, getPos } = props;
|
||||
const mathResultContainer = useRef<HTMLDivElement>(null);
|
||||
const mathPreviewContainer = useRef<HTMLDivElement>(null);
|
||||
@ -84,9 +86,9 @@ export default function MathInlineView(props: NodeViewProps) {
|
||||
></div>
|
||||
{((isEditing && !preview?.trim().length) ||
|
||||
(!isEditing && !node.attrs.text.trim().length)) && (
|
||||
<div>Empty equation</div>
|
||||
<div>{t("Empty equation")}</div>
|
||||
)}
|
||||
{error && <div>Invalid equation</div>}
|
||||
{error && <div>{t("Invalid equation")}</div>}
|
||||
</NodeViewWrapper>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown p={"xs"}>
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import classes from "./slash-menu.module.css";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const CommandList = ({
|
||||
items,
|
||||
@ -25,6 +26,7 @@ const CommandList = ({
|
||||
editor: any;
|
||||
range: any;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -104,18 +106,17 @@ const CommandList = ({
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
component="div"
|
||||
aria-label={item.title}
|
||||
>
|
||||
<item.icon size={18} />
|
||||
</ActionIcon>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500}>
|
||||
{item.title}
|
||||
{t(item.title)}
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{item.description}
|
||||
{t(item.description)}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
@ -13,9 +13,11 @@ import {
|
||||
IconRowRemove,
|
||||
IconSquareToggle,
|
||||
} from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const TableCellMenu = React.memo(
|
||||
({ editor, appendTo }: EditorMenuProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const shouldShow = useCallback(
|
||||
({ view, state, from }: ShouldShowProps) => {
|
||||
if (!state) {
|
||||
@ -58,45 +60,45 @@ export const TableCellMenu = React.memo(
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
<ActionIcon.Group>
|
||||
<Tooltip position="top" label="Merge cells">
|
||||
<Tooltip position="top" label={t("Merge cells")}>
|
||||
<ActionIcon
|
||||
onClick={mergeCells}
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Merge cells"
|
||||
aria-label={t("Merge cells")}
|
||||
>
|
||||
<IconBoxMargin size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Split cell">
|
||||
<Tooltip position="top" label={t("Split cell")}>
|
||||
<ActionIcon
|
||||
onClick={splitCell}
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Split cell"
|
||||
aria-label={t("Split cell")}
|
||||
>
|
||||
<IconSquareToggle size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Delete column">
|
||||
<Tooltip position="top" label={t("Delete column")}>
|
||||
<ActionIcon
|
||||
onClick={deleteColumn}
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Delete column"
|
||||
aria-label={t("Delete column")}
|
||||
>
|
||||
<IconColumnRemove size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Delete row">
|
||||
<Tooltip position="top" label={t("Delete row")}>
|
||||
<ActionIcon
|
||||
onClick={deleteRow}
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Delete row"
|
||||
aria-label={t("Delete row")}
|
||||
>
|
||||
<IconRowRemove size={18} />
|
||||
</ActionIcon>
|
||||
|
||||
@ -21,9 +21,11 @@ import {
|
||||
IconTrashX,
|
||||
} from "@tabler/icons-react";
|
||||
import { isCellSelection } from "@docmost/editor-ext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const TableMenu = React.memo(
|
||||
({ editor }: EditorMenuProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const shouldShow = useCallback(
|
||||
({ state }: ShouldShowProps) => {
|
||||
if (!state) {
|
||||
@ -111,79 +113,80 @@ export const TableMenu = React.memo(
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
<ActionIcon.Group>
|
||||
<Tooltip position="top" label="Add left column">
|
||||
<Tooltip position="top" label={t("Add left column")}
|
||||
>
|
||||
<ActionIcon
|
||||
onClick={addColumnLeft}
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Add left column"
|
||||
aria-label={t("Add left column")}
|
||||
>
|
||||
<IconColumnInsertLeft size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Add right column">
|
||||
<Tooltip position="top" label={t("Add right column")}>
|
||||
<ActionIcon
|
||||
onClick={addColumnRight}
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Add right column"
|
||||
aria-label={t("Add right column")}
|
||||
>
|
||||
<IconColumnInsertRight size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Delete column">
|
||||
<Tooltip position="top" label={t("Delete column")}>
|
||||
<ActionIcon
|
||||
onClick={deleteColumn}
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Delete column"
|
||||
aria-label={t("Delete column")}
|
||||
>
|
||||
<IconColumnRemove size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Add row above">
|
||||
<Tooltip position="top" label={t("Add row above")}>
|
||||
<ActionIcon
|
||||
onClick={addRowAbove}
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Add row above"
|
||||
aria-label={t("Add row above")}
|
||||
>
|
||||
<IconRowInsertTop size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Add row below">
|
||||
<Tooltip position="top" label={t("Add row below")}>
|
||||
<ActionIcon
|
||||
onClick={addRowBelow}
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Add row below"
|
||||
aria-label={t("Add row below")}
|
||||
>
|
||||
<IconRowInsertBottom size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Delete row">
|
||||
<Tooltip position="top" label={t("Delete row")}>
|
||||
<ActionIcon
|
||||
onClick={deleteRow}
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Delete row"
|
||||
aria-label={t("Delete row")}
|
||||
>
|
||||
<IconRowRemove size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Delete table">
|
||||
<Tooltip position="top" label={t("Delete table")}>
|
||||
<ActionIcon
|
||||
onClick={deleteTable}
|
||||
variant="default"
|
||||
size="lg"
|
||||
color="red"
|
||||
aria-label="Delete table"
|
||||
aria-label={t("Delete table")}
|
||||
>
|
||||
<IconTrashX size={18} />
|
||||
</ActionIcon>
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { handleVideoUpload } from "@docmost/editor-ext";
|
||||
import { uploadFile } from "@/features/page/services/page-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import {getFileUploadSizeLimit} from "@/lib/config.ts";
|
||||
import {formatBytes} from "@/lib";
|
||||
import { getFileUploadSizeLimit } from "@/lib/config.ts";
|
||||
import { formatBytes } from "@/lib";
|
||||
import i18n from "i18next";
|
||||
|
||||
export const uploadVideoAction = handleVideoUpload({
|
||||
onUpload: async (file: File, pageId: string): Promise<any> => {
|
||||
@ -24,11 +25,12 @@ export const uploadVideoAction = handleVideoUpload({
|
||||
if (file.size > getFileUploadSizeLimit()) {
|
||||
notifications.show({
|
||||
color: "red",
|
||||
message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`,
|
||||
message: i18n.t("File exceeds the {{limit}} attachment limit", {
|
||||
limit: formatBytes(getFileUploadSizeLimit()),
|
||||
}),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -17,8 +17,10 @@ import {
|
||||
IconLayoutAlignRight,
|
||||
} from "@tabler/icons-react";
|
||||
import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function VideoMenu({ editor }: EditorMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldShow = useCallback(
|
||||
({ state }: ShouldShowProps) => {
|
||||
if (!state) {
|
||||
@ -96,11 +98,11 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
<ActionIcon.Group className="actionIconGroup">
|
||||
<Tooltip position="top" label="Align video left">
|
||||
<Tooltip position="top" label={t("Align left")}>
|
||||
<ActionIcon
|
||||
onClick={alignVideoLeft}
|
||||
size="lg"
|
||||
aria-label="Align video left"
|
||||
aria-label={t("Align left")}
|
||||
variant={
|
||||
editor.isActive("video", { align: "left" }) ? "light" : "default"
|
||||
}
|
||||
@ -109,11 +111,11 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Align video center">
|
||||
<Tooltip position="top" label={t("Align center")}>
|
||||
<ActionIcon
|
||||
onClick={alignVideoCenter}
|
||||
size="lg"
|
||||
aria-label="Align video center"
|
||||
aria-label={t("Align center")}
|
||||
variant={
|
||||
editor.isActive("video", { align: "center" })
|
||||
? "light"
|
||||
@ -124,11 +126,11 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Align video right">
|
||||
<Tooltip position="top" label={t("Align right")}>
|
||||
<ActionIcon
|
||||
onClick={alignVideoRight}
|
||||
size="lg"
|
||||
aria-label="Align video right"
|
||||
aria-label={t("Align right")}
|
||||
variant={
|
||||
editor.isActive("video", { align: "right" }) ? "light" : "default"
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ import {
|
||||
CustomCodeBlock,
|
||||
Drawio,
|
||||
Excalidraw,
|
||||
Embed
|
||||
Embed,
|
||||
} from "@docmost/editor-ext";
|
||||
import {
|
||||
randomElement,
|
||||
@ -64,6 +64,7 @@ import clojure from "highlight.js/lib/languages/clojure";
|
||||
import fortran from "highlight.js/lib/languages/fortran";
|
||||
import haskell from "highlight.js/lib/languages/haskell";
|
||||
import scala from "highlight.js/lib/languages/scala";
|
||||
import i18n from "@/i18n.ts";
|
||||
|
||||
const lowlight = createLowlight(common);
|
||||
lowlight.register("mermaid", plaintext);
|
||||
@ -94,13 +95,13 @@ export const mainExtensions = [
|
||||
Placeholder.configure({
|
||||
placeholder: ({ node }) => {
|
||||
if (node.type.name === "heading") {
|
||||
return `Heading ${node.attrs.level}`;
|
||||
return i18n.t("Heading {{level}}", { level: node.attrs.level });
|
||||
}
|
||||
if (node.type.name === "detailsSummary") {
|
||||
return "Toggle title";
|
||||
return i18n.t("Toggle title");
|
||||
}
|
||||
if (node.type.name === "paragraph") {
|
||||
return 'Write anything. Enter "/" for commands';
|
||||
return i18n.t('Write anything. Enter "/" for commands');
|
||||
}
|
||||
},
|
||||
includeChildren: true,
|
||||
@ -184,7 +185,7 @@ export const mainExtensions = [
|
||||
}),
|
||||
Embed.configure({
|
||||
view: EmbedView,
|
||||
})
|
||||
}),
|
||||
] as any;
|
||||
|
||||
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];
|
||||
|
||||
@ -19,6 +19,7 @@ import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
|
||||
import { History } from "@tiptap/extension-history";
|
||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface TitleEditorProps {
|
||||
pageId: string;
|
||||
@ -35,6 +36,7 @@ export function TitleEditor({
|
||||
spaceSlug,
|
||||
editable,
|
||||
}: TitleEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const [debouncedTitleState, setDebouncedTitleState] = useState(null);
|
||||
const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 500);
|
||||
const {
|
||||
@ -59,7 +61,7 @@ export function TitleEditor({
|
||||
}),
|
||||
Text,
|
||||
Placeholder.configure({
|
||||
placeholder: "Untitled",
|
||||
placeholder: t("Untitled"),
|
||||
showOnlyWhenEditable: false,
|
||||
}),
|
||||
History.configure({
|
||||
|
||||
Reference in New Issue
Block a user