@tuturuuu/ui 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/package.json +79 -67
  3. package/src/components/ui/__tests__/avatar.test.tsx +8 -5
  4. package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
  5. package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
  6. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
  8. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
  9. package/src/components/ui/chart.test.tsx +29 -0
  10. package/src/components/ui/chart.tsx +12 -3
  11. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
  12. package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
  13. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
  14. package/src/components/ui/custom/common-footer.tsx +16 -1
  15. package/src/components/ui/custom/production-indicator.tsx +1 -1
  16. package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
  17. package/src/components/ui/custom/settings/task-settings.tsx +18 -0
  18. package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
  19. package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
  20. package/src/components/ui/custom/sidebar-context.tsx +61 -61
  21. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
  22. package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
  23. package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
  24. package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
  25. package/src/components/ui/custom/workspace-select.tsx +33 -12
  26. package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
  27. package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
  28. package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
  29. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
  30. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
  31. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
  32. package/src/components/ui/finance/invoices/hooks.ts +75 -20
  33. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
  34. package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
  35. package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
  36. package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
  37. package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
  38. package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
  39. package/src/components/ui/finance/invoices/utils.test.ts +50 -0
  40. package/src/components/ui/finance/invoices/utils.ts +75 -17
  41. package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
  42. package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
  43. package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
  44. package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
  45. package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
  46. package/src/components/ui/finance/transactions/form-types.ts +2 -0
  47. package/src/components/ui/finance/transactions/form.test.tsx +43 -0
  48. package/src/components/ui/finance/transactions/form.tsx +60 -0
  49. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
  50. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
  51. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
  52. package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
  53. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  54. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
  55. package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
  56. package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
  57. package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
  58. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
  59. package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
  60. package/src/components/ui/legacy/meet/page.test.ts +180 -0
  61. package/src/components/ui/legacy/meet/page.tsx +87 -39
  62. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
  63. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
  64. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
  65. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
  66. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
  67. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
  68. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
  69. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
  70. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
  71. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
  72. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
  73. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
  74. package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
  75. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
  76. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
  77. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
  78. package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
  79. package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
  80. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
  81. package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
  82. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
  83. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
  84. package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
  85. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
  86. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
  87. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
  88. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
  89. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
  90. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
  91. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
  92. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
  93. package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
  94. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
  95. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
  96. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
  97. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
  98. package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
  99. package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
  100. package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
  101. package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
  102. package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
  103. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
  104. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
  105. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
  106. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
  107. package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
  108. package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
  109. package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
  110. package/src/hooks/use-calendar-sync.tsx +22 -277
  111. package/src/hooks/use-calendar.tsx +95 -525
  112. package/src/hooks/use-task-actions.ts +43 -117
  113. package/src/hooks/use-user-config.ts +1 -1
  114. package/src/hooks/use-workspace-config.ts +6 -2
  115. package/src/hooks/use-workspace-presence.ts +1 -1
  116. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
@@ -1,15 +1,18 @@
1
1
  'use client';
2
2
 
3
3
  import { type QueryClient, useMutation } from '@tanstack/react-query';
4
- import { bulkWorkspaceTasks } from '@tuturuuu/internal-api/tasks';
5
4
  import type { Task } from '@tuturuuu/types/primitives/Task';
6
5
  import { toast } from '@tuturuuu/ui/sonner';
7
6
  import type { BoardBroadcastFn } from '../../../../shared/board-broadcast-context';
8
7
  import type { BulkOperationI18n } from './bulk-operation-i18n';
9
8
  import type { WorkspaceMember } from './bulk-operation-types';
10
9
  import {
10
+ bulkWorkspaceTasksByEffectiveWorkspace,
11
11
  getInternalApiOptions,
12
12
  getTaskForRelationMutation,
13
+ restoreFailedBoardTasks,
14
+ snapshotBoardTaskCaches,
15
+ updateBoardTaskCaches,
13
16
  } from './bulk-operation-utils';
14
17
 
