diff --git a/src/application/__tests__/view-utils.test.ts b/src/application/__tests__/view-utils.test.ts index 345507c0..acd4a63d 100644 --- a/src/application/__tests__/view-utils.test.ts +++ b/src/application/__tests__/view-utils.test.ts @@ -3,9 +3,11 @@ import { View, ViewLayout } from '../types'; import { isDatabaseLayout, isDatabaseContainer, + isEmbeddedView, getDatabaseIdFromExtra, isReferencedDatabaseView, getFirstChildView, + getDatabaseTabViewIds, } from '../view-utils'; /** @@ -472,6 +474,116 @@ describe('view-utils', () => { }); }); + describe('getDatabaseTabViewIds', () => { + it('filters embedded views from container tabs', () => { + const gridView = createMockView({ + view_id: 'grid-view', + layout: ViewLayout.Grid, + extra: { is_space: false, database_id: 'db-1' }, + }); + const embeddedBoardView = createMockView({ + view_id: 'board-view', + layout: ViewLayout.Board, + extra: { is_space: false, database_id: 'db-1', embedded: true }, + }); + const container = createMockView({ + view_id: 'container', + layout: ViewLayout.Grid, + extra: { + is_space: false, + is_database_container: true, + database_id: 'db-1', + }, + children: [gridView, embeddedBoardView], + }); + + expect(isEmbeddedView(gridView)).toBe(false); + expect(isEmbeddedView(embeddedBoardView)).toBe(true); + expect(getDatabaseTabViewIds(gridView.view_id, container)).toEqual([gridView.view_id]); + }); + + it('shows only the embedded view when opening it from the sidebar', () => { + const gridView = createMockView({ + view_id: 'grid-view', + layout: ViewLayout.Grid, + extra: { is_space: false, database_id: 'db-1' }, + }); + const embeddedBoardView = createMockView({ + view_id: 'board-view', + layout: ViewLayout.Board, + extra: { is_space: false, database_id: 'db-1', embedded: true }, + }); + const container = createMockView({ + view_id: 'container', + layout: ViewLayout.Grid, + extra: { + is_space: false, + is_database_container: true, + database_id: 'db-1', + }, + children: [gridView, embeddedBoardView], + }); + + expect(getDatabaseTabViewIds(embeddedBoardView.view_id, container)).toEqual([ + embeddedBoardView.view_id, + ]); + }); + + it('falls back to display tabs when opening a container directly', () => { + const gridView = createMockView({ + view_id: 'grid-view', + layout: ViewLayout.Grid, + extra: { is_space: false, database_id: 'db-1' }, + }); + const embeddedBoardView = createMockView({ + view_id: 'board-view', + layout: ViewLayout.Board, + extra: { is_space: false, database_id: 'db-1', embedded: true }, + }); + const container = createMockView({ + view_id: 'container', + layout: ViewLayout.Grid, + extra: { + is_space: false, + is_database_container: true, + database_id: 'db-1', + }, + children: [gridView, embeddedBoardView], + }); + + expect(getDatabaseTabViewIds(container.view_id, container)).toEqual([gridView.view_id]); + }); + + it('keeps all tabs when a database only has embedded views', () => { + const embeddedGridView = createMockView({ + view_id: 'embedded-grid', + layout: ViewLayout.Grid, + extra: { is_space: false, database_id: 'db-1', embedded: true }, + }); + const embeddedBoardView = createMockView({ + view_id: 'embedded-board', + layout: ViewLayout.Board, + extra: { is_space: false, database_id: 'db-1', embedded: true }, + }); + const container = createMockView({ + view_id: 'container', + layout: ViewLayout.Grid, + extra: { + is_space: false, + is_database_container: true, + database_id: 'db-1', + embedded: true, + }, + children: [embeddedGridView, embeddedBoardView], + }); + + expect(getDatabaseTabViewIds(embeddedGridView.view_id, container)).toEqual([ + embeddedGridView.view_id, + embeddedBoardView.view_id, + ]); + }); + }); + /** * Backward Compatibility: Views without container support * diff --git a/src/application/types.ts b/src/application/types.ts index 81c322a9..f2b7f4b5 100644 --- a/src/application/types.ts +++ b/src/application/types.ts @@ -924,6 +924,10 @@ export interface ViewExtra { }; is_hidden_space?: boolean; + // Whether this view is embedded inside a document (e.g. a linked database view). + // This is aligned with Desktop/Flutter and server-side `EXTRA_KEY_EMBEDDED`. + embedded?: boolean; + // Database container support (aligned with Desktop/Flutter) // Reference: AppFlowy-Premium/frontend/doc/context/database_container_behavior.md is_database_container?: boolean; // True if this view is a database container diff --git a/src/application/view-utils.ts b/src/application/view-utils.ts index 904a3275..0d23c64a 100644 --- a/src/application/view-utils.ts +++ b/src/application/view-utils.ts @@ -25,6 +25,16 @@ export function isDatabaseLayout(layout: ViewLayout): boolean { ); } +/** + * Check if a view is marked as embedded in its extra. + * + * Embedded views are created inside documents (e.g. database blocks) and should not + * appear as tabs in the "source" database container page. + */ +export function isEmbeddedView(view: View | null | undefined): boolean { + return view?.extra?.embedded === true; +} + /** * Check if view is a database container. * @@ -91,3 +101,40 @@ export function getFirstChildView(view: View | null | undefined): View | undefin return undefined; } + +/** + * Returns the list of database view IDs that should be displayed in the tab bar. + * + * Mirrors Desktop/Flutter behavior: + * - Database containers can have both non-embedded "display views" and embedded views. + * - Embedded views should not appear as tabs when viewing the source database container. + * - When navigating directly to an embedded child view from the sidebar, show only that view. + */ +export function getDatabaseTabViewIds(currentViewId: string, containerView: View): string[] { + const children = containerView.children ?? []; + const childViewIds = children.map((child) => child.view_id); + + if (childViewIds.length === 0) { + return [currentViewId]; + } + + const nonEmbeddedChildIds = children + .filter((child) => !isEmbeddedView(child)) + .map((child) => child.view_id); + + const displayViewIds = nonEmbeddedChildIds.length > 0 ? nonEmbeddedChildIds : childViewIds; + + // If the current view is one of the display views, show the full display list. + if (displayViewIds.includes(currentViewId)) { + return displayViewIds; + } + + // If the current view is a child but not a display view, treat it as an embedded + // view opened as a standalone page and only show itself as a single tab. + if (childViewIds.includes(currentViewId)) { + return [currentViewId]; + } + + // Otherwise, treat it as opening the container (or a stale route param). + return displayViewIds; +} diff --git a/src/components/app/DatabaseView.tsx b/src/components/app/DatabaseView.tsx index 80ad1ec5..d7bdc210 100644 --- a/src/components/app/DatabaseView.tsx +++ b/src/components/app/DatabaseView.tsx @@ -2,7 +2,7 @@ import { Suspense, useCallback, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { ViewComponentProps, ViewLayout, YDatabase, YjsEditorKey } from '@/application/types'; -import { isDatabaseContainer } from '@/application/view-utils'; +import { getDatabaseTabViewIds, isDatabaseContainer } from '@/application/view-utils'; import { findView } from '@/components/_shared/outline/utils'; import ComponentLoading from '@/components/_shared/progress/ComponentLoading'; import CalendarSkeleton from '@/components/_shared/skeleton/CalendarSkeleton'; @@ -23,7 +23,7 @@ function DatabaseView(props: ViewComponentProps) { * The database's page ID in the folder/outline structure. * This is the main entry point for the database and remains constant. */ - const databasePageId = viewMeta.viewId; + const databasePageId = viewMeta.viewId || ''; const view = useMemo(() => { if (!outline || !databasePageId) return; @@ -53,12 +53,12 @@ function DatabaseView(props: ViewComponentProps) { const visibleViewIds = useMemo(() => { if (containerView) { - return containerView.children?.map((v) => v.view_id) || []; + return getDatabaseTabViewIds(databasePageId, containerView); } if (!view) return []; return [view.view_id, ...(view.children?.map((v) => v.view_id) || [])]; - }, [containerView, view]); + }, [containerView, view, databasePageId]); const pageMeta = useMemo(() => { if (!pageView) {