Skip to content

Commit cdf7235

Browse files
committed
fix: create db view order
1 parent ae677e5 commit cdf7235

12 files changed

Lines changed: 552 additions & 24 deletions

File tree

src/application/database-yjs/__tests__/useAddDatabaseView.test.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ describe('useAddDatabaseView', () => {
9292
activeViewId,
9393
expect.objectContaining({
9494
parent_view_id: containerId,
95+
prev_view_id: activeViewId,
9596
database_id: databaseId,
9697
layout: ViewLayout.Calendar,
9798
name: 'Calendar',
@@ -154,6 +155,7 @@ describe('useAddDatabaseView', () => {
154155
baseViewId,
155156
expect.objectContaining({
156157
parent_view_id: documentId,
158+
prev_view_id: baseViewId,
157159
database_id: databaseId,
158160
layout: ViewLayout.Board,
159161
name: 'Board',
@@ -221,5 +223,69 @@ describe('useAddDatabaseView', () => {
221223
embedded: false,
222224
})
223225
);
226+
227+
const [, payload] = createDatabaseView.mock.calls[0];
228+
229+
expect(payload.prev_view_id).toBeUndefined();
230+
});
231+
232+
it('uses the container last child as prev_view_id when the current route is the container itself', async () => {
233+
const databaseId = 'db-1';
234+
const containerId = 'container-view-id';
235+
const firstChildId = 'grid-view-id';
236+
const secondChildId = 'board-view-id';
237+
238+
const createDatabaseView = jest.fn().mockResolvedValue({
239+
view_id: 'new-view-id',
240+
database_id: databaseId,
241+
});
242+
243+
const loadViewMeta = jest.fn(async (viewId: string) => {
244+
if (viewId === containerId) {
245+
return createView({
246+
view_id: containerId,
247+
layout: ViewLayout.Grid,
248+
extra: { is_space: false, is_database_container: true },
249+
children: [
250+
createView({ view_id: firstChildId, layout: ViewLayout.Grid, parent_view_id: containerId }),
251+
createView({ view_id: secondChildId, layout: ViewLayout.Board, parent_view_id: containerId }),
252+
],
253+
});
254+
}
255+
256+
return null;
257+
});
258+
259+
const contextValue: DatabaseContextState = {
260+
readOnly: false,
261+
databaseDoc: createDatabaseDoc(databaseId),
262+
databasePageId: containerId,
263+
activeViewId: containerId,
264+
rowDocMap: {},
265+
workspaceId: 'workspace-id',
266+
createDatabaseView,
267+
loadViewMeta,
268+
isDocumentBlock: false,
269+
};
270+
271+
const { result } = renderHook(() => useAddDatabaseView(), {
272+
wrapper: ({ children }) => <DatabaseContext.Provider value={contextValue}>{children}</DatabaseContext.Provider>,
273+
});
274+
275+
await act(async () => {
276+
await result.current(DatabaseViewLayout.Calendar, 'Calendar');
277+
});
278+
279+
expect(createDatabaseView).toHaveBeenCalledWith(
280+
containerId,
281+
expect.objectContaining({
282+
parent_view_id: containerId,
283+
prev_view_id: secondChildId,
284+
database_id: databaseId,
285+
layout: ViewLayout.Calendar,
286+
name: 'Calendar',
287+
embedded: false,
288+
})
289+
);
224290
});
225291
});

src/application/database-yjs/__tests__/useDatabaseViewsSelector.test.tsx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,21 @@ jest.mock('@/utils/runtime-config', () => ({
1010
getConfigValue: (_key: string, fallback: string) => fallback,
1111
}));
1212

13-
function createDatabaseDocWithViews(viewIdsInInsertionOrder: string[]): YDoc {
13+
function createDatabaseDocWithViews(
14+
viewIdsInInsertionOrder: Array<string | { viewId: string; createdAt?: string }>
15+
): YDoc {
1416
const doc = new Y.Doc() as unknown as YDoc;
1517
const sharedRoot = doc.getMap(YjsEditorKey.data_section);
1618
const database = new Y.Map();
1719
const views = new Y.Map();
1820

19-
viewIdsInInsertionOrder.forEach((viewId) => {
21+
viewIdsInInsertionOrder.forEach((input, index) => {
22+
const viewId = typeof input === 'string' ? input : input.viewId;
23+
const createdAt =
24+
typeof input === 'string' ? new Date(Date.UTC(2024, 0, index + 1)).toISOString() : input.createdAt;
2025
const view = new Y.Map();
2126

22-
view.set('created_at', new Date().toISOString());
27+
view.set(YjsDatabaseKey.created_at, createdAt);
2328
views.set(viewId, view);
2429
});
2530

@@ -29,6 +34,40 @@ function createDatabaseDocWithViews(viewIdsInInsertionOrder: string[]): YDoc {
2934
}
3035

3136
describe('useDatabaseViewsSelector', () => {
37+
it('sorts standalone database views by created_at instead of raw Yjs insertion order', () => {
38+
const gridId = 'grid-id';
39+
const boardId = 'board-id';
40+
const calendarId = 'calendar-id';
41+
42+
const databaseDoc = createDatabaseDocWithViews([
43+
{ viewId: boardId, createdAt: '2024-01-02T00:00:00.000Z' },
44+
{ viewId: gridId, createdAt: '2024-01-01T00:00:00.000Z' },
45+
{ viewId: calendarId, createdAt: '2024-01-03T00:00:00.000Z' },
46+
]);
47+
48+
const contextValue: DatabaseContextState = {
49+
readOnly: true,
50+
databaseDoc,
51+
databasePageId: gridId,
52+
activeViewId: gridId,
53+
rowDocMap: null,
54+
workspaceId: 'workspace-id',
55+
};
56+
57+
const { result } = renderHook(
58+
() => useDatabaseViewsSelector(gridId),
59+
{
60+
wrapper: ({ children }) => (
61+
<DatabaseContext.Provider value={contextValue}>
62+
{children}
63+
</DatabaseContext.Provider>
64+
),
65+
}
66+
);
67+
68+
expect(result.current.viewIds).toEqual([gridId, boardId, calendarId]);
69+
});
70+
3271
it('preserves visibleViewIds ordering (folder/outline order)', () => {
3372
const gridId = 'grid-id';
3473
const boardId = 'board-id';

src/application/database-yjs/dispatch.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2220,10 +2220,19 @@ export function useAddDatabaseView() {
22202220
const viewLayout = layoutToViewLayout[layout];
22212221
const name = layoutToName[layout];
22222222

2223-
const tabsParentViewId = await (async (): Promise<string> => {
2223+
const getLastChildViewId = (view: View | null | undefined): string | undefined => {
2224+
const children = view?.children ?? [];
2225+
2226+
return children.length > 0 ? children[children.length - 1].view_id : undefined;
2227+
};
2228+
2229+
const { tabsParentViewId, prevViewId } = await (async (): Promise<{
2230+
tabsParentViewId: string;
2231+
prevViewId?: string;
2232+
}> => {
22242233
// Best-effort: fall back to previous behavior if meta lookup isn't available.
22252234
if (!loadViewMeta) {
2226-
return databasePageId;
2235+
return { tabsParentViewId: databasePageId };
22272236
}
22282237

22292238
const safeLoadViewMeta = async (viewId: string): Promise<View | null> => {
@@ -2238,34 +2247,47 @@ export function useAddDatabaseView() {
22382247

22392248
// If the current view itself is a container, attach under it.
22402249
if (currentMeta && isDatabaseContainer(currentMeta)) {
2241-
return currentMeta.view_id;
2250+
return {
2251+
tabsParentViewId: currentMeta.view_id,
2252+
prevViewId: getLastChildViewId(currentMeta),
2253+
};
22422254
}
22432255

22442256
const parentId = currentMeta?.parent_view_id;
22452257

22462258
if (!parentId) {
2247-
return databasePageId;
2259+
return { tabsParentViewId: databasePageId };
22482260
}
22492261

22502262
// If parent is a database container, attach under the container (Scenario 4).
22512263
const parentMeta = await safeLoadViewMeta(parentId);
22522264

22532265
if (isDatabaseContainer(parentMeta)) {
2254-
return parentId;
2266+
return {
2267+
tabsParentViewId: parentId,
2268+
prevViewId: currentMeta?.view_id,
2269+
};
22552270
}
22562271

22572272
// Embedded databases without a container attach under the document (Scenario 3).
22582273
if (isDocumentBlock) {
2259-
return parentId;
2274+
return {
2275+
tabsParentViewId: parentId,
2276+
prevViewId: currentMeta?.view_id,
2277+
};
22602278
}
22612279

22622280
// Backward-compatible fallback: attach under the current database view.
2263-
return databasePageId;
2281+
return {
2282+
tabsParentViewId: databasePageId,
2283+
prevViewId: getLastChildViewId(currentMeta),
2284+
};
22642285
})();
22652286

22662287
// Create new view as a child of the database container (or document for embedded linked views).
22672288
const response = await createDatabaseView(requestViewId, {
22682289
parent_view_id: tabsParentViewId,
2290+
prev_view_id: prevViewId,
22692291
database_id: databaseId,
22702292
layout: viewLayout,
22712293
name: nameOverride ?? name,

src/application/database-yjs/selector.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,26 @@ export function useDatabaseViewsSelector(databasePageId: string, visibleViewIds?
107107
}
108108
>;
109109

110+
const insertionOrder = new Map<string, number>();
111+
112+
const getCreatedAtSortValue = (viewId: string): number => {
113+
const createdAt = views.get(viewId)?.get(YjsDatabaseKey.created_at);
114+
115+
if (!createdAt) {
116+
return Number.POSITIVE_INFINITY;
117+
}
118+
119+
const numericValue = Number(createdAt);
120+
121+
if (Number.isFinite(numericValue)) {
122+
return numericValue;
123+
}
124+
125+
const timestampValue = Date.parse(createdAt);
126+
127+
return Number.isFinite(timestampValue) ? timestampValue : Number.POSITIVE_INFINITY;
128+
};
129+
110130
// Step 1: Get all non-inline views from Yjs (don't filter by embedded yet)
111131
// See: flowy-database2/src/services/database/database_editor.rs:get_database_view_ids()
112132
let allViewIds = Object.keys(viewsObj).filter((viewId) => {
@@ -119,6 +139,10 @@ export function useDatabaseViewsSelector(databasePageId: string, visibleViewIds?
119139
return !isInline;
120140
});
121141

142+
allViewIds.forEach((viewId, index) => {
143+
insertionOrder.set(viewId, index);
144+
});
145+
122146
// Step 2: Apply context-specific filtering (separate concerns)
123147
if (stableVisibleViewIds !== undefined && stableVisibleViewIds.length > 0) {
124148
// For embedded databases: show ONLY views in visibleViewIds
@@ -136,6 +160,16 @@ export function useDatabaseViewsSelector(databasePageId: string, visibleViewIds?
136160

137161
return !isEmbedded;
138162
});
163+
164+
allViewIds.sort((left, right) => {
165+
const createdAtDiff = getCreatedAtSortValue(left) - getCreatedAtSortValue(right);
166+
167+
if (createdAtDiff !== 0) {
168+
return createdAtDiff;
169+
}
170+
171+
return (insertionOrder.get(left) ?? 0) - (insertionOrder.get(right) ?? 0);
172+
});
139173
}
140174

141175
setViewIds(allViewIds);

src/application/services/js-services/http/page-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export async function createDatabaseView(workspaceId: string, viewId: string, pa
136136
return executeAPIRequest<CreateDatabaseViewResponse>(() =>
137137
getAxios()?.post<APIResponse<CreateDatabaseViewResponse>>(url, {
138138
parent_view_id: payload.parent_view_id,
139+
prev_view_id: payload.prev_view_id,
139140
database_id: payload.database_id,
140141
layout: payload.layout,
141142
name: payload.name,

src/application/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,6 +1391,8 @@ export interface DuplicatePageOptions {
13911391

13921392
export interface CreateDatabaseViewPayload {
13931393
parent_view_id: string;
1394+
/** Insert the new database view after this sibling. When omitted the backend prepends. */
1395+
prev_view_id?: string;
13941396
database_id: string;
13951397
layout: ViewLayout;
13961398
name?: string;

0 commit comments

Comments
 (0)