Skip to content

Commit 59c31c5

Browse files
authored
Fix database row order refresh (#345)
1 parent dfba7be commit 59c31c5

2 files changed

Lines changed: 118 additions & 5 deletions

File tree

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
YDatabaseField,
1616
YDatabaseFilter,
1717
YDatabaseFilters,
18+
YDatabaseRowOrders,
1819
YDatabaseSorts,
1920
YDatabaseView,
2021
YDoc,
@@ -32,6 +33,8 @@ type DatabaseFixture = {
3233
databaseDoc: YDoc;
3334
filters: YDatabaseFilters;
3435
rowMap: Record<RowId, YDoc>;
36+
rowOrders: YDatabaseRowOrders;
37+
view: YDatabaseView;
3538
viewId: string;
3639
};
3740

@@ -103,6 +106,8 @@ function createDatabaseFixture(): DatabaseFixture {
103106
[fieldId]: createCell(FieldType.RichText, 'skip'),
104107
}),
105108
},
109+
rowOrders,
110+
view,
106111
viewId,
107112
};
108113
}
@@ -170,6 +175,84 @@ describe('useRowOrdersSelector', () => {
170175
});
171176
});
172177

178+
it('updates unconditioned row order immediately when rows are added or removed', async () => {
179+
const fixture = createDatabaseFixture();
180+
const { result } = renderHook(() => useRowOrdersSelector(), {
181+
wrapper: createWrapper(fixture),
182+
});
183+
184+
await waitFor(() => {
185+
expect(result.current?.map((row) => row.id)).toEqual(['row-c', 'row-a', 'row-b']);
186+
});
187+
188+
act(() => {
189+
fixture.rowOrders.insert(1, [{ id: 'row-new', height: 44 }]);
190+
});
191+
192+
expect(result.current?.map((row) => row.id)).toEqual(['row-c', 'row-new', 'row-a', 'row-b']);
193+
194+
act(() => {
195+
fixture.rowOrders.delete(0);
196+
});
197+
198+
expect(result.current?.map((row) => row.id)).toEqual(['row-new', 'row-a', 'row-b']);
199+
});
200+
201+
it('resubscribes when the row order array is replaced', async () => {
202+
const fixture = createDatabaseFixture();
203+
const { result } = renderHook(() => useRowOrdersSelector(), {
204+
wrapper: createWrapper(fixture),
205+
});
206+
207+
await waitFor(() => {
208+
expect(result.current?.map((row) => row.id)).toEqual(['row-c', 'row-a', 'row-b']);
209+
});
210+
211+
const replacementRowOrders = new Y.Array<{ id: RowId; height: number }>() as YDatabaseRowOrders;
212+
213+
replacementRowOrders.push([{ id: 'row-replacement', height: 44 }]);
214+
215+
act(() => {
216+
fixture.view.set(YjsDatabaseKey.row_orders, replacementRowOrders);
217+
});
218+
219+
await waitFor(() => {
220+
expect(result.current?.map((row) => row.id)).toEqual(['row-replacement']);
221+
});
222+
223+
act(() => {
224+
replacementRowOrders.push([{ id: 'row-new', height: 44 }]);
225+
});
226+
227+
expect(result.current?.map((row) => row.id)).toEqual(['row-replacement', 'row-new']);
228+
});
229+
230+
it('does not publish raw row order immediately when filters are active', async () => {
231+
const fixture = createDatabaseFixture();
232+
const { result } = renderHook(() => useRowOrdersSelector(), {
233+
wrapper: createWrapper(fixture),
234+
});
235+
236+
await waitFor(() => {
237+
expect(result.current?.map((row) => row.id)).toEqual(['row-c', 'row-a', 'row-b']);
238+
});
239+
240+
act(() => {
241+
fixture.filters.push([createTextFilter('match')]);
242+
jest.advanceTimersByTime(250);
243+
});
244+
245+
await waitFor(() => {
246+
expect(result.current?.map((row) => row.id)).toEqual(['row-a', 'row-b']);
247+
});
248+
249+
act(() => {
250+
fixture.rowOrders.insert(0, [{ id: 'row-new', height: 44 }]);
251+
});
252+
253+
expect(result.current?.map((row) => row.id)).toEqual(['row-a', 'row-b']);
254+
});
255+
173256
it('requests missing row docs while a conditioned view is loading', async () => {
174257
const fixture = createDatabaseFixture();
175258
const ensureRow = jest.fn(async () => undefined);

src/application/database-yjs/selector.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,7 @@ export function useRowsByGroup(groupId: string) {
11821182
export function useRowOrdersSelector() {
11831183
const rows = useRowMap();
11841184
const view = useDatabaseView();
1185+
const rowOrders = view?.get(YjsDatabaseKey.row_orders);
11851186
const viewId = useDatabaseViewId();
11861187
const sorts = view?.get(YjsDatabaseKey.sorts);
11871188
const fields = useDatabaseFields();
@@ -1316,6 +1317,29 @@ export function useRowOrdersSelector() {
13161317
[blobPrefetchComplete, ensureRow, loadRowFromSeed, markConditionRowsUnavailable, seedsReady]
13171318
);
13181319

1320+
const syncUnconditionedRowOrders = useCallback(() => {
1321+
const originalRowOrders = rowOrders?.toJSON() as Row[] | undefined;
1322+
1323+
if (!originalRowOrders) return false;
1324+
1325+
const conditionSignature = getConditionSignature(sorts, filters);
1326+
const conditionStateKey = `${viewId ?? ''}:${conditionSignature}`;
1327+
const currentHasConditions = conditionSignature !== '';
1328+
1329+
if (conditionSignatureRef.current !== conditionStateKey) {
1330+
conditionSignatureRef.current = conditionStateKey;
1331+
filtersAppliedRef.current = false;
1332+
pendingConditionRowLoadsRef.current.clear();
1333+
unavailableConditionRowsRef.current.clear();
1334+
}
1335+
1336+
if (currentHasConditions) return false;
1337+
1338+
filtersAppliedRef.current = false;
1339+
setRowOrdersState({ rows: originalRowOrders, conditionSignature: conditionStateKey });
1340+
return true;
1341+
}, [filters, rowOrders, sorts, viewId]);
1342+
13191343
// Getter for relation cell text (used in sorting/filtering)
13201344
const relationTextGetter = useCallback(
13211345
(rowId: string, fieldId: string) => {
@@ -1381,7 +1405,7 @@ export function useRowOrdersSelector() {
13811405
const onConditionsChange = useCallback(() => {
13821406
const shouldLogConditionCompute = shouldLogDatabaseConditionPerformance();
13831407
const computeStartedAt = shouldLogConditionCompute ? performance.now() : 0;
1384-
const originalRowOrders = view?.get(YjsDatabaseKey.row_orders)?.toJSON() as Row[] | undefined;
1408+
const originalRowOrders = rowOrders?.toJSON() as Row[] | undefined;
13851409

13861410
if (!originalRowOrders) return;
13871411

@@ -1477,7 +1501,7 @@ export function useRowOrdersSelector() {
14771501
filters,
14781502
rowDocsForConditions,
14791503
sorts,
1480-
view,
1504+
rowOrders,
14811505
relationTextGetter,
14821506
rollupValueGetter,
14831507
rollupTextGetter,
@@ -1511,7 +1535,13 @@ export function useRowOrdersSelector() {
15111535
onConditionsChange();
15121536
}, 200);
15131537

1514-
view?.get(YjsDatabaseKey.row_orders)?.observeDeep(debouncedChange);
1538+
const handleRowOrdersChange = () => {
1539+
if (!syncUnconditionedRowOrders()) {
1540+
debouncedChange();
1541+
}
1542+
};
1543+
1544+
rowOrders?.observeDeep(handleRowOrdersChange);
15151545

15161546
const observers = new Map<string, () => void>();
15171547
let relationFieldIds: string[] = [];
@@ -1593,7 +1623,7 @@ export function useRowOrdersSelector() {
15931623
});
15941624

15951625
return () => {
1596-
view?.get(YjsDatabaseKey.row_orders)?.unobserveDeep(debouncedChange);
1626+
rowOrders?.unobserveDeep(handleRowOrdersChange);
15971627
sorts?.unobserveDeep(handleSortFilterChange);
15981628
filters?.unobserveDeep(handleSortFilterChange);
15991629
fields?.unobserveDeep(handleFieldChange);
@@ -1606,7 +1636,7 @@ export function useRowOrdersSelector() {
16061636
}
16071637
});
16081638
};
1609-
}, [onConditionsChange, view, fields, filters, sorts, rows, viewId]);
1639+
}, [onConditionsChange, rowOrders, fields, filters, sorts, rows, viewId, syncUnconditionedRowOrders]);
16101640

16111641
// Set up rollup field observers (extracted hook)
16121642
useRollupFieldObservers(onConditionsChange, rollupWatchVersion);

0 commit comments

Comments
 (0)