Skip to content

Commit 074a63a

Browse files
committed
fix: ensure row doc before cell updates
1 parent 53b075e commit 074a63a

3 files changed

Lines changed: 298 additions & 234 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { renderHook, waitFor } from '@testing-library/react';
2+
import type React from 'react';
3+
import * as Y from 'yjs';
4+
5+
import { DatabaseContext, DatabaseContextState, FieldType } from '@/application/database-yjs';
6+
import { useUpdateCellDispatch } from '@/application/database-yjs/dispatch';
7+
import {
8+
RowId,
9+
YDatabase,
10+
YDatabaseField,
11+
YDatabaseFields,
12+
YDatabaseView,
13+
YDatabaseViews,
14+
YDoc,
15+
YjsDatabaseKey,
16+
YjsEditorKey,
17+
} from '@/application/types';
18+
19+
import { createRowDoc } from './test-helpers';
20+
21+
jest.mock('@/utils/runtime-config', () => ({
22+
getConfigValue: (_key: string, fallback: string) => fallback,
23+
}));
24+
25+
const databaseId = 'database-id';
26+
const viewId = 'view-id';
27+
const rowId = 'row-id';
28+
const fieldId = 'name-field-id';
29+
30+
function createTextField(): YDatabaseField {
31+
const field = new Y.Map() as YDatabaseField;
32+
33+
field.set(YjsDatabaseKey.id, fieldId);
34+
field.set(YjsDatabaseKey.name, 'Name');
35+
field.set(YjsDatabaseKey.type, FieldType.RichText);
36+
37+
return field;
38+
}
39+
40+
function createDatabaseDoc(): YDoc {
41+
const doc = new Y.Doc({ guid: databaseId }) as YDoc;
42+
const sharedRoot = doc.getMap(YjsEditorKey.data_section);
43+
const database = new Y.Map() as YDatabase;
44+
const fields = new Y.Map<YDatabaseField>() as YDatabaseFields;
45+
const views = new Y.Map<YDatabaseView>() as YDatabaseViews;
46+
const view = new Y.Map() as YDatabaseView;
47+
48+
fields.set(fieldId, createTextField());
49+
views.set(viewId, view);
50+
database.set(YjsDatabaseKey.id, databaseId);
51+
database.set(YjsDatabaseKey.fields, fields);
52+
database.set(YjsDatabaseKey.views, views);
53+
sharedRoot.set(YjsEditorKey.database, database);
54+
55+
return doc;
56+
}
57+
58+
function getCellData(rowDoc: YDoc) {
59+
const row = rowDoc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row);
60+
const cells = row?.get(YjsDatabaseKey.cells);
61+
62+
return cells?.get(fieldId)?.get(YjsDatabaseKey.data);
63+
}
64+
65+
function createWrapper(contextValue: DatabaseContextState) {
66+
return ({ children }: { children: React.ReactNode }) => (
67+
<DatabaseContext.Provider value={contextValue}>{children}</DatabaseContext.Provider>
68+
);
69+
}
70+
71+
describe('useUpdateCellDispatch', () => {
72+
it('ensures a missing row doc before committing the cell update', async () => {
73+
const databaseDoc = createDatabaseDoc();
74+
const rowDoc = createRowDoc(rowId, databaseId, {});
75+
const ensureRow = jest.fn<Promise<YDoc>, [RowId]>().mockResolvedValue(rowDoc);
76+
const contextValue: DatabaseContextState = {
77+
readOnly: false,
78+
databaseDoc,
79+
databasePageId: viewId,
80+
activeViewId: viewId,
81+
rowMap: {},
82+
ensureRow,
83+
workspaceId: 'workspace-id',
84+
};
85+
const { result } = renderHook(() => useUpdateCellDispatch(rowId, fieldId), {
86+
wrapper: createWrapper(contextValue),
87+
});
88+
89+
result.current('Recovered value');
90+
91+
await waitFor(() => {
92+
expect(getCellData(rowDoc)).toBe('Recovered value');
93+
});
94+
expect(ensureRow).toHaveBeenCalledWith(rowId);
95+
});
96+
});

src/application/database-yjs/dispatch.ts

Lines changed: 2 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { getDefaultFilterCondition } from '@/application/database-yjs/filter';
4040
import { DEFAULT_FIELD_WRAP } from '@/application/database-yjs/const';
4141
import { getOptionsFromRow } from '@/application/database-yjs/row';
4242
import { getMetaIdMap } from '@/application/database-yjs/row_meta';
43-
import { useBoardLayoutSettings, useCalendarLayoutSetting, useFieldSelector, useFieldType } from '@/application/database-yjs/selector';
43+
import { useBoardLayoutSettings, useCalendarLayoutSetting, useFieldType } from '@/application/database-yjs/selector';
4444
import { deleteCollabDB } from '@/application/db';
4545
import { deleteOutboxByObjectId } from '@/application/sync-outbox';
4646
import { executeOperations } from '@/application/slate-yjs/utils/yjs';
@@ -1727,158 +1727,7 @@ export function useUpdateRowMetaDispatch(rowId: string) {
17271727
);
17281728
}
17291729

