Skip to content

Commit d78e8f1

Browse files
committed
feat(tasks): add visual indicator for unsynced local changes
- Implements a local state `unsyncedTaskUuids` to track items modified on the frontend but not yet pushed to the backend. - Adds an 'Unsynced' badge to task rows for immediate visual feedback on creation, edit, completion, or deletion. - Implements optimistic UI updates for task status changes (completed/deleted) to improve perceived performance. - Automatically clears all unsynced indicators upon successful synchronization with the backend. - Displays the number of pending local changes on the button and includes unit tests. - Handles temporary negative IDs for newly created tasks to prevent UI confusion before a real ID is assigned. Fixes #143
1 parent 0ffd79e commit d78e8f1

File tree

5 files changed

+592
-18
lines changed

5 files changed

+592
-18
lines changed

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

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export const TaskDialog = ({
5959
onMarkComplete,
6060
onMarkDeleted,
6161
isOverdue,
62+
isUnsynced,
6263
}: EditTaskDialogProps) => {
6364
const handleDialogOpenChange = (open: boolean) => {
6465
if (open) {
@@ -107,7 +108,7 @@ export const TaskDialog = ({
107108
: 'dark:text-white text-black'
108109
}`}
109110
>
110-
{task.id}
111+
{task.id < 0 ? '-' : task.id}
111112
</span>
112113
</TableCell>
113114
<TableCell className="flex items-center space-x-2 py-2">
@@ -127,6 +128,11 @@ export const TaskDialog = ({
127128
{task.project === '' ? '' : task.project}
128129
</Badge>
129130
)}
131+
{isUnsynced && (
132+
<Badge variant={'destructive'} className="animate-pulse">
133+
Unsynced
134+
</Badge>
135+
)}
130136
</TableCell>
131137
<TableCell className="py-2">
132138
<Badge
@@ -174,7 +180,7 @@ export const TaskDialog = ({
174180
<TableRow>
175181
<TableCell>ID:</TableCell>
176182
<TableCell className="flex items-center gap-3">
177-
{task.id}
183+
{task.id < 0 ? '-' : task.id}
178184
{task.status === 'pending' && isOverdue(task.due) && (
179185
<Badge className="bg-red-600 text-white shadow-lg shadow-red-700/40 animate-pulse">
180186
Overdue
@@ -275,6 +281,7 @@ export const TaskDialog = ({
275281
<Button
276282
variant="ghost"
277283
size="icon"
284+
aria-label="save"
278285
onClick={() => {
279286
onSaveDueDate(task, editState.editedDueDate);
280287
onUpdateState({ isEditingDueDate: false });
@@ -285,6 +292,7 @@ export const TaskDialog = ({
285292
<Button
286293
variant="ghost"
287294
size="icon"
295+
aria-label="cancel"
288296
onClick={() =>
289297
onUpdateState({
290298
editedDueDate: task.due || '',
@@ -360,6 +368,7 @@ export const TaskDialog = ({
360368
<Button
361369
variant="ghost"
362370
size="icon"
371+
aria-label="save"
363372
onClick={() => {
364373
onSaveStartDate(task, editState.editedStartDate);
365374
onUpdateState({ isEditingStartDate: false });
@@ -371,6 +380,7 @@ export const TaskDialog = ({
371380
<Button
372381
variant="ghost"
373382
size="icon"
383+
aria-label="cancel"
374384
onClick={() =>
375385
onUpdateState({
376386
editedStartDate: task.start || '',
@@ -443,6 +453,7 @@ export const TaskDialog = ({
443453
<Button
444454
variant="ghost"
445455
size="icon"
456+
aria-label="save"
446457
onClick={() => {
447458
onSaveEndDate(task, editState.editedEndDate);
448459
onUpdateState({ isEditingEndDate: false });
@@ -453,6 +464,7 @@ export const TaskDialog = ({
453464
<Button
454465
variant="ghost"
455466
size="icon"
467+
aria-label="cancel"
456468
onClick={() =>
457469
onUpdateState({
458470
editedEndDate: task.end || '',
@@ -525,6 +537,7 @@ export const TaskDialog = ({
525537
<Button
526538
variant="ghost"
527539
size="icon"
540+
aria-label="save"
528541
onClick={() => {
529542
onSaveWaitDate(task, editState.editedWaitDate);
530543
onUpdateState({ isEditingWaitDate: false });
@@ -536,6 +549,7 @@ export const TaskDialog = ({
536549
<Button
537550
variant="ghost"
538551
size="icon"
552+
aria-label="cancel"
539553
onClick={() =>
540554
onUpdateState({
541555
editedWaitDate: task.wait || '',
@@ -657,7 +671,10 @@ export const TaskDialog = ({
657671
Add Dependency
658672
</Button>
659673
{editState.dependsDropdownOpen && (
660-
<div className="absolute left-0 top-full mt-1 z-50 w-full bg-background border rounded-md shadow-lg max-h-60 overflow-y-auto">
674+
<div
675+
data-testid="dependency-dropdown"
676+
className="absolute left-0 top-full mt-1 z-50 w-full bg-background border rounded-md shadow-lg max-h-60 overflow-y-auto"
677+
>
661678
<Input
662679
type="text"
663680
placeholder="Search tasks..."
@@ -715,6 +732,7 @@ export const TaskDialog = ({
715732
<Button
716733
variant="ghost"
717734
size="icon"
735+
aria-label="save"
718736
onClick={() => {
719737
onSaveDepends(task, editState.editedDepends);
720738
onUpdateState({
@@ -728,6 +746,7 @@ export const TaskDialog = ({
728746
<Button
729747
variant="ghost"
730748
size="icon"
749+
aria-label="cancel"
731750
onClick={() => {
732751
onUpdateState({
733752
isEditingDepends: false,
@@ -769,6 +788,7 @@ export const TaskDialog = ({
769788
<Button
770789
variant="ghost"
771790
size="icon"
791+
aria-label="save"
772792
onClick={() => {
773793
onSavePriority(task, editState.editedPriority);
774794
onUpdateState({ isEditingPriority: false });
@@ -779,6 +799,7 @@ export const TaskDialog = ({
779799
<Button
780800
variant="ghost"
781801
size="icon"
802+
aria-label="cancel"
782803
onClick={() => {
783804
onUpdateState({
784805
editedPriority: task.priority || 'NONE',
@@ -1058,6 +1079,7 @@ export const TaskDialog = ({
10581079
<Button
10591080
variant="ghost"
10601081
size="icon"
1082+
aria-label="save"
10611083
onClick={() => {
10621084
onSaveEntryDate(task, editState.editedEntryDate);
10631085
onUpdateState({ isEditingEntryDate: false });
@@ -1069,6 +1091,7 @@ export const TaskDialog = ({
10691091
<Button
10701092
variant="ghost"
10711093
size="icon"
1094+
aria-label="cancel"
10721095
onClick={() =>
10731096
onUpdateState({
10741097
isEditingEntryDate: false,
@@ -1158,6 +1181,7 @@ export const TaskDialog = ({
11581181
<Button
11591182
variant="ghost"
11601183
size="icon"
1184+
aria-label="save"
11611185
onClick={() =>
11621186
onSaveRecur(task, editState.editedRecur)
11631187
}
@@ -1167,6 +1191,7 @@ export const TaskDialog = ({
11671191
<Button
11681192
variant="ghost"
11691193
size="icon"
1194+
aria-label="cancel"
11701195
onClick={() =>
11711196
onUpdateState({
11721197
isEditingRecur: false,

0 commit comments

Comments
 (0)