mirror of
https://github.com/docmost/docmost.git
synced 2025-11-13 01:22:36 +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 {
|
||||
IconArrowsHorizontal,
|
||||
IconDots,
|
||||
IconDownload,
|
||||
IconFileExport,
|
||||
IconHistory,
|
||||
IconLink,
|
||||
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 { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx";
|
||||
import PageExportModal from "@/features/page/components/page-export-modal.tsx";
|
||||
import ExportModal from "@/components/common/export-modal";
|
||||
|
||||
interface PageHeaderMenuProps {
|
||||
readOnly?: boolean;
|
||||
@ -126,7 +127,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item
|
||||
leftSection={<IconDownload size={16} />}
|
||||
leftSection={<IconFileExport size={16} />}
|
||||
onClick={openExportModal}
|
||||
>
|
||||
Export
|
||||
@ -154,8 +155,9 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
<PageExportModal
|
||||
pageId={page.id}
|
||||
<ExportModal
|
||||
type="page"
|
||||
id={page.id}
|
||||
open={exportOpened}
|
||||
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 { useState } from "react";
|
||||
import * as React from "react";
|
||||
@ -57,8 +57,18 @@ export default function PageExportModal({
|
||||
<Text size="md">Format</Text>
|
||||
</div>
|
||||
<ExportFormatSelection format={format} onChange={handleChange} />
|
||||
|
||||
</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">
|
||||
<Button onClick={onClose} variant="default">
|
||||
Cancel
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconDotsVertical,
|
||||
IconFileDescription,
|
||||
IconFileDescription, IconFileExport,
|
||||
IconLink,
|
||||
IconPlus,
|
||||
IconPointFilled,
|
||||
@ -39,7 +39,12 @@ import {
|
||||
import { IPage, SidebarPagesParams } from "@/features/page/types/page.types.ts";
|
||||
import { queryClient } from "@/main.tsx";
|
||||
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 { useQueryEmit } from "@/features/websocket/use-query-emit.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 { extractPageSlugId } from "@/lib";
|
||||
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
||||
import ExportModal from "@/components/common/export-modal";
|
||||
|
||||
interface SpaceTreeProps {
|
||||
spaceId: string;
|
||||
@ -402,6 +408,8 @@ function NodeMenu({ node, treeApi }: NodeMenuProps) {
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
const { spaceSlug } = useParams();
|
||||
const { openDeleteModal } = useDeletePageModal();
|
||||
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||
useDisclosure(false);
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const pageUrl =
|
||||
@ -411,56 +419,76 @@ function NodeMenu({ node, treeApi }: NodeMenuProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu shadow="md" width={200}>
|
||||
<Menu.Target>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
c="gray"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<IconDotsVertical
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={2}
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<>
|
||||
<Menu shadow="md" width={200}>
|
||||
<Menu.Target>
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
c="gray"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<IconDotsVertical
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={2}
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
leftSection={<IconLink style={{ width: rem(14), height: rem(14) }} />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleCopyLink();
|
||||
}}
|
||||
>
|
||||
Copy link
|
||||
</Menu.Item>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
leftSection={<IconLink size={16} />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleCopyLink();
|
||||
}}
|
||||
>
|
||||
Copy link
|
||||
</Menu.Item>
|
||||
|
||||
{!(treeApi.props.disableEdit as boolean) && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
leftSection={<IconFileExport size={16} />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openExportModal();
|
||||
}}
|
||||
>
|
||||
Export page
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
c="red"
|
||||
leftSection={
|
||||
<IconTrash style={{ width: rem(14), height: rem(14) }} />
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openDeleteModal({ onConfirm: () => treeApi?.delete(node) });
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
{!(treeApi.props.disableEdit as boolean) && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item
|
||||
c="red"
|
||||
leftSection={
|
||||
<IconTrash size={16} />
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openDeleteModal({ onConfirm: () => treeApi?.delete(node) });
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</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 {
|
||||
pageId: string;
|
||||
format: ExportFormat;
|
||||
includeChildren?: boolean;
|
||||
}
|
||||
|
||||
export enum ExportFormat {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -26,14 +26,18 @@ api.interceptors.request.use(
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
// we need the response headers
|
||||
if (response.request.responseURL.includes("/api/pages/export")) {
|
||||
return response;
|
||||
// we need the response headers for these endpoints
|
||||
const exemptEndpoints = ["/api/pages/export", "/api/spaces/export"];
|
||||
if (response.request.responseURL) {
|
||||
const path = new URL(response.request.responseURL)?.pathname;
|
||||
if (path && exemptEndpoints.includes(path)) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
return response.data;
|
||||
@ -72,7 +76,7 @@ api.interceptors.response.use(
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function redirectToLogin() {
|
||||
|
||||
Reference in New Issue
Block a user