mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
fix: let tasks be completed again and say when the server refuses #958
Ticking a task sent `progressUpdated`, which JSCalendar 2.0 dropped and Stalwart rejects with `invalidProperties`, so the whole update failed. The checkbox fired the toggle with `void`, so the task silently stayed open. - drop `progressUpdated` from the toggle patch, like webmail 6b30e91c - flip the checkbox optimistically and put it back when the update fails - show a "Failed to update task" alert with the server's reason
This commit is contained in:
+2
-1
@@ -79,7 +79,8 @@
|
||||
},
|
||||
"tasks": {
|
||||
"add_placeholder": "Add a task…",
|
||||
"add_time": "Add time"
|
||||
"add_time": "Add time",
|
||||
"update_error": "Failed to update task"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -609,6 +609,20 @@ export default function CalendarScreen() {
|
||||
[t],
|
||||
);
|
||||
|
||||
// The store flips the checkbox optimistically and reverts it when the
|
||||
// server refuses; say so instead of leaving the user guessing.
|
||||
const handleToggleTask = React.useCallback(
|
||||
(id: string) => {
|
||||
toggleTaskComplete(id).catch((err: unknown) => {
|
||||
Alert.alert(
|
||||
t('calendar.tasks.update_error', 'Failed to update task'),
|
||||
err instanceof Error ? err.message : undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
[toggleTaskComplete, t],
|
||||
);
|
||||
|
||||
// Clone the event one day later and open it in the editor (webmail's
|
||||
// handleDuplicateFromDetail).
|
||||
const handleDuplicateFromDetail = React.useCallback(
|
||||
@@ -942,7 +956,7 @@ export default function CalendarScreen() {
|
||||
onClose={() => { setTasksVisible(false); setTasksInitialId(null); }}
|
||||
onCreate={createTask}
|
||||
onUpdate={updateTask}
|
||||
onToggle={toggleTaskComplete}
|
||||
onToggle={handleToggleTask}
|
||||
onDelete={deleteTask}
|
||||
/>
|
||||
|
||||
|
||||
@@ -638,4 +638,58 @@ describe('calendar-store', () => {
|
||||
expect(mockGetEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleTaskComplete', () => {
|
||||
it('completes a task without sending progressUpdated, which Stalwart rejects (#958)', async () => {
|
||||
useCalendarStore.setState({
|
||||
tasks: [{ id: 't1', '@type': 'Task', progress: 'needs-action' } as any],
|
||||
});
|
||||
mockUpdateEvent.mockResolvedValue(undefined);
|
||||
|
||||
await useCalendarStore.getState().toggleTaskComplete('t1');
|
||||
|
||||
expect(mockUpdateEvent).toHaveBeenCalledWith(
|
||||
't1',
|
||||
{ progress: 'completed', percentComplete: 100 },
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(useCalendarStore.getState().tasks[0].progress).toBe('completed');
|
||||
});
|
||||
|
||||
it('reopens a completed task as needs-action on its owning account', async () => {
|
||||
useCalendarStore.setState({
|
||||
tasks: [{ id: 'acc-2:t1', originalId: 't1', accountId: 'acc-2', progress: 'completed' } as any],
|
||||
});
|
||||
mockUpdateEvent.mockResolvedValue(undefined);
|
||||
|
||||
await useCalendarStore.getState().toggleTaskComplete('acc-2:t1');
|
||||
|
||||
expect(mockUpdateEvent).toHaveBeenCalledWith(
|
||||
't1',
|
||||
{ progress: 'needs-action', percentComplete: 0 },
|
||||
undefined,
|
||||
'acc-2',
|
||||
);
|
||||
expect(useCalendarStore.getState().tasks[0].progress).toBe('needs-action');
|
||||
});
|
||||
|
||||
it('flips the task at once and reverts it when the server refuses', async () => {
|
||||
useCalendarStore.setState({
|
||||
tasks: [{ id: 't1', progress: 'needs-action', percentComplete: 20 } as any],
|
||||
});
|
||||
let reject!: (err: Error) => void;
|
||||
mockUpdateEvent.mockReturnValue(new Promise((_, r) => { reject = r; }));
|
||||
|
||||
const pending = useCalendarStore.getState().toggleTaskComplete('t1');
|
||||
expect(useCalendarStore.getState().tasks[0].progress).toBe('completed');
|
||||
|
||||
reject(new Error('invalidProperties'));
|
||||
await expect(pending).rejects.toThrow('invalidProperties');
|
||||
expect(useCalendarStore.getState().tasks[0]).toMatchObject({
|
||||
progress: 'needs-action',
|
||||
percentComplete: 20,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -712,12 +712,22 @@ export const useCalendarStore = create<CalendarState>()(
|
||||
const task = get().tasks.find((t) => t.id === id);
|
||||
if (!task) return;
|
||||
const completed = task.progress === 'completed';
|
||||
// Un-completing goes back to needs-action (not in-process), like webmail;
|
||||
// progressUpdated records when the state changed (RFC 8984 §5.2.4).
|
||||
// Un-completing goes back to needs-action (not in-process), like webmail.
|
||||
// No progressUpdated: JSCalendar 2.0 dropped it and Stalwart rejects the
|
||||
// whole update with invalidProperties (#958).
|
||||
const next: Partial<CalendarEvent> = completed
|
||||
? { progress: 'needs-action', percentComplete: 0, progressUpdated: new Date().toISOString() }
|
||||
: { progress: 'completed', percentComplete: 100, progressUpdated: new Date().toISOString() };
|
||||
await get().updateTask(id, next);
|
||||
? { progress: 'needs-action', percentComplete: 0 }
|
||||
: { progress: 'completed', percentComplete: 100 };
|
||||
// Flip the checkbox right away and put it back if the server refuses.
|
||||
const patchTask = (changes: Partial<CalendarEvent>) =>
|
||||
set({ tasks: get().tasks.map((t) => (t.id === id ? { ...t, ...changes } : t)) });
|
||||
patchTask(next);
|
||||
try {
|
||||
await apiUpdateEvent(task.originalId || id, next, undefined, task.accountId);
|
||||
} catch (err) {
|
||||
patchTask({ progress: task.progress, percentComplete: task.percentComplete });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
deleteTask: async (id) => {
|
||||
|
||||
Reference in New Issue
Block a user