mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-26 06:31:07 +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:
@@ -5,36 +5,38 @@ import {
|
||||
Text,
|
||||
Tooltip,
|
||||
UnstyledButton,
|
||||
} from '@mantine/core';
|
||||
import { spotlight } from '@mantine/spotlight';
|
||||
} from "@mantine/core";
|
||||
import { spotlight } from "@mantine/spotlight";
|
||||
import {
|
||||
IconArrowDown,
|
||||
IconDots,
|
||||
IconFileExport,
|
||||
IconHome,
|
||||
IconPlus,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
} from '@tabler/icons-react';
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import classes from './space-sidebar.module.css';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useAtom } from 'jotai';
|
||||
import { SearchSpotlight } from '@/features/search/search-spotlight.tsx';
|
||||
import { treeApiAtom } from '@/features/page/tree/atoms/tree-api-atom.ts';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import clsx from 'clsx';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import SpaceSettingsModal from '@/features/space/components/settings-modal.tsx';
|
||||
import { useGetSpaceBySlugQuery } from '@/features/space/queries/space-query.ts';
|
||||
import { getSpaceUrl } from '@/lib/config.ts';
|
||||
import SpaceTree from '@/features/page/tree/components/space-tree.tsx';
|
||||
import { useSpaceAbility } from '@/features/space/permissions/use-space-ability.ts';
|
||||
import classes from "./space-sidebar.module.css";
|
||||
import React, { useMemo } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { SearchSpotlight } from "@/features/search/search-spotlight.tsx";
|
||||
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
||||
import { Link, useLocation, useParams } from "react-router-dom";
|
||||
import clsx from "clsx";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx";
|
||||
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import SpaceTree from "@/features/page/tree/components/space-tree.tsx";
|
||||
import { useSpaceAbility } from "@/features/space/permissions/use-space-ability.ts";
|
||||
import {
|
||||
SpaceCaslAction,
|
||||
SpaceCaslSubject,
|
||||
} from '@/features/space/permissions/permissions.type.ts';
|
||||
import PageImportModal from '@/features/page/components/page-import-modal.tsx';
|
||||
import { SwitchSpace } from './switch-space';
|
||||
} from "@/features/space/permissions/permissions.type.ts";
|
||||
import PageImportModal from "@/features/page/components/page-import-modal.tsx";
|
||||
import { SwitchSpace } from "./switch-space";
|
||||
import ExportModal from "@/components/common/export-modal";
|
||||
|
||||
export function SpaceSidebar() {
|
||||
const [tree] = useAtom(treeApiAtom);
|
||||
@@ -52,7 +54,7 @@ export function SpaceSidebar() {
|
||||
}
|
||||
|
||||
function handleCreatePage() {
|
||||
tree?.create({ parentId: null, type: 'internal', index: 0 });
|
||||
tree?.create({ parentId: null, type: "internal", index: 0 });
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -61,7 +63,7 @@ export function SpaceSidebar() {
|
||||
<div
|
||||
className={classes.section}
|
||||
style={{
|
||||
border: 'none',
|
||||
border: "none",
|
||||
marginTop: 2,
|
||||
marginBottom: 3,
|
||||
}}
|
||||
@@ -78,7 +80,7 @@ export function SpaceSidebar() {
|
||||
classes.menu,
|
||||
location.pathname.toLowerCase() === getSpaceUrl(spaceSlug)
|
||||
? classes.activeButton
|
||||
: ''
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<div className={classes.menuItemInner}>
|
||||
@@ -191,6 +193,8 @@ interface SpaceMenuProps {
|
||||
function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) {
|
||||
const [importOpened, { open: openImportModal, close: closeImportModal }] =
|
||||
useDisclosure(false);
|
||||
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||
useDisclosure(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -215,6 +219,13 @@ function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) {
|
||||
Import pages
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
onClick={openExportModal}
|
||||
leftSection={<IconFileExport size={16} />}
|
||||
>
|
||||
Export space
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item
|
||||
@@ -231,6 +242,13 @@ function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) {
|
||||
open={importOpened}
|
||||
onClose={closeImportModal}
|
||||
/>
|
||||
|
||||
<ExportModal
|
||||
type="space"
|
||||
id={spaceId}
|
||||
open={exportOpened}
|
||||
onClose={closeExportModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import { useSpaceQuery } from '@/features/space/queries/space-query.ts';
|
||||
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 { useDisclosure } from "@mantine/hooks";
|
||||
import ExportModal from "@/components/common/export-modal.tsx";
|
||||
|
||||
interface SpaceDetailsProps {
|
||||
spaceId: string;
|
||||
@@ -10,6 +12,8 @@ interface SpaceDetailsProps {
|
||||
}
|
||||
export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
||||
const { data: space, isLoading } = useSpaceQuery(spaceId);
|
||||
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||
useDisclosure(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -22,6 +26,22 @@ export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
||||
|
||||
{!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" />
|
||||
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
@@ -34,6 +54,13 @@ export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
||||
|
||||
<DeleteSpaceModal space={space} />
|
||||
</Group>
|
||||
|
||||
<ExportModal
|
||||
type="space"
|
||||
id={space.id}
|
||||
open={exportOpened}
|
||||
onClose={closeExportModal}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,56 +1,72 @@
|
||||
import api from '@/lib/api-client';
|
||||
import api from "@/lib/api-client";
|
||||
import {
|
||||
IAddSpaceMember,
|
||||
IChangeSpaceMemberRole,
|
||||
IExportSpaceParams,
|
||||
IRemoveSpaceMember,
|
||||
ISpace,
|
||||
} from "@/features/space/types/space.types";
|
||||
import { IPagination, QueryParams } from "@/lib/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);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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(
|
||||
spaceId: string
|
||||
): Promise<IPagination<IUser>> {
|
||||
const req = await api.post<any>('/spaces/members', { spaceId });
|
||||
const req = await api.post<any>("/spaces/members", { spaceId });
|
||||
return req.data;
|
||||
}
|
||||
|
||||
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(
|
||||
data: IRemoveSpaceMember
|
||||
): Promise<void> {
|
||||
await api.post('/spaces/members/remove', data);
|
||||
await api.post("/spaces/members/remove", data);
|
||||
}
|
||||
|
||||
export async function changeMemberRole(
|
||||
data: IChangeSpaceMemberRole
|
||||
): 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,
|
||||
SpaceCaslSubject,
|
||||
} from "@/features/space/permissions/permissions.type.ts";
|
||||
import { ExportFormat } from "@/features/page/types/page.types.ts";
|
||||
|
||||
export interface ISpace {
|
||||
id: string;
|
||||
@@ -68,3 +69,9 @@ export interface SpaceGroupInfo {
|
||||
}
|
||||
|
||||
export type ISpaceMember = { role: string } & (SpaceUserInfo | SpaceGroupInfo);
|
||||
|
||||
export interface IExportSpaceParams {
|
||||
spaceId: string;
|
||||
format: ExportFormat;
|
||||
includeAttachments?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user