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);
};
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}
>