mirror of
https://github.com/docmost/docmost.git
synced 2026-05-14 04:24:04 +08:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb83d12c8b | |||
| 0f29eb8842 | |||
| 08135a2fba | |||
| d92a94244f | |||
| 5012a68d85 | |||
| 5a3377790e | |||
| 3b85f4b616 | |||
| cb2a0398c7 | |||
| 95b7be61df | |||
| b0c557272d | |||
| dddfd48934 | |||
| aa6eec754e | |||
| 97a7701f5d | |||
| b97eb85d05 | |||
| 1615e0f4ad | |||
| 1cb2535de3 | |||
| 83bc273cb0 | |||
| c7beaa3742 | |||
| 4a228e5a51 | |||
| edff375476 | |||
| 95016b2bfc | |||
| ca83712364 | |||
| 39550fe906 |
+4
-1
@@ -43,4 +43,7 @@ POSTMARK_TOKEN=
|
|||||||
# for custom drawio server
|
# for custom drawio server
|
||||||
DRAWIO_URL=
|
DRAWIO_URL=
|
||||||
|
|
||||||
DISABLE_TELEMETRY=false
|
DISABLE_TELEMETRY=false
|
||||||
|
|
||||||
|
# Enable debug logging in production (default: false)
|
||||||
|
DEBUG_MODE=false
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.22.0",
|
"version": "0.22.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { useTrackOrigin } from "@/hooks/use-track-origin";
|
|||||||
import SpacesPage from "@/pages/spaces/spaces.tsx";
|
import SpacesPage from "@/pages/spaces/spaces.tsx";
|
||||||
import { MfaChallengePage } from "@/ee/mfa/pages/mfa-challenge-page";
|
import { MfaChallengePage } from "@/ee/mfa/pages/mfa-challenge-page";
|
||||||
import { MfaSetupRequiredPage } from "@/ee/mfa/pages/mfa-setup-required-page";
|
import { MfaSetupRequiredPage } from "@/ee/mfa/pages/mfa-setup-required-page";
|
||||||
import SpaceTrash from "@/pages/space/trash.tsx";
|
import SpaceTrash from "@/pages/space/space-trash.tsx";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -50,10 +50,7 @@ export default function App() {
|
|||||||
<Route path={"/forgot-password"} element={<ForgotPassword />} />
|
<Route path={"/forgot-password"} element={<ForgotPassword />} />
|
||||||
<Route path={"/password-reset"} element={<PasswordReset />} />
|
<Route path={"/password-reset"} element={<PasswordReset />} />
|
||||||
<Route path={"/login/mfa"} element={<MfaChallengePage />} />
|
<Route path={"/login/mfa"} element={<MfaChallengePage />} />
|
||||||
<Route
|
<Route path={"/login/mfa/setup"} element={<MfaSetupRequiredPage />} />
|
||||||
path={"/login/mfa/setup"}
|
|
||||||
element={<MfaSetupRequiredPage />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!isCloud() && (
|
{!isCloud() && (
|
||||||
<Route path={"/setup/register"} element={<SetupWorkspace />} />
|
<Route path={"/setup/register"} element={<SetupWorkspace />} />
|
||||||
|
|||||||
@@ -29,19 +29,22 @@ export default function ExportModal({
|
|||||||
}: ExportModalProps) {
|
}: ExportModalProps) {
|
||||||
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
|
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
|
||||||
const [includeChildren, setIncludeChildren] = useState<boolean>(false);
|
const [includeChildren, setIncludeChildren] = useState<boolean>(false);
|
||||||
const [includeAttachments, setIncludeAttachments] = useState<boolean>(true);
|
const [includeAttachments, setIncludeAttachments] = useState<boolean>(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
try {
|
try {
|
||||||
if (type === "page") {
|
if (type === "page") {
|
||||||
await exportPage({ pageId: id, format, includeChildren });
|
await exportPage({
|
||||||
|
pageId: id,
|
||||||
|
format,
|
||||||
|
includeChildren,
|
||||||
|
includeAttachments,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (type === "space") {
|
if (type === "space") {
|
||||||
await exportSpace({ spaceId: id, format, includeAttachments });
|
await exportSpace({ spaceId: id, format, includeAttachments });
|
||||||
}
|
}
|
||||||
setIncludeChildren(false);
|
|
||||||
setIncludeAttachments(true);
|
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
@@ -96,6 +99,18 @@ export default function ExportModal({
|
|||||||
checked={includeChildren}
|
checked={includeChildren}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
<Group justify="space-between" wrap="nowrap" mt="md">
|
||||||
|
<div>
|
||||||
|
<Text size="md">{t("Include attachments")}</Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={(event) =>
|
||||||
|
setIncludeAttachments(event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
checked={includeAttachments}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Group, Text } from "@mantine/core";
|
import { Group, Text } from "@mantine/core";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { User } from "server/dist/database/types/entity.types";
|
import { IUser } from '@/features/user/types/user.types.ts';
|
||||||
|
|
||||||
interface UserInfoProps {
|
interface UserInfoProps {
|
||||||
user: User;
|
user: Partial<IUser>;
|
||||||
size?: string;
|
size?: string;
|
||||||
}
|
}
|
||||||
export function UserInfo({ user, size }: UserInfoProps) {
|
export function UserInfo({ user, size }: UserInfoProps) {
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { useState, useCallback, useRef } from "react";
|
||||||
|
import { useAiGenerateStreamMutation } from "@/ee/ai/queries/ai-query.ts";
|
||||||
|
import { AiGenerateDto } from "@/ee/ai/types/ai.types.ts";
|
||||||
|
|
||||||
|
export function useAiStream() {
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
const mutation = useAiGenerateStreamMutation();
|
||||||
|
|
||||||
|
const startStream = useCallback(
|
||||||
|
async (data: AiGenerateDto) => {
|
||||||
|
setContent("");
|
||||||
|
setIsStreaming(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = await mutation.mutateAsync({
|
||||||
|
...data,
|
||||||
|
onChunk: (chunk) => {
|
||||||
|
setContent((prev) => prev + chunk.content);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("AI stream error:", error);
|
||||||
|
setIsStreaming(false);
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
setIsStreaming(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
abortControllerRef.current = controller;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to start stream:", error);
|
||||||
|
setIsStreaming(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mutation]
|
||||||
|
);
|
||||||
|
|
||||||
|
const stopStream = useCallback(() => {
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
setIsStreaming(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetContent = useCallback(() => {
|
||||||
|
setContent("");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
isStreaming,
|
||||||
|
startStream,
|
||||||
|
stopStream,
|
||||||
|
resetContent,
|
||||||
|
isLoading: mutation.isPending,
|
||||||
|
error: mutation.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
UseMutationResult,
|
||||||
|
useQuery,
|
||||||
|
UseQueryResult,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
generateAiContent,
|
||||||
|
generateAiContentStream,
|
||||||
|
getAiConfig,
|
||||||
|
} from "@/ee/ai/services/ai-service.ts";
|
||||||
|
import {
|
||||||
|
AiConfigResponse,
|
||||||
|
AiContentResponse,
|
||||||
|
AiGenerateDto,
|
||||||
|
AiStreamChunk,
|
||||||
|
AiStreamError,
|
||||||
|
} from "@/ee/ai/types/ai.types.ts";
|
||||||
|
|
||||||
|
export function useAiGenerateMutation(): UseMutationResult<
|
||||||
|
AiContentResponse,
|
||||||
|
Error,
|
||||||
|
AiGenerateDto
|
||||||
|
> {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: AiGenerateDto) => generateAiContent(data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StreamCallbacks {
|
||||||
|
onChunk: (chunk: AiStreamChunk) => void;
|
||||||
|
onError?: (error: AiStreamError) => void;
|
||||||
|
onComplete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAiGenerateStreamMutation(): UseMutationResult<
|
||||||
|
AbortController,
|
||||||
|
Error,
|
||||||
|
AiGenerateDto & StreamCallbacks
|
||||||
|
> {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ onChunk, onError, onComplete, ...data }) =>
|
||||||
|
generateAiContentStream(data, onChunk, onError, onComplete),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import api from "@/lib/api-client.ts";
|
||||||
|
import {
|
||||||
|
AiGenerateDto,
|
||||||
|
AiContentResponse,
|
||||||
|
AiStreamChunk,
|
||||||
|
AiStreamError,
|
||||||
|
} from "@/ee/ai/types/ai.types.ts";
|
||||||
|
|
||||||
|
export async function generateAiContent(
|
||||||
|
data: AiGenerateDto,
|
||||||
|
): Promise<AiContentResponse> {
|
||||||
|
const req = await api.post<AiContentResponse>("/ai/generate", data);
|
||||||
|
return req.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateAiContentStream(
|
||||||
|
data: AiGenerateDto,
|
||||||
|
onChunk: (chunk: AiStreamChunk) => void,
|
||||||
|
onError?: (error: AiStreamError) => void,
|
||||||
|
onComplete?: () => void,
|
||||||
|
): Promise<AbortController> {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/ai/generate/stream", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
signal: abortController.signal,
|
||||||
|
credentials: "include", // This ensures cookies are sent, matching axios withCredentials
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error("Response body is not readable");
|
||||||
|
}
|
||||||
|
|
||||||
|
const processStream = async () => {
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
const lines = chunk.split("\n");
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith("data: ")) {
|
||||||
|
const data = line.slice(6);
|
||||||
|
if (data === "[DONE]") {
|
||||||
|
onComplete?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
if (parsed.error) {
|
||||||
|
onError?.(parsed);
|
||||||
|
} else {
|
||||||
|
onChunk(parsed);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore parse errors for incomplete chunks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name !== "AbortError") {
|
||||||
|
onError?.({ error: error.message });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
processStream();
|
||||||
|
} catch (error) {
|
||||||
|
onError?.({ error: error.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
return abortController;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
export enum AiAction {
|
||||||
|
IMPROVE_WRITING = "improve_writing",
|
||||||
|
FIX_SPELLING_GRAMMAR = "fix_spelling_grammar",
|
||||||
|
MAKE_SHORTER = "make_shorter",
|
||||||
|
MAKE_LONGER = "make_longer",
|
||||||
|
SIMPLIFY = "simplify",
|
||||||
|
CHANGE_TONE = "change_tone",
|
||||||
|
SUMMARIZE = "summarize",
|
||||||
|
CONTINUE_WRITING = "continue_writing",
|
||||||
|
TRANSLATE = "translate",
|
||||||
|
CUSTOM = "custom",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiGenerateDto {
|
||||||
|
action?: AiAction;
|
||||||
|
content: string;
|
||||||
|
prompt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiContentResponse {
|
||||||
|
content: string;
|
||||||
|
usage?: {
|
||||||
|
promptTokens: number;
|
||||||
|
completionTokens: number;
|
||||||
|
totalTokens: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiConfigResponse {
|
||||||
|
configured: boolean;
|
||||||
|
availableActions: AiAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiStreamChunk {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiStreamError {
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ function CommentListWithTabs() {
|
|||||||
const spaceRules = space?.membership?.permissions;
|
const spaceRules = space?.membership?.permissions;
|
||||||
const spaceAbility = useSpaceAbility(spaceRules);
|
const spaceAbility = useSpaceAbility(spaceRules);
|
||||||
|
|
||||||
|
|
||||||
const canComment: boolean = spaceAbility.can(
|
const canComment: boolean = spaceAbility.can(
|
||||||
SpaceCaslAction.Manage,
|
SpaceCaslAction.Manage,
|
||||||
SpaceCaslSubject.Page
|
SpaceCaslSubject.Page
|
||||||
@@ -179,6 +180,17 @@ function CommentListWithTabs() {
|
|||||||
userSpaceRole={space?.membership?.role}
|
userSpaceRole={space?.membership?.role}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{canComment && (
|
||||||
|
<>
|
||||||
|
<Divider my={4} />
|
||||||
|
<CommentEditorWithActions
|
||||||
|
commentId={comment.id}
|
||||||
|
onSave={handleAddReply}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
import { Modal, Button, Group, Text, Select, Switch } from "@mantine/core";
|
|
||||||
import { exportPage } from "@/features/page/services/page-service.ts";
|
|
||||||
import { useState } from "react";
|
|
||||||
import * as React from "react";
|
|
||||||
import { ExportFormat } from "@/features/page/types/page.types.ts";
|
|
||||||
import { notifications } from "@mantine/notifications";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
interface PageExportModalProps {
|
|
||||||
pageId: string;
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PageExportModal({
|
|
||||||
pageId,
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
}: PageExportModalProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
|
|
||||||
|
|
||||||
const handleExport = async () => {
|
|
||||||
try {
|
|
||||||
await exportPage({ pageId: pageId, format });
|
|
||||||
onClose();
|
|
||||||
} catch (err) {
|
|
||||||
notifications.show({
|
|
||||||
message: t("Export failed:") + err.response?.data.message,
|
|
||||||
color: "red",
|
|
||||||
});
|
|
||||||
console.error("export error", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (format: ExportFormat) => {
|
|
||||||
setFormat(format);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal.Root
|
|
||||||
opened={open}
|
|
||||||
onClose={onClose}
|
|
||||||
size={500}
|
|
||||||
padding="xl"
|
|
||||||
yOffset="10vh"
|
|
||||||
xOffset={0}
|
|
||||||
mah={400}
|
|
||||||
>
|
|
||||||
<Modal.Overlay />
|
|
||||||
<Modal.Content style={{ overflow: "hidden" }}>
|
|
||||||
<Modal.Header py={0}>
|
|
||||||
<Modal.Title fw={500}>{t("Export page")}</Modal.Title>
|
|
||||||
<Modal.CloseButton />
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Body>
|
|
||||||
<Group justify="space-between" wrap="nowrap">
|
|
||||||
<div>
|
|
||||||
<Text size="md">{t("Format")}</Text>
|
|
||||||
</div>
|
|
||||||
<ExportFormatSelection format={format} onChange={handleChange} />
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group justify="space-between" wrap="nowrap" pt="md">
|
|
||||||
<div>
|
|
||||||
<Text size="md">{t("Include subpages")}</Text>
|
|
||||||
</div>
|
|
||||||
<Switch defaultChecked />
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group justify="center" mt="md">
|
|
||||||
<Button onClick={onClose} variant="default">
|
|
||||||
{t("Cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleExport}>{t("Export")}</Button>
|
|
||||||
</Group>
|
|
||||||
</Modal.Body>
|
|
||||||
</Modal.Content>
|
|
||||||
</Modal.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExportFormatSelection {
|
|
||||||
format: ExportFormat;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
}
|
|
||||||
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
data={[
|
|
||||||
{ value: "markdown", label: "Markdown" },
|
|
||||||
{ value: "html", label: "HTML" },
|
|
||||||
]}
|
|
||||||
defaultValue={format}
|
|
||||||
onChange={onChange}
|
|
||||||
styles={{ wrapper: { maxWidth: 120 } }}
|
|
||||||
comboboxProps={{ width: "120" }}
|
|
||||||
allowDeselect={false}
|
|
||||||
withCheckIcon={false}
|
|
||||||
aria-label={t("Select export format")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -126,8 +126,9 @@ export function useUpdatePageMutation() {
|
|||||||
export function useRemovePageMutation() {
|
export function useRemovePageMutation() {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (pageId: string) => deletePage(pageId, false),
|
mutationFn: (pageId: string) => deletePage(pageId, false),
|
||||||
onSuccess: () => {
|
onSuccess: (_, pageId) => {
|
||||||
notifications.show({ message: "Page moved to trash" });
|
notifications.show({ message: "Page moved to trash" });
|
||||||
|
invalidateOnDeletePage(pageId);
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
predicate: (item) =>
|
predicate: (item) =>
|
||||||
["trash-list"].includes(item.queryKey[0] as string),
|
["trash-list"].includes(item.queryKey[0] as string),
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ import { UserInfo } from "@/components/common/user-info.tsx";
|
|||||||
import Paginate from "@/components/common/paginate.tsx";
|
import Paginate from "@/components/common/paginate.tsx";
|
||||||
import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search";
|
import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search";
|
||||||
|
|
||||||
export default function SpaceTrash() {
|
export default function Trash() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { spaceSlug } = useParams();
|
const { spaceSlug } = useParams();
|
||||||
const { page, setPage } = usePaginateAndSearch();
|
const { page, setPage } = usePaginateAndSearch();
|
||||||
@@ -79,6 +79,7 @@ export interface IExportPageParams {
|
|||||||
pageId: string;
|
pageId: string;
|
||||||
format: ExportFormat;
|
format: ExportFormat;
|
||||||
includeChildren?: boolean;
|
includeChildren?: boolean;
|
||||||
|
includeAttachments?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ExportFormat {
|
export enum ExportFormat {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import Trash from "@/features/page/trash/components/trash.tsx";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
||||||
|
import { useSpaceAbility } from "@/features/space/permissions/use-space-ability.ts";
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
SpaceCaslAction,
|
||||||
|
SpaceCaslSubject,
|
||||||
|
} from "@/features/space/permissions/permissions.type.ts";
|
||||||
|
|
||||||
|
export default function SpaceTrash() {
|
||||||
|
const { spaceSlug } = useParams();
|
||||||
|
const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
|
||||||
|
|
||||||
|
const spaceRules = space?.membership?.permissions;
|
||||||
|
const spaceAbility = useSpaceAbility(spaceRules);
|
||||||
|
|
||||||
|
if (!space) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Trash />;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.22.0",
|
"version": "0.22.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -70,6 +70,7 @@
|
|||||||
"nanoid": "3.3.11",
|
"nanoid": "3.3.11",
|
||||||
"nestjs-kysely": "^1.2.0",
|
"nestjs-kysely": "^1.2.0",
|
||||||
"nodemailer": "^7.0.3",
|
"nodemailer": "^7.0.3",
|
||||||
|
"openai": "^5.12.2",
|
||||||
"openid-client": "^5.7.1",
|
"openid-client": "^5.7.1",
|
||||||
"otpauth": "^9.4.0",
|
"otpauth": "^9.4.0",
|
||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ export class InternalLogFilter extends ConsoleLogger {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.allowedLogLevels =
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
process.env.NODE_ENV === 'production'
|
const isDebugMode = process.env.DEBUG_MODE === 'true';
|
||||||
? ['log', 'error', 'fatal']
|
|
||||||
: ['log', 'debug', 'verbose', 'warn', 'error', 'fatal'];
|
if (isProduction && !isDebugMode) {
|
||||||
|
this.allowedLogLevels = ['log', 'error', 'fatal'];
|
||||||
|
} else {
|
||||||
|
this.allowedLogLevels = ['log', 'debug', 'verbose', 'warn', 'error', 'fatal'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private isLogLevelAllowed(level: string): boolean {
|
private isLogLevelAllowed(level: string): boolean {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class CommentController {
|
|||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
const page = await this.pageRepo.findById(createCommentDto.pageId);
|
const page = await this.pageRepo.findById(createCommentDto.pageId);
|
||||||
if (!page) {
|
if (!page || page.deletedAt) {
|
||||||
throw new NotFoundException('Page not found');
|
throw new NotFoundException('Page not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,10 @@ export class CommentController {
|
|||||||
throw new NotFoundException('Comment not found');
|
throw new NotFoundException('Comment not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const ability = await this.spaceAbility.createForUser(user, comment.spaceId);
|
const ability = await this.spaceAbility.createForUser(
|
||||||
|
user,
|
||||||
|
comment.spaceId,
|
||||||
|
);
|
||||||
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
|
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
|
||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IsOptional, IsString } from 'class-validator';
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class DeletedPageDto {
|
export class DeletedPageDto {
|
||||||
@IsOptional()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
}
|
}
|
||||||
@@ -194,7 +194,7 @@ export class PageController {
|
|||||||
deletedPageDto.spaceId,
|
deletedPageDto.spaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
|
if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) {
|
||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ export class PageService {
|
|||||||
.selectFrom('pages')
|
.selectFrom('pages')
|
||||||
.select(['position'])
|
.select(['position'])
|
||||||
.where('spaceId', '=', spaceId)
|
.where('spaceId', '=', spaceId)
|
||||||
.orderBy('position', 'desc')
|
.where('deletedAt', 'is', null)
|
||||||
|
.orderBy('position', (ob) => ob.collate('C').desc())
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (parentPageId) {
|
if (parentPageId) {
|
||||||
@@ -190,7 +191,7 @@ export class PageService {
|
|||||||
'deletedAt',
|
'deletedAt',
|
||||||
])
|
])
|
||||||
.select((eb) => this.pageRepo.withHasChildren(eb))
|
.select((eb) => this.pageRepo.withHasChildren(eb))
|
||||||
.orderBy('position', 'asc')
|
.orderBy('position', (ob) => ob.collate('C').asc())
|
||||||
.where('deletedAt', 'is', null)
|
.where('deletedAt', 'is', null)
|
||||||
.where('spaceId', '=', spaceId);
|
.where('spaceId', '=', spaceId);
|
||||||
|
|
||||||
@@ -261,35 +262,7 @@ export class PageService {
|
|||||||
|
|
||||||
if (isDuplicateInSameSpace) {
|
if (isDuplicateInSameSpace) {
|
||||||
// For duplicate in same space, position right after the original page
|
// For duplicate in same space, position right after the original page
|
||||||
let siblingQuery = this.db
|
nextPosition = generateJitteredKeyBetween(rootPage.position, null);
|
||||||
.selectFrom('pages')
|
|
||||||
.select(['position'])
|
|
||||||
.where('spaceId', '=', rootPage.spaceId)
|
|
||||||
.where('position', '>', rootPage.position);
|
|
||||||
|
|
||||||
if (rootPage.parentPageId) {
|
|
||||||
siblingQuery = siblingQuery.where(
|
|
||||||
'parentPageId',
|
|
||||||
'=',
|
|
||||||
rootPage.parentPageId,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
siblingQuery = siblingQuery.where('parentPageId', 'is', null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextSibling = await siblingQuery
|
|
||||||
.orderBy('position', 'asc')
|
|
||||||
.limit(1)
|
|
||||||
.executeTakeFirst();
|
|
||||||
|
|
||||||
if (nextSibling) {
|
|
||||||
nextPosition = generateJitteredKeyBetween(
|
|
||||||
rootPage.position,
|
|
||||||
nextSibling.position,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
nextPosition = generateJitteredKeyBetween(rootPage.position, null);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// For copy to different space, position at the end
|
// For copy to different space, position at the end
|
||||||
nextPosition = await this.nextPagePosition(spaceId);
|
nextPosition = await this.nextPagePosition(spaceId);
|
||||||
@@ -434,25 +407,35 @@ export class PageService {
|
|||||||
attachment.id,
|
attachment.id,
|
||||||
newAttachmentId,
|
newAttachmentId,
|
||||||
);
|
);
|
||||||
await this.storageService.copy(attachment.filePath, newPathFile);
|
|
||||||
await this.db
|
try {
|
||||||
.insertInto('attachments')
|
await this.storageService.copy(attachment.filePath, newPathFile);
|
||||||
.values({
|
|
||||||
id: newAttachmentId,
|
await this.db
|
||||||
type: attachment.type,
|
.insertInto('attachments')
|
||||||
filePath: newPathFile,
|
.values({
|
||||||
fileName: attachment.fileName,
|
id: newAttachmentId,
|
||||||
fileSize: attachment.fileSize,
|
type: attachment.type,
|
||||||
mimeType: attachment.mimeType,
|
filePath: newPathFile,
|
||||||
fileExt: attachment.fileExt,
|
fileName: attachment.fileName,
|
||||||
creatorId: attachment.creatorId,
|
fileSize: attachment.fileSize,
|
||||||
workspaceId: attachment.workspaceId,
|
mimeType: attachment.mimeType,
|
||||||
pageId: newPageId,
|
fileExt: attachment.fileExt,
|
||||||
spaceId: spaceId,
|
creatorId: attachment.creatorId,
|
||||||
})
|
workspaceId: attachment.workspaceId,
|
||||||
.execute();
|
pageId: newPageId,
|
||||||
|
spaceId: spaceId,
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
`Duplicate page: failed to copy attachment ${attachment.id}`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
// Continue with other attachments even if one fails
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.log(err);
|
this.logger.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export class TrashCleanupService {
|
|||||||
@Interval('trash-cleanup', 24 * 60 * 60 * 1000) // every 24 hours
|
@Interval('trash-cleanup', 24 * 60 * 60 * 1000) // every 24 hours
|
||||||
async cleanupOldTrash() {
|
async cleanupOldTrash() {
|
||||||
try {
|
try {
|
||||||
this.logger.log('Starting trash cleanup job');
|
this.logger.debug('Starting trash cleanup job');
|
||||||
|
|
||||||
const retentionDate = new Date();
|
const retentionDate = new Date();
|
||||||
retentionDate.setDate(retentionDate.getDate() - this.RETENTION_DAYS);
|
retentionDate.setDate(retentionDate.getDate() - this.RETENTION_DAYS);
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export class SearchService {
|
|||||||
.$if(Boolean(searchParams.creatorId), (qb) =>
|
.$if(Boolean(searchParams.creatorId), (qb) =>
|
||||||
qb.where('creatorId', '=', searchParams.creatorId),
|
qb.where('creatorId', '=', searchParams.creatorId),
|
||||||
)
|
)
|
||||||
|
.where('deletedAt', 'is', null)
|
||||||
.orderBy('rank', 'desc')
|
.orderBy('rank', 'desc')
|
||||||
.limit(searchParams.limit | 20)
|
.limit(searchParams.limit | 20)
|
||||||
.offset(searchParams.offset || 0);
|
.offset(searchParams.offset || 0);
|
||||||
@@ -191,6 +192,7 @@ export class SearchService {
|
|||||||
sql`LOWER(f_unaccent(${`%${query}%`}))`,
|
sql`LOWER(f_unaccent(${`%${query}%`}))`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.where('deletedAt', 'is', null)
|
||||||
.where('workspaceId', '=', workspaceId)
|
.where('workspaceId', '=', workspaceId)
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
|
|
||||||
|
|||||||
@@ -108,12 +108,12 @@ export class ShareService {
|
|||||||
includeCreator: true,
|
includeCreator: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
page.content = await this.updatePublicAttachments(page);
|
if (!page || page.deletedAt) {
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
throw new NotFoundException('Shared page not found');
|
throw new NotFoundException('Shared page not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
page.content = await this.updatePublicAttachments(page);
|
||||||
|
|
||||||
return { page, share };
|
return { page, share };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +132,7 @@ export class ShareService {
|
|||||||
sql`0`.as('level'),
|
sql`0`.as('level'),
|
||||||
])
|
])
|
||||||
.where(isValidUUID(pageId) ? 'id' : 'slugId', '=', pageId)
|
.where(isValidUUID(pageId) ? 'id' : 'slugId', '=', pageId)
|
||||||
|
.where('deletedAt', 'is', null)
|
||||||
.unionAll((union) =>
|
.unionAll((union) =>
|
||||||
union
|
union
|
||||||
.selectFrom('pages as p')
|
.selectFrom('pages as p')
|
||||||
@@ -144,7 +145,8 @@ export class ShareService {
|
|||||||
// Increase the level by 1 for each ancestor.
|
// Increase the level by 1 for each ancestor.
|
||||||
sql`ph.level + 1`.as('level'),
|
sql`ph.level + 1`.as('level'),
|
||||||
])
|
])
|
||||||
.innerJoin('page_hierarchy as ph', 'ph.parentPageId', 'p.id'),
|
.innerJoin('page_hierarchy as ph', 'ph.parentPageId', 'p.id')
|
||||||
|
.where('p.deletedAt', 'is', null),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.selectFrom('page_hierarchy')
|
.selectFrom('page_hierarchy')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { KyselyDB, KyselyTransaction } from '../../types/kysely.types';
|
import { KyselyDB, KyselyTransaction } from '../../types/kysely.types';
|
||||||
import { dbOrTx } from '../../utils';
|
import { dbOrTx, executeTx } from '../../utils';
|
||||||
import {
|
import {
|
||||||
InsertablePage,
|
InsertablePage,
|
||||||
Page,
|
Page,
|
||||||
@@ -183,14 +183,20 @@ export class PageRepo {
|
|||||||
|
|
||||||
const pageIds = descendants.map((d) => d.id);
|
const pageIds = descendants.map((d) => d.id);
|
||||||
|
|
||||||
await this.db
|
if (pageIds.length > 0) {
|
||||||
.updateTable('pages')
|
await executeTx(this.db, async (trx) => {
|
||||||
.set({
|
await trx
|
||||||
deletedById: deletedById,
|
.updateTable('pages')
|
||||||
deletedAt: currentDate,
|
.set({
|
||||||
})
|
deletedById: deletedById,
|
||||||
.where('id', 'in', pageIds)
|
deletedAt: currentDate,
|
||||||
.execute();
|
})
|
||||||
|
.where('id', 'in', pageIds)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await trx.deleteFrom('shares').where('pageId', 'in', pageIds).execute();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async restorePage(pageId: string): Promise<void> {
|
async restorePage(pageId: string): Promise<void> {
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: 576cf4fa42...4100345c18
@@ -213,4 +213,16 @@ export class EnvironmentService {
|
|||||||
getPostHogKey(): string {
|
getPostHogKey(): string {
|
||||||
return this.configService.get<string>('POSTHOG_KEY');
|
return this.configService.get<string>('POSTHOG_KEY');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOpenAiApiKey(): string {
|
||||||
|
return this.configService.get<string>('OPENAI_API_KEY');
|
||||||
|
}
|
||||||
|
|
||||||
|
getOpenAiApiUrl(): string {
|
||||||
|
return this.configService.get<string>('OPENAI_API_URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
getOpenAiModel(): string {
|
||||||
|
return this.configService.get<string>('OPENAI_MODEL');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export class ExportPageDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
includeChildren?: boolean;
|
includeChildren?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
includeAttachments?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExportSpaceDto {
|
export class ExportSpaceDto {
|
||||||
|
|||||||
@@ -55,40 +55,22 @@ export class ExportController {
|
|||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileExt = getExportExtension(dto.format);
|
const zipFileBuffer = await this.exportService.exportPages(
|
||||||
const fileName = sanitize(page.title || 'untitled') + fileExt;
|
dto.pageId,
|
||||||
|
|
||||||
if (dto.includeChildren) {
|
|
||||||
const zipFileBuffer = await this.exportService.exportPageWithChildren(
|
|
||||||
dto.pageId,
|
|
||||||
dto.format,
|
|
||||||
);
|
|
||||||
|
|
||||||
const newName = path.parse(fileName).name + '.zip';
|
|
||||||
|
|
||||||
res.headers({
|
|
||||||
'Content-Type': 'application/zip',
|
|
||||||
'Content-Disposition':
|
|
||||||
'attachment; filename="' + encodeURIComponent(newName) + '"',
|
|
||||||
});
|
|
||||||
|
|
||||||
res.send(zipFileBuffer);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawContent = await this.exportService.exportPage(
|
|
||||||
dto.format,
|
dto.format,
|
||||||
page,
|
dto.includeAttachments,
|
||||||
true,
|
dto.includeChildren,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fileName = sanitize(page.title || 'untitled') + '.zip';
|
||||||
|
|
||||||
res.headers({
|
res.headers({
|
||||||
'Content-Type': getMimeType(fileExt),
|
'Content-Type': 'application/zip',
|
||||||
'Content-Disposition':
|
'Content-Disposition':
|
||||||
'attachment; filename="' + encodeURIComponent(fileName) + '"',
|
'attachment; filename="' + encodeURIComponent(fileName) + '"',
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send(rawContent);
|
res.send(zipFileBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
|
|||||||
@@ -89,10 +89,28 @@ export class ExportService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async exportPageWithChildren(pageId: string, format: string) {
|
async exportPages(
|
||||||
const pages = await this.pageRepo.getPageAndDescendants(pageId, {
|
pageId: string,
|
||||||
includeContent: true,
|
format: string,
|
||||||
});
|
includeAttachments: boolean,
|
||||||
|
includeChildren: boolean,
|
||||||
|
) {
|
||||||
|
let pages: Page[];
|
||||||
|
|
||||||
|
if (includeChildren) {
|
||||||
|
//@ts-ignore
|
||||||
|
pages = await this.pageRepo.getPageAndDescendants(pageId, {
|
||||||
|
includeContent: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Only fetch the single page when includeChildren is false
|
||||||
|
const page = await this.pageRepo.findById(pageId, {
|
||||||
|
includeContent: true,
|
||||||
|
});
|
||||||
|
if (page){
|
||||||
|
pages = [page];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!pages || pages.length === 0) {
|
if (!pages || pages.length === 0) {
|
||||||
throw new BadRequestException('No pages to export');
|
throw new BadRequestException('No pages to export');
|
||||||
@@ -105,7 +123,7 @@ export class ExportService {
|
|||||||
const tree = buildTree(pages as Page[]);
|
const tree = buildTree(pages as Page[]);
|
||||||
|
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
await this.zipPages(tree, format, zip);
|
await this.zipPages(tree, format, zip, includeAttachments);
|
||||||
|
|
||||||
const zipFile = zip.generateNodeStream({
|
const zipFile = zip.generateNodeStream({
|
||||||
type: 'nodebuffer',
|
type: 'nodebuffer',
|
||||||
@@ -168,7 +186,7 @@ export class ExportService {
|
|||||||
tree: PageExportTree,
|
tree: PageExportTree,
|
||||||
format: string,
|
format: string,
|
||||||
zip: JSZip,
|
zip: JSZip,
|
||||||
includeAttachments = true,
|
includeAttachments: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const slugIdToPath: Record<string, string> = {};
|
const slugIdToPath: Record<string, string> = {};
|
||||||
|
|
||||||
@@ -200,7 +218,8 @@ export class ExportService {
|
|||||||
|
|
||||||
if (includeAttachments) {
|
if (includeAttachments) {
|
||||||
await this.zipAttachments(updatedJsonContent, page.spaceId, folder);
|
await this.zipAttachments(updatedJsonContent, page.spaceId, folder);
|
||||||
updatedJsonContent = updateAttachmentUrlsToLocalPaths(updatedJsonContent);
|
updatedJsonContent =
|
||||||
|
updateAttachmentUrlsToLocalPaths(updatedJsonContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageTitle = getPageTitle(page.title);
|
const pageTitle = getPageTitle(page.title);
|
||||||
|
|||||||
@@ -69,8 +69,17 @@ function taskList(turndownService: TurndownService) {
|
|||||||
'input[type="checkbox"]',
|
'input[type="checkbox"]',
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const isChecked = checkbox.checked;
|
const isChecked = checkbox.checked;
|
||||||
|
|
||||||
return `- ${isChecked ? '[x]' : '[ ]'} ${content.trim()} \n`;
|
// Process content like regular list items
|
||||||
|
content = content
|
||||||
|
.replace(/^\n+/, '') // remove leading newlines
|
||||||
|
.replace(/\n+$/, '\n') // replace trailing newlines with just a single one
|
||||||
|
.replace(/\n/gm, '\n '); // indent nested content with 2 spaces
|
||||||
|
|
||||||
|
// Create the checkbox prefix
|
||||||
|
const prefix = `- ${isChecked ? '[x]' : '[ ]'} `;
|
||||||
|
|
||||||
|
return prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export class ImportService {
|
|||||||
.selectFrom('pages')
|
.selectFrom('pages')
|
||||||
.select(['id', 'position'])
|
.select(['id', 'position'])
|
||||||
.where('spaceId', '=', spaceId)
|
.where('spaceId', '=', spaceId)
|
||||||
.orderBy('position', 'desc')
|
.orderBy('position', (ob) => ob.collate('C').desc())
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.where('parentPageId', 'is', null)
|
.where('parentPageId', 'is', null)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|||||||
@@ -40,8 +40,11 @@ export class LocalDriver implements StorageDriver {
|
|||||||
|
|
||||||
async copy(fromFilePath: string, toFilePath: string): Promise<void> {
|
async copy(fromFilePath: string, toFilePath: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const fromFullPath = this._fullPath(fromFilePath);
|
||||||
|
const toFullPath = this._fullPath(toFilePath);
|
||||||
|
|
||||||
if (await this.exists(fromFilePath)) {
|
if (await this.exists(fromFilePath)) {
|
||||||
await fs.copy(fromFilePath, toFilePath);
|
await fs.copy(fromFullPath, toFullPath);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(`Failed to copy file: ${(err as Error).message}`);
|
throw new Error(`Failed to copy file: ${(err as Error).message}`);
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "docmost",
|
"name": "docmost",
|
||||||
"homepage": "https://docmost.com",
|
"homepage": "https://docmost.com",
|
||||||
"version": "0.22.0",
|
"version": "0.22.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nx run-many -t build",
|
"build": "nx run-many -t build",
|
||||||
|
|||||||
Generated
+20
@@ -543,6 +543,9 @@ importers:
|
|||||||
nodemailer:
|
nodemailer:
|
||||||
specifier: ^7.0.3
|
specifier: ^7.0.3
|
||||||
version: 7.0.3
|
version: 7.0.3
|
||||||
|
openai:
|
||||||
|
specifier: ^5.12.2
|
||||||
|
version: 5.12.2(ws@8.18.2)(zod@3.25.56)
|
||||||
openid-client:
|
openid-client:
|
||||||
specifier: ^5.7.1
|
specifier: ^5.7.1
|
||||||
version: 5.7.1
|
version: 5.7.1
|
||||||
@@ -7655,6 +7658,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
openai@5.12.2:
|
||||||
|
resolution: {integrity: sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
ws: ^8.18.0
|
||||||
|
zod: ^3.23.8
|
||||||
|
peerDependenciesMeta:
|
||||||
|
ws:
|
||||||
|
optional: true
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
openid-client@5.7.1:
|
openid-client@5.7.1:
|
||||||
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
|
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
|
||||||
|
|
||||||
@@ -18262,6 +18277,11 @@ snapshots:
|
|||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
is-wsl: 2.2.0
|
is-wsl: 2.2.0
|
||||||
|
|
||||||
|
openai@5.12.2(ws@8.18.2)(zod@3.25.56):
|
||||||
|
optionalDependencies:
|
||||||
|
ws: 8.18.2
|
||||||
|
zod: 3.25.56
|
||||||
|
|
||||||
openid-client@5.7.1:
|
openid-client@5.7.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
jose: 4.15.9
|
jose: 4.15.9
|
||||||
|
|||||||
Reference in New Issue
Block a user