Compare commits

...

6 Commits

Author SHA1 Message Date
Philipinho b4fa981d80 fix shared page mention view for non-logged in users 2026-03-11 11:34:13 +00:00
Philip Okugbe 66c26af34b noop audit module (#1994) 2026-03-05 09:29:39 +00:00
Philip Okugbe b4f009513e fix: resize handle clipping (#1990) 2026-03-04 12:24:46 +00:00
Philipinho fcffa3dfa0 fix media 2026-03-04 12:08:08 +00:00
Philipinho 1980b94825 0.70.1 2026-03-04 11:57:31 +00:00
Philip Okugbe bea1637519 fix: image fallback regression (#1989)
* fix: image fallback regression

* fix image preview on upload

* fix image loading
2026-03-04 11:51:43 +00:00
15 changed files with 140 additions and 53 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "client", "name": "client",
"private": true, "private": true,
"version": "0.70.0", "version": "0.70.1",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
@@ -2,6 +2,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
max-width: 100%;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
animation: pulse 1.2s ease-in-out infinite; animation: pulse 1.2s ease-in-out infinite;
@@ -3,6 +3,7 @@ import { ActionIcon, Anchor, Text } from "@mantine/core";
import { IconFileDescription } from "@tabler/icons-react"; import { IconFileDescription } from "@tabler/icons-react";
import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
import { usePageQuery } from "@/features/page/queries/page-query.ts"; import { usePageQuery } from "@/features/page/queries/page-query.ts";
import { useSharePageQuery } from "@/features/share/queries/share-query.ts";
import { import {
buildPageUrl, buildPageUrl,
buildSharedPageUrl, buildSharedPageUrl,
@@ -13,17 +14,23 @@ import classes from "./mention.module.css";
export default function MentionView(props: NodeViewProps) { export default function MentionView(props: NodeViewProps) {
const { node } = props; const { node } = props;
const { label, entityType, entityId, slugId, anchorId } = node.attrs; const { label, entityType, entityId, slugId, anchorId } = node.attrs;
const isPageMention = entityType === "page";
const { spaceSlug, pageSlug } = useParams(); const { spaceSlug, pageSlug } = useParams();
const { shareId } = useParams(); const { shareId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const isShareRoute = location.pathname.startsWith("/share");
const { const {
data: page, data: page,
isLoading, isLoading,
isError, isError,
} = usePageQuery({ pageId: entityType === "page" ? slugId : null }); } = usePageQuery({ pageId: isPageMention && !isShareRoute ? slugId : null });
const location = useLocation(); const { data: sharedPage } = useSharePageQuery({
const isShareRoute = location.pathname.startsWith("/share"); pageId: isPageMention && isShareRoute ? slugId : undefined,
});
const currentPageSlugId = extractPageSlugId(pageSlug); const currentPageSlugId = extractPageSlugId(pageSlug);
const isSamePage = currentPageSlugId === slugId; const isSamePage = currentPageSlugId === slugId;
@@ -39,10 +46,12 @@ export default function MentionView(props: NodeViewProps) {
} }
}; };
const sharePageTitle = sharedPage?.page?.title || label;
const shareSlugUrl = buildSharedPageUrl({ const shareSlugUrl = buildSharedPageUrl({
shareId, shareId,
pageSlugId: slugId, pageSlugId: slugId,
pageTitle: label, pageTitle: sharePageTitle,
anchorId, anchorId,
}); });
@@ -54,21 +63,59 @@ export default function MentionView(props: NodeViewProps) {
</Text> </Text>
)} )}
{entityType === "page" && isError && ( {isPageMention && isShareRoute && (
<Text component="span" c="dimmed" size="sm">
{label}
</Text>
)}
{entityType === "page" && !isError && (
<Anchor <Anchor
component={Link} component={Link}
fw={500} fw={500}
to={ to={shareSlugUrl}
isShareRoute onClick={handleClick}
? shareSlugUrl underline="never"
: buildPageUrl(page?.space?.slug || spaceSlug, slugId, page?.title || label, anchorId) className={classes.pageMentionLink}
} >
<ActionIcon
variant="transparent"
color="gray"
component="span"
size={18}
style={{ verticalAlign: "text-bottom" }}
>
<IconFileDescription size={18} />
</ActionIcon>
<span className={classes.pageMentionText}>
{sharePageTitle}
</span>
</Anchor>
)}
{isPageMention && !isShareRoute && isError && (
<Anchor
component={Link}
fw={500}
to={buildPageUrl(spaceSlug, slugId, label, anchorId)}
onClick={handleClick}
underline="never"
className={classes.pageMentionLink}
>
<ActionIcon
variant="transparent"
color="gray"
component="span"
size={18}
style={{ verticalAlign: "text-bottom" }}
>
<IconFileDescription size={18} />
</ActionIcon>
<span className={classes.pageMentionText}>
{label}
</span>
</Anchor>
)}
{isPageMention && !isShareRoute && !isError && (
<Anchor
component={Link}
fw={500}
to={buildPageUrl(page?.space?.slug || spaceSlug, slugId, page?.title || label, anchorId)}
onClick={handleClick} onClick={handleClick}
underline="never" underline="never"
className={classes.pageMentionLink} className={classes.pageMentionLink}
@@ -25,9 +25,9 @@ export default function SubpagesView(props: NodeViewProps) {
// Get subpages from shared tree if we're in a shared context // Get subpages from shared tree if we're in a shared context
const sharedSubpages = useSharedPageSubpages(currentPageId); const sharedSubpages = useSharedPageSubpages(currentPageId);
const { data, isLoading, error } = useGetSidebarPagesQuery({ const { data, isLoading, error } = useGetSidebarPagesQuery(
pageId: currentPageId, shareId ? null : { pageId: currentPageId },
}); );
const subpages = useMemo(() => { const subpages = useMemo(() => {
// If we're in a shared context, use the shared subpages // If we're in a shared context, use the shared subpages
@@ -2,6 +2,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
max-width: 100%;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
animation: pulse 1.2s ease-in-out infinite; animation: pulse 1.2s ease-in-out infinite;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"version": "0.70.0", "version": "0.70.1",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
+2
View File
@@ -25,6 +25,7 @@ import { CacheModule } from '@nestjs/cache-manager';
import KeyvRedis from '@keyv/redis'; import KeyvRedis from '@keyv/redis';
import { LoggerModule } from './common/logger/logger.module'; import { LoggerModule } from './common/logger/logger.module';
import { ClsModule } from 'nestjs-cls'; import { ClsModule } from 'nestjs-cls';
import { NoopAuditModule } from './integrations/audit/audit.module';
const enterpriseModules = []; const enterpriseModules = [];
try { try {
@@ -47,6 +48,7 @@ try {
middleware: { mount: true }, middleware: { mount: true },
}), }),
LoggerModule, LoggerModule,
NoopAuditModule,
CoreModule, CoreModule,
DatabaseModule, DatabaseModule,
EnvironmentModule, EnvironmentModule,
-11
View File
@@ -20,10 +20,6 @@ import { AuditContextMiddleware } from '../common/middlewares/audit-context.midd
import { ShareModule } from './share/share.module'; import { ShareModule } from './share/share.module';
import { NotificationModule } from './notification/notification.module'; import { NotificationModule } from './notification/notification.module';
import { WatcherModule } from './watcher/watcher.module'; import { WatcherModule } from './watcher/watcher.module';
import {
AUDIT_SERVICE,
NoopAuditService,
} from '../integrations/audit/audit.service';
import { ClsMiddleware } from 'nestjs-cls'; import { ClsMiddleware } from 'nestjs-cls';
@Module({ @Module({
@@ -43,13 +39,6 @@ import { ClsMiddleware } from 'nestjs-cls';
NotificationModule, NotificationModule,
WatcherModule, WatcherModule,
], ],
providers: [
{
provide: AUDIT_SERVICE,
useClass: NoopAuditService,
},
],
exports: [AUDIT_SERVICE],
}) })
export class CoreModule implements NestModule { export class CoreModule implements NestModule {
configure(consumer: MiddlewareConsumer) { configure(consumer: MiddlewareConsumer) {
@@ -0,0 +1,14 @@
import { Global, Module } from '@nestjs/common';
import { AUDIT_SERVICE, NoopAuditService } from './audit.service';
@Global()
@Module({
providers: [
{
provide: AUDIT_SERVICE,
useClass: NoopAuditService,
},
],
exports: [AUDIT_SERVICE],
})
export class NoopAuditModule {}
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "docmost", "name": "docmost",
"homepage": "https://docmost.com", "homepage": "https://docmost.com",
"version": "0.70.0", "version": "0.70.1",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "nx run-many -t build", "build": "nx run-many -t build",
+8 -5
View File
@@ -1,6 +1,7 @@
import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core"; import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
import type { ResizableNodeViewDirection } from "@tiptap/core"; import type { ResizableNodeViewDirection } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react"; import { ReactNodeViewRenderer } from "@tiptap/react";
import { normalizeFileUrl } from "./media-utils";
export type DrawioResizeOptions = { export type DrawioResizeOptions = {
enabled: boolean; enabled: boolean;
@@ -223,7 +224,7 @@ export const Drawio = Node.create<DrawioOptions>({
} }
const el = document.createElement("img"); const el = document.createElement("img");
el.src = node.attrs.src; el.src = normalizeFileUrl(node.attrs.src);
el.alt = node.attrs.title || ""; el.alt = node.attrs.title || "";
el.style.display = "block"; el.style.display = "block";
el.style.maxWidth = "100%"; el.style.maxWidth = "100%";
@@ -259,7 +260,7 @@ export const Drawio = Node.create<DrawioOptions>({
} }
if (updatedNode.attrs.src !== currentNode.attrs.src) { if (updatedNode.attrs.src !== currentNode.attrs.src) {
el.src = updatedNode.attrs.src || ""; el.src = normalizeFileUrl(updatedNode.attrs.src);
} }
const w = updatedNode.attrs.width; const w = updatedNode.attrs.width;
@@ -317,12 +318,14 @@ export const Drawio = Node.create<DrawioOptions>({
}); });
} }
// Hide until image loads // Show skeleton background while image loads from server
dom.style.visibility = "hidden";
dom.style.pointerEvents = "none"; dom.style.pointerEvents = "none";
dom.style.background =
"light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))";
el.onload = () => { el.onload = () => {
dom.style.visibility = "";
dom.style.pointerEvents = ""; dom.style.pointerEvents = "";
dom.style.background = "";
}; };
return nodeView; return nodeView;
+8 -5
View File
@@ -1,6 +1,7 @@
import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core"; import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
import type { ResizableNodeViewDirection } from "@tiptap/core"; import type { ResizableNodeViewDirection } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react"; import { ReactNodeViewRenderer } from "@tiptap/react";
import { normalizeFileUrl } from "./media-utils";
export type ExcalidrawResizeOptions = { export type ExcalidrawResizeOptions = {
enabled: boolean; enabled: boolean;
@@ -223,7 +224,7 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
} }
const el = document.createElement("img"); const el = document.createElement("img");
el.src = node.attrs.src; el.src = normalizeFileUrl(node.attrs.src);
el.alt = node.attrs.title || ""; el.alt = node.attrs.title || "";
el.style.display = "block"; el.style.display = "block";
el.style.maxWidth = "100%"; el.style.maxWidth = "100%";
@@ -259,7 +260,7 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
} }
if (updatedNode.attrs.src !== currentNode.attrs.src) { if (updatedNode.attrs.src !== currentNode.attrs.src) {
el.src = updatedNode.attrs.src || ""; el.src = normalizeFileUrl(updatedNode.attrs.src);
} }
const w = updatedNode.attrs.width; const w = updatedNode.attrs.width;
@@ -317,12 +318,14 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
}); });
} }
// Hide until image loads // Show skeleton background while image loads from server
dom.style.visibility = "hidden";
dom.style.pointerEvents = "none"; dom.style.pointerEvents = "none";
dom.style.background =
"light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))";
el.onload = () => { el.onload = () => {
dom.style.visibility = "";
dom.style.pointerEvents = ""; dom.style.pointerEvents = "";
dom.style.background = "";
}; };
return nodeView; return nodeView;
+15 -5
View File
@@ -6,6 +6,7 @@ import {
Range, Range,
ResizableNodeView, ResizableNodeView,
} from "@tiptap/core"; } from "@tiptap/core";
import { normalizeFileUrl } from "../media-utils";
import type { ResizableNodeViewDirection } from "@tiptap/core"; import type { ResizableNodeViewDirection } from "@tiptap/core";
export type ImageResizeOptions = { export type ImageResizeOptions = {
@@ -252,11 +253,18 @@ export const TiptapImage = Image.extend<ImageOptions>({
} }
}); });
el.src = HTMLAttributes.src; el.src = normalizeFileUrl(HTMLAttributes.src);
el.style.display = "block"; el.style.display = "block";
el.style.maxWidth = "100%"; el.style.maxWidth = "100%";
el.style.borderRadius = "8px"; el.style.borderRadius = "8px";
if (typeof node.attrs.width === "number" && node.attrs.width > 0) {
el.style.width = `${node.attrs.width}px`;
if (typeof node.attrs.height === "number" && node.attrs.height > 0) {
el.style.height = `${node.attrs.height}px`;
}
}
let currentNode = node; let currentNode = node;
const nodeView = new ResizableNodeView({ const nodeView = new ResizableNodeView({
@@ -287,7 +295,7 @@ export const TiptapImage = Image.extend<ImageOptions>({
} }
if (updatedNode.attrs.src !== currentNode.attrs.src) { if (updatedNode.attrs.src !== currentNode.attrs.src) {
el.src = updatedNode.attrs.src || ""; el.src = normalizeFileUrl(updatedNode.attrs.src);
} }
if (updatedNode.attrs.alt !== currentNode.attrs.alt) { if (updatedNode.attrs.alt !== currentNode.attrs.alt) {
@@ -352,12 +360,14 @@ export const TiptapImage = Image.extend<ImageOptions>({
}); });
} }
// Hide until image loads (official TipTap pattern) // Show skeleton background while image loads from server
dom.style.visibility = "hidden";
dom.style.pointerEvents = "none"; dom.style.pointerEvents = "none";
dom.style.background =
"light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))";
el.onload = () => { el.onload = () => {
dom.style.visibility = "";
dom.style.pointerEvents = ""; dom.style.pointerEvents = "";
dom.style.background = "";
}; };
return nodeView; return nodeView;
@@ -1,5 +1,12 @@
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
export function normalizeFileUrl(src: string): string {
if (src && src.startsWith("/files/")) {
return "/api" + src;
}
return src || "";
}
export type UploadFn = ( export type UploadFn = (
file: File, file: File,
editor: Editor, editor: Editor,
+15 -5
View File
@@ -1,5 +1,6 @@
import { ReactNodeViewRenderer } from "@tiptap/react"; import { ReactNodeViewRenderer } from "@tiptap/react";
import { Range, Node, mergeAttributes, ResizableNodeView } from "@tiptap/core"; import { Range, Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
import { normalizeFileUrl } from "../media-utils";
import type { ResizableNodeViewDirection } from "@tiptap/core"; import type { ResizableNodeViewDirection } from "@tiptap/core";
export type VideoResizeOptions = { export type VideoResizeOptions = {
@@ -223,13 +224,20 @@ export const TiptapVideo = Node.create<VideoOptions>({
} }
const el = document.createElement("video"); const el = document.createElement("video");
el.src = node.attrs.src; el.src = normalizeFileUrl(node.attrs.src);
el.controls = true; el.controls = true;
el.preload = "metadata"; el.preload = "metadata";
el.style.display = "block"; el.style.display = "block";
el.style.maxWidth = "100%"; el.style.maxWidth = "100%";
el.style.borderRadius = "8px"; el.style.borderRadius = "8px";
if (typeof node.attrs.width === "number" && node.attrs.width > 0) {
el.style.width = `${node.attrs.width}px`;
if (typeof node.attrs.height === "number" && node.attrs.height > 0) {
el.style.height = `${node.attrs.height}px`;
}
}
let currentNode = node; let currentNode = node;
const nodeView = new ResizableNodeView({ const nodeView = new ResizableNodeView({
@@ -260,7 +268,7 @@ export const TiptapVideo = Node.create<VideoOptions>({
} }
if (updatedNode.attrs.src !== currentNode.attrs.src) { if (updatedNode.attrs.src !== currentNode.attrs.src) {
el.src = updatedNode.attrs.src || ""; el.src = normalizeFileUrl(updatedNode.attrs.src);
} }
const w = updatedNode.attrs.width; const w = updatedNode.attrs.width;
@@ -318,12 +326,14 @@ export const TiptapVideo = Node.create<VideoOptions>({
}); });
} }
// Hide until video metadata loads // Show skeleton background while video loads from server
dom.style.visibility = "hidden";
dom.style.pointerEvents = "none"; dom.style.pointerEvents = "none";
dom.style.background =
"light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))";
el.onloadedmetadata = () => { el.onloadedmetadata = () => {
dom.style.visibility = "";
dom.style.pointerEvents = ""; dom.style.pointerEvents = "";
dom.style.background = "";
}; };
return nodeView; return nodeView;