@tuturuuu/ui 0.9.0 → 0.11.0
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.
- package/CHANGELOG.md +45 -0
- package/package.json +6 -5
- package/src/components/ui/custom/__tests__/settings-dialog-search.test.ts +78 -0
- package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +27 -1
- package/src/components/ui/custom/nav-link.test.tsx +165 -0
- package/src/components/ui/custom/nav-link.tsx +69 -11
- package/src/components/ui/custom/navigation.tsx +1 -0
- package/src/components/ui/custom/settings/task-settings.tsx +104 -0
- package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
- package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
- package/src/components/ui/custom/settings-dialog-search.ts +75 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +63 -27
- package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
- package/src/components/ui/custom/workspace-select.tsx +17 -16
- package/src/components/ui/finance/invoices/components/subscription-attendance-summary.tsx +19 -5
- package/src/components/ui/finance/invoices/components/subscription-prepaid-controls.test.tsx +41 -0
- package/src/components/ui/finance/invoices/components/subscription-prepaid-controls.tsx +93 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +82 -70
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +50 -25
- package/src/components/ui/finance/invoices/hooks.ts +11 -2
- package/src/components/ui/finance/invoices/internal-api.ts +5 -0
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +92 -31
- package/src/components/ui/finance/invoices/utils.test.ts +82 -0
- package/src/components/ui/finance/invoices/utils.ts +240 -7
- package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +16 -0
- package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +4 -328
- package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +50 -37
- package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
- package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +263 -21
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +133 -14
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +112 -54
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +8 -2
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +29 -14
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +24 -1
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +7 -1
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +80 -8
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +15 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
- package/src/components/ui/tu-do/boards/form.tsx +1 -1
- package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
- package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
- package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
- package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
- package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
- package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
- package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
- package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +123 -1
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +38 -0
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +128 -7
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +222 -9
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +58 -0
- package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
- package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
- package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +16 -4
- package/src/components/ui/tu-do/shared/board-header.tsx +8 -1
- package/src/components/ui/tu-do/shared/board-switcher.tsx +70 -38
- package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
- package/src/components/ui/tu-do/shared/board-views.tsx +49 -69
- package/src/components/ui/tu-do/shared/list-view.tsx +21 -3
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +58 -4
- package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/description-versions.test.ts +97 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/description-versions.ts +210 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
- package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +56 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-change-dialog.test.tsx +63 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-change-dialog.tsx +218 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-restore-banner.tsx +83 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-version-restore-dialog.test.tsx +120 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-description-version-restore-dialog.tsx +180 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/utils.test.ts +100 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/utils.ts +109 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +390 -34
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
- package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
- package/src/components/ui/tu-do/templates/task-template-api.ts +142 -0
- package/src/components/ui/tu-do/templates/task-template-card.tsx +118 -0
- package/src/components/ui/tu-do/templates/task-template-client.tsx +258 -0
- package/src/components/ui/tu-do/templates/task-template-dialogs.test.tsx +167 -0
- package/src/components/ui/tu-do/templates/task-template-dialogs.tsx +376 -0
- package/src/components/ui/tu-do/templates/task-templates-hub.test.tsx +114 -0
- package/src/components/ui/tu-do/templates/task-templates-hub.tsx +50 -0
- package/src/components/ui/tu-do/templates/task-templates-page.tsx +6 -11
- package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
- package/src/hooks/useBoardPresence.ts +364 -0
- package/src/hooks/useBoardRealtimeEventHandler.ts +34 -90
- package/src/lib/workspace-actions.ts +2 -6
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { JSONContent } from '@tiptap/react';
|
|
2
|
+
import {
|
|
3
|
+
getDescriptionContent,
|
|
4
|
+
getTaskDescriptionPreviewText,
|
|
5
|
+
normalizeTaskDescriptionSnapshot,
|
|
6
|
+
serializeTaskDescriptionPersistenceSnapshot,
|
|
7
|
+
} from './utils';
|
|
8
|
+
|
|
9
|
+
export interface TaskDescriptionHistoryUser {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
avatar_url?: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TaskDescriptionHistoryEntryLike {
|
|
16
|
+
id: string;
|
|
17
|
+
changed_at: string;
|
|
18
|
+
change_type?: string | null;
|
|
19
|
+
field_name?: string | null;
|
|
20
|
+
old_value?: unknown;
|
|
21
|
+
new_value?: unknown;
|
|
22
|
+
user?: TaskDescriptionHistoryUser | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type TaskDescriptionVersionSource = 'new_value' | 'old_value';
|
|
26
|
+
export type TaskDescriptionVersionReason = 'before_clear' | 'tracked';
|
|
27
|
+
|
|
28
|
+
export interface RecoverableTaskDescriptionVersion {
|
|
29
|
+
id: string;
|
|
30
|
+
historyId: string;
|
|
31
|
+
changedAt: string;
|
|
32
|
+
source: TaskDescriptionVersionSource;
|
|
33
|
+
reason: TaskDescriptionVersionReason;
|
|
34
|
+
description: string;
|
|
35
|
+
content: JSONContent;
|
|
36
|
+
previewText: string;
|
|
37
|
+
user: TaskDescriptionHistoryUser | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const LEGACY_DESCRIPTION_PLACEHOLDERS = new Set(['has_content']);
|
|
41
|
+
|
|
42
|
+
function isLegacyDescriptionPlaceholder(value: string) {
|
|
43
|
+
return LEGACY_DESCRIPTION_PLACEHOLDERS.has(value.trim().toLowerCase());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hasObjectShape(value: unknown): value is Record<string, unknown> {
|
|
47
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function textToDescriptionContent(text: string): JSONContent {
|
|
51
|
+
return {
|
|
52
|
+
type: 'doc',
|
|
53
|
+
content: [
|
|
54
|
+
{
|
|
55
|
+
type: 'paragraph',
|
|
56
|
+
content: [{ type: 'text', text }],
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function coerceDescriptionValueToContent(value: unknown): JSONContent | null {
|
|
63
|
+
if (value === null || value === undefined) return null;
|
|
64
|
+
|
|
65
|
+
if (typeof value === 'string') {
|
|
66
|
+
const trimmed = value.trim();
|
|
67
|
+
if (!trimmed || isLegacyDescriptionPlaceholder(trimmed)) return null;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
71
|
+
|
|
72
|
+
if (typeof parsed === 'string') {
|
|
73
|
+
const parsedText = parsed.trim();
|
|
74
|
+
if (!parsedText || isLegacyDescriptionPlaceholder(parsedText)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return textToDescriptionContent(parsed);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (hasObjectShape(parsed)) {
|
|
81
|
+
return getDescriptionContent(parsed);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
} catch {
|
|
86
|
+
return textToDescriptionContent(value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (hasObjectShape(value)) {
|
|
91
|
+
return getDescriptionContent(value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function extractRecoverableTaskDescriptionValue(value: unknown): {
|
|
98
|
+
content: JSONContent;
|
|
99
|
+
description: string;
|
|
100
|
+
previewText: string;
|
|
101
|
+
} | null {
|
|
102
|
+
const content = normalizeTaskDescriptionSnapshot(
|
|
103
|
+
coerceDescriptionValueToContent(value)
|
|
104
|
+
);
|
|
105
|
+
if (!content) return null;
|
|
106
|
+
|
|
107
|
+
const description = serializeTaskDescriptionPersistenceSnapshot(content);
|
|
108
|
+
if (!description) return null;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
content,
|
|
112
|
+
description,
|
|
113
|
+
previewText: getTaskDescriptionPreviewText(content).trim(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function isTaskDescriptionHistoryEntry(
|
|
118
|
+
entry: TaskDescriptionHistoryEntryLike
|
|
119
|
+
) {
|
|
120
|
+
return (
|
|
121
|
+
entry.change_type === 'field_updated' && entry.field_name === 'description'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function isTaskDescriptionHistoryValueEmpty(value: unknown) {
|
|
126
|
+
return extractRecoverableTaskDescriptionValue(value) === null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function addDescriptionVersion({
|
|
130
|
+
entry,
|
|
131
|
+
reason,
|
|
132
|
+
seenDescriptions,
|
|
133
|
+
source,
|
|
134
|
+
value,
|
|
135
|
+
versions,
|
|
136
|
+
}: {
|
|
137
|
+
entry: TaskDescriptionHistoryEntryLike;
|
|
138
|
+
reason: TaskDescriptionVersionReason;
|
|
139
|
+
seenDescriptions: Set<string>;
|
|
140
|
+
source: TaskDescriptionVersionSource;
|
|
141
|
+
value: unknown;
|
|
142
|
+
versions: RecoverableTaskDescriptionVersion[];
|
|
143
|
+
}) {
|
|
144
|
+
const recoverableValue = extractRecoverableTaskDescriptionValue(value);
|
|
145
|
+
if (!recoverableValue) return;
|
|
146
|
+
if (seenDescriptions.has(recoverableValue.description)) return;
|
|
147
|
+
|
|
148
|
+
seenDescriptions.add(recoverableValue.description);
|
|
149
|
+
versions.push({
|
|
150
|
+
id: `${entry.id}:${source}`,
|
|
151
|
+
historyId: entry.id,
|
|
152
|
+
changedAt: entry.changed_at,
|
|
153
|
+
source,
|
|
154
|
+
reason,
|
|
155
|
+
description: recoverableValue.description,
|
|
156
|
+
content: recoverableValue.content,
|
|
157
|
+
previewText: recoverableValue.previewText,
|
|
158
|
+
user: entry.user ?? null,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function buildRecoverableTaskDescriptionVersions(
|
|
163
|
+
entries: TaskDescriptionHistoryEntryLike[]
|
|
164
|
+
) {
|
|
165
|
+
const versions: RecoverableTaskDescriptionVersion[] = [];
|
|
166
|
+
const seenDescriptions = new Set<string>();
|
|
167
|
+
const sortedEntries = [...entries]
|
|
168
|
+
.filter(isTaskDescriptionHistoryEntry)
|
|
169
|
+
.sort(
|
|
170
|
+
(a, b) =>
|
|
171
|
+
new Date(b.changed_at).getTime() - new Date(a.changed_at).getTime()
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
for (const entry of sortedEntries) {
|
|
175
|
+
const oldIsRecoverable =
|
|
176
|
+
extractRecoverableTaskDescriptionValue(entry.old_value) !== null;
|
|
177
|
+
const newIsEmpty = isTaskDescriptionHistoryValueEmpty(entry.new_value);
|
|
178
|
+
|
|
179
|
+
if (oldIsRecoverable && newIsEmpty) {
|
|
180
|
+
addDescriptionVersion({
|
|
181
|
+
entry,
|
|
182
|
+
reason: 'before_clear',
|
|
183
|
+
seenDescriptions,
|
|
184
|
+
source: 'old_value',
|
|
185
|
+
value: entry.old_value,
|
|
186
|
+
versions,
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
addDescriptionVersion({
|
|
192
|
+
entry,
|
|
193
|
+
reason: 'tracked',
|
|
194
|
+
seenDescriptions,
|
|
195
|
+
source: 'new_value',
|
|
196
|
+
value: entry.new_value,
|
|
197
|
+
versions,
|
|
198
|
+
});
|
|
199
|
+
addDescriptionVersion({
|
|
200
|
+
entry,
|
|
201
|
+
reason: 'tracked',
|
|
202
|
+
seenDescriptions,
|
|
203
|
+
source: 'old_value',
|
|
204
|
+
value: entry.old_value,
|
|
205
|
+
versions,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return versions;
|
|
210
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { useQuery } from '@tanstack/react-query';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
listWorkspaceTaskBoardViewableMembers,
|
|
4
|
+
listWorkspaceTaskProjects,
|
|
5
|
+
} from '@tuturuuu/internal-api/tasks';
|
|
3
6
|
import { createClient } from '@tuturuuu/supabase/next/client';
|
|
4
7
|
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
5
8
|
import {
|
|
@@ -89,10 +92,19 @@ interface UseTaskDataProps {
|
|
|
89
92
|
isOpen: boolean;
|
|
90
93
|
propAvailableLists?: TaskList[];
|
|
91
94
|
taskSearchQuery?: string;
|
|
95
|
+
canUseBoardAssignees?: boolean;
|
|
96
|
+
assigneeMemberSource?: 'workspace' | 'board' | 'workspace-and-board';
|
|
92
97
|
/** Pre-loaded data for shared task context - bypasses internal fetches when provided */
|
|
93
98
|
sharedContext?: SharedTaskContext;
|
|
94
99
|
}
|
|
95
100
|
|
|
101
|
+
type TaskDialogWorkspaceMember = {
|
|
102
|
+
id: string;
|
|
103
|
+
user_id: string;
|
|
104
|
+
display_name: string;
|
|
105
|
+
avatar_url?: string | null;
|
|
106
|
+
};
|
|
107
|
+
|
|
96
108
|
// UUID validation regex
|
|
97
109
|
const UUID_REGEX =
|
|
98
110
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -107,6 +119,8 @@ export function useTaskData({
|
|
|
107
119
|
isOpen,
|
|
108
120
|
propAvailableLists,
|
|
109
121
|
taskSearchQuery = '',
|
|
122
|
+
canUseBoardAssignees,
|
|
123
|
+
assigneeMemberSource,
|
|
110
124
|
sharedContext,
|
|
111
125
|
}: UseTaskDataProps) {
|
|
112
126
|
// If sharedContext is provided, use pre-loaded data and skip fetches
|
|
@@ -174,17 +188,19 @@ export function useTaskData({
|
|
|
174
188
|
);
|
|
175
189
|
const workspaceLabels =
|
|
176
190
|
sharedContext?.workspaceLabels || fetchedWorkspaceLabels;
|
|
191
|
+
const effectiveAssigneeMemberSource = assigneeMemberSource ?? 'workspace';
|
|
192
|
+
const shouldLoadWorkspaceMembers =
|
|
193
|
+
canUseBoardAssignees !== false && effectiveAssigneeMemberSource !== 'board';
|
|
194
|
+
const shouldLoadBoardViewableMembers =
|
|
195
|
+
canUseBoardAssignees !== false &&
|
|
196
|
+
effectiveAssigneeMemberSource !== 'workspace' &&
|
|
197
|
+
!!boardId;
|
|
177
198
|
|
|
178
199
|
// Workspace members
|
|
179
200
|
const { data: fetchedWorkspaceMembers = [] } = useQuery<
|
|
180
|
-
|
|
181
|
-
id: string;
|
|
182
|
-
user_id: string;
|
|
183
|
-
display_name: string;
|
|
184
|
-
avatar_url?: string | null;
|
|
185
|
-
}>
|
|
201
|
+
TaskDialogWorkspaceMember[]
|
|
186
202
|
>({
|
|
187
|
-
queryKey: ['workspace-members', realWorkspaceId],
|
|
203
|
+
queryKey: ['workspace-members', realWorkspaceId, 'workspace'],
|
|
188
204
|
queryFn: async () => {
|
|
189
205
|
if (!realWorkspaceId || !isValidWsId) return [];
|
|
190
206
|
|
|
@@ -234,11 +250,64 @@ export function useTaskData({
|
|
|
234
250
|
(a.display_name || '').localeCompare(b.display_name || '')
|
|
235
251
|
);
|
|
236
252
|
},
|
|
237
|
-
enabled:
|
|
253
|
+
enabled:
|
|
254
|
+
!!realWorkspaceId &&
|
|
255
|
+
isOpen &&
|
|
256
|
+
isValidWsId &&
|
|
257
|
+
!hasSharedContext &&
|
|
258
|
+
shouldLoadWorkspaceMembers,
|
|
259
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
260
|
+
});
|
|
261
|
+
const { data: fetchedBoardViewableMembers = [] } = useQuery<
|
|
262
|
+
TaskDialogWorkspaceMember[]
|
|
263
|
+
>({
|
|
264
|
+
queryKey: ['task-board-viewable-members', realWorkspaceId, boardId],
|
|
265
|
+
queryFn: async () => {
|
|
266
|
+
if (!realWorkspaceId || !isValidWsId || !boardId) return [];
|
|
267
|
+
|
|
268
|
+
const payload = await listWorkspaceTaskBoardViewableMembers(
|
|
269
|
+
realWorkspaceId,
|
|
270
|
+
boardId
|
|
271
|
+
);
|
|
272
|
+
const members = Array.isArray(payload?.members) ? payload.members : [];
|
|
273
|
+
|
|
274
|
+
return members
|
|
275
|
+
.map((member) => ({
|
|
276
|
+
id: member.user_id,
|
|
277
|
+
user_id: member.user_id,
|
|
278
|
+
display_name: member.display_name || member.email || member.user_id,
|
|
279
|
+
avatar_url: member.avatar_url,
|
|
280
|
+
}))
|
|
281
|
+
.sort((a, b) =>
|
|
282
|
+
(a.display_name || '').localeCompare(b.display_name || '')
|
|
283
|
+
);
|
|
284
|
+
},
|
|
285
|
+
enabled:
|
|
286
|
+
!!realWorkspaceId &&
|
|
287
|
+
isOpen &&
|
|
288
|
+
isValidWsId &&
|
|
289
|
+
!hasSharedContext &&
|
|
290
|
+
shouldLoadBoardViewableMembers,
|
|
238
291
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
239
292
|
});
|
|
293
|
+
const mergedFetchedWorkspaceMembers = useMemo(() => {
|
|
294
|
+
const seen = new Set<string>();
|
|
295
|
+
const merged: TaskDialogWorkspaceMember[] = [];
|
|
296
|
+
|
|
297
|
+
for (const member of [
|
|
298
|
+
...fetchedWorkspaceMembers,
|
|
299
|
+
...fetchedBoardViewableMembers,
|
|
300
|
+
]) {
|
|
301
|
+
const memberId = member.user_id || member.id;
|
|
302
|
+
if (!memberId || seen.has(memberId)) continue;
|
|
303
|
+
seen.add(memberId);
|
|
304
|
+
merged.push(member);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return merged;
|
|
308
|
+
}, [fetchedBoardViewableMembers, fetchedWorkspaceMembers]);
|
|
240
309
|
const workspaceMembers =
|
|
241
|
-
sharedContext?.workspaceMembers ||
|
|
310
|
+
sharedContext?.workspaceMembers || mergedFetchedWorkspaceMembers;
|
|
242
311
|
|
|
243
312
|
// Task projects
|
|
244
313
|
const { data: fetchedTaskProjects = [] } = useQuery({
|
|
@@ -6,13 +6,17 @@ import {
|
|
|
6
6
|
import type { TaskPriority } from '@tuturuuu/types/primitives/Priority';
|
|
7
7
|
import type { CalendarHoursType } from '@tuturuuu/types/primitives/Task';
|
|
8
8
|
import { useToast } from '@tuturuuu/ui/hooks/use-toast';
|
|
9
|
-
import { invalidateTaskCaches } from '@tuturuuu/utils/task-helper';
|
|
10
9
|
import { useCallback, useState } from 'react';
|
|
11
10
|
import {
|
|
12
11
|
type BoardBroadcastFn,
|
|
13
12
|
getActiveBroadcast,
|
|
14
13
|
useBoardBroadcast,
|
|
15
14
|
} from '../../board-broadcast-context';
|
|
15
|
+
import {
|
|
16
|
+
patchTaskInVisibleCaches,
|
|
17
|
+
restoreVisibleTaskCaches,
|
|
18
|
+
snapshotVisibleTaskCaches,
|
|
19
|
+
} from '../../task-cache-patches';
|
|
16
20
|
import { updateWorkspaceTask } from './task-api';
|
|
17
21
|
|
|
18
22
|
export interface SchedulingSettings {
|
|
@@ -109,17 +113,17 @@ export function useTaskMutations({
|
|
|
109
113
|
return;
|
|
110
114
|
}
|
|
111
115
|
setEstimationSaving(true);
|
|
116
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
117
|
+
taskId,
|
|
118
|
+
]);
|
|
112
119
|
|
|
113
120
|
// Optimistic update - prevents flicker by updating cache immediately
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
);
|
|
121
|
+
patchTaskInVisibleCaches({
|
|
122
|
+
queryClient,
|
|
123
|
+
boardId,
|
|
124
|
+
taskId,
|
|
125
|
+
updater: (task) => ({ ...task, estimation_points: points }),
|
|
126
|
+
});
|
|
123
127
|
|
|
124
128
|
try {
|
|
125
129
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
@@ -135,7 +139,7 @@ export function useTaskMutations({
|
|
|
135
139
|
} catch (e: any) {
|
|
136
140
|
console.error('Failed updating estimation', e);
|
|
137
141
|
// Revert optimistic update on error
|
|
138
|
-
|
|
142
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
139
143
|
toast({
|
|
140
144
|
title: 'Failed to update estimation',
|
|
141
145
|
description: e.message || 'Please try again',
|
|
@@ -166,17 +170,17 @@ export function useTaskMutations({
|
|
|
166
170
|
if (isCreateMode || !taskId || taskId === 'new') {
|
|
167
171
|
return;
|
|
168
172
|
}
|
|
173
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
174
|
+
taskId,
|
|
175
|
+
]);
|
|
169
176
|
|
|
170
177
|
// Optimistic update - prevents flicker
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
);
|
|
178
|
+
patchTaskInVisibleCaches({
|
|
179
|
+
queryClient,
|
|
180
|
+
boardId,
|
|
181
|
+
taskId,
|
|
182
|
+
updater: (task) => ({ ...task, priority: newPriority }),
|
|
183
|
+
});
|
|
180
184
|
|
|
181
185
|
try {
|
|
182
186
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
@@ -192,7 +196,7 @@ export function useTaskMutations({
|
|
|
192
196
|
} catch (e: any) {
|
|
193
197
|
console.error('Failed updating priority', e);
|
|
194
198
|
// Revert optimistic update on error
|
|
195
|
-
|
|
199
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
196
200
|
toast({
|
|
197
201
|
title: 'Failed to update priority',
|
|
198
202
|
description: e.message || 'Please try again',
|
|
@@ -222,17 +226,17 @@ export function useTaskMutations({
|
|
|
222
226
|
}
|
|
223
227
|
|
|
224
228
|
const dateString = newDate ? newDate.toISOString() : null;
|
|
229
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
230
|
+
taskId,
|
|
231
|
+
]);
|
|
225
232
|
|
|
226
233
|
// Optimistic update - prevents flicker
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
);
|
|
234
|
+
patchTaskInVisibleCaches({
|
|
235
|
+
queryClient,
|
|
236
|
+
boardId,
|
|
237
|
+
taskId,
|
|
238
|
+
updater: (task) => ({ ...task, start_date: dateString ?? undefined }),
|
|
239
|
+
});
|
|
236
240
|
|
|
237
241
|
try {
|
|
238
242
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
@@ -248,7 +252,7 @@ export function useTaskMutations({
|
|
|
248
252
|
} catch (e: any) {
|
|
249
253
|
console.error('Failed updating start date', e);
|
|
250
254
|
// Revert optimistic update on error
|
|
251
|
-
|
|
255
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
252
256
|
toast({
|
|
253
257
|
title: 'Failed to update start date',
|
|
254
258
|
description: e.message || 'Please try again',
|
|
@@ -277,17 +281,17 @@ export function useTaskMutations({
|
|
|
277
281
|
}
|
|
278
282
|
|
|
279
283
|
const dateString = newDate ? newDate.toISOString() : null;
|
|
284
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
285
|
+
taskId,
|
|
286
|
+
]);
|
|
280
287
|
|
|
281
288
|
// Optimistic update - prevents flicker
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
);
|
|
289
|
+
patchTaskInVisibleCaches({
|
|
290
|
+
queryClient,
|
|
291
|
+
boardId,
|
|
292
|
+
taskId,
|
|
293
|
+
updater: (task) => ({ ...task, end_date: dateString }),
|
|
294
|
+
});
|
|
291
295
|
|
|
292
296
|
try {
|
|
293
297
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
@@ -303,7 +307,7 @@ export function useTaskMutations({
|
|
|
303
307
|
} catch (e: any) {
|
|
304
308
|
console.error('Failed updating end date', e);
|
|
305
309
|
// Revert optimistic update on error
|
|
306
|
-
|
|
310
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
307
311
|
toast({
|
|
308
312
|
title: 'Failed to update end date',
|
|
309
313
|
description: e.message || 'Please try again',
|
|
@@ -331,38 +335,33 @@ export function useTaskMutations({
|
|
|
331
335
|
if (isCreateMode || !taskId || taskId === 'new') {
|
|
332
336
|
return;
|
|
333
337
|
}
|
|
338
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
339
|
+
taskId,
|
|
340
|
+
]);
|
|
334
341
|
|
|
335
342
|
// Optimistic update - prevents flicker
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
);
|
|
343
|
+
patchTaskInVisibleCaches({
|
|
344
|
+
queryClient,
|
|
345
|
+
boardId,
|
|
346
|
+
taskId,
|
|
347
|
+
updater: (task) => ({ ...task, list_id: newListId }),
|
|
348
|
+
});
|
|
345
349
|
|
|
346
350
|
try {
|
|
347
351
|
const { task: updatedTask } = await updateWorkspaceTask(wsId, taskId, {
|
|
348
352
|
list_id: newListId,
|
|
349
353
|
});
|
|
350
354
|
// Update sender's own cache with DB-computed timestamps
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
: task
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
);
|
|
355
|
+
patchTaskInVisibleCaches({
|
|
356
|
+
queryClient,
|
|
357
|
+
boardId,
|
|
358
|
+
taskId,
|
|
359
|
+
updater: (task) => ({
|
|
360
|
+
...task,
|
|
361
|
+
completed_at: updatedTask.completed_at,
|
|
362
|
+
closed_at: updatedTask.closed_at,
|
|
363
|
+
}),
|
|
364
|
+
});
|
|
366
365
|
broadcast?.('task:upsert', {
|
|
367
366
|
task: {
|
|
368
367
|
id: taskId,
|
|
@@ -379,7 +378,7 @@ export function useTaskMutations({
|
|
|
379
378
|
} catch (e: any) {
|
|
380
379
|
console.error('Failed updating list', e);
|
|
381
380
|
// Revert optimistic update on error
|
|
382
|
-
|
|
381
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
383
382
|
toast({
|
|
384
383
|
title: 'Failed to update list',
|
|
385
384
|
description: e.message || 'Please try again',
|
|
@@ -412,15 +411,15 @@ export function useTaskMutations({
|
|
|
412
411
|
|
|
413
412
|
// Optimistically update the cache instead of invalidating
|
|
414
413
|
// This prevents conflicts with realtime sync
|
|
415
|
-
queryClient
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
);
|
|
414
|
+
const cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
|
|
415
|
+
taskId,
|
|
416
|
+
]);
|
|
417
|
+
patchTaskInVisibleCaches({
|
|
418
|
+
queryClient,
|
|
419
|
+
boardId,
|
|
420
|
+
taskId,
|
|
421
|
+
updater: (task) => ({ ...task, name: trimmedName }),
|
|
422
|
+
});
|
|
424
423
|
|
|
425
424
|
try {
|
|
426
425
|
const { task } = await updateWorkspaceTask(wsId, taskId, {
|
|
@@ -432,8 +431,8 @@ export function useTaskMutations({
|
|
|
432
431
|
triggerRefresh();
|
|
433
432
|
} catch (e: any) {
|
|
434
433
|
console.error('Failed updating task name', e);
|
|
435
|
-
// Revert optimistic update
|
|
436
|
-
|
|
434
|
+
// Revert optimistic update without refetching visible board caches.
|
|
435
|
+
restoreVisibleTaskCaches(queryClient, cacheSnapshot);
|
|
437
436
|
toast({
|
|
438
437
|
title: 'Failed to update task name',
|
|
439
438
|
description: e.message || 'Please try again',
|
package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx
CHANGED
|
@@ -146,4 +146,67 @@ describe('useTaskRelationships', () => {
|
|
|
146
146
|
])
|
|
147
147
|
).toEqual([newLabel, existingLabel]);
|
|
148
148
|
});
|
|
149
|
+
|
|
150
|
+
it('patches visible task caches when labels change without invalidating board task queries', async () => {
|
|
151
|
+
const cachedTask = {
|
|
152
|
+
id: 'task-1',
|
|
153
|
+
labels: [],
|
|
154
|
+
} as unknown as Task;
|
|
155
|
+
|
|
156
|
+
queryClient.setQueryData(['tasks', 'board-1'], [cachedTask]);
|
|
157
|
+
queryClient.setQueryData(['tasks-full', 'board-1', 'all'], [cachedTask]);
|
|
158
|
+
queryClient.setQueryData(['task', 'task-1'], cachedTask);
|
|
159
|
+
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
160
|
+
|
|
161
|
+
const { result } = renderHook(
|
|
162
|
+
() =>
|
|
163
|
+
useTaskRelationships({
|
|
164
|
+
wsId: 'personal',
|
|
165
|
+
taskId: 'task-1',
|
|
166
|
+
isCreateMode: false,
|
|
167
|
+
boardId: 'board-1',
|
|
168
|
+
selectedLabels: [],
|
|
169
|
+
selectedAssignees: [],
|
|
170
|
+
selectedProjects: [],
|
|
171
|
+
newLabelName: '',
|
|
172
|
+
newLabelColor: '#14b8a6',
|
|
173
|
+
newProjectName: '',
|
|
174
|
+
setSelectedLabels: vi.fn(),
|
|
175
|
+
setSelectedAssignees: vi.fn(),
|
|
176
|
+
setSelectedProjects: vi.fn(),
|
|
177
|
+
setAvailableLabels: vi.fn(),
|
|
178
|
+
setNewLabelName: vi.fn(),
|
|
179
|
+
setNewLabelColor: vi.fn(),
|
|
180
|
+
setNewProjectName: vi.fn(),
|
|
181
|
+
setShowNewLabelDialog: vi.fn(),
|
|
182
|
+
setShowNewProjectDialog: vi.fn(),
|
|
183
|
+
onUpdate: vi.fn(),
|
|
184
|
+
}),
|
|
185
|
+
{ wrapper }
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
await act(async () => {
|
|
189
|
+
await result.current.toggleLabel(newLabel);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(
|
|
193
|
+
queryClient.getQueryData<Task[]>(['tasks', 'board-1'])?.[0]?.labels
|
|
194
|
+
).toEqual([newLabel]);
|
|
195
|
+
expect(
|
|
196
|
+
queryClient.getQueryData<Task[]>(['tasks-full', 'board-1', 'all'])?.[0]
|
|
197
|
+
?.labels
|
|
198
|
+
).toEqual([newLabel]);
|
|
199
|
+
expect(queryClient.getQueryData<Task>(['task', 'task-1'])?.labels).toEqual([
|
|
200
|
+
newLabel,
|
|
201
|
+
]);
|
|
202
|
+
expect(invalidateSpy).not.toHaveBeenCalledWith({
|
|
203
|
+
queryKey: ['tasks', 'board-1'],
|
|
204
|
+
});
|
|
205
|
+
expect(invalidateSpy).not.toHaveBeenCalledWith({
|
|
206
|
+
queryKey: ['tasks-full', 'board-1'],
|
|
207
|
+
});
|
|
208
|
+
expect(mocks.broadcast).toHaveBeenCalledWith('task:relations-changed', {
|
|
209
|
+
taskId: 'task-1',
|
|
210
|
+
});
|
|
211
|
+
});
|
|
149
212
|
});
|