mirror of
https://github.com/docmost/docmost.git
synced 2026-07-24 02:52:51 +10:00
Merge branch 'main' into feat/bases
This commit is contained in:
+17
-17
@@ -21,20 +21,20 @@
|
||||
"@docmost/base-formula": "workspace:*",
|
||||
"@docmost/editor-ext": "workspace:*",
|
||||
"@excalidraw/excalidraw": "0.18.0-3a5ef40",
|
||||
"@mantine/core": "8.3.18",
|
||||
"@mantine/dates": "8.3.18",
|
||||
"@mantine/form": "8.3.18",
|
||||
"@mantine/hooks": "8.3.18",
|
||||
"@mantine/modals": "8.3.18",
|
||||
"@mantine/notifications": "8.3.18",
|
||||
"@mantine/spotlight": "8.3.18",
|
||||
"@mantine/core": "9.3.2",
|
||||
"@mantine/dates": "9.3.2",
|
||||
"@mantine/form": "9.3.2",
|
||||
"@mantine/hooks": "9.3.2",
|
||||
"@mantine/modals": "9.3.2",
|
||||
"@mantine/notifications": "9.3.2",
|
||||
"@mantine/spotlight": "9.3.2",
|
||||
"@slidoapp/emoji-mart": "5.8.7",
|
||||
"@slidoapp/emoji-mart-data": "1.2.4",
|
||||
"@slidoapp/emoji-mart-react": "1.1.5",
|
||||
"@tabler/icons-react": "3.40.0",
|
||||
"@tanstack/react-query": "5.90.17",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.14.2",
|
||||
"@tanstack/react-virtual": "3.14.3",
|
||||
"alfaaz": "1.1.0",
|
||||
"axios": "1.16.0",
|
||||
"blueimp-load-image": "5.16.0",
|
||||
@@ -43,7 +43,7 @@
|
||||
"highlightjs-sap-abap": "0.3.0",
|
||||
"i18next": "25.10.1",
|
||||
"i18next-http-backend": "3.0.6",
|
||||
"jotai": "2.18.1",
|
||||
"jotai": "2.20.1",
|
||||
"jotai-optics": "0.4.0",
|
||||
"js-cookie": "3.0.7",
|
||||
"jwt-decode": "4.0.0",
|
||||
@@ -52,16 +52,16 @@
|
||||
"mantine-form-zod-resolver": "1.3.0",
|
||||
"mermaid": "11.15.0",
|
||||
"mitt": "3.0.1",
|
||||
"nanoid": "^3.3.8",
|
||||
"posthog-js": "1.372.2",
|
||||
"react": "18.3.1",
|
||||
"nanoid": "3.3.8",
|
||||
"posthog-js": "1.391.2",
|
||||
"react": "19.2.7",
|
||||
"react-clear-modal": "^2.0.18",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dom": "19.2.7",
|
||||
"react-drawio": "1.0.7",
|
||||
"react-error-boundary": "6.1.1",
|
||||
"react-helmet-async": "3.0.0",
|
||||
"react-i18next": "16.5.8",
|
||||
"react-router-dom": "7.13.1",
|
||||
"react-router-dom": "7.18.0",
|
||||
"semver": "7.7.4",
|
||||
"socket.io-client": "4.8.3",
|
||||
"zod": "4.3.6"
|
||||
@@ -76,8 +76,8 @@
|
||||
"@types/js-cookie": "3.0.6",
|
||||
"@types/katex": "0.16.8",
|
||||
"@types/node": "22.19.1",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/react": "19.2.17",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "6.0.1",
|
||||
"eslint": "9.28.0",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
@@ -92,7 +92,7 @@
|
||||
"prettier": "3.8.1",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.57.1",
|
||||
"vite": "8.0.5",
|
||||
"vite": "8.0.16",
|
||||
"vitest": "4.1.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,6 +418,8 @@
|
||||
"Insert mermaid diagram": "Insert mermaid diagram",
|
||||
"Insert and design Drawio diagrams": "Insert and design Drawio diagrams",
|
||||
"Insert current date": "Insert current date",
|
||||
"Time": "Time",
|
||||
"Insert current time": "Insert current time",
|
||||
"Draw and sketch excalidraw diagrams": "Draw and sketch excalidraw diagrams",
|
||||
"Multiple": "Multiple",
|
||||
"Turn into": "Turn into",
|
||||
@@ -1108,6 +1110,12 @@
|
||||
"Removed {{name}} from favorites": "Removed {{name}} from favorites",
|
||||
"Page menu for {{name}}": "Page menu for {{name}}",
|
||||
"Create subpage of {{name}}": "Create subpage of {{name}}",
|
||||
"Allow personal spaces": "Allow personal spaces",
|
||||
"Members can create their own personal space.": "Members can create their own personal space.",
|
||||
"Toggle allow personal spaces": "Toggle allow personal spaces",
|
||||
"Create personal space": "Create personal space",
|
||||
"Personal space": "Personal space",
|
||||
"{{name}}'s space": "{{name}}'s space",
|
||||
"Apply": "Apply",
|
||||
"Cells that aren't already a page reference will be cleared.": "Cells that aren't already a page reference will be cleared.",
|
||||
"Cells that aren't a valid URL will be cleared.": "Cells that aren't a valid URL will be cleared.",
|
||||
|
||||
@@ -6,13 +6,21 @@ import {
|
||||
Select,
|
||||
Switch,
|
||||
Divider,
|
||||
Tooltip,
|
||||
Badge,
|
||||
} from "@mantine/core";
|
||||
import { exportPage } from "@/features/page/services/page-service.ts";
|
||||
import {
|
||||
exportPage,
|
||||
exportPageToDocx,
|
||||
} from "@/features/page/services/page-service.ts";
|
||||
import { useState } from "react";
|
||||
import { ExportFormat } from "@/features/page/types/page.types.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { exportSpace } from "@/features/space/services/space-service";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
|
||||
interface ExportModalProps {
|
||||
id: string;
|
||||
@@ -32,17 +40,25 @@ export default function ExportModal({
|
||||
const [includeAttachments, setIncludeAttachments] = useState<boolean>(false);
|
||||
const [isExporting, setIsExporting] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
const isDocx = format === ExportFormat.Docx;
|
||||
const docxEntitled = useHasFeature(Feature.DOCX_EXPORT);
|
||||
const blockedByLicense = isDocx && !docxEntitled;
|
||||
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
if (type === "page") {
|
||||
await exportPage({
|
||||
pageId: id,
|
||||
format,
|
||||
includeChildren,
|
||||
includeAttachments,
|
||||
});
|
||||
if (format === ExportFormat.Docx) {
|
||||
await exportPageToDocx({ pageId: id });
|
||||
} else {
|
||||
await exportPage({
|
||||
pageId: id,
|
||||
format,
|
||||
includeChildren,
|
||||
includeAttachments,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (type === "space") {
|
||||
await exportSpace({ spaceId: id, format, includeAttachments });
|
||||
@@ -88,10 +104,15 @@ export default function ExportModal({
|
||||
<div>
|
||||
<Text size="md">{t("Format")}</Text>
|
||||
</div>
|
||||
<ExportFormatSelection format={format} onChange={handleChange} />
|
||||
<ExportFormatSelection
|
||||
format={format}
|
||||
onChange={handleChange}
|
||||
includeDocx={type === "page"}
|
||||
docxEntitled={docxEntitled}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{type === "page" && (
|
||||
{type === "page" && !isDocx && (
|
||||
<>
|
||||
<Divider my="sm" />
|
||||
|
||||
@@ -143,7 +164,16 @@ export default function ExportModal({
|
||||
<Button onClick={onClose} variant="default">
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleExport} loading={isExporting}>{t("Export")}</Button>
|
||||
<Tooltip label={upgradeLabel} disabled={!blockedByLicense} withArrow>
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
loading={isExporting}
|
||||
disabled={blockedByLicense}
|
||||
data-disabled={blockedByLicense || undefined}
|
||||
>
|
||||
{t("Export")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
@@ -154,23 +184,49 @@ export default function ExportModal({
|
||||
interface ExportFormatSelection {
|
||||
format: ExportFormat;
|
||||
onChange: (value: string) => void;
|
||||
includeDocx?: boolean;
|
||||
docxEntitled?: boolean;
|
||||
}
|
||||
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
||||
function ExportFormatSelection({
|
||||
format,
|
||||
onChange,
|
||||
includeDocx,
|
||||
docxEntitled,
|
||||
}: ExportFormatSelection) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const data = [
|
||||
{ value: "markdown", label: "Markdown" },
|
||||
{ value: "html", label: "HTML" },
|
||||
...(includeDocx
|
||||
? [{ value: "docx", label: "Word (.docx)", disabled: !docxEntitled }]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<Select
|
||||
data={[
|
||||
{ value: "markdown", label: "Markdown" },
|
||||
{ value: "html", label: "HTML" },
|
||||
]}
|
||||
data={data}
|
||||
defaultValue={format}
|
||||
onChange={onChange}
|
||||
styles={{ wrapper: { maxWidth: 120 } }}
|
||||
comboboxProps={{ width: "120" }}
|
||||
styles={{ wrapper: { maxWidth: 140 }, option: { opacity: 1 } }}
|
||||
comboboxProps={{ width: 200 }}
|
||||
allowDeselect={false}
|
||||
withCheckIcon={false}
|
||||
aria-label={t("Select export format")}
|
||||
renderOption={({ option }) =>
|
||||
option.value === "docx" && !docxEntitled ? (
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{option.label}
|
||||
</Text>
|
||||
<Badge size="xs" mt={4}>
|
||||
{t("Enterprise")}
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
<Text size="sm">{option.label}</Text>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,16 @@ import {
|
||||
IconMoon,
|
||||
IconSettings,
|
||||
IconSun,
|
||||
IconUser,
|
||||
IconUserCircle,
|
||||
IconUsers,
|
||||
} from "@tabler/icons-react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { usePersonalSpaceQuery } from "@/ee/personal-space/queries/personal-space-query";
|
||||
import CreatePersonalSpaceModal from "@/ee/personal-space/components/create-personal-space-modal";
|
||||
import { useAtom } from "jotai";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -36,11 +43,20 @@ export default function TopMenu() {
|
||||
const user = currentUser?.user;
|
||||
const workspace = currentUser?.workspace;
|
||||
|
||||
const hasPersonalSpaces = useHasFeature(Feature.PERSONAL_SPACES);
|
||||
const settingEnabled = workspace?.settings?.spaces?.allowPersonal === true;
|
||||
const { data: personalSpace } = usePersonalSpaceQuery(hasPersonalSpaces);
|
||||
const [
|
||||
createOpened,
|
||||
{ open: openCreate, close: closeCreate },
|
||||
] = useDisclosure(false);
|
||||
|
||||
if (!user || !workspace) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu width={250} position="bottom-end" withArrow shadow={"lg"}>
|
||||
<Menu.Target>
|
||||
<UnstyledButton>
|
||||
@@ -115,6 +131,26 @@ export default function TopMenu() {
|
||||
{t("My preferences")}
|
||||
</Menu.Item>
|
||||
|
||||
{personalSpace ? (
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={getSpaceUrl(personalSpace.slug)}
|
||||
leftSection={<IconUser size={16} />}
|
||||
>
|
||||
{t("Personal space")}
|
||||
</Menu.Item>
|
||||
) : (
|
||||
hasPersonalSpaces &&
|
||||
settingEnabled && (
|
||||
<Menu.Item
|
||||
onClick={openCreate}
|
||||
leftSection={<IconUser size={16} />}
|
||||
>
|
||||
{t("Create personal space")}
|
||||
</Menu.Item>
|
||||
)
|
||||
)}
|
||||
|
||||
<Menu.Sub>
|
||||
<Menu.Sub.Target>
|
||||
<Menu.Sub.Item leftSection={<IconBrightnessFilled size={16} />}>
|
||||
@@ -160,5 +196,8 @@ export default function TopMenu() {
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
<CreatePersonalSpaceModal opened={createOpened} onClose={closeCreate} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,14 +54,17 @@ export const CustomAvatar = React.forwardRef<
|
||||
>(({ avatarUrl, name, type, color, variant, ...props }: CustomAvatarProps, ref) => {
|
||||
const avatarLink = getAvatarUrl(avatarUrl, type);
|
||||
const isInitials = !color || color === "initials";
|
||||
const resolvedColor = isInitials ? pickInitialsColor(name ?? "") : color;
|
||||
const pickedColor = isInitials ? pickInitialsColor(name ?? "") : color;
|
||||
const hue = pickedColor.split(".")[0];
|
||||
const initialsSource = sanitizeInitialsSource(name ?? "");
|
||||
|
||||
const resolvedColor = variant === "filled" ? pickedColor : hue;
|
||||
|
||||
const placeholderStyles =
|
||||
isInitials && variant !== "filled"
|
||||
? {
|
||||
placeholder: {
|
||||
color: `var(--mantine-color-${resolvedColor.split(".")[0]}-9)`,
|
||||
color: `var(--mantine-color-${hue}-9)`,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -177,7 +177,7 @@ export default function ChatInput({
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!editor || isStreaming) return;
|
||||
if (!editor || editor.isDestroyed || isStreaming) return;
|
||||
const json = editor.getJSON();
|
||||
const text = editorJsonToText(json).trim();
|
||||
const readyAttachments = pendingAttachments.filter((a) => !a.uploading);
|
||||
@@ -264,7 +264,7 @@ export default function ChatInput({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editor && autofocus) {
|
||||
if (editor && !editor.isDestroyed && autofocus) {
|
||||
editor.commands.focus();
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { ActionIcon, TextInput } from "@mantine/core";
|
||||
import { useDebouncedCallback, useMediaQuery } from "@mantine/hooks";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type JSX,
|
||||
} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useAtom } from "jotai";
|
||||
import { IconArrowUp } from "@tabler/icons-react";
|
||||
@@ -14,7 +21,7 @@ import { ResultPreview } from "./result-preview.tsx";
|
||||
import classes from "./ai-menu.module.css";
|
||||
import { marked } from "marked";
|
||||
import { DOMSerializer } from "@tiptap/pm/model";
|
||||
import { copyToClipboard, htmlToMarkdown } from "@docmost/editor-ext";
|
||||
import { copyToClipboard, htmlToMarkdown, isEditorReady } from "@docmost/editor-ext";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
interface EditorAiMenuProps {
|
||||
@@ -49,7 +56,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
|
||||
});
|
||||
}, [prompt, output, activeCommandSet]);
|
||||
const updateMenuPlacement = useCallback(() => {
|
||||
if (!editor || !showAiMenu) return;
|
||||
if (!isEditorReady(editor) || !showAiMenu) return;
|
||||
|
||||
const { view } = editor;
|
||||
const { from, to } = editor.state.selection;
|
||||
@@ -95,7 +102,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
|
||||
);
|
||||
const handleGenerate = useCallback(
|
||||
(item?: CommandItem) => {
|
||||
if (!editor || isLoading) return;
|
||||
if (!isEditorReady(editor) || isLoading) return;
|
||||
|
||||
let command: CommandItem | null = item || null;
|
||||
|
||||
@@ -158,6 +165,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
|
||||
return setActiveCommandSet("main");
|
||||
}
|
||||
if (item.id === "result-replace") {
|
||||
if (!isEditorReady(editor)) return setShowAiMenu(false);
|
||||
const chain = editor.chain().focus();
|
||||
|
||||
if (lastAction.action === AiAction.CONTINUE_WRITING) {
|
||||
@@ -183,6 +191,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
|
||||
return setShowAiMenu(false);
|
||||
}
|
||||
if (item.id === "result-insert-below") {
|
||||
if (!isEditorReady(editor)) return setShowAiMenu(false);
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
@@ -246,7 +255,7 @@ const EditorAiMenu = ({ editor }: EditorAiMenuProps): JSX.Element | null => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
if (!isEditorReady(editor)) return;
|
||||
|
||||
const handleClose = () => setShowAiMenu(false);
|
||||
const observer = new ResizeObserver(() => {
|
||||
|
||||
@@ -301,7 +301,7 @@ export default function AuditLogsTable({
|
||||
{expandable && (
|
||||
<Table.Tr className={classes.detailRow}>
|
||||
<Table.Td colSpan={4} p={0}>
|
||||
<Collapse in={isExpanded}>
|
||||
<Collapse expanded={isExpanded}>
|
||||
<Box
|
||||
px="md"
|
||||
py="sm"
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function BillingPlans() {
|
||||
label="Team size"
|
||||
description="Select the number of users"
|
||||
value={selectedTierValue}
|
||||
onChange={setSelectedTierValue}
|
||||
onChange={(value) => setSelectedTierValue(value)}
|
||||
data={selectData}
|
||||
w={250}
|
||||
size="md"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IconCircleCheck, IconCircleCheckFilled } from "@tabler/icons-react";
|
||||
import { useResolveCommentMutation } from "@/ee/comment/queries/comment-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { isEditorReady } from "@docmost/editor-ext";
|
||||
|
||||
interface ResolveCommentProps {
|
||||
editor: Editor;
|
||||
@@ -31,7 +32,7 @@ function ResolveComment({
|
||||
resolved: !isResolved,
|
||||
});
|
||||
|
||||
if (editor) {
|
||||
if (isEditorReady(editor)) {
|
||||
editor.commands.setCommentResolved(commentId, !isResolved);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,5 +19,7 @@ export const Feature = {
|
||||
SHARING_CONTROLS: 'sharing:controls',
|
||||
TEMPLATES: 'templates',
|
||||
VIEWER_COMMENTS: 'comment:viewer',
|
||||
PERSONAL_SPACES: 'spaces:personal',
|
||||
DOCX_EXPORT: 'export:docx',
|
||||
BASES: 'bases',
|
||||
} as const;
|
||||
|
||||
@@ -20,6 +20,7 @@ const enterpriseFeatures = [
|
||||
"Bases",
|
||||
"Kanban",
|
||||
"Templates",
|
||||
"Personal Spaces"
|
||||
];
|
||||
|
||||
export default function OssDetails() {
|
||||
|
||||
@@ -193,7 +193,7 @@ export function MfaSetupModal({
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
|
||||
<Collapse in={manualEntryOpen}>
|
||||
<Collapse expanded={manualEntryOpen}>
|
||||
<Alert
|
||||
icon={<IconAlertCircle size={20} />}
|
||||
color="gray"
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Modal, TextInput, Button, Group, Divider } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { zod4Resolver } from "mantine-form-zod-resolver";
|
||||
import { z } from "zod/v4";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useCreatePersonalSpaceMutation } from "@/ee/personal-space/queries/personal-space-query";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().trim().min(2).max(100),
|
||||
});
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function CreatePersonalSpaceModal({ opened, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const currentUser = useAtomValue(currentUserAtom);
|
||||
const createMutation = useCreatePersonalSpaceMutation();
|
||||
|
||||
const firstName = (currentUser?.user?.name ?? "").trim().split(/\s+/)[0] || "";
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
validate: zod4Resolver(formSchema),
|
||||
initialValues: {
|
||||
name: firstName ? t("{{name}}'s space", { name: firstName }) : "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (values: FormValues) => {
|
||||
try {
|
||||
const createdSpace = await createMutation.mutateAsync({
|
||||
name: values.name,
|
||||
});
|
||||
onClose();
|
||||
navigate(getSpaceUrl(createdSpace.slug));
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
message: err?.response?.data?.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={t("Create personal space")}
|
||||
closeButtonProps={{ "aria-label": t("Close") }}
|
||||
>
|
||||
<Divider size="xs" mb="md" />
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
data-autofocus
|
||||
label={t("Space name")}
|
||||
variant="filled"
|
||||
errorProps={{ role: "alert" }}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button type="submit" loading={createMutation.isPending}>
|
||||
{t("Create")}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Group, Text, Switch, Tooltip } from "@mantine/core";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label.ts";
|
||||
|
||||
export default function PersonalSpacesSetting() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">{t("Allow personal spaces")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("Members can create their own personal space.")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<PersonalSpacesToggle />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
function PersonalSpacesToggle() {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const [checked, setChecked] = useState(
|
||||
workspace?.settings?.spaces?.allowPersonal === true,
|
||||
);
|
||||
const hasPersonalSpaces = useHasFeature(Feature.PERSONAL_SPACES);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.checked;
|
||||
try {
|
||||
const updatedWorkspace = await updateWorkspace({
|
||||
allowPersonalSpaces: value,
|
||||
});
|
||||
setChecked(value);
|
||||
setWorkspace(updatedWorkspace);
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
message: err?.response?.data?.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label={upgradeLabel} disabled={hasPersonalSpaces} refProp="rootRef">
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasPersonalSpaces}
|
||||
aria-label={t("Toggle allow personal spaces")}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
import { ISpace } from "@/features/space/types/space.types";
|
||||
import {
|
||||
createPersonalSpace,
|
||||
getPersonalSpace,
|
||||
} from "@/ee/personal-space/services/personal-space-service";
|
||||
|
||||
export function usePersonalSpaceQuery(
|
||||
enabled: boolean,
|
||||
): UseQueryResult<ISpace | null, Error> {
|
||||
return useQuery({
|
||||
queryKey: ["personal-space"],
|
||||
queryFn: () => getPersonalSpace(),
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreatePersonalSpaceMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<ISpace, Error, { name?: string }>({
|
||||
mutationFn: (data) => createPersonalSpace(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["personal-space"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["spaces"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import api from "@/lib/api-client";
|
||||
import { ISpace } from "@/features/space/types/space.types";
|
||||
|
||||
export async function getPersonalSpace(): Promise<ISpace | null> {
|
||||
const req = await api.post<ISpace | null>("/personal-space/info", {});
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function createPersonalSpace(data: {
|
||||
name?: string;
|
||||
}): Promise<ISpace> {
|
||||
const req = await api.post<ISpace>("/personal-space/create", data);
|
||||
return req.data;
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export default function TemplateEditor() {
|
||||
|
||||
// Load template data into editor
|
||||
useEffect(() => {
|
||||
if (existingTemplate && editor) {
|
||||
if (existingTemplate && editor && !editor.isDestroyed) {
|
||||
loadedRef.current = false;
|
||||
setTitle(existingTemplate.title || "");
|
||||
setIcon(existingTemplate.icon || null);
|
||||
@@ -383,7 +383,8 @@ export default function TemplateEditor() {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
editor?.commands.focus("start");
|
||||
if (editor && !editor.isDestroyed)
|
||||
editor.commands.focus("start");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
|
||||
import { useCreateCommentMutation } from "@/features/comment/queries/comment-query";
|
||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { isEditorReady } from "@docmost/editor-ext";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -48,11 +49,14 @@ function CommentDialog({ editor, pageId, readOnly }: CommentDialogProps) {
|
||||
setReadOnlyCommentData(null);
|
||||
} else {
|
||||
setShowCommentPopup(false);
|
||||
editor.chain().focus().unsetCommentDecoration().run();
|
||||
if (isEditorReady(editor)) {
|
||||
editor.chain().focus().unsetCommentDecoration().run();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getSelectedText = () => {
|
||||
if (!isEditorReady(editor)) return "";
|
||||
const { from, to } = editor.state.selection;
|
||||
return editor.state.doc.textBetween(from, to);
|
||||
};
|
||||
@@ -74,24 +78,28 @@ function CommentDialog({ editor, pageId, readOnly }: CommentDialogProps) {
|
||||
|
||||
const createdComment =
|
||||
await createCommentMutation.mutateAsync(commentData);
|
||||
editor
|
||||
.chain()
|
||||
.setComment(createdComment.id)
|
||||
.unsetCommentDecoration()
|
||||
.run();
|
||||
if (isEditorReady(editor)) {
|
||||
editor
|
||||
.chain()
|
||||
.setComment(createdComment.id)
|
||||
.unsetCommentDecoration()
|
||||
.run();
|
||||
editor.commands.setTextSelection({
|
||||
from: editor.view.state.selection.from,
|
||||
to: editor.view.state.selection.from,
|
||||
});
|
||||
}
|
||||
setActiveCommentId(createdComment.id);
|
||||
|
||||
editor.commands.setTextSelection({ from: editor.view.state.selection.from, to: editor.view.state.selection.from });
|
||||
|
||||
setAsideState({ tab: "comments", isAsideOpen: true });
|
||||
setTimeout(() => {
|
||||
const selector = `div[data-comment-id="${createdComment.id}"]`;
|
||||
const commentElement = document.querySelector(selector);
|
||||
commentElement?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
|
||||
editor.view.dispatch(
|
||||
editor.state.tr.scrollIntoView()
|
||||
);
|
||||
if (isEditorReady(editor)) {
|
||||
editor.view.dispatch(editor.state.tr.scrollIntoView());
|
||||
}
|
||||
}, 400);
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -112,22 +112,24 @@ const CommentEditor = forwardRef(
|
||||
// websocket on another browser). Skip for editable editors to avoid
|
||||
// resetting the cursor position on every keystroke.
|
||||
useEffect(() => {
|
||||
if (!editable && commentEditor && defaultContent) {
|
||||
if (!editable && commentEditor && !commentEditor.isDestroyed && defaultContent) {
|
||||
commentEditor.commands.setContent(defaultContent);
|
||||
}
|
||||
}, [defaultContent, editable, commentEditor]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (autofocus) {
|
||||
commentEditor?.commands.focus("end");
|
||||
if (autofocus && commentEditor && !commentEditor.isDestroyed) {
|
||||
commentEditor.commands.focus("end");
|
||||
}
|
||||
}, 10);
|
||||
}, [commentEditor, autofocus]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
clearContent: () => {
|
||||
commentEditor.commands.clearContent();
|
||||
if (commentEditor && !commentEditor.isDestroyed) {
|
||||
commentEditor.commands.clearContent();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useAtom, useAtomValue } from "jotai";
|
||||
import { useTimeAgo } from "@/hooks/use-time-ago";
|
||||
import CommentEditor from "@/features/comment/components/comment-editor";
|
||||
import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms";
|
||||
import { isEditorReady } from "@docmost/editor-ext";
|
||||
import CommentActions from "@/features/comment/components/comment-actions";
|
||||
import CommentMenu from "@/features/comment/components/comment-menu";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
@@ -75,7 +76,9 @@ function CommentListItem({
|
||||
async function handleDeleteComment() {
|
||||
try {
|
||||
await deleteCommentMutation.mutateAsync(comment.id);
|
||||
editor?.commands.unsetComment(comment.id);
|
||||
if (isEditorReady(editor)) {
|
||||
editor.commands.unsetComment(comment.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete comment:", error);
|
||||
}
|
||||
@@ -93,7 +96,7 @@ function CommentListItem({
|
||||
resolved: !isResolved,
|
||||
});
|
||||
|
||||
if (editor) {
|
||||
if (isEditorReady(editor)) {
|
||||
editor.commands.setCommentResolved(comment.id, !isResolved);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/features/comment/atoms/comment-atom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getRelativeSelection, ySyncPluginKey } from "@tiptap/y-tiptap";
|
||||
import { isEditorReady } from "@docmost/editor-ext";
|
||||
|
||||
type ReadonlyBubbleMenuProps = {
|
||||
editor: Editor;
|
||||
@@ -29,6 +30,10 @@ export const ReadonlyBubbleMenu: FC<ReadonlyBubbleMenuProps> = ({ editor }) => {
|
||||
|
||||
const updateMenuPosition = useCallback(() => {
|
||||
if (isInteractingRef.current) return;
|
||||
if (!isEditorReady(editor)) {
|
||||
setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const pmSelection = editor.state.selection;
|
||||
if (!(pmSelection instanceof TextSelection) || pmSelection.empty) {
|
||||
@@ -97,7 +102,7 @@ export const ReadonlyBubbleMenu: FC<ReadonlyBubbleMenuProps> = ({ editor }) => {
|
||||
}, [showReadOnlyCommentPopup]);
|
||||
|
||||
const handleCommentClick = () => {
|
||||
if (!editor) return;
|
||||
if (!isEditorReady(editor)) return;
|
||||
|
||||
const view = editor.view;
|
||||
const ystate = ySyncPluginKey.getState(view.state);
|
||||
|
||||
@@ -69,7 +69,7 @@ export function ColumnsMenu({ editor }: EditorMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isCountOpen, setIsCountOpen] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copyTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const copyTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
const nodesWithMenus = [
|
||||
"callout",
|
||||
|
||||
@@ -111,8 +111,9 @@ async function reuploadPastedAttachments(
|
||||
const match = ATTACHMENT_URL_RE.exec(src);
|
||||
if (!match) return;
|
||||
|
||||
const cleanSrc = src.split("?")[0];
|
||||
const fileName =
|
||||
node.attrs.name || src.split("/").pop() || "file";
|
||||
node.attrs.name || cleanSrc.split("/").pop() || "file";
|
||||
|
||||
pastedNodes.push({
|
||||
pos,
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ export const MoreInsertsGroup: FC<Props> = ({ editor, templateMode }) => {
|
||||
<Menu.Item
|
||||
leftSection={<IconRotate2 size={16} />}
|
||||
onClick={() =>
|
||||
editor.chain().focus().insertTransclusionSource().run()
|
||||
editor.chain().focus().toggleTransclusionSource().run()
|
||||
}
|
||||
>
|
||||
{t("Synced block")}
|
||||
|
||||
@@ -28,7 +28,7 @@ import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
||||
import { useSharePageQuery } from "@/features/share/queries/share-query.ts";
|
||||
import { buildSharedPageUrl } from "@/features/page/page.utils.ts";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
import { sanitizeUrl, copyToClipboard } from "@docmost/editor-ext";
|
||||
import { sanitizeUrl, copyToClipboard, isEditorReady } from "@docmost/editor-ext";
|
||||
import { normalizeUrl } from "@/lib/utils";
|
||||
|
||||
const parseInternalLink = (
|
||||
@@ -313,7 +313,9 @@ export default function LinkView(props: MarkViewProps) {
|
||||
);
|
||||
|
||||
const handleRemoveLink = useCallback(() => {
|
||||
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
||||
if (isEditorReady(editor)) {
|
||||
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
||||
}
|
||||
setPopoverState("closed");
|
||||
}, [editor]);
|
||||
|
||||
@@ -345,7 +347,7 @@ export default function LinkView(props: MarkViewProps) {
|
||||
NodeFilter.SHOW_TEXT,
|
||||
);
|
||||
const textNode = walker.nextNode();
|
||||
if (textNode) {
|
||||
if (textNode && isEditorReady(editor)) {
|
||||
const view = editor.view as any;
|
||||
view.domObserver.stop();
|
||||
textNode.nodeValue = val;
|
||||
|
||||
+10
-2
@@ -17,6 +17,7 @@ import {
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { isEditorReady } from "@docmost/editor-ext";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { searchAndReplaceStateAtom } from "@/features/editor/components/search-and-replace/atoms/search-and-replace-state-atom.ts";
|
||||
import { useAtom } from "jotai";
|
||||
@@ -64,13 +65,13 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
|
||||
replaceButtonToggle();
|
||||
}
|
||||
// Clear search term in editor
|
||||
if (editor) {
|
||||
if (isEditorReady(editor)) {
|
||||
editor.commands.setSearchTerm("");
|
||||
}
|
||||
};
|
||||
|
||||
const goToSelection = () => {
|
||||
if (!editor) return;
|
||||
if (!isEditorReady(editor)) return;
|
||||
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
//TODO: check type error
|
||||
@@ -90,27 +91,32 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
if (!isEditorReady(editor)) return;
|
||||
editor.commands.nextSearchResult();
|
||||
goToSelection();
|
||||
};
|
||||
|
||||
const previous = () => {
|
||||
if (!isEditorReady(editor)) return;
|
||||
editor.commands.previousSearchResult();
|
||||
goToSelection();
|
||||
};
|
||||
|
||||
const replace = () => {
|
||||
if (!isEditorReady(editor)) return;
|
||||
editor.commands.setReplaceTerm(replaceText);
|
||||
editor.commands.replace();
|
||||
goToSelection();
|
||||
};
|
||||
|
||||
const replaceAll = () => {
|
||||
if (!isEditorReady(editor)) return;
|
||||
editor.commands.setReplaceTerm(replaceText);
|
||||
editor.commands.replaceAll();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditorReady(editor)) return;
|
||||
editor.commands.setSearchTerm(searchText);
|
||||
editor.commands.resetIndex();
|
||||
editor.commands.selectCurrentItem();
|
||||
@@ -118,6 +124,7 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
|
||||
|
||||
const handleOpenEvent = (e) => {
|
||||
setPageFindState({ isOpen: true });
|
||||
if (!isEditorReady(editor)) return;
|
||||
const selectedText = editor.state.doc.textBetween(
|
||||
editor.state.selection.from,
|
||||
editor.state.selection.to,
|
||||
@@ -149,6 +156,7 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
|
||||
}, [pageFindState.isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditorReady(editor)) return;
|
||||
editor.commands.setCaseSensitive(caseSensitive.isCaseSensitive);
|
||||
editor.commands.resetIndex();
|
||||
goToSelection();
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
IconMenu4,
|
||||
IconPageBreak,
|
||||
IconCalendar,
|
||||
IconClock,
|
||||
IconAppWindow,
|
||||
IconSitemap,
|
||||
IconColumns3,
|
||||
@@ -494,6 +495,25 @@ const CommandGroups: SlashMenuGroupedItemsType = {
|
||||
.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Time",
|
||||
description: "Insert current time",
|
||||
searchTerms: ["time", "now", "clock"],
|
||||
icon: IconClock,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
const currentTime = new Date().toLocaleTimeString(i18n.language, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
});
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.insertContent(currentTime)
|
||||
.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
description: "Insert inline status badge.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
||||
import { posToDOMRect, findParentNode } from "@tiptap/react";
|
||||
import { Node as PMNode } from "@tiptap/pm/model";
|
||||
import React, { useCallback } from "react";
|
||||
import React, { useCallback, type JSX } from "react";
|
||||
import { ActionIcon, Tooltip } from "@mantine/core";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -50,6 +50,7 @@ export const TableOfContents: FC<TableOfContentsProps> = (props) => {
|
||||
const headerPaddingRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const handleScrollToHeading = (position: number) => {
|
||||
if (!props.editor || props.editor.isDestroyed) return;
|
||||
const { view } = props.editor;
|
||||
|
||||
const headerOffset = parseInt(
|
||||
@@ -73,16 +74,21 @@ export const TableOfContents: FC<TableOfContentsProps> = (props) => {
|
||||
};
|
||||
|
||||
const handleUpdate = () => {
|
||||
const result = recalculateLinks(props.editor?.$nodes("heading"));
|
||||
if (!props.editor || props.editor.isDestroyed) return;
|
||||
|
||||
const result = recalculateLinks(props.editor.$nodes("heading"));
|
||||
|
||||
setLinks(result.links);
|
||||
setHeadingDOMNodes(result.nodes);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// "create" repopulates once the editor view mounts after this component
|
||||
props.editor?.on("create", handleUpdate);
|
||||
props.editor?.on("update", handleUpdate);
|
||||
|
||||
return () => {
|
||||
props.editor?.off("create", handleUpdate);
|
||||
props.editor?.off("update", handleUpdate);
|
||||
};
|
||||
}, [props.editor]);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Menu, UnstyledButton } from "@mantine/core";
|
||||
import { IconChevronDown } from "@tabler/icons-react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isCellSelection } from "@docmost/editor-ext";
|
||||
import { isCellSelection, isEditorReady } from "@docmost/editor-ext";
|
||||
import { CellChevronMenu } from "./menus/cell-chevron-menu";
|
||||
import classes from "./handle.module.css";
|
||||
|
||||
@@ -27,7 +27,9 @@ export const CellChevron = React.memo(function CellChevron({
|
||||
tablePos,
|
||||
}: CellChevronProps) {
|
||||
const { t } = useTranslation();
|
||||
const cellDom = editor.view.nodeDOM(cellPos) as HTMLElement | null;
|
||||
const cellDom = isEditorReady(editor)
|
||||
? (editor.view.nodeDOM(cellPos) as HTMLElement | null)
|
||||
: null;
|
||||
|
||||
const { refs, floatingStyles, middlewareData } = useFloating({
|
||||
placement: "top-end",
|
||||
@@ -61,6 +63,7 @@ export const CellChevron = React.memo(function CellChevron({
|
||||
});
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
if (!isEditorReady(editor)) return;
|
||||
const current = editor.state.selection;
|
||||
|
||||
// Preserve an existing multi-cell CellSelection that already covers
|
||||
@@ -86,6 +89,7 @@ export const CellChevron = React.memo(function CellChevron({
|
||||
}, [editor, cellPos]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
if (!isEditorReady(editor)) return;
|
||||
editor.commands.unfreezeHandles();
|
||||
}, [editor]);
|
||||
|
||||
|
||||
+3
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
import type { Editor } from "@tiptap/react";
|
||||
import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import { isEditorReady } from "@docmost/editor-ext";
|
||||
import { buildRowOrColumnSelection, Orientation } from "../lib/select-row-column";
|
||||
|
||||
interface Args {
|
||||
@@ -19,6 +20,7 @@ export function useColumnRowMenuLifecycle({
|
||||
tablePos,
|
||||
}: Args) {
|
||||
const onOpen = useCallback(() => {
|
||||
if (!isEditorReady(editor)) return;
|
||||
const selection = buildRowOrColumnSelection(
|
||||
editor.state,
|
||||
tableNode,
|
||||
@@ -33,6 +35,7 @@ export function useColumnRowMenuLifecycle({
|
||||
}, [editor, orientation, index, tableNode, tablePos]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
if (!isEditorReady(editor)) return;
|
||||
editor.commands.unfreezeHandles();
|
||||
}, [editor]);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback } from "react";
|
||||
import type { Editor } from "@tiptap/react";
|
||||
import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import { TableMap } from "@tiptap/pm/tables";
|
||||
import { isEditorReady } from "@docmost/editor-ext";
|
||||
|
||||
type Scope =
|
||||
| { kind: "col"; index: number }
|
||||
@@ -15,6 +16,7 @@ export function useTableClear(
|
||||
scope: Scope,
|
||||
) {
|
||||
return useCallback(() => {
|
||||
if (!isEditorReady(editor)) return;
|
||||
const tr = editor.state.tr;
|
||||
const tableStart = tablePos + 1;
|
||||
const map = TableMap.get(tableNode);
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { useCallback, useMemo } from "react";
|
||||
import type { Editor } from "@tiptap/react";
|
||||
import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import { TableMap } from "@tiptap/pm/tables";
|
||||
import { moveColumn, moveRow } from "@docmost/editor-ext";
|
||||
import { isEditorReady, moveColumn, moveRow } from "@docmost/editor-ext";
|
||||
|
||||
export type MoveDirection = "left" | "right" | "up" | "down";
|
||||
|
||||
@@ -25,7 +25,7 @@ export function useTableMoveRowColumn(
|
||||
const canMove = target >= 0 && target <= maxIndex;
|
||||
|
||||
const handleMove = useCallback(() => {
|
||||
if (!canMove) return;
|
||||
if (!canMove || !isEditorReady(editor)) return;
|
||||
const tr = editor.state.tr;
|
||||
const moved =
|
||||
orientation === "col"
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import {
|
||||
convertArrayOfRowsToTableNode,
|
||||
convertTableNodeToArrayOfRows,
|
||||
isEditorReady,
|
||||
transpose,
|
||||
} from "@docmost/editor-ext";
|
||||
import {
|
||||
@@ -63,7 +64,7 @@ export function useTableSort({
|
||||
}, [tableNode, orientation, index]);
|
||||
|
||||
const handleSort = useCallback(() => {
|
||||
if (!canSort) return;
|
||||
if (!canSort || !isEditorReady(editor)) return;
|
||||
|
||||
const rows = convertTableNodeToArrayOfRows(tableNode);
|
||||
const axes = orientation === "col" ? rows : transpose(rows);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from "react";
|
||||
import React, { useCallback, type JSX } from "react";
|
||||
import {
|
||||
EditorMenuProps,
|
||||
ShouldShowProps,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { posToDOMRect, findParentNode } from "@tiptap/react";
|
||||
import { Node as PMNode } from "@tiptap/pm/model";
|
||||
import React, { useCallback } from "react";
|
||||
import React, { useCallback, type JSX } from "react";
|
||||
import {
|
||||
EditorMenuProps,
|
||||
ShouldShowProps,
|
||||
|
||||
@@ -160,7 +160,11 @@ export function TitleEditor({
|
||||
const debounceUpdate = useDebouncedCallback(saveTitle, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (titleEditor && title !== titleEditor.getText()) {
|
||||
if (
|
||||
titleEditor &&
|
||||
!titleEditor.isDestroyed &&
|
||||
title !== titleEditor.getText()
|
||||
) {
|
||||
titleEditor.commands.setContent(title);
|
||||
}
|
||||
}, [pageId, title, titleEditor]);
|
||||
|
||||
@@ -34,7 +34,7 @@ export function HistoryEditor({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor || !content) return;
|
||||
if (!editor || editor.isDestroyed || !content) return;
|
||||
|
||||
let decorationSet = DecorationSet.empty;
|
||||
let addedCount = 0;
|
||||
|
||||
@@ -132,6 +132,25 @@ export async function exportPage(data: IExportPageParams): Promise<void> {
|
||||
saveAs(req.data, decodedFileName);
|
||||
}
|
||||
|
||||
export async function exportPageToDocx(data: { pageId: string }): Promise<void> {
|
||||
const req = await api.post("/docx-export", data, {
|
||||
responseType: "blob",
|
||||
});
|
||||
|
||||
const fileName = req?.headers["content-disposition"]
|
||||
.split("filename=")[1]
|
||||
.replace(/"/g, "");
|
||||
|
||||
let decodedFileName = fileName;
|
||||
try {
|
||||
decodedFileName = decodeURIComponent(fileName);
|
||||
} catch (err) {
|
||||
// fallback to raw filename
|
||||
}
|
||||
|
||||
saveAs(req.data, decodedFileName);
|
||||
}
|
||||
|
||||
export async function importPage(file: File, spaceId: string) {
|
||||
const formData = new FormData();
|
||||
formData.append("spaceId", spaceId);
|
||||
|
||||
@@ -99,4 +99,5 @@ export interface IExportPageParams {
|
||||
export enum ExportFormat {
|
||||
HTML = "html",
|
||||
Markdown = "markdown",
|
||||
Docx = "docx",
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function ShareShell({
|
||||
const [fullWidth, setFullWidth] = useAtom(sharedPageFullWidthAtom);
|
||||
const [sidebarWidth, setSidebarWidth] = useAtom(sidebarWidthAtom);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const sidebarRef = useRef<HTMLElement | null>(null);
|
||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const startResizing = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -17,8 +17,8 @@ const formSchema = z.object({
|
||||
.min(2)
|
||||
.max(100)
|
||||
.regex(
|
||||
/^[a-zA-Z0-9]+$/,
|
||||
"Space slug must be alphanumeric. No special characters",
|
||||
/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/,
|
||||
"Space slug must start with a letter or number and may contain hyphens and underscores",
|
||||
),
|
||||
description: z.string().max(500),
|
||||
});
|
||||
|
||||
@@ -15,8 +15,8 @@ const formSchema = z.object({
|
||||
.min(2)
|
||||
.max(100)
|
||||
.regex(
|
||||
/^[a-zA-Z0-9]+$/,
|
||||
"Space slug must be alphanumeric. No special characters",
|
||||
/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/,
|
||||
"Space slug must start with a letter or number and may contain hyphens and underscores",
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface ISpace {
|
||||
description: string;
|
||||
logo?: string;
|
||||
slug: string;
|
||||
isPersonal?: boolean;
|
||||
hostname: string;
|
||||
creatorId: string;
|
||||
createdAt: Date;
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ export function WorkspaceInviteForm({ onClose }: Props) {
|
||||
defaultValue={UserRole.MEMBER}
|
||||
allowDeselect={false}
|
||||
checkIconPosition="right"
|
||||
onChange={setRole}
|
||||
onChange={(value) => setRole(value)}
|
||||
/>
|
||||
|
||||
<MultiGroupSelect
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface IWorkspace {
|
||||
trashRetentionDays?: number;
|
||||
restrictApiToAdmins?: boolean;
|
||||
allowMemberTemplates?: boolean;
|
||||
allowPersonalSpaces?: boolean;
|
||||
isScimEnabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -36,6 +37,7 @@ export interface IWorkspaceSettings {
|
||||
sharing?: IWorkspaceSharingSettings;
|
||||
api?: IWorkspaceApiSettings;
|
||||
templates?: IWorkspaceTemplateSettings;
|
||||
spaces?: IWorkspaceSpaceSettings;
|
||||
}
|
||||
|
||||
export interface IWorkspaceApiSettings {
|
||||
@@ -57,6 +59,10 @@ export interface IWorkspaceTemplateSettings {
|
||||
allowMemberTemplates?: boolean;
|
||||
}
|
||||
|
||||
export interface IWorkspaceSpaceSettings {
|
||||
allowPersonal?: boolean;
|
||||
}
|
||||
|
||||
export interface ICreateInvite {
|
||||
role: string;
|
||||
emails: string[];
|
||||
|
||||
@@ -13,6 +13,7 @@ api.interceptors.response.use(
|
||||
const exemptEndpoints = [
|
||||
"/api/pages/export",
|
||||
"/api/spaces/export",
|
||||
"/api/docx-export",
|
||||
"/api/bases/export-csv",
|
||||
];
|
||||
if (response.request.responseURL) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Helmet } from "react-helmet-async";
|
||||
import ManageHostname from "@/ee/components/manage-hostname.tsx";
|
||||
import { Divider } from "@mantine/core";
|
||||
import AllowMemberTemplates from "@/ee/security/components/allow-member-templates.tsx";
|
||||
import PersonalSpacesSetting from "@/ee/personal-space/components/personal-spaces-setting.tsx";
|
||||
|
||||
export default function WorkspaceSettings() {
|
||||
const { t } = useTranslation();
|
||||
@@ -22,6 +23,9 @@ export default function WorkspaceSettings() {
|
||||
<Divider my="md" />
|
||||
<AllowMemberTemplates />
|
||||
|
||||
<Divider my="md" />
|
||||
<PersonalSpacesSetting />
|
||||
|
||||
{isCloud() && (
|
||||
<>
|
||||
<Divider my="md" />
|
||||
|
||||
@@ -25,3 +25,11 @@
|
||||
.mantine-Input-input[data-variant="default"] {
|
||||
border-color: var(--mantine-color-gray-6);
|
||||
}
|
||||
|
||||
/* better contrast for disabled inputs */
|
||||
.mantine-Input-input:disabled,
|
||||
.mantine-Input-input[data-disabled] {
|
||||
opacity: 0.7;
|
||||
color: var(--mantine-color-dimmed);
|
||||
-webkit-text-fill-color: var(--mantine-color-dimmed);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
Badge,
|
||||
createTheme,
|
||||
CSSVariablesResolver,
|
||||
MantineColorsTuple,
|
||||
Tabs,
|
||||
v8CssVariablesResolver,
|
||||
} from "@mantine/core";
|
||||
|
||||
const blue: MantineColorsTuple = [
|
||||
@@ -37,6 +39,15 @@ export const theme = createTheme({
|
||||
red,
|
||||
},
|
||||
components: {
|
||||
// Size badges to their content; fit-content collapses inside table cells.
|
||||
Badge: Badge.extend({
|
||||
styles: (_theme, props) => ({
|
||||
root:
|
||||
props.fullWidth || props.circle
|
||||
? {}
|
||||
: { width: "max-content", maxWidth: "100%" },
|
||||
}),
|
||||
}),
|
||||
Tabs: Tabs.extend({
|
||||
vars: (theme, props) => ({
|
||||
root: {
|
||||
@@ -68,9 +79,11 @@ export const theme = createTheme({
|
||||
|
||||
export const mantineCssResolver: CSSVariablesResolver = (theme) => ({
|
||||
variables: {
|
||||
...v8CssVariablesResolver(theme).variables,
|
||||
"--input-error-size": theme.fontSizes.sm,
|
||||
},
|
||||
light: {
|
||||
...v8CssVariablesResolver(theme).light,
|
||||
"--mantine-color-dimmed": "#4b5563",
|
||||
"--mantine-color-dark-light-color": "#4e5359",
|
||||
"--mantine-color-dark-light-hover": "var(--mantine-color-gray-light-hover)",
|
||||
@@ -106,6 +119,7 @@ export const mantineCssResolver: CSSVariablesResolver = (theme) => ({
|
||||
"--mantine-color-orange-light-color": "#a63508",
|
||||
},
|
||||
dark: {
|
||||
...v8CssVariablesResolver(theme).dark,
|
||||
"--mantine-color-dark-light-color": "var(--mantine-color-gray-4)",
|
||||
"--mantine-color-dark-light-hover": "var(--mantine-color-default-hover)",
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ES2021",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user