mirror of
https://github.com/docmost/docmost.git
synced 2025-11-11 07:42:05 +10:00
feat: space export (#506)
* wip * Space export * option to export pages with children * include attachments in exports * unified export UI * cleanup * fix: change export icon * add export button to space settings * cleanups * export name
This commit is contained in:
149
apps/client/src/components/common/export-modal.tsx
Normal file
149
apps/client/src/components/common/export-modal.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Text,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
Divider,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { exportPage } 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";
|
||||||
|
|
||||||
|
interface ExportModalProps {
|
||||||
|
id: string;
|
||||||
|
type: "space" | "page";
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExportModal({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: ExportModalProps) {
|
||||||
|
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
|
||||||
|
const [includeChildren, setIncludeChildren] = useState<boolean>(false);
|
||||||
|
const [includeAttachments, setIncludeAttachments] = useState<boolean>(true);
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
if (type === "page") {
|
||||||
|
await exportPage({ pageId: id, format, includeChildren });
|
||||||
|
}
|
||||||
|
if (type === "space") {
|
||||||
|
await exportSpace({ spaceId: id, format, includeAttachments });
|
||||||
|
}
|
||||||
|
setIncludeChildren(false);
|
||||||
|
setIncludeAttachments(true);
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
notifications.show({
|
||||||
|
message: "Export failed:" + err.response?.data.message,
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
console.error("export error", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (format: ExportFormat) => {
|
||||||
|
setFormat(format);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal.Root
|
||||||
|
opened={open}
|
||||||
|
onClose={onClose}
|
||||||
|
size={500}
|
||||||
|
padding="xl"
|
||||||
|
yOffset="10vh"
|
||||||
|
xOffset={0}
|
||||||
|
mah={400}
|
||||||
|
>
|
||||||
|
<Modal.Overlay />
|
||||||
|
<Modal.Content style={{ overflow: "hidden" }}>
|
||||||
|
<Modal.Header py={0}>
|
||||||
|
<Modal.Title fw={500}>Export {type}</Modal.Title>
|
||||||
|
<Modal.CloseButton />
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<div>
|
||||||
|
<Text size="md">Format</Text>
|
||||||
|
</div>
|
||||||
|
<ExportFormatSelection format={format} onChange={handleChange} />
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{type === "page" && (
|
||||||
|
<>
|
||||||
|
<Divider my="sm" />
|
||||||
|
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<div>
|
||||||
|
<Text size="md">Include subpages</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={(event) =>
|
||||||
|
setIncludeChildren(event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
checked={includeChildren}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "space" && (
|
||||||
|
<>
|
||||||
|
<Divider my="sm" />
|
||||||
|
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<div>
|
||||||
|
<Text size="md">Include attachments</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={(event) =>
|
||||||
|
setIncludeAttachments(event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
checked={includeAttachments}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Group justify="center" mt="md">
|
||||||
|
<Button onClick={onClose} variant="default">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleExport}>Export</Button>
|
||||||
|
</Group>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExportFormatSelection {
|
||||||
|
format: ExportFormat;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
{ value: "markdown", label: "Markdown" },
|
||||||
|
{ value: "html", label: "HTML" },
|
||||||
|
]}
|
||||||
|
defaultValue={format}
|
||||||
|
onChange={onChange}
|
||||||
|
styles={{ wrapper: { maxWidth: 120 } }}
|
||||||
|
comboboxProps={{ width: "120" }}
|
||||||
|
allowDeselect={false}
|
||||||
|
withCheckIcon={false}
|
||||||
|
aria-label="Select export format"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ import { ActionIcon, Group, Menu, Tooltip } from "@mantine/core";
|
|||||||
import {
|
import {
|
||||||
IconArrowsHorizontal,
|
IconArrowsHorizontal,
|
||||||
IconDots,
|
IconDots,
|
||||||
IconDownload,
|
IconFileExport,
|
||||||
IconHistory,
|
IconHistory,
|
||||||
IconLink,
|
IconLink,
|
||||||
IconMessage,
|
IconMessage,
|
||||||
@ -24,6 +24,7 @@ import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
|||||||
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
||||||
import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx";
|
import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx";
|
||||||
import PageExportModal from "@/features/page/components/page-export-modal.tsx";
|
import PageExportModal from "@/features/page/components/page-export-modal.tsx";
|
||||||
|
import ExportModal from "@/components/common/export-modal";
|
||||||
|
|
||||||
interface PageHeaderMenuProps {
|
interface PageHeaderMenuProps {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@ -126,7 +127,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
|||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<IconDownload size={16} />}
|
leftSection={<IconFileExport size={16} />}
|
||||||
onClick={openExportModal}
|
onClick={openExportModal}
|
||||||
>
|
>
|
||||||
Export
|
Export
|
||||||
@ -154,8 +155,9 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
|||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
<PageExportModal
|
<ExportModal
|
||||||
pageId={page.id}
|
type="page"
|
||||||
|
id={page.id}
|
||||||
open={exportOpened}
|
open={exportOpened}
|
||||||
onClose={closeExportModal}
|
onClose={closeExportModal}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Modal, Button, Group, Text, Select } from "@mantine/core";
|
import { Modal, Button, Group, Text, Select, Switch } from "@mantine/core";
|
||||||
import { exportPage } from "@/features/page/services/page-service.ts";
|
import { exportPage } from "@/features/page/services/page-service.ts";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@ -57,8 +57,18 @@ export default function PageExportModal({
|
|||||||
<Text size="md">Format</Text>
|
<Text size="md">Format</Text>
|
||||||
</div>
|
</div>
|
||||||
<ExportFormatSelection format={format} onChange={handleChange} />
|
<ExportFormatSelection format={format} onChange={handleChange} />
|
||||||
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
<Group justify="space-between" wrap="nowrap" pt="md">
|
||||||
|
<div>
|
||||||
|
<Text size="md">Include subpages</Text>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
|
||||||
<Group justify="center" mt="md">
|
<Group justify="center" mt="md">
|
||||||
<Button onClick={onClose} variant="default">
|
<Button onClick={onClose} variant="default">
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import {
|
|||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
IconChevronRight,
|
IconChevronRight,
|
||||||
IconDotsVertical,
|
IconDotsVertical,
|
||||||
IconFileDescription,
|
IconFileDescription, IconFileExport,
|
||||||
IconLink,
|
IconLink,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconPointFilled,
|
IconPointFilled,
|
||||||
@ -39,7 +39,12 @@ import {
|
|||||||
import { IPage, SidebarPagesParams } from "@/features/page/types/page.types.ts";
|
import { IPage, SidebarPagesParams } from "@/features/page/types/page.types.ts";
|
||||||
import { queryClient } from "@/main.tsx";
|
import { queryClient } from "@/main.tsx";
|
||||||
import { OpenMap } from "react-arborist/dist/main/state/open-slice";
|
import { OpenMap } from "react-arborist/dist/main/state/open-slice";
|
||||||
import { useClipboard, useElementSize, useMergedRef } from "@mantine/hooks";
|
import {
|
||||||
|
useClipboard,
|
||||||
|
useDisclosure,
|
||||||
|
useElementSize,
|
||||||
|
useMergedRef,
|
||||||
|
} from "@mantine/hooks";
|
||||||
import { dfs } from "react-arborist/dist/module/utils";
|
import { dfs } from "react-arborist/dist/module/utils";
|
||||||
import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
|
import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
|
||||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||||
@ -47,6 +52,7 @@ import { notifications } from "@mantine/notifications";
|
|||||||
import { getAppUrl } from "@/lib/config.ts";
|
import { getAppUrl } from "@/lib/config.ts";
|
||||||
import { extractPageSlugId } from "@/lib";
|
import { extractPageSlugId } from "@/lib";
|
||||||
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
||||||
|
import ExportModal from "@/components/common/export-modal";
|
||||||
|
|
||||||
interface SpaceTreeProps {
|
interface SpaceTreeProps {
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
@ -402,6 +408,8 @@ function NodeMenu({ node, treeApi }: NodeMenuProps) {
|
|||||||
const clipboard = useClipboard({ timeout: 500 });
|
const clipboard = useClipboard({ timeout: 500 });
|
||||||
const { spaceSlug } = useParams();
|
const { spaceSlug } = useParams();
|
||||||
const { openDeleteModal } = useDeletePageModal();
|
const { openDeleteModal } = useDeletePageModal();
|
||||||
|
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||||
|
useDisclosure(false);
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
const handleCopyLink = () => {
|
||||||
const pageUrl =
|
const pageUrl =
|
||||||
@ -411,56 +419,76 @@ function NodeMenu({ node, treeApi }: NodeMenuProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu shadow="md" width={200}>
|
<>
|
||||||
<Menu.Target>
|
<Menu shadow="md" width={200}>
|
||||||
<ActionIcon
|
<Menu.Target>
|
||||||
variant="transparent"
|
<ActionIcon
|
||||||
c="gray"
|
variant="transparent"
|
||||||
onClick={(e) => {
|
c="gray"
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.preventDefault();
|
||||||
}}
|
e.stopPropagation();
|
||||||
>
|
}}
|
||||||
<IconDotsVertical
|
>
|
||||||
style={{ width: rem(20), height: rem(20) }}
|
<IconDotsVertical
|
||||||
stroke={2}
|
style={{ width: rem(20), height: rem(20) }}
|
||||||
/>
|
stroke={2}
|
||||||
</ActionIcon>
|
/>
|
||||||
</Menu.Target>
|
</ActionIcon>
|
||||||
|
</Menu.Target>
|
||||||
|
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<IconLink style={{ width: rem(14), height: rem(14) }} />}
|
leftSection={<IconLink size={16} />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleCopyLink();
|
handleCopyLink();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Copy link
|
Copy link
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
{!(treeApi.props.disableEdit as boolean) && (
|
<Menu.Item
|
||||||
<>
|
leftSection={<IconFileExport size={16} />}
|
||||||
<Menu.Divider />
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
openExportModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Export page
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Item
|
{!(treeApi.props.disableEdit as boolean) && (
|
||||||
c="red"
|
<>
|
||||||
leftSection={
|
<Menu.Divider />
|
||||||
<IconTrash style={{ width: rem(14), height: rem(14) }} />
|
|
||||||
}
|
<Menu.Item
|
||||||
onClick={(e) => {
|
c="red"
|
||||||
e.preventDefault();
|
leftSection={
|
||||||
e.stopPropagation();
|
<IconTrash size={16} />
|
||||||
openDeleteModal({ onConfirm: () => treeApi?.delete(node) });
|
}
|
||||||
}}
|
onClick={(e) => {
|
||||||
>
|
e.preventDefault();
|
||||||
Delete
|
e.stopPropagation();
|
||||||
</Menu.Item>
|
openDeleteModal({ onConfirm: () => treeApi?.delete(node) });
|
||||||
</>
|
}}
|
||||||
)}
|
>
|
||||||
</Menu.Dropdown>
|
Delete
|
||||||
</Menu>
|
</Menu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<ExportModal
|
||||||
|
type="page"
|
||||||
|
id={node.id}
|
||||||
|
open={exportOpened}
|
||||||
|
onClose={closeExportModal}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -48,6 +48,7 @@ export interface IPageInput {
|
|||||||
export interface IExportPageParams {
|
export interface IExportPageParams {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
format: ExportFormat;
|
format: ExportFormat;
|
||||||
|
includeChildren?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ExportFormat {
|
export enum ExportFormat {
|
||||||
|
|||||||
@ -5,36 +5,38 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
} from '@mantine/core';
|
} from "@mantine/core";
|
||||||
import { spotlight } from '@mantine/spotlight';
|
import { spotlight } from "@mantine/spotlight";
|
||||||
import {
|
import {
|
||||||
IconArrowDown,
|
IconArrowDown,
|
||||||
IconDots,
|
IconDots,
|
||||||
|
IconFileExport,
|
||||||
IconHome,
|
IconHome,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
} from '@tabler/icons-react';
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
import classes from './space-sidebar.module.css';
|
import classes from "./space-sidebar.module.css";
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from "react";
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from "jotai";
|
||||||
import { SearchSpotlight } from '@/features/search/search-spotlight.tsx';
|
import { SearchSpotlight } from "@/features/search/search-spotlight.tsx";
|
||||||
import { treeApiAtom } from '@/features/page/tree/atoms/tree-api-atom.ts';
|
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
||||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
import { Link, useLocation, useParams } from "react-router-dom";
|
||||||
import clsx from 'clsx';
|
import clsx from "clsx";
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import SpaceSettingsModal from '@/features/space/components/settings-modal.tsx';
|
import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx";
|
||||||
import { useGetSpaceBySlugQuery } from '@/features/space/queries/space-query.ts';
|
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
||||||
import { getSpaceUrl } from '@/lib/config.ts';
|
import { getSpaceUrl } from "@/lib/config.ts";
|
||||||
import SpaceTree from '@/features/page/tree/components/space-tree.tsx';
|
import SpaceTree from "@/features/page/tree/components/space-tree.tsx";
|
||||||
import { useSpaceAbility } from '@/features/space/permissions/use-space-ability.ts';
|
import { useSpaceAbility } from "@/features/space/permissions/use-space-ability.ts";
|
||||||
import {
|
import {
|
||||||
SpaceCaslAction,
|
SpaceCaslAction,
|
||||||
SpaceCaslSubject,
|
SpaceCaslSubject,
|
||||||
} from '@/features/space/permissions/permissions.type.ts';
|
} from "@/features/space/permissions/permissions.type.ts";
|
||||||
import PageImportModal from '@/features/page/components/page-import-modal.tsx';
|
import PageImportModal from "@/features/page/components/page-import-modal.tsx";
|
||||||
import { SwitchSpace } from './switch-space';
|
import { SwitchSpace } from "./switch-space";
|
||||||
|
import ExportModal from "@/components/common/export-modal";
|
||||||
|
|
||||||
export function SpaceSidebar() {
|
export function SpaceSidebar() {
|
||||||
const [tree] = useAtom(treeApiAtom);
|
const [tree] = useAtom(treeApiAtom);
|
||||||
@ -52,7 +54,7 @@ export function SpaceSidebar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleCreatePage() {
|
function handleCreatePage() {
|
||||||
tree?.create({ parentId: null, type: 'internal', index: 0 });
|
tree?.create({ parentId: null, type: "internal", index: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -61,7 +63,7 @@ export function SpaceSidebar() {
|
|||||||
<div
|
<div
|
||||||
className={classes.section}
|
className={classes.section}
|
||||||
style={{
|
style={{
|
||||||
border: 'none',
|
border: "none",
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
marginBottom: 3,
|
marginBottom: 3,
|
||||||
}}
|
}}
|
||||||
@ -78,7 +80,7 @@ export function SpaceSidebar() {
|
|||||||
classes.menu,
|
classes.menu,
|
||||||
location.pathname.toLowerCase() === getSpaceUrl(spaceSlug)
|
location.pathname.toLowerCase() === getSpaceUrl(spaceSlug)
|
||||||
? classes.activeButton
|
? classes.activeButton
|
||||||
: ''
|
: ""
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={classes.menuItemInner}>
|
<div className={classes.menuItemInner}>
|
||||||
@ -191,6 +193,8 @@ interface SpaceMenuProps {
|
|||||||
function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) {
|
function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) {
|
||||||
const [importOpened, { open: openImportModal, close: closeImportModal }] =
|
const [importOpened, { open: openImportModal, close: closeImportModal }] =
|
||||||
useDisclosure(false);
|
useDisclosure(false);
|
||||||
|
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||||
|
useDisclosure(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -215,6 +219,13 @@ function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) {
|
|||||||
Import pages
|
Import pages
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
|
<Menu.Item
|
||||||
|
onClick={openExportModal}
|
||||||
|
leftSection={<IconFileExport size={16} />}
|
||||||
|
>
|
||||||
|
Export space
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@ -231,6 +242,13 @@ function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) {
|
|||||||
open={importOpened}
|
open={importOpened}
|
||||||
onClose={closeImportModal}
|
onClose={closeImportModal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ExportModal
|
||||||
|
type="space"
|
||||||
|
id={spaceId}
|
||||||
|
open={exportOpened}
|
||||||
|
onClose={closeExportModal}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSpaceQuery } from '@/features/space/queries/space-query.ts';
|
import { useSpaceQuery } from '@/features/space/queries/space-query.ts';
|
||||||
import { EditSpaceForm } from '@/features/space/components/edit-space-form.tsx';
|
import { EditSpaceForm } from '@/features/space/components/edit-space-form.tsx';
|
||||||
import { Divider, Group, Text } from '@mantine/core';
|
import { Button, Divider, Group, Text } from '@mantine/core';
|
||||||
import DeleteSpaceModal from './delete-space-modal';
|
import DeleteSpaceModal from './delete-space-modal';
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import ExportModal from "@/components/common/export-modal.tsx";
|
||||||
|
|
||||||
interface SpaceDetailsProps {
|
interface SpaceDetailsProps {
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
@ -10,6 +12,8 @@ interface SpaceDetailsProps {
|
|||||||
}
|
}
|
||||||
export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
||||||
const { data: space, isLoading } = useSpaceQuery(spaceId);
|
const { data: space, isLoading } = useSpaceQuery(spaceId);
|
||||||
|
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||||
|
useDisclosure(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -22,6 +26,22 @@ export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
|||||||
|
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
|
<Divider my="lg" />
|
||||||
|
|
||||||
|
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||||
|
<div>
|
||||||
|
<Text size="md">Export space</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Export all pages and attachments in this space
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={openExportModal}>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Divider my="lg" />
|
<Divider my="lg" />
|
||||||
|
|
||||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||||
@ -34,6 +54,13 @@ export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
|||||||
|
|
||||||
<DeleteSpaceModal space={space} />
|
<DeleteSpaceModal space={space} />
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
<ExportModal
|
||||||
|
type="space"
|
||||||
|
id={space.id}
|
||||||
|
open={exportOpened}
|
||||||
|
onClose={closeExportModal}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,56 +1,72 @@
|
|||||||
import api from '@/lib/api-client';
|
import api from "@/lib/api-client";
|
||||||
import {
|
import {
|
||||||
IAddSpaceMember,
|
IAddSpaceMember,
|
||||||
IChangeSpaceMemberRole,
|
IChangeSpaceMemberRole,
|
||||||
|
IExportSpaceParams,
|
||||||
IRemoveSpaceMember,
|
IRemoveSpaceMember,
|
||||||
ISpace,
|
ISpace,
|
||||||
} from "@/features/space/types/space.types";
|
} from "@/features/space/types/space.types";
|
||||||
import { IPagination, QueryParams } from "@/lib/types.ts";
|
import { IPagination, QueryParams } from "@/lib/types.ts";
|
||||||
import { IUser } from "@/features/user/types/user.types.ts";
|
import { IUser } from "@/features/user/types/user.types.ts";
|
||||||
|
import { saveAs } from "file-saver";
|
||||||
|
|
||||||
export async function getSpaces(params?: QueryParams): Promise<IPagination<ISpace>> {
|
export async function getSpaces(
|
||||||
|
params?: QueryParams
|
||||||
|
): Promise<IPagination<ISpace>> {
|
||||||
const req = await api.post("/spaces", params);
|
const req = await api.post("/spaces", params);
|
||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSpaceById(spaceId: string): Promise<ISpace> {
|
export async function getSpaceById(spaceId: string): Promise<ISpace> {
|
||||||
const req = await api.post<ISpace>('/spaces/info', { spaceId });
|
const req = await api.post<ISpace>("/spaces/info", { spaceId });
|
||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createSpace(data: Partial<ISpace>): Promise<ISpace> {
|
export async function createSpace(data: Partial<ISpace>): Promise<ISpace> {
|
||||||
const req = await api.post<ISpace>('/spaces/create', data);
|
const req = await api.post<ISpace>("/spaces/create", data);
|
||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSpace(data: Partial<ISpace>): Promise<ISpace> {
|
export async function updateSpace(data: Partial<ISpace>): Promise<ISpace> {
|
||||||
const req = await api.post<ISpace>('/spaces/update', data);
|
const req = await api.post<ISpace>("/spaces/update", data);
|
||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteSpace(spaceId: string): Promise<void> {
|
export async function deleteSpace(spaceId: string): Promise<void> {
|
||||||
await api.post<void>('/spaces/delete', { spaceId });
|
await api.post<void>("/spaces/delete", { spaceId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSpaceMembers(
|
export async function getSpaceMembers(
|
||||||
spaceId: string
|
spaceId: string
|
||||||
): Promise<IPagination<IUser>> {
|
): Promise<IPagination<IUser>> {
|
||||||
const req = await api.post<any>('/spaces/members', { spaceId });
|
const req = await api.post<any>("/spaces/members", { spaceId });
|
||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addSpaceMember(data: IAddSpaceMember): Promise<void> {
|
export async function addSpaceMember(data: IAddSpaceMember): Promise<void> {
|
||||||
await api.post('/spaces/members/add', data);
|
await api.post("/spaces/members/add", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeSpaceMember(
|
export async function removeSpaceMember(
|
||||||
data: IRemoveSpaceMember
|
data: IRemoveSpaceMember
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await api.post('/spaces/members/remove', data);
|
await api.post("/spaces/members/remove", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function changeMemberRole(
|
export async function changeMemberRole(
|
||||||
data: IChangeSpaceMemberRole
|
data: IChangeSpaceMemberRole
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await api.post('/spaces/members/change-role', data);
|
await api.post("/spaces/members/change-role", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportSpace(data: IExportSpaceParams): Promise<void> {
|
||||||
|
const req = await api.post("/spaces/export", data, {
|
||||||
|
responseType: "blob",
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileName = req?.headers["content-disposition"]
|
||||||
|
.split("filename=")[1]
|
||||||
|
.replace(/"/g, "");
|
||||||
|
|
||||||
|
saveAs(req.data, decodeURIComponent(fileName));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
SpaceCaslAction,
|
SpaceCaslAction,
|
||||||
SpaceCaslSubject,
|
SpaceCaslSubject,
|
||||||
} from "@/features/space/permissions/permissions.type.ts";
|
} from "@/features/space/permissions/permissions.type.ts";
|
||||||
|
import { ExportFormat } from "@/features/page/types/page.types.ts";
|
||||||
|
|
||||||
export interface ISpace {
|
export interface ISpace {
|
||||||
id: string;
|
id: string;
|
||||||
@ -68,3 +69,9 @@ export interface SpaceGroupInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ISpaceMember = { role: string } & (SpaceUserInfo | SpaceGroupInfo);
|
export type ISpaceMember = { role: string } & (SpaceUserInfo | SpaceGroupInfo);
|
||||||
|
|
||||||
|
export interface IExportSpaceParams {
|
||||||
|
spaceId: string;
|
||||||
|
format: ExportFormat;
|
||||||
|
includeAttachments?: boolean;
|
||||||
|
}
|
||||||
@ -26,14 +26,18 @@ api.interceptors.request.use(
|
|||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
// we need the response headers
|
// we need the response headers for these endpoints
|
||||||
if (response.request.responseURL.includes("/api/pages/export")) {
|
const exemptEndpoints = ["/api/pages/export", "/api/spaces/export"];
|
||||||
return response;
|
if (response.request.responseURL) {
|
||||||
|
const path = new URL(response.request.responseURL)?.pathname;
|
||||||
|
if (path && exemptEndpoints.includes(path)) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
@ -72,7 +76,7 @@ api.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function redirectToLogin() {
|
function redirectToLogin() {
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import {StarterKit} from '@tiptap/starter-kit';
|
import { StarterKit } from '@tiptap/starter-kit';
|
||||||
import {TextAlign} from '@tiptap/extension-text-align';
|
import { TextAlign } from '@tiptap/extension-text-align';
|
||||||
import {TaskList} from '@tiptap/extension-task-list';
|
import { TaskList } from '@tiptap/extension-task-list';
|
||||||
import {TaskItem} from '@tiptap/extension-task-item';
|
import { TaskItem } from '@tiptap/extension-task-item';
|
||||||
import {Underline} from '@tiptap/extension-underline';
|
import { Underline } from '@tiptap/extension-underline';
|
||||||
import {Superscript} from '@tiptap/extension-superscript';
|
import { Superscript } from '@tiptap/extension-superscript';
|
||||||
import SubScript from '@tiptap/extension-subscript';
|
import SubScript from '@tiptap/extension-subscript';
|
||||||
import {Highlight} from '@tiptap/extension-highlight';
|
import { Highlight } from '@tiptap/extension-highlight';
|
||||||
import {Typography} from '@tiptap/extension-typography';
|
import { Typography } from '@tiptap/extension-typography';
|
||||||
import {TextStyle} from '@tiptap/extension-text-style';
|
import { TextStyle } from '@tiptap/extension-text-style';
|
||||||
import {Color} from '@tiptap/extension-color';
|
import { Color } from '@tiptap/extension-color';
|
||||||
import {Youtube} from '@tiptap/extension-youtube';
|
import { Youtube } from '@tiptap/extension-youtube';
|
||||||
import Table from '@tiptap/extension-table';
|
import Table from '@tiptap/extension-table';
|
||||||
import TableHeader from '@tiptap/extension-table-header';
|
import TableHeader from '@tiptap/extension-table-header';
|
||||||
import {
|
import {
|
||||||
@ -30,14 +30,15 @@ import {
|
|||||||
Attachment,
|
Attachment,
|
||||||
Drawio,
|
Drawio,
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
Embed
|
Embed,
|
||||||
} from '@docmost/editor-ext';
|
} from '@docmost/editor-ext';
|
||||||
import {generateText, JSONContent} from '@tiptap/core';
|
import { generateText, getSchema, JSONContent } from '@tiptap/core';
|
||||||
import {generateHTML} from '../common/helpers/prosemirror/html';
|
import { generateHTML } from '../common/helpers/prosemirror/html';
|
||||||
// @tiptap/html library works best for generating prosemirror json state but not HTML
|
// @tiptap/html library works best for generating prosemirror json state but not HTML
|
||||||
// see: https://github.com/ueberdosis/tiptap/issues/5352
|
// see: https://github.com/ueberdosis/tiptap/issues/5352
|
||||||
// see:https://github.com/ueberdosis/tiptap/issues/4089
|
// see:https://github.com/ueberdosis/tiptap/issues/4089
|
||||||
import {generateJSON} from '@tiptap/html';
|
import { generateJSON } from '@tiptap/html';
|
||||||
|
import { Node } from '@tiptap/pm/model';
|
||||||
|
|
||||||
export const tiptapExtensions = [
|
export const tiptapExtensions = [
|
||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
@ -73,7 +74,7 @@ export const tiptapExtensions = [
|
|||||||
CustomCodeBlock,
|
CustomCodeBlock,
|
||||||
Drawio,
|
Drawio,
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
Embed
|
Embed,
|
||||||
] as any;
|
] as any;
|
||||||
|
|
||||||
export function jsonToHtml(tiptapJson: any) {
|
export function jsonToHtml(tiptapJson: any) {
|
||||||
@ -88,6 +89,10 @@ export function jsonToText(tiptapJson: JSONContent) {
|
|||||||
return generateText(tiptapJson, tiptapExtensions);
|
return generateText(tiptapJson, tiptapExtensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function jsonToNode(tiptapJson: JSONContent) {
|
||||||
|
return Node.fromJSON(getSchema(tiptapExtensions), tiptapJson);
|
||||||
|
}
|
||||||
|
|
||||||
export function getPageId(documentName: string) {
|
export function getPageId(documentName: string) {
|
||||||
return documentName.split('.')[1];
|
return documentName.split('.')[1];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -160,4 +160,30 @@ export class PageRepo {
|
|||||||
.whereRef('spaces.id', '=', 'pages.spaceId'),
|
.whereRef('spaces.id', '=', 'pages.spaceId'),
|
||||||
).as('space');
|
).as('space');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPageAndDescendants(parentPageId: string) {
|
||||||
|
return this.db
|
||||||
|
.withRecursive('page_hierarchy', (db) =>
|
||||||
|
db
|
||||||
|
.selectFrom('pages')
|
||||||
|
.select(['id', 'slugId', 'title', 'icon', 'content', 'parentPageId'])
|
||||||
|
.where('id', '=', parentPageId)
|
||||||
|
.unionAll((exp) =>
|
||||||
|
exp
|
||||||
|
.selectFrom('pages as p')
|
||||||
|
.select([
|
||||||
|
'p.id',
|
||||||
|
'p.slugId',
|
||||||
|
'p.title',
|
||||||
|
'p.icon',
|
||||||
|
'p.content',
|
||||||
|
'p.parentPageId',
|
||||||
|
])
|
||||||
|
.innerJoin('page_hierarchy as ph', 'p.parentPageId', 'ph.id'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.selectFrom('page_hierarchy')
|
||||||
|
.selectAll()
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,5 +22,19 @@ export class ExportPageDto {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
includeFiles?: boolean;
|
includeChildren?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ExportSpaceDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
spaceId: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsIn(['html', 'markdown'])
|
||||||
|
format: ExportFormat;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
includeAttachments?: boolean;
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ExportService } from './export.service';
|
import { ExportService } from './export.service';
|
||||||
import { ExportPageDto } from './dto/export-dto';
|
import { ExportPageDto, ExportSpaceDto } from './dto/export-dto';
|
||||||
import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
||||||
import { User } from '@docmost/db/types/entity.types';
|
import { User } from '@docmost/db/types/entity.types';
|
||||||
import SpaceAbilityFactory from '../../core/casl/abilities/space-ability.factory';
|
import SpaceAbilityFactory from '../../core/casl/abilities/space-ability.factory';
|
||||||
@ -54,10 +54,28 @@ export class ImportController {
|
|||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawContent = await this.exportService.exportPage(dto.format, page);
|
|
||||||
|
|
||||||
const fileExt = getExportExtension(dto.format);
|
const fileExt = getExportExtension(dto.format);
|
||||||
const fileName = sanitize(page.title || 'Untitled') + fileExt;
|
const fileName = sanitize(page.title || 'untitled') + fileExt;
|
||||||
|
|
||||||
|
if (dto.includeChildren) {
|
||||||
|
const zipFileBuffer = await this.exportService.exportPageWithChildren(
|
||||||
|
dto.pageId,
|
||||||
|
dto.format,
|
||||||
|
);
|
||||||
|
|
||||||
|
const newName = fileName + '.zip';
|
||||||
|
|
||||||
|
res.headers({
|
||||||
|
'Content-Type': 'application/zip',
|
||||||
|
'Content-Disposition':
|
||||||
|
'attachment; filename="' + encodeURIComponent(newName) + '"',
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send(zipFileBuffer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawContent = await this.exportService.exportPage(dto.format, page);
|
||||||
|
|
||||||
res.headers({
|
res.headers({
|
||||||
'Content-Type': getMimeType(fileExt),
|
'Content-Type': getMimeType(fileExt),
|
||||||
@ -67,4 +85,34 @@ export class ImportController {
|
|||||||
|
|
||||||
res.send(rawContent);
|
res.send(rawContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('spaces/export')
|
||||||
|
async exportSpace(
|
||||||
|
@Body() dto: ExportSpaceDto,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@Res() res: FastifyReply,
|
||||||
|
) {
|
||||||
|
const ability = await this.spaceAbility.createForUser(user, dto.spaceId);
|
||||||
|
if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportFile = await this.exportService.exportSpace(
|
||||||
|
dto.spaceId,
|
||||||
|
dto.format,
|
||||||
|
dto.includeAttachments,
|
||||||
|
);
|
||||||
|
|
||||||
|
res.headers({
|
||||||
|
'Content-Type': 'application/zip',
|
||||||
|
'Content-Disposition':
|
||||||
|
'attachment; filename="' +
|
||||||
|
encodeURIComponent(sanitize(exportFile.fileName)) +
|
||||||
|
'"',
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send(exportFile.fileBuffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ExportService } from './export.service';
|
import { ExportService } from './export.service';
|
||||||
import { ImportController } from './export.controller';
|
import { ImportController } from './export.controller';
|
||||||
|
import { StorageModule } from '../storage/storage.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [StorageModule],
|
||||||
providers: [ExportService],
|
providers: [ExportService],
|
||||||
controllers: [ImportController],
|
controllers: [ImportController],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,19 +1,48 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { jsonToHtml } from '../../collaboration/collaboration.util';
|
import { jsonToHtml } from '../../collaboration/collaboration.util';
|
||||||
import { turndown } from './turndown-utils';
|
import { turndown } from './turndown-utils';
|
||||||
import { ExportFormat } from './dto/export-dto';
|
import { ExportFormat } from './dto/export-dto';
|
||||||
import { Page } from '@docmost/db/types/entity.types';
|
import { Page } from '@docmost/db/types/entity.types';
|
||||||
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
||||||
|
import * as JSZip from 'jszip';
|
||||||
|
import { StorageService } from '../storage/storage.service';
|
||||||
|
import {
|
||||||
|
buildTree,
|
||||||
|
computeLocalPath,
|
||||||
|
getAttachmentIds,
|
||||||
|
getExportExtension,
|
||||||
|
getPageTitle,
|
||||||
|
getProsemirrorContent,
|
||||||
|
PageExportTree,
|
||||||
|
replaceInternalLinks,
|
||||||
|
updateAttachmentUrls,
|
||||||
|
} from './utils';
|
||||||
|
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExportService {
|
export class ExportService {
|
||||||
|
private readonly logger = new Logger(ExportService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly pageRepo: PageRepo,
|
||||||
|
@InjectKysely() private readonly db: KyselyDB,
|
||||||
|
private readonly storageService: StorageService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async exportPage(format: string, page: Page) {
|
async exportPage(format: string, page: Page) {
|
||||||
const titleNode = {
|
const titleNode = {
|
||||||
type: 'heading',
|
type: 'heading',
|
||||||
attrs: { level: 1 },
|
attrs: { level: 1 },
|
||||||
content: [{ type: 'text', text: page.title }],
|
content: [{ type: 'text', text: getPageTitle(page.title) }],
|
||||||
};
|
};
|
||||||
|
|
||||||
let prosemirrorJson: any = page.content || { type: 'doc', content: [] };
|
let prosemirrorJson: any = getProsemirrorContent(page.content);
|
||||||
|
|
||||||
if (page.title) {
|
if (page.title) {
|
||||||
prosemirrorJson.content.unshift(titleNode);
|
prosemirrorJson.content.unshift(titleNode);
|
||||||
@ -22,7 +51,13 @@ export class ExportService {
|
|||||||
const pageHtml = jsonToHtml(prosemirrorJson);
|
const pageHtml = jsonToHtml(prosemirrorJson);
|
||||||
|
|
||||||
if (format === ExportFormat.HTML) {
|
if (format === ExportFormat.HTML) {
|
||||||
return `<!DOCTYPE html><html><head><title>${page.title}</title></head><body>${pageHtml}</body></html>`;
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>${getPageTitle(page.title)}</title>
|
||||||
|
</head>
|
||||||
|
<body>${pageHtml}</body>
|
||||||
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format === ExportFormat.Markdown) {
|
if (format === ExportFormat.Markdown) {
|
||||||
@ -31,4 +66,156 @@ export class ExportService {
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async exportPageWithChildren(pageId: string, format: string) {
|
||||||
|
const pages = await this.pageRepo.getPageAndDescendants(pageId);
|
||||||
|
|
||||||
|
if (!pages || pages.length === 0) {
|
||||||
|
throw new BadRequestException('No pages to export');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentPageIndex = pages.findIndex((obj) => obj.id === pageId);
|
||||||
|
// set to null to make export of pages with parentId work
|
||||||
|
pages[parentPageIndex].parentPageId = null;
|
||||||
|
|
||||||
|
const tree = buildTree(pages as Page[]);
|
||||||
|
|
||||||
|
const zip = new JSZip();
|
||||||
|
await this.zipPages(tree, format, zip);
|
||||||
|
|
||||||
|
const zipFile = zip.generateNodeStream({
|
||||||
|
type: 'nodebuffer',
|
||||||
|
streamFiles: true,
|
||||||
|
compression: 'DEFLATE',
|
||||||
|
});
|
||||||
|
|
||||||
|
return zipFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportSpace(
|
||||||
|
spaceId: string,
|
||||||
|
format: string,
|
||||||
|
includeAttachments: boolean,
|
||||||
|
) {
|
||||||
|
const space = await this.db
|
||||||
|
.selectFrom('spaces')
|
||||||
|
.selectAll()
|
||||||
|
.where('id', '=', spaceId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
if (!space) {
|
||||||
|
throw new NotFoundException('Space not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = await this.db
|
||||||
|
.selectFrom('pages')
|
||||||
|
.select([
|
||||||
|
'pages.id',
|
||||||
|
'pages.slugId',
|
||||||
|
'pages.title',
|
||||||
|
'pages.content',
|
||||||
|
'pages.parentPageId',
|
||||||
|
])
|
||||||
|
.where('spaceId', '=', spaceId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
const tree = buildTree(pages as Page[]);
|
||||||
|
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
await this.zipPages(tree, format, zip, includeAttachments);
|
||||||
|
|
||||||
|
const zipFile = zip.generateNodeStream({
|
||||||
|
type: 'nodebuffer',
|
||||||
|
streamFiles: true,
|
||||||
|
compression: 'DEFLATE',
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileName = `${space.name}-space-export${getExportExtension(format)}.zip`;
|
||||||
|
return {
|
||||||
|
fileBuffer: zipFile,
|
||||||
|
fileName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async zipPages(
|
||||||
|
tree: PageExportTree,
|
||||||
|
format: string,
|
||||||
|
zip: JSZip,
|
||||||
|
includeAttachments = true,
|
||||||
|
): Promise<void> {
|
||||||
|
const slugIdToPath: Record<string, string> = {};
|
||||||
|
|
||||||
|
computeLocalPath(tree, format, null, '', slugIdToPath);
|
||||||
|
|
||||||
|
const stack: { folder: JSZip; parentPageId: string }[] = [
|
||||||
|
{ folder: zip, parentPageId: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const { folder, parentPageId } = stack.pop();
|
||||||
|
const children = tree[parentPageId] || [];
|
||||||
|
|
||||||
|
for (const page of children) {
|
||||||
|
const childPages = tree[page.id] || [];
|
||||||
|
|
||||||
|
const prosemirrorJson = getProsemirrorContent(page.content);
|
||||||
|
|
||||||
|
const currentPagePath = slugIdToPath[page.slugId];
|
||||||
|
|
||||||
|
let updatedJsonContent = replaceInternalLinks(
|
||||||
|
prosemirrorJson,
|
||||||
|
slugIdToPath,
|
||||||
|
currentPagePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (includeAttachments) {
|
||||||
|
await this.zipAttachments(updatedJsonContent, page.spaceId, folder);
|
||||||
|
updatedJsonContent = updateAttachmentUrls(updatedJsonContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageTitle = getPageTitle(page.title);
|
||||||
|
const pageExportContent = await this.exportPage(format, {
|
||||||
|
...page,
|
||||||
|
content: updatedJsonContent,
|
||||||
|
});
|
||||||
|
|
||||||
|
folder.file(
|
||||||
|
`${pageTitle}${getExportExtension(format)}`,
|
||||||
|
pageExportContent,
|
||||||
|
);
|
||||||
|
if (childPages.length > 0) {
|
||||||
|
const pageFolder = folder.folder(pageTitle);
|
||||||
|
stack.push({ folder: pageFolder, parentPageId: page.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async zipAttachments(prosemirrorJson: any, spaceId: string, zip: JSZip) {
|
||||||
|
const attachmentIds = getAttachmentIds(prosemirrorJson);
|
||||||
|
|
||||||
|
if (attachmentIds.length > 0) {
|
||||||
|
const attachments = await this.db
|
||||||
|
.selectFrom('attachments')
|
||||||
|
.selectAll()
|
||||||
|
.where('id', 'in', attachmentIds)
|
||||||
|
.where('spaceId', '=', spaceId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
attachments.map(async (attachment) => {
|
||||||
|
try {
|
||||||
|
const fileBuffer = await this.storageService.read(
|
||||||
|
attachment.filePath,
|
||||||
|
);
|
||||||
|
const filePath = `/files/${attachment.id}/${attachment.fileName}`;
|
||||||
|
zip.file(filePath, fileBuffer);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.debug(`Attachment export error ${attachment.id}`, err);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,11 @@
|
|||||||
|
import { jsonToNode } from 'src/collaboration/collaboration.util';
|
||||||
import { ExportFormat } from './dto/export-dto';
|
import { ExportFormat } from './dto/export-dto';
|
||||||
|
import { Node } from '@tiptap/pm/model';
|
||||||
|
import { validate as isValidUUID } from 'uuid';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { Page } from '@docmost/db/types/entity.types';
|
||||||
|
|
||||||
|
export type PageExportTree = Record<string, Page[]>;
|
||||||
|
|
||||||
export function getExportExtension(format: string) {
|
export function getExportExtension(format: string) {
|
||||||
if (format === ExportFormat.HTML) {
|
if (format === ExportFormat.HTML) {
|
||||||
@ -10,3 +17,171 @@ export function getExportExtension(format: string) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPageTitle(title: string) {
|
||||||
|
return title ? title : 'untitled';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProsemirrorContent(content: any) {
|
||||||
|
return (
|
||||||
|
content ?? {
|
||||||
|
type: 'doc',
|
||||||
|
content: [{ type: 'paragraph', attrs: { textAlign: 'left' } }],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAttachmentIds(prosemirrorJson: any) {
|
||||||
|
const doc = jsonToNode(prosemirrorJson);
|
||||||
|
const attachmentIds = [];
|
||||||
|
|
||||||
|
doc?.descendants((node: Node) => {
|
||||||
|
if (isAttachmentNode(node.type.name)) {
|
||||||
|
if (node.attrs.attachmentId && isValidUUID(node.attrs.attachmentId)) {
|
||||||
|
if (!attachmentIds.includes(node.attrs.attachmentId)) {
|
||||||
|
attachmentIds.push(node.attrs.attachmentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return attachmentIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAttachmentNode(nodeType: string) {
|
||||||
|
const attachmentNodeTypes = [
|
||||||
|
'attachment',
|
||||||
|
'image',
|
||||||
|
'video',
|
||||||
|
'excalidraw',
|
||||||
|
'drawio',
|
||||||
|
];
|
||||||
|
return attachmentNodeTypes.includes(nodeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateAttachmentUrls(prosemirrorJson: any) {
|
||||||
|
const doc = jsonToNode(prosemirrorJson);
|
||||||
|
|
||||||
|
doc?.descendants((node: Node) => {
|
||||||
|
if (isAttachmentNode(node.type.name)) {
|
||||||
|
if (node.attrs.src && node.attrs.src.startsWith('/files')) {
|
||||||
|
//@ts-expect-error
|
||||||
|
node.attrs.src = node.attrs.src.replace('/files', 'files');
|
||||||
|
} else if (node.attrs.url && node.attrs.url.startsWith('/files')) {
|
||||||
|
//@ts-expect-error
|
||||||
|
node.attrs.url = node.attrs.url.replace('/files', 'files');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return doc.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceInternalLinks(
|
||||||
|
prosemirrorJson: any,
|
||||||
|
slugIdToPath: Record<string, string>,
|
||||||
|
currentPagePath: string,
|
||||||
|
) {
|
||||||
|
const doc = jsonToNode(prosemirrorJson);
|
||||||
|
const internalLinkRegex =
|
||||||
|
/^(https?:\/\/)?([^\/]+)?(\/s\/([^\/]+)\/)?p\/([a-zA-Z0-9-]+)\/?$/;
|
||||||
|
|
||||||
|
doc.descendants((node: Node) => {
|
||||||
|
for (const mark of node.marks) {
|
||||||
|
if (mark.type.name === 'link' && mark.attrs.href) {
|
||||||
|
const match = mark.attrs.href.match(internalLinkRegex);
|
||||||
|
if (match) {
|
||||||
|
const markLink = mark.attrs.href;
|
||||||
|
|
||||||
|
const slugId = extractPageSlugId(match[5]);
|
||||||
|
const localPath = slugIdToPath[slugId];
|
||||||
|
|
||||||
|
if (!localPath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relativePath = computeRelativePath(currentPagePath, localPath);
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
mark.attrs.href = relativePath;
|
||||||
|
//@ts-expect-error
|
||||||
|
mark.attrs.target = '_self';
|
||||||
|
if (node.isText) {
|
||||||
|
// if link and text are same, use page title
|
||||||
|
if (markLink === node.text) {
|
||||||
|
//@ts-expect-error
|
||||||
|
node.text = getInternalLinkPageName(relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return doc.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInternalLinkPageName(path: string): string {
|
||||||
|
return decodeURIComponent(
|
||||||
|
path?.split('/').pop().split('.').slice(0, -1).join('.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractPageSlugId(input: string): string {
|
||||||
|
if (!input) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const parts = input.split('-');
|
||||||
|
return parts.length > 1 ? parts[parts.length - 1] : input;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTree(pages: Page[]): PageExportTree {
|
||||||
|
const tree: PageExportTree = {};
|
||||||
|
const titleCount: Record<string, Record<string, number>> = {};
|
||||||
|
|
||||||
|
for (const page of pages) {
|
||||||
|
const parentPageId = page.parentPageId;
|
||||||
|
|
||||||
|
if (!titleCount[parentPageId]) {
|
||||||
|
titleCount[parentPageId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = getPageTitle(page.title);
|
||||||
|
|
||||||
|
if (titleCount[parentPageId][title]) {
|
||||||
|
title = `${title} (${titleCount[parentPageId][title]})`;
|
||||||
|
titleCount[parentPageId][getPageTitle(page.title)] += 1;
|
||||||
|
} else {
|
||||||
|
titleCount[parentPageId][title] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
page.title = title;
|
||||||
|
if (!tree[parentPageId]) {
|
||||||
|
tree[parentPageId] = [];
|
||||||
|
}
|
||||||
|
tree[parentPageId].push(page);
|
||||||
|
}
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeLocalPath(
|
||||||
|
tree: PageExportTree,
|
||||||
|
format: string,
|
||||||
|
parentPageId: string | null,
|
||||||
|
currentPath: string,
|
||||||
|
slugIdToPath: Record<string, string>,
|
||||||
|
) {
|
||||||
|
const children = tree[parentPageId] || [];
|
||||||
|
|
||||||
|
for (const page of children) {
|
||||||
|
const title = encodeURIComponent(getPageTitle(page.title));
|
||||||
|
const localPath = `${currentPath}${title}`;
|
||||||
|
slugIdToPath[page.slugId] = `${localPath}${getExportExtension(format)}`;
|
||||||
|
|
||||||
|
computeLocalPath(tree, format, page.id, `${localPath}/`, slugIdToPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeRelativePath(from: string, to: string) {
|
||||||
|
return path.relative(path.dirname(from), to);
|
||||||
|
}
|
||||||
|
|||||||
@ -63,6 +63,7 @@
|
|||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"fractional-indexing-jittered": "^0.9.1",
|
"fractional-indexing-jittered": "^0.9.1",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"y-indexeddb": "^9.0.12",
|
"y-indexeddb": "^9.0.12",
|
||||||
"yjs": "^13.6.18"
|
"yjs": "^13.6.18"
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { mergeAttributes } from "@tiptap/core";
|
||||||
import TiptapLink from "@tiptap/extension-link";
|
import TiptapLink from "@tiptap/extension-link";
|
||||||
import { Plugin } from "@tiptap/pm/state";
|
import { Plugin } from "@tiptap/pm/state";
|
||||||
import { EditorView } from "@tiptap/pm/view";
|
import { EditorView } from "@tiptap/pm/view";
|
||||||
@ -5,6 +6,24 @@ import { EditorView } from "@tiptap/pm/view";
|
|||||||
export const LinkExtension = TiptapLink.extend({
|
export const LinkExtension = TiptapLink.extend({
|
||||||
inclusive: false,
|
inclusive: false,
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'a[href]:not([data-type="button"]):not([href *= "javascript:" i])',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
"a",
|
||||||
|
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||||
|
class: "link",
|
||||||
|
}),
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
addProseMirrorPlugins() {
|
addProseMirrorPlugins() {
|
||||||
const { editor } = this;
|
const { editor } = this;
|
||||||
|
|
||||||
|
|||||||
70
pnpm-lock.yaml
generated
70
pnpm-lock.yaml
generated
@ -154,6 +154,9 @@ importers:
|
|||||||
ioredis:
|
ioredis:
|
||||||
specifier: ^5.4.1
|
specifier: ^5.4.1
|
||||||
version: 5.4.1
|
version: 5.4.1
|
||||||
|
jszip:
|
||||||
|
specifier: ^3.10.1
|
||||||
|
version: 3.10.1
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
version: 10.0.0
|
version: 10.0.0
|
||||||
@ -5434,6 +5437,9 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
immediate@3.0.6:
|
||||||
|
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||||
|
|
||||||
import-fresh@3.3.0:
|
import-fresh@3.3.0:
|
||||||
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
|
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -5552,6 +5558,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
isarray@1.0.0:
|
||||||
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
|
|
||||||
isexe@2.0.0:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
@ -5830,6 +5839,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
||||||
engines: {node: '>=12', npm: '>=6'}
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
|
|
||||||
|
jszip@3.10.1:
|
||||||
|
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||||
|
|
||||||
jwa@1.4.1:
|
jwa@1.4.1:
|
||||||
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
|
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
|
||||||
|
|
||||||
@ -5940,6 +5952,9 @@ packages:
|
|||||||
libphonenumber-js@1.10.58:
|
libphonenumber-js@1.10.58:
|
||||||
resolution: {integrity: sha512-53A0IpJFL9LdHbpeatwizf8KSwPICrqn9H0g3Y7WQ+Jgeu9cQ4Ew3WrRtrLBu/CX2lXd5+rgT01/tGlkbkzOjw==}
|
resolution: {integrity: sha512-53A0IpJFL9LdHbpeatwizf8KSwPICrqn9H0g3Y7WQ+Jgeu9cQ4Ew3WrRtrLBu/CX2lXd5+rgT01/tGlkbkzOjw==}
|
||||||
|
|
||||||
|
lie@3.3.0:
|
||||||
|
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||||
|
|
||||||
light-my-request@5.13.0:
|
light-my-request@5.13.0:
|
||||||
resolution: {integrity: sha512-9IjUN9ZyCS9pTG+KqTDEQo68Sui2lHsYBrfMyVUTTZ3XhH8PMZq7xO94Kr+eP9dhi/kcKsx4N41p2IXEBil1pQ==}
|
resolution: {integrity: sha512-9IjUN9ZyCS9pTG+KqTDEQo68Sui2lHsYBrfMyVUTTZ3XhH8PMZq7xO94Kr+eP9dhi/kcKsx4N41p2IXEBil1pQ==}
|
||||||
|
|
||||||
@ -6429,6 +6444,9 @@ packages:
|
|||||||
package-json-from-dist@1.0.0:
|
package-json-from-dist@1.0.0:
|
||||||
resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==}
|
resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==}
|
||||||
|
|
||||||
|
pako@1.0.11:
|
||||||
|
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -6696,6 +6714,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==}
|
resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
|
process-nextick-args@2.0.1:
|
||||||
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
|
|
||||||
process-warning@3.0.0:
|
process-warning@3.0.0:
|
||||||
resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==}
|
resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==}
|
||||||
|
|
||||||
@ -6958,6 +6979,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
readable-stream@2.3.8:
|
||||||
|
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
readable-stream@3.6.2:
|
||||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@ -7118,6 +7142,9 @@ packages:
|
|||||||
rxjs@7.8.1:
|
rxjs@7.8.1:
|
||||||
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
|
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
|
||||||
|
|
||||||
|
safe-buffer@5.1.2:
|
||||||
|
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||||
|
|
||||||
safe-buffer@5.2.1:
|
safe-buffer@5.2.1:
|
||||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
@ -7190,6 +7217,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
setimmediate@1.0.5:
|
||||||
|
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||||
|
|
||||||
setprototypeof@1.2.0:
|
setprototypeof@1.2.0:
|
||||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||||
|
|
||||||
@ -7314,6 +7344,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
string_decoder@1.1.1:
|
||||||
|
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||||
|
|
||||||
string_decoder@1.3.0:
|
string_decoder@1.3.0:
|
||||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||||
|
|
||||||
@ -13818,6 +13851,8 @@ snapshots:
|
|||||||
image-size@0.5.5:
|
image-size@0.5.5:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
immediate@3.0.6: {}
|
||||||
|
|
||||||
import-fresh@3.3.0:
|
import-fresh@3.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
parent-module: 1.0.1
|
parent-module: 1.0.1
|
||||||
@ -13958,6 +13993,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
|
|
||||||
|
isarray@1.0.0: {}
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
isomorphic.js@0.2.5: {}
|
isomorphic.js@0.2.5: {}
|
||||||
@ -14446,6 +14483,13 @@ snapshots:
|
|||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
semver: 7.6.0
|
semver: 7.6.0
|
||||||
|
|
||||||
|
jszip@3.10.1:
|
||||||
|
dependencies:
|
||||||
|
lie: 3.3.0
|
||||||
|
pako: 1.0.11
|
||||||
|
readable-stream: 2.3.8
|
||||||
|
setimmediate: 1.0.5
|
||||||
|
|
||||||
jwa@1.4.1:
|
jwa@1.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-equal-constant-time: 1.0.1
|
buffer-equal-constant-time: 1.0.1
|
||||||
@ -14539,6 +14583,10 @@ snapshots:
|
|||||||
|
|
||||||
libphonenumber-js@1.10.58: {}
|
libphonenumber-js@1.10.58: {}
|
||||||
|
|
||||||
|
lie@3.3.0:
|
||||||
|
dependencies:
|
||||||
|
immediate: 3.0.6
|
||||||
|
|
||||||
light-my-request@5.13.0:
|
light-my-request@5.13.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
cookie: 0.6.0
|
cookie: 0.6.0
|
||||||
@ -15035,6 +15083,8 @@ snapshots:
|
|||||||
|
|
||||||
package-json-from-dist@1.0.0: {}
|
package-json-from-dist@1.0.0: {}
|
||||||
|
|
||||||
|
pako@1.0.11: {}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
@ -15288,6 +15338,8 @@ snapshots:
|
|||||||
|
|
||||||
proc-log@3.0.0: {}
|
proc-log@3.0.0: {}
|
||||||
|
|
||||||
|
process-nextick-args@2.0.1: {}
|
||||||
|
|
||||||
process-warning@3.0.0: {}
|
process-warning@3.0.0: {}
|
||||||
|
|
||||||
process@0.11.10: {}
|
process@0.11.10: {}
|
||||||
@ -15612,6 +15664,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
|
|
||||||
|
readable-stream@2.3.8:
|
||||||
|
dependencies:
|
||||||
|
core-util-is: 1.0.3
|
||||||
|
inherits: 2.0.4
|
||||||
|
isarray: 1.0.0
|
||||||
|
process-nextick-args: 2.0.1
|
||||||
|
safe-buffer: 5.1.2
|
||||||
|
string_decoder: 1.1.1
|
||||||
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
readable-stream@3.6.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
@ -15780,6 +15842,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
|
|
||||||
|
safe-buffer@5.1.2: {}
|
||||||
|
|
||||||
safe-buffer@5.2.1: {}
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
safe-regex2@2.0.0:
|
safe-regex2@2.0.0:
|
||||||
@ -15847,6 +15911,8 @@ snapshots:
|
|||||||
gopd: 1.0.1
|
gopd: 1.0.1
|
||||||
has-property-descriptors: 1.0.2
|
has-property-descriptors: 1.0.2
|
||||||
|
|
||||||
|
setimmediate@1.0.5: {}
|
||||||
|
|
||||||
setprototypeof@1.2.0: {}
|
setprototypeof@1.2.0: {}
|
||||||
|
|
||||||
shallowequal@1.1.0: {}
|
shallowequal@1.1.0: {}
|
||||||
@ -15983,6 +16049,10 @@ snapshots:
|
|||||||
emoji-regex: 9.2.2
|
emoji-regex: 9.2.2
|
||||||
strip-ansi: 7.1.0
|
strip-ansi: 7.1.0
|
||||||
|
|
||||||
|
string_decoder@1.1.1:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.1.2
|
||||||
|
|
||||||
string_decoder@1.3.0:
|
string_decoder@1.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|||||||
Reference in New Issue
Block a user