1730-
function updateDateCell(
1731-
cell: YDatabaseCell,
1732-
payload: {
1733-
data: string;
1734-
endTimestamp?: string;
1735-
includeTime?: boolean;
1736-
isRange?: boolean;
1737-
reminderId?: string;
1738-
}
1739-
) {
1740-
cell.set(YjsDatabaseKey.data, payload.data);
1741-
1742-
if (payload.endTimestamp !== undefined) {
1743-
cell.set(YjsDatabaseKey.end_timestamp, payload.endTimestamp);
1744-
}
1745-
1746-
if (payload.includeTime !== undefined) {
1747-
Log.debug('includeTime', payload.includeTime);
1748-
cell.set(YjsDatabaseKey.include_time, payload.includeTime);
1749-
}
1750-
1751-
if (payload.isRange !== undefined) {
1752-
cell.set(YjsDatabaseKey.is_range, payload.isRange);
1753-
}
1754-
1755-
if (payload.reminderId !== undefined) {
1756-
cell.set(YjsDatabaseKey.reminder_id, payload.reminderId);
1757-
}
1758-
}
1759-
1760-
export function useUpdateCellDispatch(rowId: string, fieldId: string) {
1761-
const rowMap = useRowMap();
1762-
const { field } = useFieldSelector(fieldId);
1763-
1764-
return useCallback(
1765-
(
1766-
data: string | Y.Array<string>,
1767-
dateOpts?: {
1768-
endTimestamp?: string;
1769-
includeTime?: boolean;
1770-
isRange?: boolean;
1771-
reminderId?: string;
1772-
}
1773-
) => {
1774-
const rowDoc = rowMap?.[rowId];
1775-
1776-
if (!rowDoc) {
1777-
Log.warn('[useUpdateCellDispatch] Row doc not found', { rowId, fieldId });
1778-
return;
1779-
}
1780-
1781-
const rowSharedRoot = rowDoc.getMap(YjsEditorKey.data_section) as YSharedRoot;
1782-
const row = rowSharedRoot.get(YjsEditorKey.database_row);
1783-
1784-
if (!row) {
1785-
Log.warn('[useUpdateCellDispatch] Row data not found', { rowId, fieldId });
1786-
return;
1787-
}
1788-
1789-
const cells = row.get(YjsDatabaseKey.cells);
1790-
1791-
if (!cells) {
1792-
Log.warn('[useUpdateCellDispatch] Row cells not found', { rowId, fieldId });
1793-
return;
1794-
}
1795-
1796-
const cell = cells.get(fieldId);
1797-
1798-
const type = Number(field.get(YjsDatabaseKey.type));
1799-
1800-
rowDoc.transact(() => {
1801-
if (!cell) {
1802-
const newCell = new Y.Map() as YDatabaseCell;
1803-
1804-
newCell.set(YjsDatabaseKey.created_at, String(dayjs().unix()));
1805-
newCell.set(YjsDatabaseKey.field_type, type);
1806-
newCell.set(YjsDatabaseKey.data, data);
1807-
newCell.set(YjsDatabaseKey.last_modified, String(dayjs().unix()));
1808-
1809-
if (dateOpts && (typeof data === 'string' || typeof data === 'number')) {
1810-
updateDateCell(newCell, {
1811-
data,
1812-
...dateOpts,
1813-
});
1814-
}
1815-
1816-
cells.set(fieldId, newCell);
1817-
} else {
1818-
cell.set(YjsDatabaseKey.data, data);
1819-
1820-
if (dateOpts && (typeof data === 'string' || typeof data === 'number')) {
1821-
updateDateCell(cell, {
1822-
data,
1823-
...dateOpts,
1824-
});
1825-
}
1826-
1827-
cell.set(YjsDatabaseKey.field_type, type);
1828-
cell.set(YjsDatabaseKey.last_modified, String(dayjs().unix()));
1829-
}
1830-
1831-
row.set(YjsDatabaseKey.last_modified, String(dayjs().unix()));
1832-
});
1833-
},
1834-
[field, fieldId, rowMap, rowId]
1835-
);
1836-
}
1837-
1838-
export function useUpdateStartEndTimeCell() {
1839-
const rowMap = useRowMap();
1840-
1841-
return useCallback(
1842-
(rowId: string, fieldId: string, startTimestamp: string, endTimestamp?: string, isAllDay?: boolean) => {
1843-
const rowDoc = rowMap?.[rowId];
1844-
1845-
if (!rowDoc) {
1846-
throw new Error(`Row not found`);
1847-
}
1848-
1849-
const rowSharedRoot = rowDoc.getMap(YjsEditorKey.data_section) as YSharedRoot;
1850-
const row = rowSharedRoot.get(YjsEditorKey.database_row);
1851-
1852-
const cells = row.get(YjsDatabaseKey.cells);
1853-
1854-
rowDoc.transact(() => {
1855-
let cell = cells.get(fieldId);
1856-
1857-
if (!cell) {
1858-
cell = new Y.Map() as YDatabaseCell;
1859-
cell.set(YjsDatabaseKey.field_type, FieldType.DateTime);
1860-
1861-
cell.set(YjsDatabaseKey.created_at, String(dayjs().unix()));
1862-
cells.set(fieldId, cell);
1863-
}
1864-
1865-
1866-
cell.set(YjsDatabaseKey.data, startTimestamp);
1867-
cell.set(YjsDatabaseKey.last_modified, String(dayjs().unix()));
1868-
1869-
updateDateCell(cell, {
1870-
data: startTimestamp,
1871-
endTimestamp,
1872-
isRange: !!endTimestamp,
1873-
includeTime: !isAllDay,
1874-
});
1875-
row.set(YjsDatabaseKey.last_modified, String(dayjs().unix()));
1876-
});
1877-
1878-
},
1879-
[rowMap]
1880-
);
1881-
}
1730+
export { useUpdateCellDispatch, useUpdateStartEndTimeCell } from './dispatch/cell';
18821731

18831732
function generateBoardSetting(database: YDatabase): YDatabaseFieldSettings {
18841733
const fieldSettingsMap = new Y.Map() as YDatabaseFieldSettings;

0 commit comments

Comments
 (0)