mirror of
https://github.com/docmost/docmost.git
synced 2025-11-16 14:21:08 +10:00
bug fixes and UI
This commit is contained in:
20
apps/client/src/components/icons/confluence-icon.tsx
Normal file
20
apps/client/src/components/icons/confluence-icon.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { rem } from "@mantine/core";
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function ConfluenceIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path d="M.87 18.257c-.248.382-.53.875-.763 1.245a.764.764 0 0 0 .255 1.04l4.965 3.054a.764.764 0 0 0 1.058-.26c.199-.332.454-.763.733-1.221 1.967-3.247 3.945-2.853 7.508-1.146l4.957 2.337a.764.764 0 0 0 1.028-.382l2.364-5.346a.764.764 0 0 0-.382-1 599.851 599.851 0 0 1-4.965-2.361C10.911 10.97 5.224 11.185.87 18.257zM23.131 5.743c.249-.405.531-.875.764-1.25a.764.764 0 0 0-.256-1.034L18.675.404a.764.764 0 0 0-1.058.26c-.195.335-.451.763-.734 1.225-1.966 3.246-3.945 2.85-7.508 1.146L4.437.694a.764.764 0 0 0-1.027.382L1.046 6.422a.764.764 0 0 0 .382 1c1.039.49 3.105 1.467 4.965 2.361 6.698 3.246 12.392 3.029 16.738-4.04z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import api from "@/lib/api-client";
|
||||
import { IFileTask } from "@/features/file-task/types/file-task.types.ts";
|
||||
|
||||
export async function getFileTaskById(fileTaskId: string): Promise<IFileTask> {
|
||||
const req = await api.post<IFileTask>("/file-tasks/info", {
|
||||
fileTaskId: fileTaskId,
|
||||
});
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function getFileTasks(): Promise<IFileTask[]> {
|
||||
const req = await api.post<IFileTask[]>("/file-tasks");
|
||||
return req.data;
|
||||
}
|
||||
17
apps/client/src/features/file-task/types/file-task.types.ts
Normal file
17
apps/client/src/features/file-task/types/file-task.types.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export interface IFileTask {
|
||||
id: string;
|
||||
type: "import" | "export";
|
||||
source: string;
|
||||
status: string;
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
fileSize: number;
|
||||
fileExt: string;
|
||||
errorMessage: string | null;
|
||||
creatorId: string;
|
||||
spaceId: string;
|
||||
workspaceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
}
|
||||
@ -1,18 +1,36 @@
|
||||
import { Modal, Button, SimpleGrid, FileButton } from "@mantine/core";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
SimpleGrid,
|
||||
FileButton,
|
||||
Group,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconBrandNotion,
|
||||
IconCheck,
|
||||
IconFileCode,
|
||||
IconFileTypeZip,
|
||||
IconMarkdown,
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
import { importPage } from "@/features/page/services/page-service.ts";
|
||||
import {
|
||||
importPage,
|
||||
importZip,
|
||||
} from "@/features/page/services/page-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom.ts";
|
||||
import { useAtom } from "jotai";
|
||||
import { buildTree } from "@/features/page/tree/utils";
|
||||
import { IPage } from "@/features/page/types/page.types.ts";
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConfluenceIcon } from "@/components/icons/confluence-icon.tsx";
|
||||
import { getFileImportSizeLimit, isCloud } from "@/lib/config.ts";
|
||||
import { formatBytes } from "@/lib";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { getFileTaskById } from "@/features/file-task/services/file-task-service.ts";
|
||||
|
||||
interface PageImportModalProps {
|
||||
spaceId: string;
|
||||
@ -59,6 +77,113 @@ interface ImportFormatSelection {
|
||||
function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
||||
const { t } = useTranslation();
|
||||
const [treeData, setTreeData] = useAtom(treeDataAtom);
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
const [fileTaskId, setFileTaskId] = useState<string | null>(null);
|
||||
|
||||
const canUseConfluence = isCloud() || workspace?.hasLicenseKey;
|
||||
|
||||
const handleZipUpload = async (selectedFile: File, source: string) => {
|
||||
if (!selectedFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
|
||||
try {
|
||||
const importTask = await importZip(selectedFile, spaceId, source);
|
||||
notifications.show({
|
||||
id: "import",
|
||||
title: t("Importing pages"),
|
||||
message: t(
|
||||
"Page import is in progress. Refresh this tab after a while.",
|
||||
),
|
||||
loading: true,
|
||||
withCloseButton: false,
|
||||
autoClose: false,
|
||||
});
|
||||
|
||||
setFileTaskId(importTask.id);
|
||||
console.log("taskId set", importTask.id);
|
||||
} catch (err) {
|
||||
console.log("Failed to import page", err);
|
||||
notifications.update({
|
||||
id: "import",
|
||||
color: "red",
|
||||
title: t("Failed to import pages"),
|
||||
message: t("Unable to import pages. Please try again."),
|
||||
icon: <IconX size={18} />,
|
||||
loading: false,
|
||||
withCloseButton: true,
|
||||
autoClose: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!fileTaskId) return;
|
||||
|
||||
const intervalId = setInterval(async () => {
|
||||
try {
|
||||
const fileTask = await getFileTaskById(fileTaskId);
|
||||
const status = fileTask.status;
|
||||
|
||||
if (status === "success") {
|
||||
notifications.update({
|
||||
id: "import",
|
||||
color: "teal",
|
||||
title: t("Import complete"),
|
||||
message: t("Your pages were successfully imported."),
|
||||
icon: <IconCheck size={18} />,
|
||||
loading: false,
|
||||
withCloseButton: true,
|
||||
autoClose: 5000,
|
||||
});
|
||||
clearInterval(intervalId);
|
||||
setFileTaskId(null);
|
||||
}
|
||||
|
||||
if (status === "failed") {
|
||||
notifications.update({
|
||||
id: "import",
|
||||
color: "red",
|
||||
title: t("Page import failed"),
|
||||
message: t(
|
||||
"Something went wrong while importing pages: {{reason}}.",
|
||||
{
|
||||
reason: fileTask.errorMessage,
|
||||
},
|
||||
),
|
||||
icon: <IconX size={18} />,
|
||||
loading: false,
|
||||
withCloseButton: true,
|
||||
autoClose: 5000,
|
||||
});
|
||||
clearInterval(intervalId);
|
||||
setFileTaskId(null);
|
||||
console.error(fileTask.errorMessage);
|
||||
}
|
||||
} catch (err) {
|
||||
notifications.update({
|
||||
id: "import",
|
||||
color: "red",
|
||||
title: t("Import failed"),
|
||||
message: t(
|
||||
"Something went wrong while importing pages: {{reason}}.",
|
||||
{
|
||||
reason: err.response?.data.message,
|
||||
},
|
||||
),
|
||||
icon: <IconX size={18} />,
|
||||
loading: false,
|
||||
withCloseButton: true,
|
||||
autoClose: 5000,
|
||||
});
|
||||
clearInterval(intervalId);
|
||||
setFileTaskId(null);
|
||||
console.error("Failed to fetch import status", err);
|
||||
}
|
||||
}, 3000);
|
||||
}, [fileTaskId]);
|
||||
|
||||
const handleFileUpload = async (selectedFiles: File[]) => {
|
||||
if (!selectedFiles) {
|
||||
@ -120,6 +245,7 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
return (
|
||||
<>
|
||||
<SimpleGrid cols={2}>
|
||||
@ -148,7 +274,76 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
||||
</Button>
|
||||
)}
|
||||
</FileButton>
|
||||
|
||||
<FileButton
|
||||
onChange={(file) => handleZipUpload(file, "notion")}
|
||||
accept="application/zip"
|
||||
>
|
||||
{(props) => (
|
||||
<Button
|
||||
justify="start"
|
||||
variant="default"
|
||||
leftSection={<IconBrandNotion size={18} />}
|
||||
{...props}
|
||||
>
|
||||
Notion
|
||||
</Button>
|
||||
)}
|
||||
</FileButton>
|
||||
<FileButton
|
||||
onChange={(file) => handleZipUpload(file, "confluence")}
|
||||
accept="application/zip"
|
||||
>
|
||||
{(props) => (
|
||||
<Tooltip
|
||||
label="Available in enterprise edition"
|
||||
disabled={canUseConfluence}
|
||||
>
|
||||
<Button
|
||||
disabled={!canUseConfluence}
|
||||
justify="start"
|
||||
variant="default"
|
||||
leftSection={<ConfluenceIcon size={18} />}
|
||||
{...props}
|
||||
>
|
||||
Confluence
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</FileButton>
|
||||
</SimpleGrid>
|
||||
|
||||
<Group justify="center" gap="xl" mih={150}>
|
||||
<div>
|
||||
<Text ta="center" size="lg" inline>
|
||||
Import zip file
|
||||
</Text>
|
||||
<Text ta="center" size="sm" c="dimmed" inline py="sm">
|
||||
{t(
|
||||
`Upload zip file containing Markdown and HTML files. Max: {{sizeLimit}}`,
|
||||
{
|
||||
sizeLimit: formatBytes(getFileImportSizeLimit()),
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
<FileButton
|
||||
onChange={(file) => handleZipUpload(file, "generic")}
|
||||
accept="application/zip"
|
||||
>
|
||||
{(props) => (
|
||||
<Group justify="center">
|
||||
<Button
|
||||
justify="center"
|
||||
leftSection={<IconFileTypeZip size={18} />}
|
||||
{...props}
|
||||
>
|
||||
{t("Upload file")}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</FileButton>
|
||||
</div>
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,9 +7,10 @@ import {
|
||||
IPage,
|
||||
IPageInput,
|
||||
SidebarPagesParams,
|
||||
} from "@/features/page/types/page.types";
|
||||
} from '@/features/page/types/page.types';
|
||||
import { IAttachment, IPagination } from "@/lib/types.ts";
|
||||
import { saveAs } from "file-saver";
|
||||
import { IFileTask } from '@/features/file-task/types/file-task.types.ts';
|
||||
|
||||
export async function createPage(data: Partial<IPage>): Promise<IPage> {
|
||||
const req = await api.post<IPage>("/pages/create", data);
|
||||
@ -92,6 +93,25 @@ export async function importPage(file: File, spaceId: string) {
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function importZip(
|
||||
file: File,
|
||||
spaceId: string,
|
||||
source?: string,
|
||||
): Promise<IFileTask> {
|
||||
const formData = new FormData();
|
||||
formData.append("spaceId", spaceId);
|
||||
formData.append("source", source);
|
||||
formData.append("file", file);
|
||||
|
||||
const req = await api.post<any>("/pages/import-zip", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function uploadFile(
|
||||
file: File,
|
||||
pageId: string,
|
||||
|
||||
@ -70,6 +70,11 @@ export function getFileUploadSizeLimit() {
|
||||
return bytes(limit);
|
||||
}
|
||||
|
||||
export function getFileImportSizeLimit() {
|
||||
const limit = getConfigValue("FILE_IMPORT_SIZE_LIMIT", "200mb");
|
||||
return bytes(limit);
|
||||
}
|
||||
|
||||
export function getDrawioUrl() {
|
||||
return getConfigValue("DRAWIO_URL", "https://embed.diagrams.net");
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ export default defineConfig(({ mode }) => {
|
||||
const {
|
||||
APP_URL,
|
||||
FILE_UPLOAD_SIZE_LIMIT,
|
||||
FILE_IMPORT_SIZE_LIMIT,
|
||||
DRAWIO_URL,
|
||||
CLOUD,
|
||||
SUBDOMAIN_HOST,
|
||||
@ -20,6 +21,7 @@ export default defineConfig(({ mode }) => {
|
||||
"process.env": {
|
||||
APP_URL,
|
||||
FILE_UPLOAD_SIZE_LIMIT,
|
||||
FILE_IMPORT_SIZE_LIMIT,
|
||||
DRAWIO_URL,
|
||||
CLOUD,
|
||||
SUBDOMAIN_HOST,
|
||||
|
||||
Reference in New Issue
Block a user