Merge branch 'main' into feat/collab-redis

This commit is contained in:
Philipinho
2026-01-25 04:38:33 +00:00
8 changed files with 71 additions and 23 deletions
@@ -118,7 +118,14 @@ export async function exportPage(data: IExportPageParams): Promise<void> {
.split("filename=")[1] .split("filename=")[1]
.replace(/"/g, ""); .replace(/"/g, "");
saveAs(req.data, decodeURIComponent(fileName)); let decodedFileName = fileName;
try {
decodedFileName = decodeURIComponent(fileName);
} catch (err) {
// fallback to raw filename
}
saveAs(req.data, decodedFileName);
} }
export async function importPage(file: File, spaceId: string) { export async function importPage(file: File, spaceId: string) {
@@ -3,7 +3,7 @@ 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 { Button, Popover, Text } from "@mantine/core"; import { Button, Popover, Text } from "@mantine/core";
import { IconChevronDown } from "@tabler/icons-react"; import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts"; import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
@@ -21,7 +21,7 @@ export function SwitchSpace({
spaceIcon, spaceIcon,
}: SwitchSpaceProps) { }: SwitchSpaceProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [opened, { close, open, toggle }] = useDisclosure(false); const [opened, { close, toggle }] = useDisclosure(false);
const handleSelect = (value: string) => { const handleSelect = (value: string) => {
if (value) { if (value) {
@@ -44,9 +44,9 @@ export function SwitchSpace({
variant="subtle" variant="subtle"
fullWidth fullWidth
justify="space-between" justify="space-between"
rightSection={<IconChevronDown size={18} />} rightSection={opened ? <IconChevronUp size={18} /> : <IconChevronDown size={18} />}
color="gray" color="gray"
onClick={open} onClick={toggle}
> >
<CustomAvatar <CustomAvatar
name={spaceName} name={spaceName}
@@ -69,5 +69,12 @@ export async function exportSpace(data: IExportSpaceParams): Promise<void> {
.split("filename=")[1] .split("filename=")[1]
.replace(/"/g, ""); .replace(/"/g, "");
saveAs(req.data, decodeURIComponent(fileName)); let decodedFileName = fileName;
try {
decodedFileName = decodeURIComponent(fileName);
} catch (err) {
// fallback to raw filename
}
saveAs(req.data, decodedFileName);
} }
+15 -8
View File
@@ -5,13 +5,14 @@ const CONTEXTS_TO_IGNORE = [
'InstanceLoader', 'InstanceLoader',
'RoutesResolver', 'RoutesResolver',
'RouterExplorer', 'RouterExplorer',
'LegacyRouteConverter',
'WebSocketsController', 'WebSocketsController',
]; ];
export function createPinoConfig(): Params { export function createPinoConfig(): Params {
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV?.toLowerCase() === 'production';
const isDebugMode = process.env.DEBUG_MODE === 'true'; const isDebugMode = process.env.DEBUG_MODE?.toLowerCase() === 'true';
const logHttp = process.env.LOG_HTTP === 'true'; const logHttp = process.env.LOG_HTTP?.toLowerCase() === 'true';
const level = isProduction && !isDebugMode ? 'info' : 'debug'; const level = isProduction && !isDebugMode ? 'info' : 'debug';
@@ -32,14 +33,20 @@ export function createPinoConfig(): Params {
: undefined, : undefined,
formatters: { formatters: {
level: (label) => ({ level: label }), level: (label) => ({ level: label }),
log: (object: Record<string, unknown>) => { },
hooks: {
logMethod(inputArgs, method) {
if (isProduction && !isDebugMode) { if (isProduction && !isDebugMode) {
const context = object['context'] as string | undefined; for (const arg of inputArgs) {
if (context && CONTEXTS_TO_IGNORE.includes(context)) { if (typeof arg === 'object' && arg !== null && 'context' in arg) {
return { filtered: true }; const context = (arg as Record<string, unknown>)['context'];
if (typeof context === 'string' && CONTEXTS_TO_IGNORE.includes(context)) {
return;
}
}
} }
} }
return object; return method.apply(this, inputArgs);
}, },
}, },
serializers: { serializers: {
+15 -5
View File
@@ -1,4 +1,5 @@
import { jsonToNode } from 'src/collaboration/collaboration.util'; import { jsonToNode } from 'src/collaboration/collaboration.util';
import { Logger } from '@nestjs/common';
import { ExportFormat } from './dto/export-dto'; import { ExportFormat } from './dto/export-dto';
import { Node } from '@tiptap/pm/model'; import { Node } from '@tiptap/pm/model';
import { validate as isValidUUID } from 'uuid'; import { validate as isValidUUID } from 'uuid';
@@ -88,7 +89,7 @@ export function replaceInternalLinks(
// if link and text are same, use page title // if link and text are same, use page title
if (markLink === node.text) { if (markLink === node.text) {
//@ts-expect-error //@ts-expect-error
node.text = getInternalLinkPageName(relativePath); node.text = getInternalLinkPageName(relativePath, currentPagePath);
} }
} }
} }
@@ -99,10 +100,19 @@ export function replaceInternalLinks(
return doc.toJSON(); return doc.toJSON();
} }
export function getInternalLinkPageName(path: string): string { export function getInternalLinkPageName(path: string, currentFilePath?: string): string {
return decodeURIComponent( const name = path?.split('/').pop().split('.').slice(0, -1).join('.');
path?.split('/').pop().split('.').slice(0, -1).join('.'), try {
); return decodeURIComponent(name);
} catch (err) {
if (currentFilePath) {
Logger.warn(
`URI malformed in page ${currentFilePath}: ${name}. Falling back to raw name.`,
'ExportUtils',
);
}
return name;
}
} }
export function extractPageSlugId(input: string): string { export function extractPageSlugId(input: string): string {
@@ -1,4 +1,5 @@
import { getEmbedUrlAndProvider } from '@docmost/editor-ext'; import { getEmbedUrlAndProvider } from '@docmost/editor-ext';
import { Logger } from '@nestjs/common';
import * as path from 'path'; import * as path from 'path';
import { v7 } from 'uuid'; import { v7 } from 'uuid';
import { InsertableBacklink } from '@docmost/db/types/entity.types'; import { InsertableBacklink } from '@docmost/db/types/entity.types';
@@ -280,8 +281,18 @@ export async function rewriteInternalLinksToMentionHtml(
const $a = $(el); const $a = $(el);
const raw = $a.attr('href')!; const raw = $a.attr('href')!;
if (raw.startsWith('http') || raw.startsWith('/api/')) return; if (raw.startsWith('http') || raw.startsWith('/api/')) return;
let decodedRaw = raw;
try {
decodedRaw = decodeURIComponent(raw);
} catch (err) {
Logger.warn(
`URI malformed in page ${currentFilePath}: ${raw}. Falling back to raw path.`,
'ImportFormatter',
);
}
const resolved = normalize( const resolved = normalize(
path.join(path.dirname(currentFilePath), decodeURIComponent(raw)), path.join(path.dirname(currentFilePath), decodedRaw),
); );
const meta = filePathToPageMetaMap.get(resolved); const meta = filePathToPageMetaMap.get(resolved);
if (!meta) return; if (!meta) return;
@@ -1,3 +1,4 @@
import { Logger } from '@nestjs/common';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import * as path from 'path'; import * as path from 'path';
@@ -30,8 +31,13 @@ export function resolveRelativeAttachmentPath(
pageDir: string, pageDir: string,
attachmentCandidates: Map<string, string>, attachmentCandidates: Map<string, string>,
): string | null { ): string | null {
const mainRel = decodeURIComponent(raw.replace(/^\.?\/+/, '')); let mainRel = raw.replace(/^\.?\/+/, '');
const fallback = path.normalize(path.join(pageDir, mainRel)); try {
mainRel = decodeURIComponent(mainRel);
} catch (err) {
Logger.warn(`URI malformed for attachment path: ${mainRel}. Falling back to raw path.`, 'ImportUtils');
}
const fallback = path.normalize(path.join(pageDir, mainRel)).split(path.sep).join('/');
if (attachmentCandidates.has(mainRel)) { if (attachmentCandidates.has(mainRel)) {
return mainRel; return mainRel;