15
18
  export function useBulkAddAssignee(
@@ -80,17 +83,17 @@ export function useBulkAddAssignee(
80
83
  }
81
84
  }
82
85
 
83
- const result = await bulkWorkspaceTasks(
84
- wsId,
85
- {
86
- taskIds,
87
- operation: {
88
- type: 'add_assignee',
89
- assigneeId,
90
- },
86
+ const result = await bulkWorkspaceTasksByEffectiveWorkspace({
87
+ queryClient,
88
+ boardId,
89
+ defaultWorkspaceId: wsId,
90
+ taskIds,
91
+ operation: {
92
+ type: 'add_assignee',
93
+ assigneeId,
91
94
  },
92
- apiOptions
93
- );
95
+ options: apiOptions,
96
+ });
94
97
 
95
98
  if (result.successCount === 0) {
96
99
  throw new Error(
@@ -128,8 +131,9 @@ export function useBulkAddAssignee(
128
131
  },
129
132
  onMutate: async ({ assigneeId, taskIds }) => {
130
133
  await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
131
- const previousTasks = queryClient.getQueryData(['tasks', boardId]);
132
- const current = (previousTasks as Task[] | undefined) || [];
134
+ await queryClient.cancelQueries({ queryKey: ['tasks-full', boardId] });
135
+ const cacheSnapshot = snapshotBoardTaskCaches(queryClient, boardId);
136
+ const current = cacheSnapshot.previousTasks || [];
133
137
 
134
138
  const missingTaskIds = taskIds.filter((id) => {
135
139
  const task = current.find((ct) => ct.id === id);
@@ -144,42 +148,28 @@ export function useBulkAddAssignee(
144
148
  avatar_url: undefined,
145
149
  };
146
150
 
147
- queryClient.setQueryData(
148
- ['tasks', boardId],
149
- (old: Task[] | undefined) => {
150
- if (!old) return old;
151
- return old.map((task) => {
152
- if (!missingTaskIds.includes(task.id)) return task;
153
- return {
154
- ...task,
155
- assignees: [...(task.assignees || []), assigneeData],
156
- } as Task;
157
- });
158
- }
159
- );
151
+ updateBoardTaskCaches(queryClient, boardId, (old) => {
152
+ if (!old) return old;
153
+ return old.map((task) => {
154
+ if (!missingTaskIds.includes(task.id)) return task;
155
+ return {
156
+ ...task,
157
+ assignees: [...(task.assignees || []), assigneeData],
158
+ } as Task;
159
+ });
160
+ });
160
161
 
161
- return { previousTasks, modifiedTaskIds: missingTaskIds };
162
+ return { ...cacheSnapshot, modifiedTaskIds: missingTaskIds };
162
163
  },
163
164
  onError: (error, variables, context) => {
164
- if (context?.previousTasks) {
165
- const previousTaskMap = new Map(
166
- ((context.previousTasks as Task[] | undefined) ?? []).map((task) => [
167
- task.id,
168
- task,
169
- ])
170
- );
171
- const requestedTaskIdSet = new Set(variables.taskIds);
172
-
173
- queryClient.setQueryData(
174
- ['tasks', boardId],
175
- (old: Task[] | undefined) => {
176
- if (!old) return old;
177
- return old.map((task) => {
178
- if (!requestedTaskIdSet.has(task.id)) return task;
179
- return previousTaskMap.get(task.id) ?? task;
180
- });
181
- }
182
- );
165
+ if (context) {
166
+ restoreFailedBoardTasks({
167
+ queryClient,
168
+ boardId,
169
+ previousTasks: context.previousTasks,
170
+ previousFullTasks: context.previousFullTasks,
171
+ failedTaskIds: variables.taskIds,
172
+ });
183
173
  }
184
174
 
185
175
  console.error('Bulk add assignee failed', error);
@@ -188,26 +178,13 @@ export function useBulkAddAssignee(
188
178
  );
189
179
  },
190
180
  onSuccess: (data, _variables, context) => {
191
- if (
192
- data.failedTaskIds.length > 0 &&
193
- Array.isArray(context?.previousTasks)
194
- ) {
195
- const previousTaskMap = new Map(
196
- (context.previousTasks as Task[]).map((task) => [task.id, task])
197
- );
198
-
199
- queryClient.setQueryData(
200
- ['tasks', boardId],
201
- (old: Task[] | undefined) => {
202
- if (!old) return old;
203
- const failedIdSet = new Set(data.failedTaskIds);
204
- return old.map((task) => {
205
- if (!failedIdSet.has(task.id)) return task;
206
- return previousTaskMap.get(task.id) ?? task;
207
- });
208
- }
209
- );
210
- }
181
+ restoreFailedBoardTasks({
182
+ queryClient,
183
+ boardId,
184
+ previousTasks: context?.previousTasks,
185
+ previousFullTasks: context?.previousFullTasks,
186
+ failedTaskIds: data.failedTaskIds,
187
+ });
211
188
 
212
189
  const modifiedTaskIdSet = new Set(
213
190
  context?.modifiedTaskIds ?? data.succeededTaskIds
@@ -217,22 +194,17 @@ export function useBulkAddAssignee(
217
194
  );
218
195
 
219
196
  if (data.updatedAssigneesByTaskId.size > 0) {
220
- queryClient.setQueryData(
221
- ['tasks', boardId],
222
- (old: Task[] | undefined) => {
223
- if (!old) return old;
224
- return old.map((task) => {
225
- const updatedAssignees = data.updatedAssigneesByTaskId.get(
226
- task.id
227
- );
228
- if (!updatedAssignees) return task;
229
- return {
230
- ...task,
231
- assignees: updatedAssignees,
232
- };
233
- });
234
- }
235
- );
197
+ updateBoardTaskCaches(queryClient, boardId, (old) => {
198
+ if (!old) return old;
199
+ return old.map((task) => {
200
+ const updatedAssignees = data.updatedAssigneesByTaskId.get(task.id);
201
+ if (!updatedAssignees) return task;
202
+ return {
203
+ ...task,
204
+ assignees: updatedAssignees,
205
+ };
206
+ });
207
+ });
236
208
  }
237
209
 
238
210
  for (const tid of succeededModifiedTaskIds) {
@@ -280,17 +252,17 @@ export function useBulkRemoveAssignee(
280
252
  }) => {
281
253
  const apiOptions = getInternalApiOptions();
282
254
 
283
- const result = await bulkWorkspaceTasks(
284
- wsId,
285
- {
286
- taskIds,
287
- operation: {
288
- type: 'remove_assignee',
289
- assigneeId,
290
- },
255
+ const result = await bulkWorkspaceTasksByEffectiveWorkspace({
256
+ queryClient,
257
+ boardId,
258
+ defaultWorkspaceId: wsId,
259
+ taskIds,
260
+ operation: {
261
+ type: 'remove_assignee',
262
+ assigneeId,
291
263
  },
292
- apiOptions
293
- );
264
+ options: apiOptions,
265
+ });
294
266
 
295
267
  const successCount = result.successCount;
296
268
  const succeededTaskIds = [...result.succeededTaskIds];
@@ -314,8 +286,9 @@ export function useBulkRemoveAssignee(
314
286
  },
315
287
  onMutate: async ({ assigneeId, taskIds }) => {
316
288
  await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
317
- const previousTasks = queryClient.getQueryData(['tasks', boardId]);
318
- const current = (previousTasks as Task[] | undefined) || [];
289
+ await queryClient.cancelQueries({ queryKey: ['tasks-full', boardId] });
290
+ const cacheSnapshot = snapshotBoardTaskCaches(queryClient, boardId);
291
+ const current = cacheSnapshot.previousTasks || [];
319
292
  const modifiedTaskIds = taskIds.filter((id) => {
320
293
  const task = current.find((ct) => ct.id === id);
321
294
  return !!task?.assignees?.some(
@@ -324,45 +297,31 @@ export function useBulkRemoveAssignee(
324
297
  });
325
298
  const taskIdSet = new Set(taskIds);
326
299
 
327
- queryClient.setQueryData(
328
- ['tasks', boardId],
329
- (old: Task[] | undefined) => {
330
- if (!old) return old;
331
- return old.map((task) =>
332
- taskIdSet.has(task.id)
333
- ? {
334
- ...task,
335
- assignees: (task.assignees || []).filter(
336
- (a) => a.id !== assigneeId
337
- ),
338
- }
339
- : task
340
- );
341
- }
342
- );
300
+ updateBoardTaskCaches(queryClient, boardId, (old) => {
301
+ if (!old) return old;
302
+ return old.map((task) =>
303
+ taskIdSet.has(task.id)
304
+ ? {
305
+ ...task,
306
+ assignees: (task.assignees || []).filter(
307
+ (a) => a.id !== assigneeId
308
+ ),
309
+ }
310
+ : task
311
+ );
312
+ });
343
313
 
344
- return { previousTasks, modifiedTaskIds };
314
+ return { ...cacheSnapshot, modifiedTaskIds };
345
315
  },
346
316
  onError: (error, variables, context) => {
347
- if (context?.previousTasks) {
348
- const previousTaskMap = new Map(
349
- ((context.previousTasks as Task[] | undefined) ?? []).map((task) => [
350
- task.id,
351
- task,
352
- ])
353
- );
354
- const requestedTaskIdSet = new Set(variables.taskIds);
355
-
356
- queryClient.setQueryData(
357
- ['tasks', boardId],
358
- (old: Task[] | undefined) => {
359
- if (!old) return old;
360
- return old.map((task) => {
361
- if (!requestedTaskIdSet.has(task.id)) return task;
362
- return previousTaskMap.get(task.id) ?? task;
363
- });
364
- }
365
- );
317
+ if (context) {
318
+ restoreFailedBoardTasks({
319
+ queryClient,
320
+ boardId,
321
+ previousTasks: context.previousTasks,
322
+ previousFullTasks: context.previousFullTasks,
323
+ failedTaskIds: variables.taskIds,
324
+ });
366
325
  }
367
326
 
368
327
  console.error('Bulk remove assignee failed', error);
@@ -372,26 +331,13 @@ export function useBulkRemoveAssignee(
372
331
  );
373
332
  },
374
333
  onSuccess: (data, _variables, context) => {
375
- if (
376
- data.failedTaskIds.length > 0 &&
377
- Array.isArray(context?.previousTasks)
378
- ) {
379
- const previousTaskMap = new Map(
380
- (context.previousTasks as Task[]).map((task) => [task.id, task])
381
- );
382
-
383
- queryClient.setQueryData(
384
- ['tasks', boardId],
385
- (old: Task[] | undefined) => {
386
- if (!old) return old;
387
- const failedIdSet = new Set(data.failedTaskIds);
388
- return old.map((task) => {
389
- if (!failedIdSet.has(task.id)) return task;
390
- return previousTaskMap.get(task.id) ?? task;
391
- });
392
- }
393
- );
394
- }
334
+ restoreFailedBoardTasks({
335
+ queryClient,
336
+ boardId,
337
+ previousTasks: context?.previousTasks,
338
+ previousFullTasks: context?.previousFullTasks,
339
+ failedTaskIds: data.failedTaskIds,
340
+ });
395
341
 
396
342
  const modifiedTaskIdSet = new Set(
397
343
  context?.modifiedTaskIds ?? data.succeededTaskIds
@@ -7,7 +7,13 @@ import { toast } from '@tuturuuu/ui/sonner';
7
7
  import type { WorkspaceLabel } from '@tuturuuu/utils/task-helper';
8
8
  import type { BoardBroadcastFn } from '../../../../shared/board-broadcast-context';
9
9
  import type { BulkOperationI18n } from './bulk-operation-i18n';
10
- import { getInternalApiOptions } from './bulk-operation-utils';
10
+ import {
11
+ getInternalApiOptions,
12
+ restoreBoardTaskCaches,
13
+ restoreFailedBoardTasks,
14
+ snapshotBoardTaskCaches,
15
+ updateBoardTaskCaches,
16
+ } from './bulk-operation-utils';
11
17
 
12
18
  export function useBulkAddLabel(
13
19
  queryClient: QueryClient,
@@ -52,8 +58,9 @@ export function useBulkAddLabel(
52
58
  },
53
59
  onMutate: async ({ labelId, taskIds }) => {
54
60
  await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
55
- const previousTasks = queryClient.getQueryData(['tasks', boardId]);
56
- const current = (previousTasks as Task[] | undefined) || [];
61
+ await queryClient.cancelQueries({ queryKey: ['tasks-full', boardId] });
62
+ const cacheSnapshot = snapshotBoardTaskCaches(queryClient, boardId);
63
+ const current = cacheSnapshot.previousTasks || [];
57
64
  const labelMeta = workspaceLabels.find((l) => l.id === labelId);
58
65
 
59
66
  const missingTaskIds = taskIds.filter((id) => {
@@ -61,34 +68,31 @@ export function useBulkAddLabel(
61
68
  return !task?.labels?.some((l) => l.id === labelId);
62
69
  });
63
70
 
64
- queryClient.setQueryData(
65
- ['tasks', boardId],
66
- (old: Task[] | undefined) => {
67
- if (!old) return old;
68
- return old.map((task) => {
69
- if (!missingTaskIds.includes(task.id)) return task;
70
- return {
71
- ...task,
72
- labels: [
73
- ...(task.labels || []),
74
- {
75
- id: labelId,
76
- ws_id: wsId || labelMeta?.ws_id || '',
77
- name: labelMeta?.name || i18n?.defaultLabelName() || 'Label',
78
- color: labelMeta?.color || '#3b82f6',
79
- created_at: new Date().toISOString(),
80
- },
81
- ],
82
- } as Task;
83
- });
84
- }
85
- );
71
+ updateBoardTaskCaches(queryClient, boardId, (old) => {
72
+ if (!old) return old;
73
+ return old.map((task) => {
74
+ if (!missingTaskIds.includes(task.id)) return task;
75
+ return {
76
+ ...task,
77
+ labels: [
78
+ ...(task.labels || []),
79
+ {
80
+ id: labelId,
81
+ ws_id: wsId || labelMeta?.ws_id || '',
82
+ name: labelMeta?.name || i18n?.defaultLabelName() || 'Label',
83
+ color: labelMeta?.color || '#3b82f6',
84
+ created_at: new Date().toISOString(),
85
+ },
86
+ ],
87
+ } as Task;
88
+ });
89
+ });
86
90
 
87
- return { previousTasks, modifiedTaskIds: missingTaskIds };
91
+ return { ...cacheSnapshot, modifiedTaskIds: missingTaskIds };
88
92
  },
89
93
  onError: (error, _, context) => {
90
- if (context?.previousTasks) {
91
- queryClient.setQueryData(['tasks', boardId], context.previousTasks);
94
+ if (context) {
95
+ restoreBoardTaskCaches(queryClient, boardId, context);
92
96
  }
93
97
  console.error('Bulk add label failed', error);
94
98
  toast.error(
@@ -100,22 +104,13 @@ export function useBulkAddLabel(
100
104
  data.failures.map((failure) => failure.taskId)
101
105
  );
102
106
 
103
- if (failedTaskIds.size > 0 && Array.isArray(context?.previousTasks)) {
104
- const previousTaskMap = new Map(
105
- (context.previousTasks as Task[]).map((task) => [task.id, task])
106
- );
107
-
108
- queryClient.setQueryData(
109
- ['tasks', boardId],
110
- (old: Task[] | undefined) => {
111
- if (!old) return old;
112
- return old.map((task) => {
113
- if (!failedTaskIds.has(task.id)) return task;
114
- return previousTaskMap.get(task.id) ?? task;
115
- });
116
- }
117
- );
118
- }
107
+ restoreFailedBoardTasks({
108
+ queryClient,
109
+ boardId,
110
+ previousTasks: context?.previousTasks,
111
+ previousFullTasks: context?.previousFullTasks,
112
+ failedTaskIds,
113
+ });
119
114
 
120
115
  const modifiedTaskIds = context?.modifiedTaskIds ?? data.taskIds;
121
116
  const succeededModifiedTaskIds = modifiedTaskIds.filter(
@@ -199,34 +194,32 @@ export function useBulkRemoveLabel(
199
194
  },
200
195
  onMutate: async ({ labelId, taskIds }) => {
201
196
  await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
202
- const previousTasks = queryClient.getQueryData(['tasks', boardId]);
203
- const current = (previousTasks as Task[] | undefined) || [];
197
+ await queryClient.cancelQueries({ queryKey: ['tasks-full', boardId] });
198
+ const cacheSnapshot = snapshotBoardTaskCaches(queryClient, boardId);
199
+ const current = cacheSnapshot.previousTasks || [];
204
200
  const modifiedTaskIds = taskIds.filter((id) => {
205
201
  const task = current.find((ct) => ct.id === id);
206
202
  return !!task?.labels?.some((label) => label.id === labelId);
207
203
  });
208
204
  const taskIdSet = new Set(taskIds);
209
205
 
210
- queryClient.setQueryData(
211
- ['tasks', boardId],
212
- (old: Task[] | undefined) => {
213
- if (!old) return old;
214
- return old.map((task) =>
215
- taskIdSet.has(task.id)
216
- ? {
217
- ...task,
218
- labels: (task.labels || []).filter((l) => l.id !== labelId),
219
- }
220
- : task
221
- );
222
- }
223
- );
206
+ updateBoardTaskCaches(queryClient, boardId, (old) => {
207
+ if (!old) return old;
208
+ return old.map((task) =>
209
+ taskIdSet.has(task.id)
210
+ ? {
211
+ ...task,
212
+ labels: (task.labels || []).filter((l) => l.id !== labelId),
213
+ }
214
+ : task
215
+ );
216
+ });
224
217
 
225
- return { previousTasks, modifiedTaskIds };
218
+ return { ...cacheSnapshot, modifiedTaskIds };
226
219
  },
227
220
  onError: (error, _, context) => {
228
- if (context?.previousTasks) {
229
- queryClient.setQueryData(['tasks', boardId], context.previousTasks);
221
+ if (context) {
222
+ restoreBoardTaskCaches(queryClient, boardId, context);
230
223
  }
231
224
  console.error('Bulk remove label failed', error);
232
225
  toast.error(
@@ -239,22 +232,13 @@ export function useBulkRemoveLabel(
239
232
  data.failures.map((failure) => failure.taskId)
240
233
  );
241
234
 
242
- if (failedTaskIds.size > 0 && Array.isArray(context?.previousTasks)) {
243
- const previousTaskMap = new Map(
244
- (context.previousTasks as Task[]).map((task) => [task.id, task])
245
- );
246
-
247
- queryClient.setQueryData(
248
- ['tasks', boardId],
249
- (old: Task[] | undefined) => {
250
- if (!old) return old;
251
- return old.map((task) => {
252
- if (!failedTaskIds.has(task.id)) return task;
253
- return previousTaskMap.get(task.id) ?? task;
254
- });
255
- }
256
- );
257
- }
235
+ restoreFailedBoardTasks({
236
+ queryClient,
237
+ boardId,
238
+ previousTasks: context?.previousTasks,
239
+ previousFullTasks: context?.previousFullTasks,
240
+ failedTaskIds,
241
+ });
258
242
 
259
243
  const modifiedTaskIds = context?.modifiedTaskIds ?? data.taskIds;
260
244
  const succeededModifiedTaskIds = modifiedTaskIds.filter(