chore: fix open container

This commit is contained in:
Nathan
2025-12-14 22:31:45 +08:00
parent 7307b936f2
commit db2d17bbf2
4 changed files with 167 additions and 4 deletions

View File

@@ -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
*

View File

@@ -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

View File

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

View File

@@ -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) {