Skip to content

Commit b2c6e9d

Browse files
authored
fix: add toggle for auto-sync on edit with default enabled (#419)
Added Auto Sync on Edit toggle in profile settings (on by default). When enabled, tasks auto-sync after editing. When disabled, users must manually sync. Toggle state syncs between navbar and Tasks component via custom event.
1 parent 5b33b58 commit b2c6e9d

6 files changed

Lines changed: 129 additions & 6 deletions

File tree

frontend/src/components/HomeComponents/Navbar/NavbarDesktop.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ import {
4242
exportTasksAsJSON,
4343
exportTasksAsTXT,
4444
} from '@/components/utils/ExportTasks';
45-
import { useState } from 'react';
45+
import { useState, useEffect } from 'react';
4646
import { DevLogs } from '@/components/HomeComponents/DevLogs/DevLogs';
4747
import { useTaskAutoSync } from '@/components/utils/TaskAutoSync';
4848
import { Label } from '@/components/ui/label';
49+
import { hashKey } from '../Tasks/tasks-utils';
4950

5051
export const NavbarDesktop = (
5152
props: Props & {
@@ -58,13 +59,35 @@ export const NavbarDesktop = (
5859
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
5960
const [autoSyncEnable, setAutoSyncEnable] = useState(false);
6061
const [syncInterval, setSyncInterval] = useState(1);
62+
const [autoSyncOnEdit, setAutoSyncOnEdit] = useState(true);
63+
6164
useTaskAutoSync({
6265
isLoading: props.isLoading,
6366
setIsLoading: props.setIsLoading,
6467
isAutoSyncEnabled: autoSyncEnable,
6568
syncInterval: syncInterval * 60000,
6669
});
6770

71+
// Load setting from localStorage
72+
useEffect(() => {
73+
const hashedKey = hashKey('autoSyncOnEdit', props.email);
74+
const stored = localStorage.getItem(hashedKey);
75+
if (stored !== null) {
76+
setAutoSyncOnEdit(stored === 'true');
77+
} else {
78+
localStorage.setItem(hashedKey, 'true');
79+
setAutoSyncOnEdit(true);
80+
}
81+
}, [props.email]);
82+
83+
// Save setting and notify Tasks component
84+
const handleAutoSyncOnEditChange = (checked: boolean) => {
85+
setAutoSyncOnEdit(checked);
86+
const hashedKey = hashKey('autoSyncOnEdit', props.email);
87+
localStorage.setItem(hashedKey, checked.toString());
88+
window.dispatchEvent(new Event('autoSyncOnEditChanged'));
89+
};
90+
6891
const handleExportJSON = () => {
6992
exportTasksAsJSON(props.tasks || []);
7093
setIsExportDialogOpen(false);
@@ -234,6 +257,18 @@ export const NavbarDesktop = (
234257
)}
235258
</div>
236259
</DropdownMenuItem>
260+
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
261+
<div className="flex items-center justify-between space-x-2 w-full p-1">
262+
<Label htmlFor="autosync-on-edit-switch">
263+
Auto sync on edit
264+
</Label>
265+
<Switch
266+
id="autosync-on-edit-switch"
267+
checked={autoSyncOnEdit}
268+
onCheckedChange={handleAutoSyncOnEditChange}
269+
/>
270+
</div>
271+
</DropdownMenuItem>
237272
<DropdownMenuItem onClick={handleLogout}>
238273
<LogOut className="mr-2 h-4 w-4" />
239274
<span>Log out</span>

frontend/src/components/HomeComponents/Navbar/NavbarMobile.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useState, useEffect } from 'react';
22
import {
33
Sheet,
44
SheetContent,
@@ -45,6 +45,7 @@ import { useTaskAutoSync } from '@/components/utils/TaskAutoSync';
4545
import { Label } from '@/components/ui/label';
4646
import { Switch } from '@/components/ui/switch';
4747
import { Slider } from '@/components/ui/slider';
48+
import { hashKey } from '../Tasks/tasks-utils';
4849

4950
export const NavbarMobile = (
5051
props: Props & { setIsOpen: (isOpen: boolean) => void; isOpen: boolean } & {
@@ -58,13 +59,35 @@ export const NavbarMobile = (
5859
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
5960
const [autoSyncEnable, setAutoSyncEnable] = useState(false);
6061
const [syncInterval, setSyncInterval] = useState(1);
62+
const [autoSyncOnEdit, setAutoSyncOnEdit] = useState(true);
63+
6164
useTaskAutoSync({
6265
isLoading: props.isLoading,
6366
setIsLoading: props.setIsLoading,
6467
isAutoSyncEnabled: autoSyncEnable,
6568
syncInterval: syncInterval * 60000,
6669
});
6770

71+
// Load setting from localStorage
72+
useEffect(() => {
73+
const hashedKey = hashKey('autoSyncOnEdit', props.email);
74+
const stored = localStorage.getItem(hashedKey);
75+
if (stored !== null) {
76+
setAutoSyncOnEdit(stored === 'true');
77+
} else {
78+
localStorage.setItem(hashedKey, 'true');
79+
setAutoSyncOnEdit(true);
80+
}
81+
}, [props.email]);
82+
83+
// Save setting and notify Tasks component
84+
const handleAutoSyncOnEditChange = (checked: boolean) => {
85+
setAutoSyncOnEdit(checked);
86+
const hashedKey = hashKey('autoSyncOnEdit', props.email);
87+
localStorage.setItem(hashedKey, checked.toString());
88+
window.dispatchEvent(new Event('autoSyncOnEditChanged'));
89+
};
90+
6891
const handleExportJSON = () => {
6992
exportTasksAsJSON(props.tasks || []);
7093
setIsExportDialogOpen(false);
@@ -215,6 +238,20 @@ export const NavbarMobile = (
215238
/>
216239
</div>
217240
)}
241+
242+
<div className="flex mt-2 items-center justify-between space-x-2">
243+
<Label
244+
htmlFor="autosync-on-edit-switch"
245+
className="text-base"
246+
>
247+
Auto Sync on Edit
248+
</Label>
249+
<Switch
250+
id="autosync-on-edit-switch"
251+
checked={autoSyncOnEdit}
252+
onCheckedChange={handleAutoSyncOnEditChange}
253+
/>
254+
</div>
218255
</div>
219256
</DialogContent>
220257
</Dialog>

frontend/src/components/HomeComponents/Navbar/__tests__/NavbarDesktop.test.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ describe('NavbarDesktop', () => {
154154
render(<NavbarDesktop {...props} />);
155155

156156
await userEvent.click(screen.getByText('CN'));
157-
await userEvent.click(screen.getByText('toggle'));
157+
158+
// Click the first toggle (Auto sync tasks)
159+
const toggleButtons = screen.getAllByText('toggle');
160+
await userEvent.click(toggleButtons[0]);
158161

159162
expect(screen.getByTestId('sync-slider')).toBeInTheDocument();
160163
});

frontend/src/components/HomeComponents/Navbar/__tests__/__snapshots__/NavbarDesktop.test.tsx.snap

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,21 @@ exports[`NavbarDesktop renders consistently (snapshot) 1`] = `
374374
</div>
375375
</div>
376376
</div>
377+
<div>
378+
<div
379+
class="flex items-center justify-between space-x-2 w-full p-1"
380+
>
381+
<label
382+
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
383+
for="autosync-on-edit-switch"
384+
>
385+
Auto sync on edit
386+
</label>
387+
<button>
388+
toggle
389+
</button>
390+
</div>
391+
</div>
377392
<div>
378393
<svg
379394
class="lucide lucide-log-out mr-2 h-4 w-4"

frontend/src/components/HomeComponents/Tasks/Tasks.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export const Tasks = (
114114
const [unsyncedTaskUuids, setUnsyncedTaskUuids] = useState<Set<string>>(
115115
new Set()
116116
);
117+
const [autoSyncOnEdit, setAutoSyncOnEdit] = useState(true);
117118
const tableRef = useRef<HTMLDivElement>(null);
118119
const [hotkeysEnabled, setHotkeysEnabled] = useState(false);
119120
const [selectedIndex, setSelectedIndex] = useState(0);
@@ -212,6 +213,30 @@ export const Tasks = (
212213
setPinnedTasks(getPinnedTasks(props.email));
213214
}, [props.email]);
214215

216+
// Load setting and listen for changes from navbar
217+
useEffect(() => {
218+
const hashedKey = hashKey('autoSyncOnEdit', props.email);
219+
const stored = localStorage.getItem(hashedKey);
220+
if (stored !== null) {
221+
setAutoSyncOnEdit(stored === 'true');
222+
} else {
223+
localStorage.setItem(hashedKey, 'true');
224+
setAutoSyncOnEdit(true);
225+
}
226+
227+
const handleStorageChange = () => {
228+
const updated = localStorage.getItem(hashedKey);
229+
if (updated !== null) {
230+
setAutoSyncOnEdit(updated === 'true');
231+
}
232+
};
233+
234+
window.addEventListener('autoSyncOnEditChanged', handleStorageChange);
235+
return () => {
236+
window.removeEventListener('autoSyncOnEditChanged', handleStorageChange);
237+
};
238+
}, [props.email]);
239+
215240
useEffect(() => {
216241
const fetchTasksForEmail = async () => {
217242
try {
@@ -432,6 +457,12 @@ export const Tasks = (
432457
annotations,
433458
});
434459

460+
// Auto-sync after edit if enabled (on by default)
461+
if (autoSyncOnEdit) {
462+
await new Promise((resolve) => setTimeout(resolve, 1000));
463+
await syncTasksWithTwAndDb();
464+
}
465+
435466
setIsAddTaskOpen(false);
436467
} catch (error) {
437468
console.error('Failed to edit task:', error);

frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jest.mock('../tasks-utils', () => {
3737
getTimeSinceLastSync: jest
3838
.fn()
3939
.mockReturnValue('Last updated 5 minutes ago'),
40-
hashKey: jest.fn().mockReturnValue('mockHashedKey'),
40+
hashKey: jest.fn((key: string) => `mockHashedKey-${key}`),
4141
getPinnedTasks: jest.fn().mockReturnValue(new Set()),
4242
togglePinnedTask: jest.fn(),
4343
};
@@ -203,6 +203,8 @@ describe('Tasks Component', () => {
203203
beforeEach(() => {
204204
localStorageMock.clear();
205205
jest.clearAllMocks();
206+
// Disable auto-sync on edit for tests (to avoid unexpected sync calls)
207+
localStorageMock.setItem('mockHashedKey-autoSyncOnEdit', 'false');
206208
});
207209

208210
describe('Rendering & Basic UI', () => {
@@ -255,7 +257,7 @@ describe('Tasks Component', () => {
255257

256258
describe('LocalStorage', () => {
257259
test('loads "tasksPerPage" from localStorage on initial render', async () => {
258-
localStorageMock.setItem('mockHashedKey', '20');
260+
localStorageMock.setItem('mockHashedKey-tasksPerPage', '20');
259261

260262
render(<Tasks {...mockProps} />);
261263

@@ -277,7 +279,7 @@ describe('Tasks Component', () => {
277279
expect(screen.getByTestId('total-pages')).toHaveTextContent('4');
278280

279281
expect(localStorageMock.setItem).toHaveBeenCalledWith(
280-
'mockHashedKey',
282+
'mockHashedKey-tasksPerPage',
281283
'5'
282284
);
283285

0 commit comments

Comments
 (0)