From a817efb238afe27eed65e54a45435ec4ac94dc70 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 15 Mar 2026 10:24:42 +0800 Subject: [PATCH 1/2] fix: open db link inside doucment --- src/components/app/ViewModal.tsx | 42 ++++++++----------- .../editor/components/leaf/Leaf.tsx | 2 +- .../components/leaf/mention/MentionLeaf.tsx | 2 +- .../components/leaf/mention/MentionPage.tsx | 31 +++++++------- 4 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/components/app/ViewModal.tsx b/src/components/app/ViewModal.tsx index 6516d9e9..e1074000 100644 --- a/src/components/app/ViewModal.tsx +++ b/src/components/app/ViewModal.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { APP_EVENTS } from '@/application/constants'; -import { UIVariant, ViewComponentProps, ViewLayout, ViewMetaProps, YDoc, YDocWithMeta } from '@/application/types'; +import { UIVariant, View, ViewComponentProps, ViewLayout, ViewMetaProps, YDoc, YDocWithMeta } from '@/application/types'; import { getFirstChildView } from '@/application/view-utils'; import { ReactComponent as ArrowDownIcon } from '@/assets/icons/alt_arrow_down.svg'; import { ReactComponent as CloseIcon } from '@/assets/icons/close.svg'; @@ -46,15 +46,6 @@ const Transition = React.forwardRef(function Transition( return ; }); -/** - * Minimal view metadata used as fallback when view is not yet in outline - */ -interface FallbackViewMeta { - view_id: string; - layout: ViewLayout; - name: string; -} - function ViewModal({ viewId, open, onClose }: { viewId?: string; open: boolean; onClose: () => void }) { const workspaceId = useCurrentWorkspaceId(); const { t } = useTranslation(); @@ -87,8 +78,10 @@ function ViewModal({ viewId, open, onClose }: { viewId?: string; open: boolean; const [notFound, setNotFound] = useState(false); const [syncBound, setSyncBound] = useState(false); - // Fallback view metadata fetched from server (used when view not in outline yet) - const [fallbackMeta, setFallbackMeta] = useState(null); + // Fallback view metadata fetched from server (used when view not in outline yet). + // Stores the full View (including children/extra) so getFirstChildView can + // resolve database containers to their first child view. + const [fallbackMeta, setFallbackMeta] = useState(null); // Get view from outline const outlineView = useMemo(() => { @@ -135,11 +128,7 @@ function ViewModal({ viewId, open, onClose }: { viewId?: string; open: boolean; ViewService.get(workspaceId, effectiveViewId) .then((fetchedView) => { if (!cancelled && fetchedView) { - setFallbackMeta({ - view_id: fetchedView.view_id, - layout: fetchedView.layout, - name: fetchedView.name, - }); + setFallbackMeta(fetchedView); } }) .catch((e) => { @@ -169,14 +158,17 @@ function ViewModal({ viewId, open, onClose }: { viewId?: string; open: boolean; [loadView] ); + // Wait for metadata (outline or fallback) before loading the doc. + // This ensures database containers resolve to their first child view, + // and the correct layout is known for the page-view API call. + const resolvedView = effectiveOutlineView || fallbackMeta; + useEffect(() => { - if (open && effectiveViewId) { + if (open && effectiveViewId && resolvedView) { void loadPageDoc(effectiveViewId); } - }, [open, effectiveViewId, loadPageDoc]); + }, [open, effectiveViewId, loadPageDoc, resolvedView]); - // Use outline view if available, otherwise use fallback - const resolvedView = effectiveOutlineView || fallbackMeta; const layout = resolvedView?.layout ?? ViewLayout.Document; // Build viewMeta for the View component @@ -197,15 +189,15 @@ function ViewModal({ viewId, open, onClose }: { viewId?: string; open: boolean; }; } - // Fallback with minimal properties + // Fallback: resolvedView is a full View from the server return { name: resolvedView.name, - icon: undefined, - cover: undefined, + icon: resolvedView.icon || undefined, + cover: resolvedView.extra?.cover || undefined, layout: resolvedView.layout, visibleViewIds: [], viewId: resolvedView.view_id, - extra: undefined, + extra: resolvedView.extra, workspaceId, }; }, [resolvedView, effectiveOutlineView, workspaceId]); diff --git a/src/components/editor/components/leaf/Leaf.tsx b/src/components/editor/components/leaf/Leaf.tsx index 030f6a6c..4a603fda 100644 --- a/src/components/editor/components/leaf/Leaf.tsx +++ b/src/components/editor/components/leaf/Leaf.tsx @@ -66,7 +66,7 @@ export function Leaf({ attributes, children, leaf, text }: RenderLeafProps) { ); } - if (leaf.href) { + if (leaf.href && text.text?.trim()) { newChildren = ( {newChildren} diff --git a/src/components/editor/components/leaf/mention/MentionLeaf.tsx b/src/components/editor/components/leaf/mention/MentionLeaf.tsx index 1a9e7fd5..0bc6a6d9 100644 --- a/src/components/editor/components/leaf/mention/MentionLeaf.tsx +++ b/src/components/editor/components/leaf/mention/MentionLeaf.tsx @@ -59,7 +59,7 @@ export function MentionLeaf({ mention, text, children }: { mention: Mention; tex top: isCursorBefore ? 0 : 'auto', bottom: isCursorBefore ? 'auto' : 0, }} - className={'absolute bottom-0 right-0 overflow-hidden !text-transparent'} + className={'absolute bottom-0 right-0 overflow-hidden !text-transparent pointer-events-none'} > {children} diff --git a/src/components/editor/components/leaf/mention/MentionPage.tsx b/src/components/editor/components/leaf/mention/MentionPage.tsx index da1a82b1..5e14c044 100644 --- a/src/components/editor/components/leaf/mention/MentionPage.tsx +++ b/src/components/editor/components/leaf/mention/MentionPage.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Element, Text } from 'slate'; -import { ReactEditor, useReadOnly, useSlate } from 'slate-react'; +import { ReactEditor, useSlate } from 'slate-react'; import smoothScrollIntoViewIfNeeded from 'smooth-scroll-into-view-if-needed'; import { APP_EVENTS } from '@/application/constants'; @@ -19,7 +19,6 @@ import { useEditorContext } from '@/components/editor/EditorContext'; import './style.css'; function MentionPage({ - text, pageId, blockId, type, @@ -35,7 +34,7 @@ function MentionPage({ const currentViewId = context.viewId; const eventEmitter = context.eventEmitter; - const { navigateToView, loadViewMeta, loadView, openPageModal } = context; + const { navigateToView, loadViewMeta, loadView } = context; const [noAccess, setNoAccess] = useState(false); const [meta, setMeta] = useState(null); const [content, setContent] = useState(''); @@ -157,8 +156,6 @@ function MentionPage({ ); }, [blockId, currentViewId, icon, meta?.layout, pageId, type]); - const readOnly = useReadOnly() || editor.isElementReadOnly(text as unknown as Element); - const handleScrollToBlock = useCallback(async () => { if (blockId) { const entry = findSlateEntryByBlockId(editor as YjsEditor, blockId); @@ -184,22 +181,26 @@ function MentionPage({ { e.stopPropagation(); - if (readOnly || meta?.layout === ViewLayout.AIChat) { - void navigateToView?.(pageId, blockId); - } else { - if (noAccess) return; - if (pageId === currentViewId) { - void handleScrollToBlock(); - return; - } + if (noAccess) return; - openPageModal?.(pageId); + // Same-page block reference: scroll to the target block + if (pageId === currentViewId) { + void handleScrollToBlock(); + return; } + + // Navigate directly to the target view (matches desktop behavior, + // which always opens mentions in a new tab regardless of layout type). + // Deferred so Slate's event cycle completes before the route change + // unmounts the editor. + setTimeout(() => { + void navigateToView?.(pageId, blockId); + }, 0); }} style={{ cursor: noAccess ? 'default' : undefined, }} - className={`mention-inline cursor-pointer pr-1 underline`} + className={`mention-inline cursor-pointer pr-1 underline select-none`} contentEditable={false} data-mention-id={pageId} > From c452f43b25044a19f61838e57a395371bbe30012 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 15 Mar 2026 12:01:20 +0800 Subject: [PATCH 2/2] chore: fix test --- cypress/e2e/database2/filter-date.cy.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/database2/filter-date.cy.ts b/cypress/e2e/database2/filter-date.cy.ts index 62bb1e0c..d469af4d 100644 --- a/cypress/e2e/database2/filter-date.cy.ts +++ b/cypress/e2e/database2/filter-date.cy.ts @@ -65,7 +65,15 @@ const selectDateByDay = (day: number): void => { return text === String(day) && !el.classList.contains('day-outside'); }) .first() - .click({ force: true }); + .then(($el) => { + // In react-day-picker single mode, clicking an already-selected day deselects it. + // When a date filter is first added, it defaults to today's date. If the desired + // day matches today, clicking would toggle it off instead of keeping it selected. + // Skip the click if the day is already selected. + if ($el.attr('aria-selected') !== 'true') { + cy.wrap($el).click({ force: true }); + } + }); waitForReactUpdate(500); };