Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion frontend/src/components/HomeComponents/Navbar/NavbarDesktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ import {
exportTasksAsJSON,
exportTasksAsTXT,
} from '@/components/utils/ExportTasks';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { DevLogs } from '@/components/HomeComponents/DevLogs/DevLogs';
import { useTaskAutoSync } from '@/components/utils/TaskAutoSync';
import { Label } from '@/components/ui/label';
import { hashKey } from '../Tasks/tasks-utils';

export const NavbarDesktop = (
props: Props & {
Expand All @@ -58,13 +59,35 @@ export const NavbarDesktop = (
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
const [autoSyncEnable, setAutoSyncEnable] = useState(false);
const [syncInterval, setSyncInterval] = useState(1);
const [autoSyncOnEdit, setAutoSyncOnEdit] = useState(true);

useTaskAutoSync({
isLoading: props.isLoading,
setIsLoading: props.setIsLoading,
isAutoSyncEnabled: autoSyncEnable,
syncInterval: syncInterval * 60000,
});

// Load setting from localStorage
useEffect(() => {
const hashedKey = hashKey('autoSyncOnEdit', props.email);
const stored = localStorage.getItem(hashedKey);
if (stored !== null) {
setAutoSyncOnEdit(stored === 'true');
} else {
localStorage.setItem(hashedKey, 'true');
setAutoSyncOnEdit(true);
}
}, [props.email]);

// Save setting and notify Tasks component
const handleAutoSyncOnEditChange = (checked: boolean) => {
setAutoSyncOnEdit(checked);
const hashedKey = hashKey('autoSyncOnEdit', props.email);
localStorage.setItem(hashedKey, checked.toString());
window.dispatchEvent(new Event('autoSyncOnEditChanged'));
};

const handleExportJSON = () => {
exportTasksAsJSON(props.tasks || []);
setIsExportDialogOpen(false);
Expand Down Expand Up @@ -234,6 +257,18 @@ export const NavbarDesktop = (
)}
</div>
</DropdownMenuItem>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<div className="flex items-center justify-between space-x-2 w-full p-1">
<Label htmlFor="autosync-on-edit-switch">
Auto sync on edit
</Label>
<Switch
id="autosync-on-edit-switch"
checked={autoSyncOnEdit}
onCheckedChange={handleAutoSyncOnEditChange}
/>
</div>
</DropdownMenuItem>
<DropdownMenuItem onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
<span>Log out</span>
Expand Down
39 changes: 38 additions & 1 deletion frontend/src/components/HomeComponents/Navbar/NavbarMobile.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import {
Sheet,
SheetContent,
Expand Down Expand Up @@ -45,6 +45,7 @@ import { useTaskAutoSync } from '@/components/utils/TaskAutoSync';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Slider } from '@/components/ui/slider';
import { hashKey } from '../Tasks/tasks-utils';

export const NavbarMobile = (
props: Props & { setIsOpen: (isOpen: boolean) => void; isOpen: boolean } & {
Expand All @@ -58,13 +59,35 @@ export const NavbarMobile = (
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
const [autoSyncEnable, setAutoSyncEnable] = useState(false);
const [syncInterval, setSyncInterval] = useState(1);
const [autoSyncOnEdit, setAutoSyncOnEdit] = useState(true);

useTaskAutoSync({
isLoading: props.isLoading,
setIsLoading: props.setIsLoading,
isAutoSyncEnabled: autoSyncEnable,
syncInterval: syncInterval * 60000,
});

// Load setting from localStorage
useEffect(() => {
const hashedKey = hashKey('autoSyncOnEdit', props.email);
const stored = localStorage.getItem(hashedKey);
if (stored !== null) {
setAutoSyncOnEdit(stored === 'true');
} else {
localStorage.setItem(hashedKey, 'true');
setAutoSyncOnEdit(true);
}
}, [props.email]);

// Save setting and notify Tasks component
const handleAutoSyncOnEditChange = (checked: boolean) => {
setAutoSyncOnEdit(checked);
const hashedKey = hashKey('autoSyncOnEdit', props.email);
localStorage.setItem(hashedKey, checked.toString());
window.dispatchEvent(new Event('autoSyncOnEditChanged'));
};

const handleExportJSON = () => {
exportTasksAsJSON(props.tasks || []);
setIsExportDialogOpen(false);
Expand Down Expand Up @@ -215,6 +238,20 @@ export const NavbarMobile = (
/>
</div>
)}

<div className="flex mt-2 items-center justify-between space-x-2">
<Label
htmlFor="autosync-on-edit-switch"
className="text-base"
>
Auto Sync on Edit
</Label>
<Switch
id="autosync-on-edit-switch"
checked={autoSyncOnEdit}
onCheckedChange={handleAutoSyncOnEditChange}
/>
</div>
</div>
</DialogContent>
</Dialog>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,10 @@ describe('NavbarDesktop', () => {
render(<NavbarDesktop {...props} />);

await userEvent.click(screen.getByText('CN'));
await userEvent.click(screen.getByText('toggle'));

// Click the first toggle (Auto sync tasks)
const toggleButtons = screen.getAllByText('toggle');
await userEvent.click(toggleButtons[0]);

expect(screen.getByTestId('sync-slider')).toBeInTheDocument();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,21 @@ exports[`NavbarDesktop renders consistently (snapshot) 1`] = `
</div>
</div>
</div>
<div>
<div
class="flex items-center justify-between space-x-2 w-full p-1"
>
<label
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
for="autosync-on-edit-switch"
>
Auto sync on edit
</label>
<button>
toggle
</button>
</div>
</div>
<div>
<svg
class="lucide lucide-log-out mr-2 h-4 w-4"
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/components/HomeComponents/Tasks/Tasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export const Tasks = (
const [unsyncedTaskUuids, setUnsyncedTaskUuids] = useState<Set<string>>(
new Set()
);
const [autoSyncOnEdit, setAutoSyncOnEdit] = useState(true);
const tableRef = useRef<HTMLDivElement>(null);
const [hotkeysEnabled, setHotkeysEnabled] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
Expand Down Expand Up @@ -212,6 +213,30 @@ export const Tasks = (
setPinnedTasks(getPinnedTasks(props.email));
}, [props.email]);

// Load setting and listen for changes from navbar
useEffect(() => {
const hashedKey = hashKey('autoSyncOnEdit', props.email);
const stored = localStorage.getItem(hashedKey);
if (stored !== null) {
setAutoSyncOnEdit(stored === 'true');
} else {
localStorage.setItem(hashedKey, 'true');
setAutoSyncOnEdit(true);
}

const handleStorageChange = () => {
const updated = localStorage.getItem(hashedKey);
if (updated !== null) {
setAutoSyncOnEdit(updated === 'true');
}
};

window.addEventListener('autoSyncOnEditChanged', handleStorageChange);
return () => {
window.removeEventListener('autoSyncOnEditChanged', handleStorageChange);
};
}, [props.email]);

useEffect(() => {
const fetchTasksForEmail = async () => {
try {
Expand Down Expand Up @@ -432,6 +457,12 @@ export const Tasks = (
annotations,
});

// Auto-sync after edit if enabled (on by default)
if (autoSyncOnEdit) {
await new Promise((resolve) => setTimeout(resolve, 1000));
await syncTasksWithTwAndDb();
}

setIsAddTaskOpen(false);
} catch (error) {
console.error('Failed to edit task:', error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jest.mock('../tasks-utils', () => {
getTimeSinceLastSync: jest
.fn()
.mockReturnValue('Last updated 5 minutes ago'),
hashKey: jest.fn().mockReturnValue('mockHashedKey'),
hashKey: jest.fn((key: string) => `mockHashedKey-${key}`),
getPinnedTasks: jest.fn().mockReturnValue(new Set()),
togglePinnedTask: jest.fn(),
};
Expand Down Expand Up @@ -203,6 +203,8 @@ describe('Tasks Component', () => {
beforeEach(() => {
localStorageMock.clear();
jest.clearAllMocks();
// Disable auto-sync on edit for tests (to avoid unexpected sync calls)
localStorageMock.setItem('mockHashedKey-autoSyncOnEdit', 'false');
});

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

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

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

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

expect(localStorageMock.setItem).toHaveBeenCalledWith(
'mockHashedKey',
'mockHashedKey-tasksPerPage',
'5'
);

Expand Down
Loading