mirror of
https://github.com/docmost/docmost.git
synced 2026-05-15 05:04:06 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b4a02e94a | |||
| 3c4cab0d2a | |||
| 4de25a8b94 | |||
| cf5bbb10df | |||
| ac17521717 | |||
| 9ac180f719 | |||
| 46669fea56 | |||
| fe6ecdf1f1 | |||
| 04ae1d7270 | |||
| 1280f96f37 | |||
| 61d1cf88a7 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.23.1",
|
"version": "0.23.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
|
|||||||
@@ -527,5 +527,11 @@
|
|||||||
"Delete SSO provider": "Delete SSO provider",
|
"Delete SSO provider": "Delete SSO provider",
|
||||||
"Are you sure you want to delete this SSO provider?": "Are you sure you want to delete this SSO provider?",
|
"Are you sure you want to delete this SSO provider?": "Are you sure you want to delete this SSO provider?",
|
||||||
"Action": "Action",
|
"Action": "Action",
|
||||||
"{{ssoProviderType}} configuration": "{{ssoProviderType}} configuration"
|
"{{ssoProviderType}} configuration": "{{ssoProviderType}} configuration",
|
||||||
|
"Icon": "Icon",
|
||||||
|
"Upload image": "Upload image",
|
||||||
|
"Remove image": "Remove image",
|
||||||
|
"Failed to remove image": "Failed to remove image",
|
||||||
|
"Image exceeds 10MB limit.": "Image exceeds 10MB limit.",
|
||||||
|
"Image removed successfully": "Image removed successfully"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import React, { useRef } from "react";
|
||||||
|
import { Menu, Box, Loader } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { IconTrash, IconUpload } from "@tabler/icons-react";
|
||||||
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
|
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
|
interface AvatarUploaderProps {
|
||||||
|
currentImageUrl?: string | null;
|
||||||
|
fallbackName?: string;
|
||||||
|
radius?: string | number;
|
||||||
|
size?: string | number;
|
||||||
|
variant?: string;
|
||||||
|
type: AvatarIconType;
|
||||||
|
onUpload: (file: File) => Promise<void>;
|
||||||
|
onRemove: () => Promise<void>;
|
||||||
|
isLoading?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AvatarUploader({
|
||||||
|
currentImageUrl,
|
||||||
|
fallbackName,
|
||||||
|
radius,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
type,
|
||||||
|
onUpload,
|
||||||
|
onRemove,
|
||||||
|
isLoading = false,
|
||||||
|
disabled = false,
|
||||||
|
}: AvatarUploaderProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleFileInputChange = async (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file || disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file size (max 10MB)
|
||||||
|
const maxSizeInBytes = 10 * 1024 * 1024;
|
||||||
|
if (file.size > maxSizeInBytes) {
|
||||||
|
notifications.show({
|
||||||
|
message: t("Image exceeds 10MB limit."),
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
// Reset the input
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onUpload(file);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
notifications.show({
|
||||||
|
message: t("Failed to upload image"),
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the input so the same file can be selected again
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUploadClick = () => {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.click();
|
||||||
|
} else {
|
||||||
|
console.error("File input ref is null!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = async () => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onRemove();
|
||||||
|
notifications.show({
|
||||||
|
message: t("Image removed successfully"),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
notifications.show({
|
||||||
|
message: t("Failed to remove image"),
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileInputChange}
|
||||||
|
accept="image/png,image/jpeg,image/jpg"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Menu shadow="md" width={200} withArrow disabled={disabled || isLoading}>
|
||||||
|
<Menu.Target>
|
||||||
|
<Box style={{ position: "relative", display: "inline-block" }}>
|
||||||
|
<CustomAvatar
|
||||||
|
component="button"
|
||||||
|
size={size}
|
||||||
|
avatarUrl={currentImageUrl}
|
||||||
|
name={fallbackName}
|
||||||
|
style={{
|
||||||
|
cursor: disabled || isLoading ? "default" : "pointer",
|
||||||
|
opacity: isLoading ? 0.6 : 1,
|
||||||
|
}}
|
||||||
|
radius={radius}
|
||||||
|
variant={variant}
|
||||||
|
type={type}
|
||||||
|
/>
|
||||||
|
{isLoading && (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
zIndex: 1000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Loader size="sm" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Menu.Target>
|
||||||
|
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<IconUpload size={16} />}
|
||||||
|
disabled={isLoading || disabled}
|
||||||
|
onClick={handleUploadClick}
|
||||||
|
>
|
||||||
|
{t("Upload image")}
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
|
{currentImageUrl && (
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<IconTrash size={16} />}
|
||||||
|
color="red"
|
||||||
|
onClick={handleRemove}
|
||||||
|
disabled={isLoading || disabled}
|
||||||
|
>
|
||||||
|
{t("Remove image")}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
Group,
|
Group,
|
||||||
Menu,
|
Menu,
|
||||||
UnstyledButton,
|
|
||||||
Text,
|
Text,
|
||||||
|
UnstyledButton,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
IconBrush,
|
IconBrush,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
IconChevronRight,
|
|
||||||
IconDeviceDesktop,
|
IconDeviceDesktop,
|
||||||
IconLogout,
|
IconLogout,
|
||||||
IconMoon,
|
IconMoon,
|
||||||
@@ -26,6 +25,7 @@ import APP_ROUTE from "@/lib/app-route.ts";
|
|||||||
import useAuth from "@/features/auth/hooks/use-auth.ts";
|
import useAuth from "@/features/auth/hooks/use-auth.ts";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||||
|
|
||||||
export default function TopMenu() {
|
export default function TopMenu() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -50,6 +50,7 @@ export default function TopMenu() {
|
|||||||
name={workspace?.name}
|
name={workspace?.name}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
type={AvatarIconType.WORKSPACE_ICON}
|
||||||
/>
|
/>
|
||||||
<Text fw={500} size="sm" lh={1} mr={3} lineClamp={1}>
|
<Text fw={500} size="sm" lh={1} mr={3} lineClamp={1}>
|
||||||
{workspace?.name}
|
{workspace?.name}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Avatar } from "@mantine/core";
|
import { Avatar } from "@mantine/core";
|
||||||
import { getAvatarUrl } from "@/lib/config.ts";
|
import { getAvatarUrl } from "@/lib/config.ts";
|
||||||
|
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||||
|
|
||||||
interface CustomAvatarProps {
|
interface CustomAvatarProps {
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
@@ -11,13 +12,15 @@ interface CustomAvatarProps {
|
|||||||
variant?: string;
|
variant?: string;
|
||||||
style?: any;
|
style?: any;
|
||||||
component?: any;
|
component?: any;
|
||||||
|
type?: AvatarIconType;
|
||||||
|
mt?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CustomAvatar = React.forwardRef<
|
export const CustomAvatar = React.forwardRef<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
CustomAvatarProps
|
CustomAvatarProps
|
||||||
>(({ avatarUrl, name, ...props }: CustomAvatarProps, ref) => {
|
>(({ avatarUrl, name, type, ...props }: CustomAvatarProps, ref) => {
|
||||||
const avatarLink = getAvatarUrl(avatarUrl);
|
const avatarLink = getAvatarUrl(avatarUrl, type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import api from "@/lib/api-client";
|
||||||
|
import {
|
||||||
|
AvatarIconType,
|
||||||
|
IAttachment,
|
||||||
|
} from "@/features/attachments/types/attachment.types.ts";
|
||||||
|
|
||||||
|
export async function uploadIcon(
|
||||||
|
file: File,
|
||||||
|
type: AvatarIconType,
|
||||||
|
spaceId?: string,
|
||||||
|
): Promise<IAttachment> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("type", type);
|
||||||
|
if (spaceId) {
|
||||||
|
formData.append("spaceId", spaceId);
|
||||||
|
}
|
||||||
|
formData.append("image", file);
|
||||||
|
|
||||||
|
return await api.post("/attachments/upload-image", formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadUserAvatar(file: File): Promise<IAttachment> {
|
||||||
|
return uploadIcon(file, AvatarIconType.AVATAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadSpaceIcon(
|
||||||
|
file: File,
|
||||||
|
spaceId: string,
|
||||||
|
): Promise<IAttachment> {
|
||||||
|
return uploadIcon(file, AvatarIconType.SPACE_ICON, spaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadWorkspaceIcon(file: File): Promise<IAttachment> {
|
||||||
|
return uploadIcon(file, AvatarIconType.WORKSPACE_ICON);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeIcon(
|
||||||
|
type: AvatarIconType,
|
||||||
|
spaceId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const payload: { spaceId?: string; type: string } = { type };
|
||||||
|
|
||||||
|
if (spaceId) {
|
||||||
|
payload.spaceId = spaceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.post("/attachments/remove-icon", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeAvatar(): Promise<void> {
|
||||||
|
await removeIcon(AvatarIconType.AVATAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeSpaceIcon(spaceId: string): Promise<void> {
|
||||||
|
await removeIcon(AvatarIconType.SPACE_ICON, spaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeWorkspaceIcon(): Promise<void> {
|
||||||
|
await removeIcon(AvatarIconType.WORKSPACE_ICON);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export {
|
||||||
|
uploadIcon,
|
||||||
|
uploadUserAvatar,
|
||||||
|
uploadSpaceIcon,
|
||||||
|
uploadWorkspaceIcon,
|
||||||
|
removeAvatar,
|
||||||
|
removeSpaceIcon,
|
||||||
|
removeWorkspaceIcon,
|
||||||
|
} from "./attachment-service.ts";
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export interface IAttachment {
|
||||||
|
id: string;
|
||||||
|
fileName: string;
|
||||||
|
filePath: string;
|
||||||
|
fileSize: number;
|
||||||
|
fileExt: string;
|
||||||
|
mimeType: string;
|
||||||
|
type: string;
|
||||||
|
creatorId: string;
|
||||||
|
pageId: string | null;
|
||||||
|
spaceId: string | null;
|
||||||
|
workspaceId: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AvatarIconType {
|
||||||
|
AVATAR = "avatar",
|
||||||
|
SPACE_ICON = "space-icon",
|
||||||
|
WORKSPACE_ICON = "workspace-icon",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AttachmentType {
|
||||||
|
AVATAR = "avatar",
|
||||||
|
WORKSPACE_ICON = "workspace-icon",
|
||||||
|
SPACE_ICON = "space-icon",
|
||||||
|
FILE = "file",
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
EventExit,
|
EventExit,
|
||||||
EventSave,
|
EventSave,
|
||||||
} from "react-drawio";
|
} from "react-drawio";
|
||||||
import { IAttachment } from "@/lib/types";
|
import { IAttachment } from "@/features/attachments/types/attachment.types";
|
||||||
import { decodeBase64ToSvgString, svgStringToFile } from "@/lib/utils";
|
import { decodeBase64ToSvgString, svgStringToFile } from "@/lib/utils";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { IconEdit } from "@tabler/icons-react";
|
import { IconEdit } from "@tabler/icons-react";
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { useDisclosure } from "@mantine/hooks";
|
|||||||
import { getFileUrl } from "@/lib/config.ts";
|
import { getFileUrl } from "@/lib/config.ts";
|
||||||
import "@excalidraw/excalidraw/index.css";
|
import "@excalidraw/excalidraw/index.css";
|
||||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||||
import { IAttachment } from "@/lib/types";
|
import { IAttachment } from "@/features/attachments/types/attachment.types";
|
||||||
import ReactClearModal from "react-clear-modal";
|
import ReactClearModal from "react-clear-modal";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { IconEdit } from "@tabler/icons-react";
|
import { IconEdit } from "@tabler/icons-react";
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export const mainExtensions = [
|
|||||||
}),
|
}),
|
||||||
CustomTable.configure({
|
CustomTable.configure({
|
||||||
resizable: true,
|
resizable: true,
|
||||||
lastColumnResizable: false,
|
lastColumnResizable: true,
|
||||||
allowTableNodeSelection: true,
|
allowTableNodeSelection: true,
|
||||||
}),
|
}),
|
||||||
TableRow,
|
TableRow,
|
||||||
|
|||||||
@@ -94,7 +94,12 @@
|
|||||||
|
|
||||||
hr {
|
hr {
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid #ced4da;
|
@mixin light {
|
||||||
|
border-top: 1px solid var(--mantine-color-gray-4);
|
||||||
|
}
|
||||||
|
@mixin dark {
|
||||||
|
border-top: 1px solid var(--mantine-color-dark-4);
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom.ts";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { buildTree } from "@/features/page/tree/utils";
|
import { buildTree } from "@/features/page/tree/utils";
|
||||||
import { IPage } from "@/features/page/types/page.types.ts";
|
import { IPage } from "@/features/page/types/page.types.ts";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ConfluenceIcon } from "@/components/icons/confluence-icon.tsx";
|
import { ConfluenceIcon } from "@/components/icons/confluence-icon.tsx";
|
||||||
import { getFileImportSizeLimit, isCloud } from "@/lib/config.ts";
|
import { getFileImportSizeLimit, isCloud } from "@/lib/config.ts";
|
||||||
@@ -84,6 +84,12 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
const [fileTaskId, setFileTaskId] = useState<string | null>(null);
|
const [fileTaskId, setFileTaskId] = useState<string | null>(null);
|
||||||
const emit = useQueryEmit();
|
const emit = useQueryEmit();
|
||||||
|
|
||||||
|
const markdownFileRef = useRef<() => void>(null);
|
||||||
|
const htmlFileRef = useRef<() => void>(null);
|
||||||
|
const notionFileRef = useRef<() => void>(null);
|
||||||
|
const confluenceFileRef = useRef<() => void>(null);
|
||||||
|
const zipFileRef = useRef<() => void>(null);
|
||||||
|
|
||||||
const canUseConfluence = isCloud() || workspace?.hasLicenseKey;
|
const canUseConfluence = isCloud() || workspace?.hasLicenseKey;
|
||||||
|
|
||||||
const handleZipUpload = async (selectedFile: File, source: string) => {
|
const handleZipUpload = async (selectedFile: File, source: string) => {
|
||||||
@@ -116,6 +122,15 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setFileTaskId(importTask.id);
|
setFileTaskId(importTask.id);
|
||||||
|
|
||||||
|
// Reset file input after successful upload
|
||||||
|
if (source === "notion" && notionFileRef.current) {
|
||||||
|
notionFileRef.current();
|
||||||
|
} else if (source === "confluence" && confluenceFileRef.current) {
|
||||||
|
confluenceFileRef.current();
|
||||||
|
} else if (source === "generic" && zipFileRef.current) {
|
||||||
|
zipFileRef.current();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Failed to upload import file", err);
|
console.log("Failed to upload import file", err);
|
||||||
notifications.update({
|
notifications.update({
|
||||||
@@ -243,6 +258,10 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
setTreeData(fullTree);
|
setTreeData(fullTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset file inputs after successful upload
|
||||||
|
if (markdownFileRef.current) markdownFileRef.current();
|
||||||
|
if (htmlFileRef.current) htmlFileRef.current();
|
||||||
|
|
||||||
const pageCountText =
|
const pageCountText =
|
||||||
pageCount === 1 ? `1 ${t("page")}` : `${pageCount} ${t("pages")}`;
|
pageCount === 1 ? `1 ${t("page")}` : `${pageCount} ${t("pages")}`;
|
||||||
|
|
||||||
@@ -272,7 +291,7 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SimpleGrid cols={2}>
|
<SimpleGrid cols={2}>
|
||||||
<FileButton onChange={handleFileUpload} accept=".md" multiple>
|
<FileButton onChange={handleFileUpload} accept=".md" multiple resetRef={markdownFileRef}>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Button
|
<Button
|
||||||
justify="start"
|
justify="start"
|
||||||
@@ -285,7 +304,7 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
)}
|
)}
|
||||||
</FileButton>
|
</FileButton>
|
||||||
|
|
||||||
<FileButton onChange={handleFileUpload} accept="text/html" multiple>
|
<FileButton onChange={handleFileUpload} accept="text/html" multiple resetRef={htmlFileRef}>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Button
|
<Button
|
||||||
justify="start"
|
justify="start"
|
||||||
@@ -301,6 +320,7 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
<FileButton
|
<FileButton
|
||||||
onChange={(file) => handleZipUpload(file, "notion")}
|
onChange={(file) => handleZipUpload(file, "notion")}
|
||||||
accept="application/zip"
|
accept="application/zip"
|
||||||
|
resetRef={notionFileRef}
|
||||||
>
|
>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -316,6 +336,7 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
<FileButton
|
<FileButton
|
||||||
onChange={(file) => handleZipUpload(file, "confluence")}
|
onChange={(file) => handleZipUpload(file, "confluence")}
|
||||||
accept="application/zip"
|
accept="application/zip"
|
||||||
|
resetRef={confluenceFileRef}
|
||||||
>
|
>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -352,6 +373,7 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
|||||||
<FileButton
|
<FileButton
|
||||||
onChange={(file) => handleZipUpload(file, "generic")}
|
onChange={(file) => handleZipUpload(file, "generic")}
|
||||||
accept="application/zip"
|
accept="application/zip"
|
||||||
|
resetRef={zipFileRef}
|
||||||
>
|
>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Group justify="center">
|
<Group justify="center">
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import {
|
|||||||
SidebarPagesParams,
|
SidebarPagesParams,
|
||||||
} from '@/features/page/types/page.types';
|
} from '@/features/page/types/page.types';
|
||||||
import { QueryParams } from "@/lib/types";
|
import { QueryParams } from "@/lib/types";
|
||||||
import { IAttachment, IPagination } from "@/lib/types.ts";
|
import { IPagination } from "@/lib/types.ts";
|
||||||
import { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
import { InfiniteData } from "@tanstack/react-query";
|
import { InfiniteData } from "@tanstack/react-query";
|
||||||
import { IFileTask } from '@/features/file-task/types/file-task.types.ts';
|
import { IFileTask } from '@/features/file-task/types/file-task.types.ts';
|
||||||
|
import { IAttachment } from '@/features/attachments/types/attachment.types.ts';
|
||||||
|
|
||||||
export async function createPage(data: Partial<IPage>): Promise<IPage> {
|
export async function createPage(data: Partial<IPage>): Promise<IPage> {
|
||||||
const req = await api.post<IPage>("/pages/create", data);
|
const req = await api.post<IPage>("/pages/create", data);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconExternalLink, IconWorld } from "@tabler/icons-react";
|
import { IconExternalLink, IconWorld, IconLock } from "@tabler/icons-react";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
useCreateShareMutation,
|
useCreateShareMutation,
|
||||||
@@ -18,23 +18,27 @@ import {
|
|||||||
useShareForPageQuery,
|
useShareForPageQuery,
|
||||||
useUpdateShareMutation,
|
useUpdateShareMutation,
|
||||||
} from "@/features/share/queries/share-query.ts";
|
} from "@/features/share/queries/share-query.ts";
|
||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
import { extractPageSlugId, getPageIcon } from "@/lib";
|
import { extractPageSlugId, getPageIcon } from "@/lib";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CopyTextButton from "@/components/common/copy.tsx";
|
import CopyTextButton from "@/components/common/copy.tsx";
|
||||||
import { getAppUrl } from "@/lib/config.ts";
|
import { getAppUrl, isCloud } from "@/lib/config.ts";
|
||||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||||
import classes from "@/features/share/components/share.module.css";
|
import classes from "@/features/share/components/share.module.css";
|
||||||
|
import useTrial from "@/ee/hooks/use-trial.tsx";
|
||||||
|
import { getCheckoutLink } from "@/ee/billing/services/billing-service.ts";
|
||||||
|
|
||||||
interface ShareModalProps {
|
interface ShareModalProps {
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
}
|
}
|
||||||
export default function ShareModal({ readOnly }: ShareModalProps) {
|
export default function ShareModal({ readOnly }: ShareModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { pageSlug } = useParams();
|
const { pageSlug } = useParams();
|
||||||
const pageId = extractPageSlugId(pageSlug);
|
const pageId = extractPageSlugId(pageSlug);
|
||||||
const { data: share } = useShareForPageQuery(pageId);
|
const { data: share } = useShareForPageQuery(pageId);
|
||||||
const { spaceSlug } = useParams();
|
const { spaceSlug } = useParams();
|
||||||
|
const { isTrial } = useTrial();
|
||||||
const createShareMutation = useCreateShareMutation();
|
const createShareMutation = useCreateShareMutation();
|
||||||
const updateShareMutation = useUpdateShareMutation();
|
const updateShareMutation = useUpdateShareMutation();
|
||||||
const deleteShareMutation = useDeleteShareMutation();
|
const deleteShareMutation = useDeleteShareMutation();
|
||||||
@@ -61,7 +65,7 @@ export default function ShareModal({ readOnly }: ShareModalProps) {
|
|||||||
createShareMutation.mutateAsync({
|
createShareMutation.mutateAsync({
|
||||||
pageId: pageId,
|
pageId: pageId,
|
||||||
includeSubPages: true,
|
includeSubPages: true,
|
||||||
searchIndexing: true,
|
searchIndexing: false,
|
||||||
});
|
});
|
||||||
setIsPagePublic(value);
|
setIsPagePublic(value);
|
||||||
} else {
|
} else {
|
||||||
@@ -92,26 +96,29 @@ export default function ShareModal({ readOnly }: ShareModalProps) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const shareLink = useMemo(() => (
|
const shareLink = useMemo(
|
||||||
<Group my="sm" gap={4} wrap="nowrap">
|
() => (
|
||||||
<TextInput
|
<Group my="sm" gap={4} wrap="nowrap">
|
||||||
variant="filled"
|
<TextInput
|
||||||
value={publicLink}
|
variant="filled"
|
||||||
readOnly
|
value={publicLink}
|
||||||
rightSection={<CopyTextButton text={publicLink} />}
|
readOnly
|
||||||
style={{ width: "100%" }}
|
rightSection={<CopyTextButton text={publicLink} />}
|
||||||
/>
|
style={{ width: "100%" }}
|
||||||
<ActionIcon
|
/>
|
||||||
component="a"
|
<ActionIcon
|
||||||
variant="default"
|
component="a"
|
||||||
target="_blank"
|
variant="default"
|
||||||
href={publicLink}
|
target="_blank"
|
||||||
size="sm"
|
href={publicLink}
|
||||||
>
|
size="sm"
|
||||||
<IconExternalLink size={16} />
|
>
|
||||||
</ActionIcon>
|
<IconExternalLink size={16} />
|
||||||
</Group>
|
</ActionIcon>
|
||||||
), [publicLink]);
|
</Group>
|
||||||
|
),
|
||||||
|
[publicLink],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover width={350} position="bottom" withArrow shadow="md">
|
<Popover width={350} position="bottom" withArrow shadow="md">
|
||||||
@@ -135,7 +142,28 @@ export default function ShareModal({ readOnly }: ShareModalProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown style={{ userSelect: "none" }}>
|
<Popover.Dropdown style={{ userSelect: "none" }}>
|
||||||
{isDescendantShared ? (
|
{isCloud() && isTrial ? (
|
||||||
|
<>
|
||||||
|
<Group justify="center" mb="sm">
|
||||||
|
<IconLock size={20} stroke={1.5} />
|
||||||
|
</Group>
|
||||||
|
<Text size="sm" ta="center" fw={500} mb="xs">
|
||||||
|
{t("Upgrade to share pages")}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" ta="center" mb="sm">
|
||||||
|
{t(
|
||||||
|
"Page sharing is available on paid plans. Upgrade to share your pages publicly.",
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
onClick={() => navigate("/settings/billing")}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{t("Upgrade Plan")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : isDescendantShared ? (
|
||||||
<>
|
<>
|
||||||
<Text size="sm">{t("Inherits public sharing from")}</Text>
|
<Text size="sm">{t("Inherits public sharing from")}</Text>
|
||||||
<Anchor
|
<Anchor
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {Modal, Tabs, rem, Group, ScrollArea, Text} from "@mantine/core";
|
import { Modal, Tabs, rem, Group, ScrollArea, Text } from "@mantine/core";
|
||||||
import SpaceMembersList from "@/features/space/components/space-members.tsx";
|
import SpaceMembersList from "@/features/space/components/space-members.tsx";
|
||||||
import AddSpaceMembersModal from "@/features/space/components/add-space-members-modal.tsx";
|
import AddSpaceMembersModal from "@/features/space/components/add-space-members-modal.tsx";
|
||||||
import React, {useMemo} from "react";
|
import React from "react";
|
||||||
import SpaceDetails from "@/features/space/components/space-details.tsx";
|
import SpaceDetails from "@/features/space/components/space-details.tsx";
|
||||||
import {useSpaceQuery} from "@/features/space/queries/space-query.ts";
|
import { useSpaceQuery } from "@/features/space/queries/space-query.ts";
|
||||||
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,
|
||||||
@@ -39,16 +39,18 @@ export default function SpaceSettingsModal({
|
|||||||
xOffset={0}
|
xOffset={0}
|
||||||
mah={400}
|
mah={400}
|
||||||
>
|
>
|
||||||
<Modal.Overlay/>
|
<Modal.Overlay />
|
||||||
<Modal.Content style={{overflow: "hidden"}}>
|
<Modal.Content style={{ overflow: "hidden" }}>
|
||||||
<Modal.Header py={0}>
|
<Modal.Header py={0}>
|
||||||
<Modal.Title>
|
<Modal.Title>
|
||||||
<Text fw={500} lineClamp={1}>{space?.name}</Text>
|
<Text fw={500} lineClamp={1}>
|
||||||
|
{space?.name}
|
||||||
|
</Text>
|
||||||
</Modal.Title>
|
</Modal.Title>
|
||||||
<Modal.CloseButton/>
|
<Modal.CloseButton />
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<div style={{height: rem(600)}}>
|
<div style={{ height: rem(600) }}>
|
||||||
<Tabs defaultValue="members">
|
<Tabs defaultValue="members">
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Tab fw={500} value="general">
|
<Tabs.Tab fw={500} value="general">
|
||||||
@@ -60,13 +62,15 @@ export default function SpaceSettingsModal({
|
|||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|
||||||
<Tabs.Panel value="general">
|
<Tabs.Panel value="general">
|
||||||
<SpaceDetails
|
<ScrollArea h={550} scrollbarSize={4} pr={8}>
|
||||||
spaceId={space?.id}
|
<SpaceDetails
|
||||||
readOnly={spaceAbility.cannot(
|
spaceId={space?.id}
|
||||||
SpaceCaslAction.Manage,
|
readOnly={spaceAbility.cannot(
|
||||||
SpaceCaslSubject.Settings,
|
SpaceCaslAction.Manage,
|
||||||
)}
|
SpaceCaslSubject.Settings,
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
</ScrollArea>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|
||||||
<Tabs.Panel value="members">
|
<Tabs.Panel value="members">
|
||||||
@@ -74,7 +78,7 @@ export default function SpaceSettingsModal({
|
|||||||
{spaceAbility.can(
|
{spaceAbility.can(
|
||||||
SpaceCaslAction.Manage,
|
SpaceCaslAction.Manage,
|
||||||
SpaceCaslSubject.Member,
|
SpaceCaslSubject.Member,
|
||||||
) && <AddSpaceMembersModal spaceId={space?.id}/>}
|
) && <AddSpaceMembersModal spaceId={space?.id} />}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<SpaceMembersList
|
<SpaceMembersList
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useDebouncedValue } from "@mantine/hooks";
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
import { Avatar, Group, Select, SelectProps, Text } from "@mantine/core";
|
import { Group, Select, SelectProps, Text } from "@mantine/core";
|
||||||
import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts";
|
import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts";
|
||||||
import { ISpace } from "../../types/space.types";
|
import { ISpace } from "../../types/space.types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
|
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||||
|
|
||||||
interface SpaceSelectProps {
|
interface SpaceSelectProps {
|
||||||
onChange: (value: ISpace) => void;
|
onChange: (value: ISpace) => void;
|
||||||
@@ -16,7 +18,14 @@ interface SpaceSelectProps {
|
|||||||
|
|
||||||
const renderSelectOption: SelectProps["renderOption"] = ({ option }) => (
|
const renderSelectOption: SelectProps["renderOption"] = ({ option }) => (
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<Avatar color="initials" variant="filled" name={option.label} size={20} />
|
<CustomAvatar
|
||||||
|
name={option.label}
|
||||||
|
avatarUrl={option?.["icon"]}
|
||||||
|
type={AvatarIconType.SPACE_ICON}
|
||||||
|
color="initials"
|
||||||
|
variant="filled"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Text size="sm" lineClamp={1}>
|
<Text size="sm" lineClamp={1}>
|
||||||
{option.label}
|
{option.label}
|
||||||
@@ -50,6 +59,7 @@ export function SpaceSelect({
|
|||||||
return {
|
return {
|
||||||
label: space.name,
|
label: space.name,
|
||||||
value: space.slug,
|
value: space.slug,
|
||||||
|
icon: space.logo,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,12 +86,11 @@ export function SpaceSelect({
|
|||||||
onChange={(slug) =>
|
onChange={(slug) =>
|
||||||
onChange(spaces.items?.find((item) => item.slug === slug))
|
onChange(spaces.items?.find((item) => item.slug === slug))
|
||||||
}
|
}
|
||||||
// duct tape
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
nothingFoundMessage={t("No space found")}
|
nothingFoundMessage={t("No space found")}
|
||||||
limit={50}
|
limit={50}
|
||||||
checkIconPosition="right"
|
checkIconPosition="right"
|
||||||
comboboxProps={{ width, withinPortal: true, position: "bottom" }}
|
comboboxProps={{ width, withinPortal: true, position: "bottom", keepMounted: false, dropdownPadding: 0 }}
|
||||||
dropdownOpened={opened}
|
dropdownOpened={opened}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -74,7 +74,11 @@ export function SpaceSidebar() {
|
|||||||
marginBottom: 3,
|
marginBottom: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SwitchSpace spaceName={space?.name} spaceSlug={space?.slug} />
|
<SwitchSpace
|
||||||
|
spaceName={space?.name}
|
||||||
|
spaceSlug={space?.slug}
|
||||||
|
spaceIcon={space?.logo}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={classes.section}>
|
<div className={classes.section}>
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
import classes from './switch-space.module.css';
|
import classes from "./switch-space.module.css";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import { SpaceSelect } from './space-select';
|
import { SpaceSelect } from "./space-select";
|
||||||
import { getSpaceUrl } from '@/lib/config';
|
import { getSpaceUrl } from "@/lib/config";
|
||||||
import { Avatar, Button, Popover, Text } from '@mantine/core';
|
import { Button, Popover, Text } from "@mantine/core";
|
||||||
import { IconChevronDown } from '@tabler/icons-react';
|
import { IconChevronDown } from "@tabler/icons-react";
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
|
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
interface SwitchSpaceProps {
|
interface SwitchSpaceProps {
|
||||||
spaceName: string;
|
spaceName: string;
|
||||||
spaceSlug: string;
|
spaceSlug: string;
|
||||||
|
spaceIcon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SwitchSpace({ spaceName, spaceSlug }: SwitchSpaceProps) {
|
export function SwitchSpace({
|
||||||
|
spaceName,
|
||||||
|
spaceSlug,
|
||||||
|
spaceIcon,
|
||||||
|
}: SwitchSpaceProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [opened, { close, open, toggle }] = useDisclosure(false);
|
const [opened, { close, open, toggle }] = useDisclosure(false);
|
||||||
|
|
||||||
@@ -40,11 +48,13 @@ export function SwitchSpace({ spaceName, spaceSlug }: SwitchSpaceProps) {
|
|||||||
color="gray"
|
color="gray"
|
||||||
onClick={open}
|
onClick={open}
|
||||||
>
|
>
|
||||||
<Avatar
|
<CustomAvatar
|
||||||
size={20}
|
name={spaceName}
|
||||||
|
avatarUrl={spaceIcon}
|
||||||
|
type={AvatarIconType.SPACE_ICON}
|
||||||
color="initials"
|
color="initials"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
name={spaceName}
|
size={20}
|
||||||
/>
|
/>
|
||||||
<Text className={classes.spaceName} size="md" fw={500} lineClamp={1}>
|
<Text className={classes.spaceName} size="md" fw={500} lineClamp={1}>
|
||||||
{spaceName}
|
{spaceName}
|
||||||
@@ -55,7 +65,7 @@ export function SwitchSpace({ spaceName, spaceSlug }: SwitchSpaceProps) {
|
|||||||
<SpaceSelect
|
<SpaceSelect
|
||||||
label={spaceName}
|
label={spaceName}
|
||||||
value={spaceSlug}
|
value={spaceSlug}
|
||||||
onChange={space => handleSelect(space.slug)}
|
onChange={(space) => handleSelect(space.slug)}
|
||||||
width={300}
|
width={300}
|
||||||
opened={true}
|
opened={true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
import React from 'react';
|
import React, { useState } 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 { Button, Divider, Group, Text } from '@mantine/core';
|
import { Button, Divider, Text } from "@mantine/core";
|
||||||
import DeleteSpaceModal from './delete-space-modal';
|
import DeleteSpaceModal from "./delete-space-modal";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import ExportModal from "@/components/common/export-modal.tsx";
|
import ExportModal from "@/components/common/export-modal.tsx";
|
||||||
|
import AvatarUploader from "@/components/common/avatar-uploader.tsx";
|
||||||
|
import {
|
||||||
|
uploadSpaceIcon,
|
||||||
|
removeSpaceIcon,
|
||||||
|
} from "@/features/attachments/services/attachment-service.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||||
|
import { queryClient } from "@/main.tsx";
|
||||||
|
import {
|
||||||
|
ResponsiveSettingsContent,
|
||||||
|
ResponsiveSettingsControl,
|
||||||
|
ResponsiveSettingsRow,
|
||||||
|
} from "@/components/ui/responsive-settings-row.tsx";
|
||||||
|
|
||||||
interface SpaceDetailsProps {
|
interface SpaceDetailsProps {
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
@@ -13,9 +25,40 @@ interface SpaceDetailsProps {
|
|||||||
}
|
}
|
||||||
export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data: space, isLoading } = useSpaceQuery(spaceId);
|
const { data: space, isLoading, refetch } = useSpaceQuery(spaceId);
|
||||||
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||||
useDisclosure(false);
|
useDisclosure(false);
|
||||||
|
const [isIconUploading, setIsIconUploading] = useState(false);
|
||||||
|
|
||||||
|
const handleIconUpload = async (file: File) => {
|
||||||
|
setIsIconUploading(true);
|
||||||
|
try {
|
||||||
|
await uploadSpaceIcon(file, spaceId);
|
||||||
|
await refetch();
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
predicate: (item) => ["spaces"].includes(item.queryKey[0] as string),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// skip
|
||||||
|
} finally {
|
||||||
|
setIsIconUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIconRemove = async () => {
|
||||||
|
setIsIconUploading(true);
|
||||||
|
try {
|
||||||
|
await removeSpaceIcon(spaceId);
|
||||||
|
await refetch();
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
predicate: (item) => ["spaces"].includes(item.queryKey[0] as string),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// skip
|
||||||
|
} finally {
|
||||||
|
setIsIconUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -24,38 +67,56 @@ export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
|||||||
<Text my="md" fw={600}>
|
<Text my="md" fw={600}>
|
||||||
{t("Details")}
|
{t("Details")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: "20px" }}>
|
||||||
|
<Text size="sm" fw={500} mb="xs">
|
||||||
|
{t("Icon")}
|
||||||
|
</Text>
|
||||||
|
<AvatarUploader
|
||||||
|
currentImageUrl={space.logo}
|
||||||
|
fallbackName={space.name}
|
||||||
|
size={"60px"}
|
||||||
|
variant="filled"
|
||||||
|
|
||||||
|
type={AvatarIconType.SPACE_ICON}
|
||||||
|
onUpload={handleIconUpload}
|
||||||
|
onRemove={handleIconRemove}
|
||||||
|
isLoading={isIconUploading}
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<EditSpaceForm space={space} readOnly={readOnly} />
|
<EditSpaceForm space={space} readOnly={readOnly} />
|
||||||
|
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
<Divider my="lg" />
|
<Divider my="lg" />
|
||||||
|
|
||||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
<ResponsiveSettingsRow>
|
||||||
<div>
|
<ResponsiveSettingsContent>
|
||||||
<Text size="md">{t("Export space")}</Text>
|
<Text size="md">{t("Export space")}</Text>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{t("Export all pages and attachments in this space.")}
|
{t("Export all pages and attachments in this space.")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</ResponsiveSettingsContent>
|
||||||
|
<ResponsiveSettingsControl>
|
||||||
<Button onClick={openExportModal}>
|
<Button onClick={openExportModal}>{t("Export")}</Button>
|
||||||
{t("Export")}
|
</ResponsiveSettingsControl>
|
||||||
</Button>
|
</ResponsiveSettingsRow>
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Divider my="lg" />
|
<Divider my="lg" />
|
||||||
|
|
||||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
<ResponsiveSettingsRow>
|
||||||
<div>
|
<ResponsiveSettingsContent>
|
||||||
<Text size="md">{t("Delete space")}</Text>
|
<Text size="md">{t("Delete space")}</Text>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{t("Delete this space with all its pages and data.")}
|
{t("Delete this space with all its pages and data.")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</ResponsiveSettingsContent>
|
||||||
|
<ResponsiveSettingsControl>
|
||||||
<DeleteSpaceModal space={space} />
|
<DeleteSpaceModal space={space} />
|
||||||
</Group>
|
</ResponsiveSettingsControl>
|
||||||
|
</ResponsiveSettingsRow>
|
||||||
|
|
||||||
<ExportModal
|
<ExportModal
|
||||||
type="space"
|
type="space"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cardSection {
|
.cardSection {
|
||||||
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7));
|
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Text, Avatar, SimpleGrid, Card, rem, Group, Button } from "@mantine/core";
|
import { Text, SimpleGrid, Card, rem, Group, Button } from "@mantine/core";
|
||||||
import React, { useEffect } from 'react';
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
prefetchSpace,
|
prefetchSpace,
|
||||||
useGetSpacesQuery,
|
useGetSpacesQuery,
|
||||||
@@ -10,6 +10,8 @@ import classes from "./space-grid.module.css";
|
|||||||
import { formatMemberCount } from "@/lib";
|
import { formatMemberCount } from "@/lib";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { IconArrowRight } from "@tabler/icons-react";
|
import { IconArrowRight } from "@tabler/icons-react";
|
||||||
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
|
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||||
|
|
||||||
export default function SpaceGrid() {
|
export default function SpaceGrid() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -27,8 +29,10 @@ export default function SpaceGrid() {
|
|||||||
withBorder
|
withBorder
|
||||||
>
|
>
|
||||||
<Card.Section className={classes.cardSection} h={40}></Card.Section>
|
<Card.Section className={classes.cardSection} h={40}></Card.Section>
|
||||||
<Avatar
|
<CustomAvatar
|
||||||
name={space.name}
|
name={space.name}
|
||||||
|
avatarUrl={space.logo}
|
||||||
|
type={AvatarIconType.SPACE_ICON}
|
||||||
color="initials"
|
color="initials"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="md"
|
size="md"
|
||||||
@@ -54,7 +58,7 @@ export default function SpaceGrid() {
|
|||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 1, xs: 2, sm: 3 }}>{cards}</SimpleGrid>
|
<SimpleGrid cols={{ base: 1, xs: 2, sm: 3 }}>{cards}</SimpleGrid>
|
||||||
|
|
||||||
{data?.items && data.items.length > 9 && (
|
{data?.items && data.items.length > 9 && (
|
||||||
<Group justify="flex-end" mt="lg">
|
<Group justify="flex-end" mt="lg">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Table, Group, Text, Avatar } from "@mantine/core";
|
import { Group, Table, Text } from "@mantine/core";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts";
|
import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts";
|
||||||
import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx";
|
import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx";
|
||||||
@@ -6,11 +6,22 @@ import { useDisclosure } from "@mantine/hooks";
|
|||||||
import { formatMemberCount } from "@/lib";
|
import { formatMemberCount } from "@/lib";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Paginate from "@/components/common/paginate.tsx";
|
import Paginate from "@/components/common/paginate.tsx";
|
||||||
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
|
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { userAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||||
|
import { UserRole } from "@/lib/types.ts";
|
||||||
|
import { useIsEEOnly } from "@/hooks/use-is-cloud-ee.tsx";
|
||||||
|
|
||||||
export default function SpaceList() {
|
export default function SpaceList() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const { data, isLoading } = useGetSpacesQuery({ page });
|
const [user] = useAtom(userAtom);
|
||||||
|
const isEEOnly = useIsEEOnly();
|
||||||
|
const { data, isLoading } = useGetSpacesQuery({
|
||||||
|
page,
|
||||||
|
...(isEEOnly && user.role === UserRole.OWNER && { includeAllSpaces: true }),
|
||||||
|
});
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
const [selectedSpaceId, setSelectedSpaceId] = useState<string>(null);
|
const [selectedSpaceId, setSelectedSpaceId] = useState<string>(null);
|
||||||
|
|
||||||
@@ -39,8 +50,10 @@ export default function SpaceList() {
|
|||||||
>
|
>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<Avatar
|
<CustomAvatar
|
||||||
color="initials"
|
color="initials"
|
||||||
|
avatarUrl={space.logo}
|
||||||
|
type={AvatarIconType.SPACE_ICON}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
name={space.name}
|
name={space.name}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Space,
|
Space,
|
||||||
Menu,
|
Menu,
|
||||||
Avatar,
|
|
||||||
Anchor,
|
Anchor,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconDots, IconSettings } from "@tabler/icons-react";
|
import { IconDots, IconSettings } from "@tabler/icons-react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { formatMemberCount } from "@/lib";
|
import { formatMemberCount } from "@/lib";
|
||||||
import { getSpaceUrl } from "@/lib/config";
|
import { getSpaceUrl } from "@/lib/config";
|
||||||
@@ -22,6 +21,8 @@ import Paginate from "@/components/common/paginate";
|
|||||||
import NoTableResults from "@/components/common/no-table-results";
|
import NoTableResults from "@/components/common/no-table-results";
|
||||||
import SpaceSettingsModal from "@/features/space/components/settings-modal";
|
import SpaceSettingsModal from "@/features/space/components/settings-modal";
|
||||||
import classes from "./all-spaces-list.module.css";
|
import classes from "./all-spaces-list.module.css";
|
||||||
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
|
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||||
|
|
||||||
interface AllSpacesListProps {
|
interface AllSpacesListProps {
|
||||||
spaces: any[];
|
spaces: any[];
|
||||||
@@ -87,11 +88,13 @@ export default function AllSpacesList({
|
|||||||
className={classes.spaceLink}
|
className={classes.spaceLink}
|
||||||
onMouseEnter={() => prefetchSpace(space.slug, space.id)}
|
onMouseEnter={() => prefetchSpace(space.slug, space.id)}
|
||||||
>
|
>
|
||||||
<Avatar
|
<CustomAvatar
|
||||||
|
name={space.name}
|
||||||
|
avatarUrl={space.logo}
|
||||||
|
type={AvatarIconType.SPACE_ICON}
|
||||||
color="initials"
|
color="initials"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
name={space.name}
|
size="md"
|
||||||
size={40}
|
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Text fz="sm" fw={500} lineClamp={1}>
|
<Text fz="sm" fw={500} lineClamp={1}>
|
||||||
|
|||||||
@@ -152,13 +152,36 @@ export function useDeleteSpaceMutation() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const spaces = queryClient.getQueryData(["spaces"]) as any;
|
// Remove space-specific queries
|
||||||
|
if (variables.id) {
|
||||||
|
queryClient.removeQueries({
|
||||||
|
queryKey: ["space", variables.id],
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Invalidate recent changes
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["recent-changes"],
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["recent-changes", variables.id],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update spaces list cache
|
||||||
|
/* const spaces = queryClient.getQueryData(["spaces"]) as any;
|
||||||
if (spaces) {
|
if (spaces) {
|
||||||
spaces.items = spaces.items?.filter(
|
spaces.items = spaces.items?.filter(
|
||||||
(space: ISpace) => space.id !== variables.id,
|
(space: ISpace) => space.id !== variables.id,
|
||||||
);
|
);
|
||||||
queryClient.setQueryData(["spaces"], spaces);
|
queryClient.setQueryData(["spaces"], spaces);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
// Invalidate all spaces queries to refresh lists
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
predicate: (item) => ["spaces"].includes(item.queryKey[0] as string),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
const errorMessage = error["response"]?.data?.message;
|
const errorMessage = error["response"]?.data?.message;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
ISpaceMember,
|
ISpaceMember,
|
||||||
} 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 { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
|
|
||||||
export async function getSpaces(
|
export async function getSpaces(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface ISpace {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon: string;
|
logo?: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
creatorId: string;
|
creatorId: string;
|
||||||
@@ -74,4 +74,4 @@ export interface IExportSpaceParams {
|
|||||||
spaceId: string;
|
spaceId: string;
|
||||||
format: ExportFormat;
|
format: ExportFormat;
|
||||||
includeAttachments?: boolean;
|
includeAttachments?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,58 @@
|
|||||||
import { focusAtom } from "jotai-optics";
|
import {
|
||||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
currentUserAtom,
|
||||||
|
userAtom,
|
||||||
|
} from "@/features/user/atoms/current-user-atom.ts";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import AvatarUploader from "@/components/common/avatar-uploader.tsx";
|
||||||
import { FileButton, Tooltip } from "@mantine/core";
|
import {
|
||||||
import { uploadAvatar } from "@/features/user/services/user-service.ts";
|
uploadUserAvatar,
|
||||||
import { useTranslation } from "react-i18next";
|
removeAvatar,
|
||||||
|
} from "@/features/attachments/services/attachment-service.ts";
|
||||||
const userAtom = focusAtom(currentUserAtom, (optic) => optic.prop("user"));
|
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||||
|
|
||||||
export default function AccountAvatar() {
|
export default function AccountAvatar() {
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [currentUser] = useAtom(currentUserAtom);
|
const [currentUser] = useAtom(currentUserAtom);
|
||||||
const [, setUser] = useAtom(userAtom);
|
const [, setUser] = useAtom(userAtom);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
|
||||||
|
|
||||||
const handleFileChange = async (selectedFile: File) => {
|
const handleUpload = async (selectedFile: File) => {
|
||||||
if (!selectedFile) {
|
setIsLoading(true);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFile(selectedFile);
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
const avatar = await uploadUserAvatar(selectedFile);
|
||||||
const avatar = await uploadAvatar(selectedFile);
|
if (currentUser?.user) {
|
||||||
|
setUser({ ...currentUser.user, avatarUrl: avatar.fileName });
|
||||||
setUser((prev) => ({ ...prev, avatarUrl: avatar.fileName }));
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
// skip
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await removeAvatar();
|
||||||
|
if (currentUser?.user) {
|
||||||
|
setUser({ ...currentUser.user, avatarUrl: null });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// skip
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<AvatarUploader
|
||||||
<FileButton onChange={handleFileChange} accept="image/png,image/jpeg">
|
currentImageUrl={currentUser?.user.avatarUrl}
|
||||||
{(props) => (
|
fallbackName={currentUser?.user.name}
|
||||||
<Tooltip label={t("Change photo")} position="bottom">
|
size="60px"
|
||||||
<CustomAvatar
|
type={AvatarIconType.AVATAR}
|
||||||
{...props}
|
onUpload={handleUpload}
|
||||||
component="button"
|
onRemove={handleRemove}
|
||||||
size="60px"
|
isLoading={isLoading}
|
||||||
avatarUrl={currentUser?.user.avatarUrl}
|
/>
|
||||||
name={currentUser?.user.name}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</FileButton>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,3 @@ export async function updateUser(data: Partial<IUser>): Promise<IUser> {
|
|||||||
const req = await api.post<IUser>("/users/update", data);
|
const req = await api.post<IUser>("/users/update", data);
|
||||||
return req.data as IUser;
|
return req.data as IUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadAvatar(file: File): Promise<any> {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("type", "avatar");
|
|
||||||
formData.append("image", file);
|
|
||||||
|
|
||||||
const req = await api.post("/attachments/upload-image", formData, {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "multipart/form-data",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return req;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { Text } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import AvatarUploader from "@/components/common/avatar-uploader.tsx";
|
||||||
|
import {
|
||||||
|
uploadWorkspaceIcon,
|
||||||
|
removeWorkspaceIcon,
|
||||||
|
} from "@/features/attachments/services/attachment-service.ts";
|
||||||
|
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||||
|
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||||
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
|
|
||||||
|
export default function WorkspaceIcon() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||||
|
const { isAdmin } = useUserRole();
|
||||||
|
|
||||||
|
const handleIconUpload = async (file: File) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await uploadWorkspaceIcon(file);
|
||||||
|
if (workspace) {
|
||||||
|
setWorkspace({ ...workspace, logo: result.fileName });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIconRemove = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await removeWorkspaceIcon();
|
||||||
|
if (workspace) {
|
||||||
|
setWorkspace({ ...workspace, logo: null });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: "24px" }}>
|
||||||
|
<Text size="sm" fw={500} mb="xs">
|
||||||
|
{t("Icon")}
|
||||||
|
</Text>
|
||||||
|
<AvatarUploader
|
||||||
|
currentImageUrl={workspace?.logo}
|
||||||
|
fallbackName={workspace?.name}
|
||||||
|
type={AvatarIconType.WORKSPACE_ICON}
|
||||||
|
size="60px"
|
||||||
|
radius="sm"
|
||||||
|
variant="filled"
|
||||||
|
onUpload={handleIconUpload}
|
||||||
|
onRemove={handleIconRemove}
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={!isAdmin}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -109,15 +109,3 @@ export async function getAppVersion(): Promise<IVersion> {
|
|||||||
return req.data;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadLogo(file: File) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("type", "workspace-logo");
|
|
||||||
formData.append("image", file);
|
|
||||||
|
|
||||||
const req = await api.post("/attachments/upload-image", formData, {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "multipart/form-data",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return req.data;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import { isCloud } from "@/lib/config";
|
import { isCloud } from "@/lib/config";
|
||||||
import { useLicense } from "@/ee/hooks/use-license";
|
import { useLicense } from "@/ee/hooks/use-license";
|
||||||
|
import { useAtom } from "jotai/index";
|
||||||
|
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||||
|
import usePlan from "@/ee/hooks/use-plan";
|
||||||
|
|
||||||
export const useIsCloudEE = () => {
|
export const useIsCloudEE = () => {
|
||||||
const { hasLicenseKey } = useLicense();
|
const { hasLicenseKey } = useLicense();
|
||||||
return isCloud() || !!hasLicenseKey;
|
return isCloud() || !!hasLicenseKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useIsEEOnly = () => {
|
||||||
|
const { hasLicenseKey } = useLicense();
|
||||||
|
const { isBusiness } = usePlan();
|
||||||
|
return (isCloud() && isBusiness) || !!hasLicenseKey;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import bytes from "bytes";
|
import bytes from "bytes";
|
||||||
import { castToBoolean } from "@/lib/utils.tsx";
|
import { castToBoolean } from "@/lib/utils.tsx";
|
||||||
|
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -41,11 +42,14 @@ export function isCloud(): boolean {
|
|||||||
return castToBoolean(getConfigValue("CLOUD"));
|
return castToBoolean(getConfigValue("CLOUD"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAvatarUrl(avatarUrl: string) {
|
export function getAvatarUrl(
|
||||||
|
avatarUrl: string,
|
||||||
|
type: AvatarIconType = AvatarIconType.AVATAR,
|
||||||
|
) {
|
||||||
if (!avatarUrl) return null;
|
if (!avatarUrl) return null;
|
||||||
if (avatarUrl?.startsWith("http")) return avatarUrl;
|
if (avatarUrl?.startsWith("http")) return avatarUrl;
|
||||||
|
|
||||||
return getBackendUrl() + "/attachments/img/avatar/" + avatarUrl;
|
return getBackendUrl() + `/attachments/img/${type}/` + encodeURI(avatarUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSpaceUrl(spaceSlug: string) {
|
export function getSpaceUrl(spaceSlug: string) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export interface QueryParams {
|
|||||||
query?: string;
|
query?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
includeAllSpaces?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UserRole {
|
export enum UserRole {
|
||||||
@@ -36,20 +37,3 @@ export type IPagination<T> = {
|
|||||||
items: T[];
|
items: T[];
|
||||||
meta: IPaginationMeta;
|
meta: IPaginationMeta;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IAttachment {
|
|
||||||
id: string;
|
|
||||||
fileName: string;
|
|
||||||
filePath: string;
|
|
||||||
fileSize: number;
|
|
||||||
fileExt: string;
|
|
||||||
mimeType: string;
|
|
||||||
type: string;
|
|
||||||
creatorId: string;
|
|
||||||
pageId: string | null;
|
|
||||||
spaceId: string | null;
|
|
||||||
workspaceId: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt: string | null;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||||
import WorkspaceNameForm from "@/features/workspace/components/settings/components/workspace-name-form";
|
import WorkspaceNameForm from "@/features/workspace/components/settings/components/workspace-name-form";
|
||||||
|
import WorkspaceIcon from "@/features/workspace/components/settings/components/workspace-icon.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getAppName, isCloud } from "@/lib/config.ts";
|
import { getAppName, isCloud } from "@/lib/config.ts";
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
@@ -14,6 +15,7 @@ export default function WorkspaceSettings() {
|
|||||||
<title>Workspace Settings - {getAppName()}</title>
|
<title>Workspace Settings - {getAppName()}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<SettingsTitle title={t("General")} />
|
<SettingsTitle title={t("General")} />
|
||||||
|
<WorkspaceIcon />
|
||||||
<WorkspaceNameForm />
|
<WorkspaceNameForm />
|
||||||
|
|
||||||
{isCloud() && (
|
{isCloud() && (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.23.1",
|
"version": "0.23.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -84,7 +84,8 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"sanitize-filename-ts": "^1.0.2",
|
"sanitize-filename-ts": "1.0.2",
|
||||||
|
"sharp": "0.34.3",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"stripe": "^17.5.0",
|
"stripe": "^17.5.0",
|
||||||
"tmp-promise": "^3.0.3",
|
"tmp-promise": "^3.0.3",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
|||||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||||
import { findHighestUserSpaceRole } from '@docmost/db/repos/space/utils';
|
import { findHighestUserSpaceRole } from '@docmost/db/repos/space/utils';
|
||||||
import { SpaceRole } from '../../common/helpers/types/permission';
|
import { SpaceRole, UserRole } from '../../common/helpers/types/permission';
|
||||||
import { getPageId } from '../collaboration.util';
|
import { getPageId } from '../collaboration.util';
|
||||||
import { JwtCollabPayload, JwtType } from '../../core/auth/dto/jwt-payload';
|
import { JwtCollabPayload, JwtType } from '../../core/auth/dto/jwt-payload';
|
||||||
|
|
||||||
@@ -63,7 +63,10 @@ export class AuthenticationExtension implements Extension {
|
|||||||
|
|
||||||
const userSpaceRole = findHighestUserSpaceRole(userSpaceRoles);
|
const userSpaceRole = findHighestUserSpaceRole(userSpaceRoles);
|
||||||
|
|
||||||
if (!userSpaceRole) {
|
// if role not found but user is a workspace owner, grant them readonly permission
|
||||||
|
if (!userSpaceRole && user.role === UserRole.OWNER) {
|
||||||
|
data.connection.readOnly = true;
|
||||||
|
} else if (!userSpaceRole) {
|
||||||
this.logger.warn(`User not authorized to access page: ${pageId}`);
|
this.logger.warn(`User not authorized to access page: ${pageId}`);
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,9 @@ export function extractDateFromUuid7(uuid7: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeFileName(fileName: string): string {
|
export function sanitizeFileName(fileName: string): string {
|
||||||
const sanitizedFilename = sanitize(fileName).replace(/ /g, '_');
|
const sanitizedFilename = sanitize(fileName)
|
||||||
|
.replace(/ /g, '_')
|
||||||
|
.replace(/#/g, '_');
|
||||||
return sanitizedFilename.slice(0, 255);
|
return sanitizedFilename.slice(0, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
export enum AttachmentType {
|
export enum AttachmentType {
|
||||||
Avatar = 'avatar',
|
Avatar = 'avatar',
|
||||||
WorkspaceLogo = 'workspace-logo',
|
WorkspaceIcon = 'workspace-icon',
|
||||||
SpaceLogo = 'space-logo',
|
SpaceIcon = 'space-icon',
|
||||||
File = 'file',
|
File = 'file',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validImageExtensions = ['.jpg', '.png', '.jpeg'];
|
export const validImageExtensions = ['.jpg', '.png', '.jpeg'];
|
||||||
export const MAX_AVATAR_SIZE = '5MB';
|
export const MAX_AVATAR_SIZE = '10MB';
|
||||||
|
|
||||||
export const inlineFileExtensions = [
|
export const inlineFileExtensions = [
|
||||||
'.jpg',
|
'.jpg',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
ForbiddenException,
|
ForbiddenException,
|
||||||
Get,
|
Get,
|
||||||
@@ -51,6 +52,7 @@ import { EnvironmentService } from '../../integrations/environment/environment.s
|
|||||||
import { TokenService } from '../auth/services/token.service';
|
import { TokenService } from '../auth/services/token.service';
|
||||||
import { JwtAttachmentPayload, JwtType } from '../auth/dto/jwt-payload';
|
import { JwtAttachmentPayload, JwtType } from '../auth/dto/jwt-payload';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { RemoveIconDto } from './dto/attachment.dto';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AttachmentController {
|
export class AttachmentController {
|
||||||
@@ -302,7 +304,7 @@ export class AttachmentController {
|
|||||||
throw new BadRequestException('Invalid image attachment type');
|
throw new BadRequestException('Invalid image attachment type');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachmentType === AttachmentType.WorkspaceLogo) {
|
if (attachmentType === AttachmentType.WorkspaceIcon) {
|
||||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||||
if (
|
if (
|
||||||
ability.cannot(
|
ability.cannot(
|
||||||
@@ -314,7 +316,7 @@ export class AttachmentController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachmentType === AttachmentType.SpaceLogo) {
|
if (attachmentType === AttachmentType.SpaceIcon) {
|
||||||
if (!spaceId) {
|
if (!spaceId) {
|
||||||
throw new BadRequestException('spaceId is required');
|
throw new BadRequestException('spaceId is required');
|
||||||
}
|
}
|
||||||
@@ -372,8 +374,59 @@ export class AttachmentController {
|
|||||||
});
|
});
|
||||||
return res.send(fileStream);
|
return res.send(fileStream);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
// this.logger.error(err);
|
||||||
throw new NotFoundException('File not found');
|
throw new NotFoundException('File not found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('attachments/remove-icon')
|
||||||
|
async removeIcon(
|
||||||
|
@Body() dto: RemoveIconDto,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
const { type, spaceId } = dto;
|
||||||
|
|
||||||
|
// remove current user avatar
|
||||||
|
if (type === AttachmentType.Avatar) {
|
||||||
|
await this.attachmentService.removeUserAvatar(user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove space icon
|
||||||
|
if (type === AttachmentType.SpaceIcon) {
|
||||||
|
if (!spaceId) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'spaceId is required to change space icons',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const spaceAbility = await this.spaceAbility.createForUser(user, spaceId);
|
||||||
|
if (
|
||||||
|
spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Settings)
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.attachmentService.removeSpaceIcon(spaceId, workspace.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove workspace icon
|
||||||
|
if (type === AttachmentType.WorkspaceIcon) {
|
||||||
|
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||||
|
if (
|
||||||
|
ability.cannot(
|
||||||
|
WorkspaceCaslAction.Manage,
|
||||||
|
WorkspaceCaslSubject.Settings,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
await this.attachmentService.removeWorkspaceIcon(workspace);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { MultipartFile } from '@fastify/multipart';
|
import { MultipartFile } from '@fastify/multipart';
|
||||||
import { randomBytes } from 'crypto';
|
|
||||||
import { sanitize } from 'sanitize-filename-ts';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { AttachmentType } from './attachment.constants';
|
import { AttachmentType } from './attachment.constants';
|
||||||
|
import { sanitizeFileName } from '../../common/helpers';
|
||||||
|
import * as sharp from 'sharp';
|
||||||
|
|
||||||
export interface PreparedFile {
|
export interface PreparedFile {
|
||||||
buffer: Buffer;
|
buffer: Buffer;
|
||||||
@@ -22,10 +22,8 @@ export async function prepareFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rand = randomBytes(8).toString('hex');
|
|
||||||
|
|
||||||
const buffer = await file.toBuffer();
|
const buffer = await file.toBuffer();
|
||||||
const sanitizedFilename = sanitize(file.filename).replace(/ /g, '_');
|
const sanitizedFilename = sanitizeFileName(file.filename);
|
||||||
const fileName = sanitizedFilename.slice(0, 255);
|
const fileName = sanitizedFilename.slice(0, 255);
|
||||||
const fileSize = buffer.length;
|
const fileSize = buffer.length;
|
||||||
const fileExtension = path.extname(file.filename).toLowerCase();
|
const fileExtension = path.extname(file.filename).toLowerCase();
|
||||||
@@ -58,9 +56,9 @@ export function getAttachmentFolderPath(
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case AttachmentType.Avatar:
|
case AttachmentType.Avatar:
|
||||||
return `${workspaceId}/avatars`;
|
return `${workspaceId}/avatars`;
|
||||||
case AttachmentType.WorkspaceLogo:
|
case AttachmentType.WorkspaceIcon:
|
||||||
return `${workspaceId}/workspace-logo`;
|
return `${workspaceId}/workspace-logos`;
|
||||||
case AttachmentType.SpaceLogo:
|
case AttachmentType.SpaceIcon:
|
||||||
return `${workspaceId}/space-logos`;
|
return `${workspaceId}/space-logos`;
|
||||||
case AttachmentType.File:
|
case AttachmentType.File:
|
||||||
return `${workspaceId}/files`;
|
return `${workspaceId}/files`;
|
||||||
@@ -70,3 +68,51 @@ export function getAttachmentFolderPath(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const validAttachmentTypes = Object.values(AttachmentType);
|
export const validAttachmentTypes = Object.values(AttachmentType);
|
||||||
|
|
||||||
|
export async function compressAndResizeIcon(
|
||||||
|
buffer: Buffer,
|
||||||
|
attachmentType?: AttachmentType,
|
||||||
|
): Promise<Buffer> {
|
||||||
|
try {
|
||||||
|
let sharpInstance = sharp(buffer);
|
||||||
|
const metadata = await sharpInstance.metadata();
|
||||||
|
|
||||||
|
const targetWidth = 300;
|
||||||
|
const targetHeight = 300;
|
||||||
|
|
||||||
|
// Only resize if image is larger than target dimensions
|
||||||
|
if (metadata.width > targetWidth || metadata.height > targetHeight) {
|
||||||
|
sharpInstance = sharpInstance.resize(targetWidth, targetHeight, {
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle based on original format
|
||||||
|
if (metadata.format === 'png') {
|
||||||
|
// Only flatten avatars to remove transparency
|
||||||
|
if (attachmentType === AttachmentType.Avatar) {
|
||||||
|
sharpInstance = sharpInstance.flatten({
|
||||||
|
background: { r: 255, g: 255, b: 255 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sharpInstance
|
||||||
|
.png({
|
||||||
|
quality: 85,
|
||||||
|
compressionLevel: 6,
|
||||||
|
})
|
||||||
|
.toBuffer();
|
||||||
|
} else {
|
||||||
|
return await sharpInstance
|
||||||
|
.jpeg({
|
||||||
|
quality: 85,
|
||||||
|
progressive: true,
|
||||||
|
mozjpeg: true,
|
||||||
|
})
|
||||||
|
.toBuffer();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { IsEnum, IsIn, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
|
||||||
|
import { AttachmentType } from '../attachment.constants';
|
||||||
|
|
||||||
|
export class RemoveIconDto {
|
||||||
|
@IsEnum(AttachmentType)
|
||||||
|
@IsIn([
|
||||||
|
AttachmentType.Avatar,
|
||||||
|
AttachmentType.SpaceIcon,
|
||||||
|
AttachmentType.WorkspaceIcon,
|
||||||
|
])
|
||||||
|
@IsNotEmpty()
|
||||||
|
type: AttachmentType;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
spaceId: string;
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { IsOptional, IsString, IsUUID } from 'class-validator';
|
|
||||||
|
|
||||||
export class AvatarUploadDto {}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class GetFileDto {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
attachmentId: string;
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import {
|
|
||||||
IsDefined,
|
|
||||||
IsNotEmpty,
|
|
||||||
IsOptional,
|
|
||||||
IsString,
|
|
||||||
IsUUID,
|
|
||||||
} from 'class-validator';
|
|
||||||
|
|
||||||
export class UploadFileDto {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
attachmentType: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsUUID()
|
|
||||||
pageId: string;
|
|
||||||
|
|
||||||
@IsDefined()
|
|
||||||
file: any;
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { StorageService } from '../../../integrations/storage/storage.service';
|
import { StorageService } from '../../../integrations/storage/storage.service';
|
||||||
import { MultipartFile } from '@fastify/multipart';
|
import { MultipartFile } from '@fastify/multipart';
|
||||||
import {
|
import {
|
||||||
|
compressAndResizeIcon,
|
||||||
getAttachmentFolderPath,
|
getAttachmentFolderPath,
|
||||||
PreparedFile,
|
PreparedFile,
|
||||||
prepareFile,
|
prepareFile,
|
||||||
@@ -16,7 +17,7 @@ import { v4 as uuid4, v7 as uuid7 } from 'uuid';
|
|||||||
import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo';
|
import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo';
|
||||||
import { AttachmentType, validImageExtensions } from '../attachment.constants';
|
import { AttachmentType, validImageExtensions } from '../attachment.constants';
|
||||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||||
import { Attachment } from '@docmost/db/types/entity.types';
|
import { Attachment, User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { executeTx } from '@docmost/db/utils';
|
import { executeTx } from '@docmost/db/utils';
|
||||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||||
@@ -132,8 +133,8 @@ export class AttachmentService {
|
|||||||
filePromise: Promise<MultipartFile>,
|
filePromise: Promise<MultipartFile>,
|
||||||
type:
|
type:
|
||||||
| AttachmentType.Avatar
|
| AttachmentType.Avatar
|
||||||
| AttachmentType.WorkspaceLogo
|
| AttachmentType.WorkspaceIcon
|
||||||
| AttachmentType.SpaceLogo,
|
| AttachmentType.SpaceIcon,
|
||||||
userId: string,
|
userId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
spaceId?: string,
|
spaceId?: string,
|
||||||
@@ -141,6 +142,9 @@ export class AttachmentService {
|
|||||||
const preparedFile: PreparedFile = await prepareFile(filePromise);
|
const preparedFile: PreparedFile = await prepareFile(filePromise);
|
||||||
validateFileType(preparedFile.fileExtension, validImageExtensions);
|
validateFileType(preparedFile.fileExtension, validImageExtensions);
|
||||||
|
|
||||||
|
const processedBuffer = await compressAndResizeIcon(preparedFile.buffer, type);
|
||||||
|
preparedFile.buffer = processedBuffer;
|
||||||
|
preparedFile.fileSize = processedBuffer.length;
|
||||||
preparedFile.fileName = uuid4() + preparedFile.fileExtension;
|
preparedFile.fileName = uuid4() + preparedFile.fileExtension;
|
||||||
|
|
||||||
const filePath = `${getAttachmentFolderPath(type, workspaceId)}/${preparedFile.fileName}`;
|
const filePath = `${getAttachmentFolderPath(type, workspaceId)}/${preparedFile.fileName}`;
|
||||||
@@ -174,7 +178,7 @@ export class AttachmentService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
trx,
|
trx,
|
||||||
);
|
);
|
||||||
} else if (type === AttachmentType.WorkspaceLogo) {
|
} else if (type === AttachmentType.WorkspaceIcon) {
|
||||||
const workspace = await this.workspaceRepo.findById(workspaceId, {
|
const workspace = await this.workspaceRepo.findById(workspaceId, {
|
||||||
trx,
|
trx,
|
||||||
});
|
});
|
||||||
@@ -186,7 +190,7 @@ export class AttachmentService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
trx,
|
trx,
|
||||||
);
|
);
|
||||||
} else if (type === AttachmentType.SpaceLogo && spaceId) {
|
} else if (type === AttachmentType.SpaceIcon && spaceId) {
|
||||||
const space = await this.spaceRepo.findById(spaceId, workspaceId, {
|
const space = await this.spaceRepo.findById(spaceId, workspaceId, {
|
||||||
trx,
|
trx,
|
||||||
});
|
});
|
||||||
@@ -205,7 +209,6 @@ export class AttachmentService {
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// delete uploaded file on db update failure
|
// delete uploaded file on db update failure
|
||||||
this.logger.error('Image upload error:', err);
|
|
||||||
await this.deleteRedundantFile(filePath);
|
await this.deleteRedundantFile(filePath);
|
||||||
throw new BadRequestException('Failed to upload image');
|
throw new BadRequestException('Failed to upload image');
|
||||||
}
|
}
|
||||||
@@ -389,4 +392,40 @@ export class AttachmentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeUserAvatar(user: User) {
|
||||||
|
if (user.avatarUrl && !user.avatarUrl.toLowerCase().startsWith('http')) {
|
||||||
|
const filePath = `${getAttachmentFolderPath(AttachmentType.Avatar, user.workspaceId)}/${user.avatarUrl}`;
|
||||||
|
await this.deleteRedundantFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userRepo.updateUser(
|
||||||
|
{ avatarUrl: null },
|
||||||
|
user.id,
|
||||||
|
user.workspaceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeSpaceIcon(spaceId: string, workspaceId: string) {
|
||||||
|
const space = await this.spaceRepo.findById(spaceId, workspaceId);
|
||||||
|
|
||||||
|
if (!space) {
|
||||||
|
throw new NotFoundException('Space not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (space.logo && !space.logo.toLowerCase().startsWith('http')) {
|
||||||
|
const filePath = `${getAttachmentFolderPath(AttachmentType.SpaceIcon, workspaceId)}/${space.logo}`;
|
||||||
|
await this.deleteRedundantFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.spaceRepo.updateSpace({ logo: null }, spaceId, workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeWorkspaceIcon(workspace: Workspace) {
|
||||||
|
if (workspace.logo && !workspace.logo.toLowerCase().startsWith('http')) {
|
||||||
|
const filePath = `${getAttachmentFolderPath(AttachmentType.WorkspaceIcon, workspace.id)}/${workspace.logo}`;
|
||||||
|
await this.deleteRedundantFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.workspaceRepo.updateWorkspace({ logo: null }, workspace.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
createMongoAbility,
|
createMongoAbility,
|
||||||
MongoAbility,
|
MongoAbility,
|
||||||
} from '@casl/ability';
|
} from '@casl/ability';
|
||||||
import { SpaceRole } from '../../../common/helpers/types/permission';
|
import { SpaceRole, UserRole } from '../../../common/helpers/types/permission';
|
||||||
import { User } from '@docmost/db/types/entity.types';
|
import { User } from '@docmost/db/types/entity.types';
|
||||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||||
import {
|
import {
|
||||||
@@ -25,13 +25,17 @@ export default class SpaceAbilityFactory {
|
|||||||
|
|
||||||
const userSpaceRole = findHighestUserSpaceRole(userSpaceRoles);
|
const userSpaceRole = findHighestUserSpaceRole(userSpaceRoles);
|
||||||
|
|
||||||
|
if (!userSpaceRole && user.role === UserRole.OWNER) {
|
||||||
|
return buildWorkspaceOwnerAbility();
|
||||||
|
}
|
||||||
|
|
||||||
switch (userSpaceRole) {
|
switch (userSpaceRole) {
|
||||||
case SpaceRole.ADMIN:
|
case SpaceRole.ADMIN:
|
||||||
return buildSpaceAdminAbility();
|
return buildSpaceAdminAbility();
|
||||||
case SpaceRole.WRITER:
|
case SpaceRole.WRITER:
|
||||||
return buildSpaceWriterAbility();
|
return buildSpaceWriterAbility(user.role);
|
||||||
case SpaceRole.READER:
|
case SpaceRole.READER:
|
||||||
return buildSpaceReaderAbility();
|
return buildSpaceReaderAbility(user.role);
|
||||||
default:
|
default:
|
||||||
throw new NotFoundException('Space permissions not found');
|
throw new NotFoundException('Space permissions not found');
|
||||||
}
|
}
|
||||||
@@ -49,23 +53,50 @@ function buildSpaceAdminAbility() {
|
|||||||
return build();
|
return build();
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSpaceWriterAbility() {
|
function buildSpaceWriterAbility(workspaceRole?: string) {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<ISpaceAbility>>(
|
const { can, build } = new AbilityBuilder<MongoAbility<ISpaceAbility>>(
|
||||||
createMongoAbility,
|
createMongoAbility,
|
||||||
);
|
);
|
||||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Settings);
|
|
||||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Member);
|
if (workspaceRole === UserRole.OWNER) {
|
||||||
|
// Workspace owners get manage permissions even with writer space role
|
||||||
|
can(SpaceCaslAction.Manage, SpaceCaslSubject.Settings);
|
||||||
|
can(SpaceCaslAction.Manage, SpaceCaslSubject.Member);
|
||||||
|
} else {
|
||||||
|
can(SpaceCaslAction.Read, SpaceCaslSubject.Settings);
|
||||||
|
can(SpaceCaslAction.Read, SpaceCaslSubject.Member);
|
||||||
|
}
|
||||||
|
|
||||||
can(SpaceCaslAction.Manage, SpaceCaslSubject.Page);
|
can(SpaceCaslAction.Manage, SpaceCaslSubject.Page);
|
||||||
can(SpaceCaslAction.Manage, SpaceCaslSubject.Share);
|
can(SpaceCaslAction.Manage, SpaceCaslSubject.Share);
|
||||||
return build();
|
return build();
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSpaceReaderAbility() {
|
function buildSpaceReaderAbility(workspaceRole?: string) {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<ISpaceAbility>>(
|
const { can, build } = new AbilityBuilder<MongoAbility<ISpaceAbility>>(
|
||||||
createMongoAbility,
|
createMongoAbility,
|
||||||
);
|
);
|
||||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Settings);
|
|
||||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Member);
|
if (workspaceRole === UserRole.OWNER) {
|
||||||
|
// Workspace owners get manage permissions even with reader space role
|
||||||
|
can(SpaceCaslAction.Manage, SpaceCaslSubject.Settings);
|
||||||
|
can(SpaceCaslAction.Manage, SpaceCaslSubject.Member);
|
||||||
|
} else {
|
||||||
|
can(SpaceCaslAction.Read, SpaceCaslSubject.Settings);
|
||||||
|
can(SpaceCaslAction.Read, SpaceCaslSubject.Member);
|
||||||
|
}
|
||||||
|
|
||||||
|
can(SpaceCaslAction.Read, SpaceCaslSubject.Page);
|
||||||
|
can(SpaceCaslAction.Read, SpaceCaslSubject.Share);
|
||||||
|
return build();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWorkspaceOwnerAbility() {
|
||||||
|
const { can, build } = new AbilityBuilder<MongoAbility<ISpaceAbility>>(
|
||||||
|
createMongoAbility,
|
||||||
|
);
|
||||||
|
can(SpaceCaslAction.Manage, SpaceCaslSubject.Settings);
|
||||||
|
can(SpaceCaslAction.Manage, SpaceCaslSubject.Member);
|
||||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Page);
|
can(SpaceCaslAction.Read, SpaceCaslSubject.Page);
|
||||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Share);
|
can(SpaceCaslAction.Read, SpaceCaslSubject.Share);
|
||||||
return build();
|
return build();
|
||||||
|
|||||||
@@ -279,4 +279,14 @@ export class SpaceMemberService {
|
|||||||
): Promise<PaginationResult<Space>> {
|
): Promise<PaginationResult<Space>> {
|
||||||
return await this.spaceMemberRepo.getUserSpaces(userId, pagination);
|
return await this.spaceMemberRepo.getUserSpaces(userId, pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAllWorkspaceSpaces(
|
||||||
|
workspaceId: string,
|
||||||
|
pagination: PaginationOptions,
|
||||||
|
): Promise<PaginationResult<Space>> {
|
||||||
|
return await this.spaceMemberRepo.getAllWorkspaceSpaces(
|
||||||
|
workspaceId,
|
||||||
|
pagination,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
} from '../casl/interfaces/workspace-ability.type';
|
} from '../casl/interfaces/workspace-ability.type';
|
||||||
import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory';
|
import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory';
|
||||||
import { CreateSpaceDto } from './dto/create-space.dto';
|
import { CreateSpaceDto } from './dto/create-space.dto';
|
||||||
|
import { SpaceRole, UserRole } from '../../common/helpers/types/permission';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('spaces')
|
@Controller('spaces')
|
||||||
@@ -52,7 +53,17 @@ export class SpaceController {
|
|||||||
@Body()
|
@Body()
|
||||||
pagination: PaginationOptions,
|
pagination: PaginationOptions,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
|
if (pagination.includeAllSpaces) {
|
||||||
|
if (user.role !== UserRole.OWNER) {
|
||||||
|
throw new ForbiddenException('Only workspace owners view all spaces');
|
||||||
|
}
|
||||||
|
return this.spaceMemberService.getAllWorkspaceSpaces(
|
||||||
|
workspace.id,
|
||||||
|
pagination,
|
||||||
|
);
|
||||||
|
}
|
||||||
return this.spaceMemberService.getUserSpaces(user.id, pagination);
|
return this.spaceMemberService.getUserSpaces(user.id, pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +93,10 @@ export class SpaceController {
|
|||||||
space.id,
|
space.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
const userSpaceRole = findHighestUserSpaceRole(userSpaceRoles);
|
let userSpaceRole = findHighestUserSpaceRole(userSpaceRoles);
|
||||||
|
if (!userSpaceRole && user.role === UserRole.OWNER) {
|
||||||
|
userSpaceRole = SpaceRole.READER;
|
||||||
|
}
|
||||||
|
|
||||||
const membership = {
|
const membership = {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
IsBoolean,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsPositive,
|
IsPositive,
|
||||||
@@ -23,4 +24,9 @@ export class PaginationOptions {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
query: string;
|
query: string;
|
||||||
|
|
||||||
|
//for space endpoint workspace owners
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
includeAllSpaces?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,4 +263,37 @@ export class SpaceMemberRepo {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAllWorkspaceSpaces(
|
||||||
|
workspaceId: string,
|
||||||
|
pagination: PaginationOptions,
|
||||||
|
) {
|
||||||
|
let query = this.db
|
||||||
|
.selectFrom('spaces')
|
||||||
|
.selectAll()
|
||||||
|
.select((eb) => [this.spaceRepo.withMemberCount(eb)])
|
||||||
|
.where('workspaceId', '=', workspaceId)
|
||||||
|
.orderBy('createdAt', 'asc');
|
||||||
|
|
||||||
|
if (pagination.query) {
|
||||||
|
query = query.where((eb) =>
|
||||||
|
eb(
|
||||||
|
sql`f_unaccent(name)`,
|
||||||
|
'ilike',
|
||||||
|
sql`f_unaccent(${'%' + pagination.query + '%'})`,
|
||||||
|
).or(
|
||||||
|
sql`f_unaccent(description)`,
|
||||||
|
'ilike',
|
||||||
|
sql`f_unaccent(${'%' + pagination.query + '%'})`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = executeWithPagination(query, {
|
||||||
|
page: pagination.page,
|
||||||
|
perPage: pagination.limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: fd34d4183a...3af21def15
@@ -47,15 +47,23 @@ export class FileTaskProcessor extends WorkerHost implements OnModuleDestroy {
|
|||||||
await this.handleFailedJob(job);
|
await this.handleFailedJob(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnWorkerEvent('stalled')
|
@OnWorkerEvent('completed')
|
||||||
async onStalled(job: Job) {
|
async onCompleted(job: Job) {
|
||||||
this.logger.error(
|
this.logger.log(
|
||||||
`Job ${job.name} stalled. . Import Task ID: ${job.data.fileTaskId}.. Job ID: ${job.id}`,
|
`Completed ${job.name} job for File task ID ${job.data.fileTaskId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set failedReason for stalled jobs since it's not automatically set
|
try {
|
||||||
job.failedReason = 'Job stalled and was marked as failed';
|
const fileTask = await this.fileTaskService.getFileTask(
|
||||||
await this.handleFailedJob(job);
|
job.data.fileTaskId,
|
||||||
|
);
|
||||||
|
if (fileTask) {
|
||||||
|
await this.storageService.delete(fileTask.filePath);
|
||||||
|
this.logger.debug(`Deleted imported zip file: ${fileTask.filePath}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`Failed to delete imported zip file:`, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleFailedJob(job: Job) {
|
private async handleFailedJob(job: Job) {
|
||||||
@@ -78,25 +86,6 @@ export class FileTaskProcessor extends WorkerHost implements OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnWorkerEvent('completed')
|
|
||||||
async onCompleted(job: Job) {
|
|
||||||
this.logger.log(
|
|
||||||
`Completed ${job.name} job for File task ID ${job.data.fileTaskId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const fileTask = await this.fileTaskService.getFileTask(
|
|
||||||
job.data.fileTaskId,
|
|
||||||
);
|
|
||||||
if (fileTask) {
|
|
||||||
await this.storageService.delete(fileTask.filePath);
|
|
||||||
this.logger.debug(`Deleted imported zip file: ${fileTask.filePath}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(`Failed to delete imported zip file:`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async onModuleDestroy(): Promise<void> {
|
async onModuleDestroy(): Promise<void> {
|
||||||
if (this.worker) {
|
if (this.worker) {
|
||||||
await this.worker.close();
|
await this.worker.close();
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { formatImportHtml } from '../utils/import-formatter';
|
|||||||
import {
|
import {
|
||||||
buildAttachmentCandidates,
|
buildAttachmentCandidates,
|
||||||
collectMarkdownAndHtmlFiles,
|
collectMarkdownAndHtmlFiles,
|
||||||
|
stripNotionID,
|
||||||
} from '../utils/import.utils';
|
} from '../utils/import.utils';
|
||||||
import { executeTx } from '@docmost/db/utils';
|
import { executeTx } from '@docmost/db/utils';
|
||||||
import { BacklinkRepo } from '@docmost/db/repos/backlink/backlink.repo';
|
import { BacklinkRepo } from '@docmost/db/repos/backlink/backlink.repo';
|
||||||
@@ -159,17 +160,12 @@ export class FileImportTaskService {
|
|||||||
.split(path.sep)
|
.split(path.sep)
|
||||||
.join('/'); // normalize to forward-slashes
|
.join('/'); // normalize to forward-slashes
|
||||||
const ext = path.extname(relPath).toLowerCase();
|
const ext = path.extname(relPath).toLowerCase();
|
||||||
let content = await fs.readFile(absPath, 'utf-8');
|
|
||||||
|
|
||||||
if (ext.toLowerCase() === '.md') {
|
|
||||||
content = await markdownToHtml(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
pagesMap.set(relPath, {
|
pagesMap.set(relPath, {
|
||||||
id: v7(),
|
id: v7(),
|
||||||
slugId: generateSlugId(),
|
slugId: generateSlugId(),
|
||||||
name: path.basename(relPath, ext),
|
name: stripNotionID(path.basename(relPath, ext)),
|
||||||
content,
|
content: '',
|
||||||
parentPageId: null,
|
parentPageId: null,
|
||||||
fileExtension: ext,
|
fileExtension: ext,
|
||||||
filePath: relPath,
|
filePath: relPath,
|
||||||
@@ -254,71 +250,160 @@ export class FileImportTaskService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const pageResults = await Promise.all(
|
// Group pages by level (topological sort for parent-child relationships)
|
||||||
Array.from(pagesMap.values()).map(async (page) => {
|
const pagesByLevel = new Map<number, Array<[string, ImportPageNode]>>();
|
||||||
const htmlContent =
|
const pageLevel = new Map<string, number>();
|
||||||
await this.importAttachmentService.processAttachments({
|
|
||||||
html: page.content,
|
|
||||||
pageRelativePath: page.filePath,
|
|
||||||
extractDir,
|
|
||||||
pageId: page.id,
|
|
||||||
fileTask,
|
|
||||||
attachmentCandidates,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { html, backlinks, pageIcon } = await formatImportHtml({
|
// Calculate levels using BFS
|
||||||
html: htmlContent,
|
const calculateLevels = () => {
|
||||||
currentFilePath: page.filePath,
|
const queue: Array<{ filePath: string; level: number }> = [];
|
||||||
filePathToPageMetaMap: filePathToPageMetaMap,
|
|
||||||
creatorId: fileTask.creatorId,
|
|
||||||
sourcePageId: page.id,
|
|
||||||
workspaceId: fileTask.workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pmState = getProsemirrorContent(
|
// Start with root pages (no parent)
|
||||||
await this.importService.processHTML(html),
|
for (const [filePath, page] of pagesMap.entries()) {
|
||||||
|
if (!page.parentPageId) {
|
||||||
|
queue.push({ filePath, level: 0 });
|
||||||
|
pageLevel.set(filePath, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BFS to assign levels
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const { filePath, level } = queue.shift()!;
|
||||||
|
const currentPage = pagesMap.get(filePath)!;
|
||||||
|
|
||||||
|
// Find children of current page
|
||||||
|
for (const [childFilePath, childPage] of pagesMap.entries()) {
|
||||||
|
if (
|
||||||
|
childPage.parentPageId === currentPage.id &&
|
||||||
|
!pageLevel.has(childFilePath)
|
||||||
|
) {
|
||||||
|
pageLevel.set(childFilePath, level + 1);
|
||||||
|
queue.push({ filePath: childFilePath, level: level + 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group pages by level
|
||||||
|
for (const [filePath, page] of pagesMap.entries()) {
|
||||||
|
const level = pageLevel.get(filePath) || 0;
|
||||||
|
if (!pagesByLevel.has(level)) {
|
||||||
|
pagesByLevel.set(level, []);
|
||||||
|
}
|
||||||
|
pagesByLevel.get(level)!.push([filePath, page]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateLevels();
|
||||||
|
|
||||||
|
if (pagesMap.size < 1) return;
|
||||||
|
|
||||||
|
// Process pages level by level sequentially to respect foreign key constraints
|
||||||
|
const allBacklinks: any[] = [];
|
||||||
|
const validPageIds = new Set<string>();
|
||||||
|
let totalPagesProcessed = 0;
|
||||||
|
|
||||||
|
// Sort levels to process in order
|
||||||
|
const sortedLevels = Array.from(pagesByLevel.keys()).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await executeTx(this.db, async (trx) => {
|
||||||
|
// Process pages level by level sequentially within the transaction
|
||||||
|
for (const level of sortedLevels) {
|
||||||
|
const levelPages = pagesByLevel.get(level)!;
|
||||||
|
|
||||||
|
for (const [filePath, page] of levelPages) {
|
||||||
|
const absPath = path.join(extractDir, filePath);
|
||||||
|
let content = await fs.readFile(absPath, 'utf-8');
|
||||||
|
|
||||||
|
if (page.fileExtension.toLowerCase() === '.md') {
|
||||||
|
content = await markdownToHtml(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlContent =
|
||||||
|
await this.importAttachmentService.processAttachments({
|
||||||
|
html: content,
|
||||||
|
pageRelativePath: page.filePath,
|
||||||
|
extractDir,
|
||||||
|
pageId: page.id,
|
||||||
|
fileTask,
|
||||||
|
attachmentCandidates,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { html, backlinks, pageIcon } = await formatImportHtml({
|
||||||
|
html: htmlContent,
|
||||||
|
currentFilePath: page.filePath,
|
||||||
|
filePathToPageMetaMap: filePathToPageMetaMap,
|
||||||
|
creatorId: fileTask.creatorId,
|
||||||
|
sourcePageId: page.id,
|
||||||
|
workspaceId: fileTask.workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pmState = getProsemirrorContent(
|
||||||
|
await this.importService.processHTML(html),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { title, prosemirrorJson } =
|
||||||
|
this.importService.extractTitleAndRemoveHeading(pmState);
|
||||||
|
|
||||||
|
const insertablePage: InsertablePage = {
|
||||||
|
id: page.id,
|
||||||
|
slugId: page.slugId,
|
||||||
|
title: title || page.name,
|
||||||
|
icon: pageIcon || null,
|
||||||
|
content: prosemirrorJson,
|
||||||
|
textContent: jsonToText(prosemirrorJson),
|
||||||
|
ydoc: await this.importService.createYdoc(prosemirrorJson),
|
||||||
|
position: page.position!,
|
||||||
|
spaceId: fileTask.spaceId,
|
||||||
|
workspaceId: fileTask.workspaceId,
|
||||||
|
creatorId: fileTask.creatorId,
|
||||||
|
lastUpdatedById: fileTask.creatorId,
|
||||||
|
parentPageId: page.parentPageId,
|
||||||
|
};
|
||||||
|
|
||||||
|
await trx.insertInto('pages').values(insertablePage).execute();
|
||||||
|
|
||||||
|
// Track valid page IDs and collect backlinks
|
||||||
|
validPageIds.add(insertablePage.id);
|
||||||
|
allBacklinks.push(...backlinks);
|
||||||
|
totalPagesProcessed++;
|
||||||
|
|
||||||
|
// Log progress periodically
|
||||||
|
if (totalPagesProcessed % 50 === 0) {
|
||||||
|
this.logger.debug(`Processed ${totalPagesProcessed} pages...`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredBacklinks = allBacklinks.filter(
|
||||||
|
({ sourcePageId, targetPageId }) =>
|
||||||
|
validPageIds.has(sourcePageId) && validPageIds.has(targetPageId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { title, prosemirrorJson } =
|
// Insert backlinks in batches
|
||||||
this.importService.extractTitleAndRemoveHeading(pmState);
|
if (filteredBacklinks.length > 0) {
|
||||||
|
const BACKLINK_BATCH_SIZE = 100;
|
||||||
|
for (
|
||||||
|
let i = 0;
|
||||||
|
i < filteredBacklinks.length;
|
||||||
|
i += BACKLINK_BATCH_SIZE
|
||||||
|
) {
|
||||||
|
const backlinkChunk = filteredBacklinks.slice(
|
||||||
|
i,
|
||||||
|
Math.min(i + BACKLINK_BATCH_SIZE, filteredBacklinks.length),
|
||||||
|
);
|
||||||
|
await this.backlinkRepo.insertBacklink(backlinkChunk, trx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const insertablePage: InsertablePage = {
|
this.logger.log(
|
||||||
id: page.id,
|
`Successfully imported ${totalPagesProcessed} pages with ${filteredBacklinks.length} backlinks`,
|
||||||
slugId: page.slugId,
|
);
|
||||||
title: title || page.name,
|
});
|
||||||
icon: pageIcon || null,
|
} catch (error) {
|
||||||
content: prosemirrorJson,
|
this.logger.error('Failed to import files:', error);
|
||||||
textContent: jsonToText(prosemirrorJson),
|
throw new Error(`File import failed: ${error?.['message']}`);
|
||||||
ydoc: await this.importService.createYdoc(prosemirrorJson),
|
}
|
||||||
position: page.position!,
|
|
||||||
spaceId: fileTask.spaceId,
|
|
||||||
workspaceId: fileTask.workspaceId,
|
|
||||||
creatorId: fileTask.creatorId,
|
|
||||||
lastUpdatedById: fileTask.creatorId,
|
|
||||||
parentPageId: page.parentPageId,
|
|
||||||
};
|
|
||||||
|
|
||||||
return { insertablePage, backlinks };
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const insertablePages = pageResults.map((r) => r.insertablePage);
|
|
||||||
const insertableBacklinks = pageResults.flatMap((r) => r.backlinks);
|
|
||||||
|
|
||||||
if (insertablePages.length < 1) return;
|
|
||||||
const validPageIds = new Set(insertablePages.map((row) => row.id));
|
|
||||||
const filteredBacklinks = insertableBacklinks.filter(
|
|
||||||
({ sourcePageId, targetPageId }) =>
|
|
||||||
validPageIds.has(sourcePageId) && validPageIds.has(targetPageId),
|
|
||||||
);
|
|
||||||
|
|
||||||
await executeTx(this.db, async (trx) => {
|
|
||||||
await trx.insertInto('pages').values(insertablePages).execute();
|
|
||||||
|
|
||||||
if (filteredBacklinks.length > 0) {
|
|
||||||
await this.backlinkRepo.insertBacklink(filteredBacklinks, trx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFileTask(fileTaskId: string) {
|
async getFileTask(fileTaskId: string) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ interface DrawioPair {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImportAttachmentService {
|
export class ImportAttachmentService {
|
||||||
private readonly logger = new Logger(ImportAttachmentService.name);
|
private readonly logger = new Logger(ImportAttachmentService.name);
|
||||||
private readonly CONCURRENT_UPLOADS = 1;
|
private readonly CONCURRENT_UPLOADS = 3;
|
||||||
private readonly MAX_RETRIES = 2;
|
private readonly MAX_RETRIES = 2;
|
||||||
private readonly RETRY_DELAY = 2000;
|
private readonly RETRY_DELAY = 2000;
|
||||||
|
|
||||||
|
|||||||
@@ -222,17 +222,40 @@ export function notionFormatter($: CheerioAPI, $root: Cheerio<any>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function unwrapFromParagraph($: CheerioAPI, $node: Cheerio<any>) {
|
export function unwrapFromParagraph($: CheerioAPI, $node: Cheerio<any>) {
|
||||||
// find the nearest <p> or <a> ancestor
|
// Keep track of processed wrappers to avoid infinite loops
|
||||||
let $wrapper = $node.closest('p, a');
|
const processedWrappers = new Set<any>();
|
||||||
|
|
||||||
|
let $wrapper = $node.closest('p, a');
|
||||||
while ($wrapper.length) {
|
while ($wrapper.length) {
|
||||||
// if the wrapper has only our node inside, replace it entirely
|
const wrapperElement = $wrapper.get(0);
|
||||||
if ($wrapper.contents().length === 1) {
|
|
||||||
|
// If we've already processed this wrapper, break to avoid infinite loop
|
||||||
|
if (processedWrappers.has(wrapperElement)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
processedWrappers.add(wrapperElement);
|
||||||
|
|
||||||
|
// Check if the wrapper contains only whitespace and our target node
|
||||||
|
const hasOnlyTargetNode =
|
||||||
|
$wrapper.contents().filter((_, el) => {
|
||||||
|
const $el = $(el);
|
||||||
|
// Skip whitespace-only text nodes. NodeType 3 = text node
|
||||||
|
if (el.nodeType === 3 && !$el.text().trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Return true if this is not our target node
|
||||||
|
return !$el.is($node) && !$node.is($el);
|
||||||
|
}).length === 0;
|
||||||
|
|
||||||
|
if (hasOnlyTargetNode) {
|
||||||
|
// Replace the wrapper entirely with our node
|
||||||
$wrapper.replaceWith($node);
|
$wrapper.replaceWith($node);
|
||||||
} else {
|
} else {
|
||||||
// otherwise just move the node to before the wrapper
|
// Move the node to before the wrapper, preserving other content
|
||||||
$wrapper.before($node);
|
$wrapper.before($node);
|
||||||
}
|
}
|
||||||
|
|
||||||
// look again for any new wrapper around $node
|
// look again for any new wrapper around $node
|
||||||
$wrapper = $node.closest('p, a');
|
$wrapper = $node.closest('p, a');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,3 +64,9 @@ export async function collectMarkdownAndHtmlFiles(
|
|||||||
await walk(dir);
|
await walk(dir);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function stripNotionID(fileName: string): string {
|
||||||
|
// Handle optional separator (space or dash) + 32 alphanumeric chars at end
|
||||||
|
const notionIdPattern = /[ -]?[a-z0-9]{32}$/i;
|
||||||
|
return fileName.replace(notionIdPattern, '').trim();
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "docmost",
|
"name": "docmost",
|
||||||
"homepage": "https://docmost.com",
|
"homepage": "https://docmost.com",
|
||||||
"version": "0.23.1",
|
"version": "0.23.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nx run-many -t build",
|
"build": "nx run-many -t build",
|
||||||
|
|||||||
@@ -2,33 +2,39 @@ import { TableCell as TiptapTableCell } from "@tiptap/extension-table-cell";
|
|||||||
|
|
||||||
export const TableCell = TiptapTableCell.extend({
|
export const TableCell = TiptapTableCell.extend({
|
||||||
name: "tableCell",
|
name: "tableCell",
|
||||||
content: "(paragraph | heading | bulletList | orderedList | taskList | blockquote | callout | image | video | attachment | mathBlock | details | codeBlock)+",
|
content:
|
||||||
|
"(paragraph | heading | bulletList | orderedList | taskList | blockquote | callout | image | video | attachment | mathBlock | details | codeBlock)+",
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
...this.parent?.(),
|
...this.parent?.(),
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
default: null,
|
default: null,
|
||||||
parseHTML: (element) => element.style.backgroundColor || null,
|
parseHTML: (element) =>
|
||||||
|
element.style.backgroundColor ||
|
||||||
|
element.getAttribute("data-background-color") ||
|
||||||
|
null,
|
||||||
renderHTML: (attributes) => {
|
renderHTML: (attributes) => {
|
||||||
if (!attributes.backgroundColor) {
|
if (!attributes.backgroundColor) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
style: `background-color: ${attributes.backgroundColor}`,
|
style: `background-color: ${attributes.backgroundColor}`,
|
||||||
'data-background-color': attributes.backgroundColor,
|
"data-background-color": attributes.backgroundColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
backgroundColorName: {
|
backgroundColorName: {
|
||||||
default: null,
|
default: null,
|
||||||
parseHTML: (element) => element.getAttribute('data-background-color-name') || null,
|
parseHTML: (element) =>
|
||||||
|
element.getAttribute("data-background-color-name") || null,
|
||||||
renderHTML: (attributes) => {
|
renderHTML: (attributes) => {
|
||||||
if (!attributes.backgroundColorName) {
|
if (!attributes.backgroundColorName) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'data-background-color-name': attributes.backgroundColorName.toLowerCase(),
|
"data-background-color-name":
|
||||||
|
attributes.backgroundColorName.toLowerCase(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,36 +2,42 @@ import { TableHeader as TiptapTableHeader } from "@tiptap/extension-table-header
|
|||||||
|
|
||||||
export const TableHeader = TiptapTableHeader.extend({
|
export const TableHeader = TiptapTableHeader.extend({
|
||||||
name: "tableHeader",
|
name: "tableHeader",
|
||||||
content: "(paragraph | heading | bulletList | orderedList | taskList | blockquote | callout | image | video | attachment | mathBlock | details | codeBlock)+",
|
content:
|
||||||
|
"(paragraph | heading | bulletList | orderedList | taskList | blockquote | callout | image | video | attachment | mathBlock | details | codeBlock)+",
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
...this.parent?.(),
|
...this.parent?.(),
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
default: null,
|
default: null,
|
||||||
parseHTML: (element) => element.style.backgroundColor || null,
|
parseHTML: (element) =>
|
||||||
|
element.style.backgroundColor ||
|
||||||
|
element.getAttribute("data-background-color") ||
|
||||||
|
null,
|
||||||
renderHTML: (attributes) => {
|
renderHTML: (attributes) => {
|
||||||
if (!attributes.backgroundColor) {
|
if (!attributes.backgroundColor) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
style: `background-color: ${attributes.backgroundColor}`,
|
style: `background-color: ${attributes.backgroundColor}`,
|
||||||
'data-background-color': attributes.backgroundColor,
|
"data-background-color": attributes.backgroundColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
backgroundColorName: {
|
backgroundColorName: {
|
||||||
default: null,
|
default: null,
|
||||||
parseHTML: (element) => element.getAttribute('data-background-color-name') || null,
|
parseHTML: (element) =>
|
||||||
|
element.getAttribute("data-background-color-name") || null,
|
||||||
renderHTML: (attributes) => {
|
renderHTML: (attributes) => {
|
||||||
if (!attributes.backgroundColorName) {
|
if (!attributes.backgroundColorName) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'data-background-color-name': attributes.backgroundColorName.toLowerCase(),
|
"data-background-color-name":
|
||||||
|
attributes.backgroundColorName.toLowerCase(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+288
-1
@@ -589,8 +589,11 @@ importers:
|
|||||||
specifier: ^7.8.2
|
specifier: ^7.8.2
|
||||||
version: 7.8.2
|
version: 7.8.2
|
||||||
sanitize-filename-ts:
|
sanitize-filename-ts:
|
||||||
specifier: ^1.0.2
|
specifier: 1.0.2
|
||||||
version: 1.0.2
|
version: 1.0.2
|
||||||
|
sharp:
|
||||||
|
specifier: 0.34.3
|
||||||
|
version: 0.34.3
|
||||||
socket.io:
|
socket.io:
|
||||||
specifier: ^4.8.1
|
specifier: ^4.8.1
|
||||||
version: 4.8.1
|
version: 4.8.1
|
||||||
@@ -1781,6 +1784,9 @@ packages:
|
|||||||
'@emnapi/runtime@1.2.0':
|
'@emnapi/runtime@1.2.0':
|
||||||
resolution: {integrity: sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==}
|
resolution: {integrity: sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==}
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.5.0':
|
||||||
|
resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
|
||||||
|
|
||||||
'@emnapi/wasi-threads@1.0.1':
|
'@emnapi/wasi-threads@1.0.1':
|
||||||
resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==}
|
resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==}
|
||||||
|
|
||||||
@@ -2283,6 +2289,128 @@ packages:
|
|||||||
'@iconify/utils@3.0.1':
|
'@iconify/utils@3.0.1':
|
||||||
resolution: {integrity: sha512-A78CUEnFGX8I/WlILxJCuIJXloL0j/OJ9PSchPAfCargEIKmUBWvvEMmKWB5oONwiUqlNt+5eRufdkLxeHIWYw==}
|
resolution: {integrity: sha512-A78CUEnFGX8I/WlILxJCuIJXloL0j/OJ9PSchPAfCargEIKmUBWvvEMmKWB5oONwiUqlNt+5eRufdkLxeHIWYw==}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.34.3':
|
||||||
|
resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.34.3':
|
||||||
|
resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.2.0':
|
||||||
|
resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.2.0':
|
||||||
|
resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.2.0':
|
||||||
|
resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.2.0':
|
||||||
|
resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-ppc64@1.2.0':
|
||||||
|
resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.2.0':
|
||||||
|
resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.2.0':
|
||||||
|
resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.0':
|
||||||
|
resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.2.0':
|
||||||
|
resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.34.3':
|
||||||
|
resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.34.3':
|
||||||
|
resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-ppc64@0.34.3':
|
||||||
|
resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.34.3':
|
||||||
|
resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.34.3':
|
||||||
|
resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.34.3':
|
||||||
|
resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.34.3':
|
||||||
|
resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.34.3':
|
||||||
|
resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [wasm32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.34.3':
|
||||||
|
resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.34.3':
|
||||||
|
resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.34.3':
|
||||||
|
resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@inquirer/checkbox@4.1.2':
|
'@inquirer/checkbox@4.1.2':
|
||||||
resolution: {integrity: sha512-PL9ixC5YsPXzXhAZFUPmkXGxfgjkdfZdPEPPmt4kFwQ4LBMDG9n/nHXYRGGZSKZJs+d1sGKWgS2GiPzVRKUdtQ==}
|
resolution: {integrity: sha512-PL9ixC5YsPXzXhAZFUPmkXGxfgjkdfZdPEPPmt4kFwQ4LBMDG9n/nHXYRGGZSKZJs+d1sGKWgS2GiPzVRKUdtQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -5265,10 +5393,17 @@ packages:
|
|||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
color-string@1.9.1:
|
||||||
|
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||||
|
|
||||||
color-support@1.1.3:
|
color-support@1.1.3:
|
||||||
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
|
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
color@4.2.3:
|
||||||
|
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||||
|
engines: {node: '>=12.5.0'}
|
||||||
|
|
||||||
columnify@1.6.0:
|
columnify@1.6.0:
|
||||||
resolution: {integrity: sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==}
|
resolution: {integrity: sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
@@ -5733,6 +5868,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
detect-libc@2.0.4:
|
||||||
|
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
detect-newline@3.1.0:
|
detect-newline@3.1.0:
|
||||||
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
|
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -6595,6 +6734,9 @@ packages:
|
|||||||
is-arrayish@0.2.1:
|
is-arrayish@0.2.1:
|
||||||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||||
|
|
||||||
|
is-arrayish@0.3.4:
|
||||||
|
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
|
||||||
|
|
||||||
is-async-function@2.0.0:
|
is-async-function@2.0.0:
|
||||||
resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==}
|
resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -8783,6 +8925,10 @@ packages:
|
|||||||
shallowequal@1.1.0:
|
shallowequal@1.1.0:
|
||||||
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
|
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
|
||||||
|
|
||||||
|
sharp@0.34.3:
|
||||||
|
resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -8814,6 +8960,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
simple-swizzle@0.2.4:
|
||||||
|
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
|
||||||
|
|
||||||
sisteransi@1.0.5:
|
sisteransi@1.0.5:
|
||||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||||
|
|
||||||
@@ -11664,6 +11813,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.5.0':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@emnapi/wasi-threads@1.0.1':
|
'@emnapi/wasi-threads@1.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -12124,6 +12278,92 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.34.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.34.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.2.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.2.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.2.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.2.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-ppc64@1.2.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.2.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.2.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.2.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.34.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.34.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-ppc64@0.34.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-ppc64': 1.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.34.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.34.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.34.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.34.3':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.34.3':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/runtime': 1.5.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.34.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.34.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.34.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@inquirer/checkbox@4.1.2(@types/node@22.13.4)':
|
'@inquirer/checkbox@4.1.2(@types/node@22.13.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@inquirer/core': 10.1.7(@types/node@22.13.4)
|
'@inquirer/core': 10.1.7(@types/node@22.13.4)
|
||||||
@@ -15487,8 +15727,18 @@ snapshots:
|
|||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
color-string@1.9.1:
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.4
|
||||||
|
simple-swizzle: 0.2.4
|
||||||
|
|
||||||
color-support@1.1.3: {}
|
color-support@1.1.3: {}
|
||||||
|
|
||||||
|
color@4.2.3:
|
||||||
|
dependencies:
|
||||||
|
color-convert: 2.0.1
|
||||||
|
color-string: 1.9.1
|
||||||
|
|
||||||
columnify@1.6.0:
|
columnify@1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
@@ -15957,6 +16207,8 @@ snapshots:
|
|||||||
|
|
||||||
detect-libc@2.0.3: {}
|
detect-libc@2.0.3: {}
|
||||||
|
|
||||||
|
detect-libc@2.0.4: {}
|
||||||
|
|
||||||
detect-newline@3.1.0: {}
|
detect-newline@3.1.0: {}
|
||||||
|
|
||||||
detect-node-es@1.1.0: {}
|
detect-node-es@1.1.0: {}
|
||||||
@@ -17053,6 +17305,8 @@ snapshots:
|
|||||||
|
|
||||||
is-arrayish@0.2.1: {}
|
is-arrayish@0.2.1: {}
|
||||||
|
|
||||||
|
is-arrayish@0.3.4: {}
|
||||||
|
|
||||||
is-async-function@2.0.0:
|
is-async-function@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
@@ -19622,6 +19876,35 @@ snapshots:
|
|||||||
|
|
||||||
shallowequal@1.1.0: {}
|
shallowequal@1.1.0: {}
|
||||||
|
|
||||||
|
sharp@0.34.3:
|
||||||
|
dependencies:
|
||||||
|
color: 4.2.3
|
||||||
|
detect-libc: 2.0.4
|
||||||
|
semver: 7.7.2
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-darwin-arm64': 0.34.3
|
||||||
|
'@img/sharp-darwin-x64': 0.34.3
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.2.0
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.2.0
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.2.0
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.2.0
|
||||||
|
'@img/sharp-libvips-linux-ppc64': 1.2.0
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.2.0
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.2.0
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.2.0
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.2.0
|
||||||
|
'@img/sharp-linux-arm': 0.34.3
|
||||||
|
'@img/sharp-linux-arm64': 0.34.3
|
||||||
|
'@img/sharp-linux-ppc64': 0.34.3
|
||||||
|
'@img/sharp-linux-s390x': 0.34.3
|
||||||
|
'@img/sharp-linux-x64': 0.34.3
|
||||||
|
'@img/sharp-linuxmusl-arm64': 0.34.3
|
||||||
|
'@img/sharp-linuxmusl-x64': 0.34.3
|
||||||
|
'@img/sharp-wasm32': 0.34.3
|
||||||
|
'@img/sharp-win32-arm64': 0.34.3
|
||||||
|
'@img/sharp-win32-ia32': 0.34.3
|
||||||
|
'@img/sharp-win32-x64': 0.34.3
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
shebang-regex: 3.0.0
|
shebang-regex: 3.0.0
|
||||||
@@ -19649,6 +19932,10 @@ snapshots:
|
|||||||
|
|
||||||
signal-exit@4.1.0: {}
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
|
simple-swizzle@0.2.4:
|
||||||
|
dependencies:
|
||||||
|
is-arrayish: 0.3.4
|
||||||
|
|
||||||
sisteransi@1.0.5: {}
|
sisteransi@1.0.5: {}
|
||||||
|
|
||||||
slash@3.0.0: {}
|
slash@3.0.0: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user