Skip to content

Commit 763e143

Browse files
committed
fix: propagate calendar time update failures
1 parent 074a63a commit 763e143

4 files changed

Lines changed: 94 additions & 41 deletions

File tree

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type React from 'react';
33
import * as Y from 'yjs';
44

55
import { DatabaseContext, DatabaseContextState, FieldType } from '@/application/database-yjs';
6-
import { useUpdateCellDispatch } from '@/application/database-yjs/dispatch';
6+
import { useUpdateCellDispatch, useUpdateStartEndTimeCell } from '@/application/database-yjs/dispatch';
77
import {
88
RowId,
99
YDatabase,
@@ -62,6 +62,13 @@ function getCellData(rowDoc: YDoc) {
6262
return cells?.get(fieldId)?.get(YjsDatabaseKey.data);
6363
}
6464

65+
function getCell(rowDoc: YDoc) {
66+
const row = rowDoc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row);
67+
const cells = row?.get(YjsDatabaseKey.cells);
68+
69+
return cells?.get(fieldId);
70+
}
71+
6572
function createWrapper(contextValue: DatabaseContextState) {
6673
return ({ children }: { children: React.ReactNode }) => (
6774
<DatabaseContext.Provider value={contextValue}>{children}</DatabaseContext.Provider>
@@ -94,3 +101,51 @@ describe('useUpdateCellDispatch', () => {
94101
expect(ensureRow).toHaveBeenCalledWith(rowId);
95102
});
96103
});
104+
105+
describe('useUpdateStartEndTimeCell', () => {
106+
it('ensures a missing row doc before committing the calendar time update', async () => {
107+
const databaseDoc = createDatabaseDoc();
108+
const rowDoc = createRowDoc(rowId, databaseId, {});
109+
const ensureRow = jest.fn<Promise<YDoc>, [RowId]>().mockResolvedValue(rowDoc);
110+
const contextValue: DatabaseContextState = {
111+
readOnly: false,
112+
databaseDoc,
113+
databasePageId: viewId,
114+
activeViewId: viewId,
115+
rowMap: {},
116+
ensureRow,
117+
workspaceId: 'workspace-id',
118+
};
119+
const { result } = renderHook(() => useUpdateStartEndTimeCell(), {
120+
wrapper: createWrapper(contextValue),
121+
});
122+
123+
await result.current(rowId, fieldId, '100', '200', false);
124+
125+
const cell = getCell(rowDoc);
126+
127+
expect(cell?.get(YjsDatabaseKey.data)).toBe('100');
128+
expect(cell?.get(YjsDatabaseKey.end_timestamp)).toBe('200');
129+
expect(cell?.get(YjsDatabaseKey.include_time)).toBe(true);
130+
expect(ensureRow).toHaveBeenCalledWith(rowId);
131+
});
132+
133+
it('rejects calendar time updates when the row doc cannot be loaded', async () => {
134+
const databaseDoc = createDatabaseDoc();
135+
const ensureRow = jest.fn<Promise<YDoc | undefined>, [RowId]>().mockResolvedValue(undefined);
136+
const contextValue: DatabaseContextState = {
137+
readOnly: false,
138+
databaseDoc,
139+
databasePageId: viewId,
140+
activeViewId: viewId,
141+
rowMap: {},
142+
ensureRow,
143+
workspaceId: 'workspace-id',
144+
};
145+
const { result } = renderHook(() => useUpdateStartEndTimeCell(), {
146+
wrapper: createWrapper(contextValue),
147+
});
148+
149+
await expect(result.current(rowId, fieldId, '100')).rejects.toThrow('Row doc not ready for cell update');
150+
});
151+
});

src/application/database-yjs/dispatch/cell.ts

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -242,47 +242,45 @@ export function useUpdateStartEndTimeCell() {
242242
const { rowMap, ensureRow } = useDatabaseContext();
243243

244244
return useCallback(
245-
(rowId: string, fieldId: string, startTimestamp: string, endTimestamp?: string, isAllDay?: boolean) => {
246-
void (async () => {
247-
let rowDoc = rowMap?.[rowId];
248-
let target = rowDoc ? getWritableRowTarget(rowDoc) : null;
245+
async (rowId: string, fieldId: string, startTimestamp: string, endTimestamp?: string, isAllDay?: boolean) => {
246+
let rowDoc = rowMap?.[rowId];
247+
let target = rowDoc ? getWritableRowTarget(rowDoc) : null;
249248

250-
if (!target && ensureRow) {
251-
rowDoc = (await ensureRow(rowId)) ?? rowDoc;
252-
target = rowDoc ? await waitForWritableRowTarget(rowDoc) : null;
253-
}
249+
if (!target && ensureRow) {
250+
rowDoc = (await ensureRow(rowId)) ?? rowDoc;
251+
target = rowDoc ? await waitForWritableRowTarget(rowDoc) : null;
252+
}
254253

255-
if (!rowDoc || !target) {
256-
Log.warn('[useUpdateStartEndTimeCell] Row doc not ready for cell update', { rowId, fieldId });
257-
return;
258-
}
254+
if (!rowDoc || !target) {
255+
const error = new Error('Row doc not ready for cell update');
256+
257+
Log.warn('[useUpdateStartEndTimeCell] Row doc not ready for cell update', { rowId, fieldId });
258+
throw error;
259+
}
259260

260-
const writableTarget = target;
261+
const writableTarget = target;
261262

262-
rowDoc.transact(() => {
263-
let cell = writableTarget.cells.get(fieldId);
263+
rowDoc.transact(() => {
264+
let cell = writableTarget.cells.get(fieldId);
264265

265-
if (!cell) {
266-
cell = new Y.Map() as YDatabaseCell;
267-
cell.set(YjsDatabaseKey.field_type, FieldType.DateTime);
266+
if (!cell) {
267+
cell = new Y.Map() as YDatabaseCell;
268+
cell.set(YjsDatabaseKey.field_type, FieldType.DateTime);
268269

269-
cell.set(YjsDatabaseKey.created_at, String(dayjs().unix()));
270-
writableTarget.cells.set(fieldId, cell);
271-
}
270+
cell.set(YjsDatabaseKey.created_at, String(dayjs().unix()));
271+
writableTarget.cells.set(fieldId, cell);
272+
}
272273

273-
cell.set(YjsDatabaseKey.data, startTimestamp);
274-
cell.set(YjsDatabaseKey.last_modified, String(dayjs().unix()));
274+
cell.set(YjsDatabaseKey.data, startTimestamp);
275+
cell.set(YjsDatabaseKey.last_modified, String(dayjs().unix()));
275276

276-
updateDateCell(cell, {
277-
data: startTimestamp,
278-
endTimestamp,
279-
isRange: !!endTimestamp,
280-
includeTime: !isAllDay,
281-
});
282-
writableTarget.row.set(YjsDatabaseKey.last_modified, String(dayjs().unix()));
277+
updateDateCell(cell, {
278+
data: startTimestamp,
279+
endTimestamp,
280+
isRange: !!endTimestamp,
281+
includeTime: !isAllDay,
283282
});
284-
})().catch((error: unknown) => {
285-
Log.error('[useUpdateStartEndTimeCell] failed to update cell', { rowId, fieldId, error });
283+
writableTarget.row.set(YjsDatabaseKey.last_modified, String(dayjs().unix()));
286284
});
287285
},
288286
[ensureRow, rowMap]

src/components/database/fullcalendar/CalendarContent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ export function CalendarContent({ onDataChange, normalToolbarRef, onDragEnd }: C
228228

229229
// Handle external event creation (FullCalendar eventReceive callback)
230230
const handleEventReceive = useCallback(
231-
(receiveInfo: EventReceiveArg) => {
231+
async (receiveInfo: EventReceiveArg) => {
232232
Log.debug('📅 FullCalendar eventReceive:', receiveInfo);
233233

234234
try {
@@ -267,7 +267,7 @@ export function CalendarContent({ onDataChange, normalToolbarRef, onDragEnd }: C
267267
}
268268

269269
// Update the row's date field to move it from NoDate to calendar
270-
updateEventTime(rowId, startTimestamp, endTimestamp, allDay);
270+
await updateEventTime(rowId, startTimestamp, endTimestamp, allDay);
271271

272272
// Mark the event as new for visual feedback
273273
setNewEventRowIds((prev) => new Set(prev).add(rowId));

src/components/database/fullcalendar/hooks/useCalendarEvents.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@ export function useCalendarEvents() {
2323

2424
// Create a function that can update any event's time directly
2525
const updateEventTime = useCallback(
26-
(rowId: string, startTimestamp: string, endTimestamp?: string, isAllDay?: boolean) => {
26+
async (rowId: string, startTimestamp: string, endTimestamp?: string, isAllDay?: boolean) => {
2727
Log.debug('📅 Updating event time:', { rowId, fieldId, startTimestamp, endTimestamp });
2828

29-
updateCell(rowId, fieldId, startTimestamp, endTimestamp, isAllDay);
29+
await updateCell(rowId, fieldId, startTimestamp, endTimestamp, isAllDay);
3030
},
3131
[fieldId, updateCell]
3232
);
3333

3434
// Handle event drop (move event to different time)
3535
const handleEventDrop = useCallback(
36-
(dropInfo: EventDropArg) => {
36+
async (dropInfo: EventDropArg) => {
3737
Log.debug('📅 Event dropped:', dropInfo.event);
3838

3939
try {
@@ -61,7 +61,7 @@ export function useCalendarEvents() {
6161
const endTimestamp = correctedEndDate ? dateToUnixTimestamp(correctedEndDate) : undefined;
6262

6363
// Update the event time
64-
updateEventTime(rowId, startTimestamp, endTimestamp, isAllDay);
64+
await updateEventTime(rowId, startTimestamp, endTimestamp, isAllDay);
6565

6666
Log.debug('📅 Event time updated successfully');
6767
} catch (error) {
@@ -74,7 +74,7 @@ export function useCalendarEvents() {
7474

7575
// Handle event resize (change event duration)
7676
const handleEventResize = useCallback(
77-
(resizeInfo: EventResizeDoneArg) => {
77+
async (resizeInfo: EventResizeDoneArg) => {
7878
Log.debug('📅 Event resized:', resizeInfo.event);
7979

8080
try {
@@ -95,7 +95,7 @@ export function useCalendarEvents() {
9595
const endTimestamp = correctedEndDate ? dateToUnixTimestamp(correctedEndDate) : undefined;
9696

9797
// Update the event time
98-
updateEventTime(rowId, startTimestamp, endTimestamp, isAllDay);
98+
await updateEventTime(rowId, startTimestamp, endTimestamp, isAllDay);
9999

100100
Log.debug('📅 Event duration updated successfully');
101101
} catch (error) {

0 commit comments

Comments
 (0)