@taskforcehq/taskforce 0.3.300 → 0.3.302
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/README.md +19 -0
- package/dist/Taskforce.module.css +26 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +13 -24
- package/dist/TaskforceCore.test.js +167 -152
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.js +9 -8
- package/dist/components/views/StandaloneLayout.js +490 -207
- package/dist/components/views/panels/FilterToolbar.d.ts +2 -0
- package/dist/components/views/panels/FilterToolbarControls.d.ts +1 -0
- package/dist/components/views/panels/FilterToolbarControls.js +1 -1
- package/dist/components/views/panels/PlanningDrawer.js +19 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +43 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +22 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +49 -0
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +241 -78
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +102 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useTaskData.js +16 -11
- package/dist/hooks/useTaskData.test.js +127 -0
- package/dist/hooks/useTaskforce.d.ts +5 -0
- package/dist/hooks/useTaskforce.js +63 -15
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +45 -23
- package/dist/mcp/runtime.test.js +56 -3
- package/dist/migrations/taskSchemaMigrations.js +53 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +15 -1
- package/dist/server/index.test.js +14 -1
- package/dist/server/routes/admin.js +25 -22
- package/dist/server/routes/auth.d.ts +5 -0
- package/dist/server/routes/auth.js +350 -16
- package/dist/server/routes/billing.js +11 -1
- package/dist/server/routes/billing.test.js +32 -2
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +11 -3
- package/dist/server/routes/sync.integration.test.js +130 -0
- package/dist/server/routes/sync.js +226 -1924
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +26 -19
- package/dist/server/routes.test.js +352 -6
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +88 -0
- package/dist/sync/contentSyncState.js +743 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +28 -0
- package/dist/sync/workspacePullFeed.js +301 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +166 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-TVmCgizS.js → AgentsModule-JG5jc3he.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CAyhnPeR.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-BOCw5e4D.js → DocumentWorkspace-iys3zfca.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-CN0r02Oi.js → InitiativesModule-DJvLJI3A.js} +1 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +1 -0
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-CY9R2zPB.js → WorkflowsModule-BguHeQaV.js} +1 -1
- package/dist/ui/assets/index-MXeWoebC.css +1 -0
- package/dist/ui/assets/index-YJ4k5QNa.js +6 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-C8EED3Oh.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/billingStatusCache.d.ts +21 -0
- package/dist/utils/billingStatusCache.js +70 -0
- package/dist/utils/taskActivity.js +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +4 -1
- package/scripts/deploy-prod.sh +41 -0
- package/scripts/deploy-staging.sh +23 -0
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-FcQSNLZd.js +0 -1
- package/dist/ui/assets/TaskSettings-Cw5zYD1L.js +0 -8
- package/dist/ui/assets/index-Bzqy5QGm.js +0 -6
- package/dist/ui/assets/index-DxRiI3tY.css +0 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { formatLocalTaskReference, parseLocalTaskReference, parseTaskReference, } from '../utils/taskReferences.js';
|
|
2
|
+
export function getTaskReferenceSequenceValue(deps, workspaceId) {
|
|
3
|
+
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
4
|
+
const row = deps.db.prepare(`
|
|
5
|
+
SELECT next_reference_number
|
|
6
|
+
FROM workspace_task_reference_sequences
|
|
7
|
+
WHERE tenant_id = ? AND workspace_id = ?
|
|
8
|
+
LIMIT 1
|
|
9
|
+
`).get(deps.tenantId, normalizedWorkspaceId);
|
|
10
|
+
return deps.sanitizeTaskReferenceNumber(row?.next_reference_number) || 0;
|
|
11
|
+
}
|
|
12
|
+
export function getLocalTaskReferenceSequenceValue(deps, workspaceId) {
|
|
13
|
+
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
14
|
+
const row = deps.db.prepare(`
|
|
15
|
+
SELECT next_reference_number
|
|
16
|
+
FROM workspace_local_task_reference_sequences
|
|
17
|
+
WHERE tenant_id = ? AND workspace_id = ?
|
|
18
|
+
LIMIT 1
|
|
19
|
+
`).get(deps.tenantId, normalizedWorkspaceId);
|
|
20
|
+
return deps.sanitizeTaskReferenceNumber(row?.next_reference_number) || 0;
|
|
21
|
+
}
|
|
22
|
+
export function getMaxTaskReferenceNumber(deps, workspaceId, excludeTaskId) {
|
|
23
|
+
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
24
|
+
const normalizedExcludeTaskId = String(excludeTaskId || '').trim();
|
|
25
|
+
const row = normalizedExcludeTaskId
|
|
26
|
+
? deps.db.prepare(`
|
|
27
|
+
SELECT MAX(reference_number) AS max_reference_number
|
|
28
|
+
FROM tasks
|
|
29
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id != ?
|
|
30
|
+
`).get(deps.tenantId, normalizedWorkspaceId, normalizedExcludeTaskId)
|
|
31
|
+
: deps.db.prepare(`
|
|
32
|
+
SELECT MAX(reference_number) AS max_reference_number
|
|
33
|
+
FROM tasks
|
|
34
|
+
WHERE tenant_id = ? AND workspace_id = ?
|
|
35
|
+
`).get(deps.tenantId, normalizedWorkspaceId);
|
|
36
|
+
return deps.sanitizeTaskReferenceNumber(row?.max_reference_number) || 0;
|
|
37
|
+
}
|
|
38
|
+
export function getMaxLocalTaskReferenceNumber(deps, workspaceId, excludeTaskId) {
|
|
39
|
+
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
40
|
+
const normalizedExcludeTaskId = String(excludeTaskId || '').trim();
|
|
41
|
+
const row = normalizedExcludeTaskId
|
|
42
|
+
? deps.db.prepare(`
|
|
43
|
+
SELECT MAX(local_reference_number) AS max_reference_number
|
|
44
|
+
FROM tasks
|
|
45
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id != ?
|
|
46
|
+
`).get(deps.tenantId, normalizedWorkspaceId, normalizedExcludeTaskId)
|
|
47
|
+
: deps.db.prepare(`
|
|
48
|
+
SELECT MAX(local_reference_number) AS max_reference_number
|
|
49
|
+
FROM tasks
|
|
50
|
+
WHERE tenant_id = ? AND workspace_id = ?
|
|
51
|
+
`).get(deps.tenantId, normalizedWorkspaceId);
|
|
52
|
+
return deps.sanitizeTaskReferenceNumber(row?.max_reference_number) || 0;
|
|
53
|
+
}
|
|
54
|
+
export function persistTaskReferenceSequence(deps, workspaceId, referenceNumber) {
|
|
55
|
+
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
56
|
+
const normalizedReferenceNumber = deps.sanitizeTaskReferenceNumber(referenceNumber);
|
|
57
|
+
if (!normalizedReferenceNumber)
|
|
58
|
+
return;
|
|
59
|
+
deps.db.prepare(`
|
|
60
|
+
INSERT INTO workspace_task_reference_sequences (tenant_id, workspace_id, next_reference_number)
|
|
61
|
+
VALUES (?, ?, ?)
|
|
62
|
+
ON CONFLICT (tenant_id, workspace_id) DO UPDATE SET
|
|
63
|
+
next_reference_number = CASE
|
|
64
|
+
WHEN workspace_task_reference_sequences.next_reference_number < EXCLUDED.next_reference_number
|
|
65
|
+
THEN EXCLUDED.next_reference_number
|
|
66
|
+
ELSE workspace_task_reference_sequences.next_reference_number
|
|
67
|
+
END
|
|
68
|
+
`).run(deps.tenantId, normalizedWorkspaceId, normalizedReferenceNumber);
|
|
69
|
+
}
|
|
70
|
+
export function persistLocalTaskReferenceSequence(deps, workspaceId, referenceNumber) {
|
|
71
|
+
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
72
|
+
const normalizedReferenceNumber = deps.sanitizeTaskReferenceNumber(referenceNumber);
|
|
73
|
+
if (!normalizedReferenceNumber)
|
|
74
|
+
return;
|
|
75
|
+
deps.db.prepare(`
|
|
76
|
+
INSERT INTO workspace_local_task_reference_sequences (tenant_id, workspace_id, next_reference_number)
|
|
77
|
+
VALUES (?, ?, ?)
|
|
78
|
+
ON CONFLICT (tenant_id, workspace_id) DO UPDATE SET
|
|
79
|
+
next_reference_number = CASE
|
|
80
|
+
WHEN workspace_local_task_reference_sequences.next_reference_number < EXCLUDED.next_reference_number
|
|
81
|
+
THEN EXCLUDED.next_reference_number
|
|
82
|
+
ELSE workspace_local_task_reference_sequences.next_reference_number
|
|
83
|
+
END
|
|
84
|
+
`).run(deps.tenantId, normalizedWorkspaceId, normalizedReferenceNumber);
|
|
85
|
+
}
|
|
86
|
+
export function allocateNextTaskReferenceNumber(deps, workspaceId, excludeTaskId) {
|
|
87
|
+
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
88
|
+
const nextReferenceNumber = Math.max(getTaskReferenceSequenceValue(deps, normalizedWorkspaceId), getMaxTaskReferenceNumber(deps, normalizedWorkspaceId, excludeTaskId)) + 1;
|
|
89
|
+
persistTaskReferenceSequence(deps, normalizedWorkspaceId, nextReferenceNumber);
|
|
90
|
+
return nextReferenceNumber;
|
|
91
|
+
}
|
|
92
|
+
export function allocateNextLocalTaskReferenceNumber(deps, workspaceId, excludeTaskId) {
|
|
93
|
+
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
94
|
+
const nextReferenceNumber = Math.max(getLocalTaskReferenceSequenceValue(deps, normalizedWorkspaceId), getMaxLocalTaskReferenceNumber(deps, normalizedWorkspaceId, excludeTaskId)) + 1;
|
|
95
|
+
persistLocalTaskReferenceSequence(deps, normalizedWorkspaceId, nextReferenceNumber);
|
|
96
|
+
return nextReferenceNumber;
|
|
97
|
+
}
|
|
98
|
+
export function relocateTaskReferenceConflict(deps, workspaceId, referenceNumber, claimantTaskId, updatedAt) {
|
|
99
|
+
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
100
|
+
const normalizedReferenceNumber = deps.sanitizeTaskReferenceNumber(referenceNumber);
|
|
101
|
+
if (!normalizedReferenceNumber)
|
|
102
|
+
return null;
|
|
103
|
+
const normalizedClaimantTaskId = String(claimantTaskId || '').trim();
|
|
104
|
+
const incumbent = deps.db.prepare(`
|
|
105
|
+
SELECT id
|
|
106
|
+
FROM tasks
|
|
107
|
+
WHERE tenant_id = ? AND workspace_id = ? AND reference_number = ? AND id != ?
|
|
108
|
+
LIMIT 1
|
|
109
|
+
`).get(deps.tenantId, normalizedWorkspaceId, normalizedReferenceNumber, normalizedClaimantTaskId);
|
|
110
|
+
const incumbentTaskId = String(incumbent?.id || '').trim();
|
|
111
|
+
if (!incumbentTaskId)
|
|
112
|
+
return null;
|
|
113
|
+
const nextReferenceNumber = allocateNextTaskReferenceNumber(deps, normalizedWorkspaceId, incumbentTaskId);
|
|
114
|
+
deps.db.prepare(`
|
|
115
|
+
UPDATE tasks
|
|
116
|
+
SET reference_number = ?, updated_at = ?
|
|
117
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
118
|
+
`).run(nextReferenceNumber, String(updatedAt || new Date().toISOString()), deps.tenantId, normalizedWorkspaceId, incumbentTaskId);
|
|
119
|
+
return { taskId: incumbentTaskId, referenceNumber: nextReferenceNumber };
|
|
120
|
+
}
|
|
121
|
+
export function reserveTaskReferenceNumber(deps, workspaceId, preferredReferenceNumber, currentTaskId) {
|
|
122
|
+
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
123
|
+
const preferred = deps.sanitizeTaskReferenceNumber(preferredReferenceNumber);
|
|
124
|
+
const normalizedCurrentTaskId = String(currentTaskId || '').trim();
|
|
125
|
+
if (preferred) {
|
|
126
|
+
const existing = deps.db.prepare(`
|
|
127
|
+
SELECT id
|
|
128
|
+
FROM tasks
|
|
129
|
+
WHERE tenant_id = ? AND workspace_id = ? AND reference_number = ?
|
|
130
|
+
LIMIT 1
|
|
131
|
+
`).get(deps.tenantId, normalizedWorkspaceId, preferred);
|
|
132
|
+
if (!existing?.id || existing.id === normalizedCurrentTaskId) {
|
|
133
|
+
persistTaskReferenceSequence(deps, normalizedWorkspaceId, preferred);
|
|
134
|
+
return preferred;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return allocateNextTaskReferenceNumber(deps, normalizedWorkspaceId, normalizedCurrentTaskId || undefined);
|
|
138
|
+
}
|
|
139
|
+
export function getTaskByReferenceNumber(deps, referenceNumber, workspaceId) {
|
|
140
|
+
const normalizedReferenceNumber = deps.sanitizeTaskReferenceNumber(referenceNumber);
|
|
141
|
+
if (!normalizedReferenceNumber)
|
|
142
|
+
return null;
|
|
143
|
+
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
144
|
+
const row = deps.db.prepare(`
|
|
145
|
+
SELECT * FROM tasks
|
|
146
|
+
WHERE tenant_id = ? AND workspace_id = ? AND reference_number = ?
|
|
147
|
+
LIMIT 1
|
|
148
|
+
`).get(deps.tenantId, normalizedWorkspaceId, normalizedReferenceNumber);
|
|
149
|
+
if (!row)
|
|
150
|
+
return null;
|
|
151
|
+
return deps.rowToTask(row);
|
|
152
|
+
}
|
|
153
|
+
export function getTaskByLocalReferenceNumber(deps, referenceNumber, workspaceId) {
|
|
154
|
+
const normalizedReferenceNumber = deps.sanitizeTaskReferenceNumber(referenceNumber);
|
|
155
|
+
if (!normalizedReferenceNumber)
|
|
156
|
+
return null;
|
|
157
|
+
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
158
|
+
const row = deps.db.prepare(`
|
|
159
|
+
SELECT * FROM tasks
|
|
160
|
+
WHERE tenant_id = ? AND workspace_id = ? AND local_reference_number = ?
|
|
161
|
+
LIMIT 1
|
|
162
|
+
`).get(deps.tenantId, normalizedWorkspaceId, normalizedReferenceNumber);
|
|
163
|
+
if (!row)
|
|
164
|
+
return null;
|
|
165
|
+
return deps.rowToTask(row);
|
|
166
|
+
}
|
|
167
|
+
export function resolveTaskIdentifier(deps, value, workspaceId) {
|
|
168
|
+
const normalizedValue = String(value || '').trim();
|
|
169
|
+
if (!normalizedValue)
|
|
170
|
+
return null;
|
|
171
|
+
const referenceNumber = parseTaskReference(normalizedValue);
|
|
172
|
+
if (referenceNumber) {
|
|
173
|
+
const exactReferenceMatch = getTaskByReferenceNumber(deps, referenceNumber, workspaceId);
|
|
174
|
+
if (exactReferenceMatch)
|
|
175
|
+
return exactReferenceMatch;
|
|
176
|
+
}
|
|
177
|
+
const localReferenceNumber = parseLocalTaskReference(normalizedValue);
|
|
178
|
+
if (localReferenceNumber) {
|
|
179
|
+
const exactLocalReferenceMatch = getTaskByLocalReferenceNumber(deps, localReferenceNumber, workspaceId);
|
|
180
|
+
if (exactLocalReferenceMatch)
|
|
181
|
+
return exactLocalReferenceMatch;
|
|
182
|
+
}
|
|
183
|
+
return deps.getTask(normalizedValue, workspaceId);
|
|
184
|
+
}
|
|
185
|
+
export function deriveTaskReferenceLabel(task) {
|
|
186
|
+
return task.referenceNumber
|
|
187
|
+
? `T-${task.referenceNumber}`
|
|
188
|
+
: formatLocalTaskReference(task.localReferenceNumber);
|
|
189
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type TaskAssignee = string;
|
|
|
27
27
|
export interface TaskItem {
|
|
28
28
|
id: string;
|
|
29
29
|
referenceNumber?: number | null;
|
|
30
|
+
localReferenceNumber?: number | null;
|
|
30
31
|
referenceLabel?: string;
|
|
31
32
|
title: string;
|
|
32
33
|
description: string | null;
|
|
@@ -330,6 +331,7 @@ export interface UserIdentityRecord {
|
|
|
330
331
|
userId: string;
|
|
331
332
|
email: string;
|
|
332
333
|
displayName?: string | null;
|
|
334
|
+
avatarUrl?: string | null;
|
|
333
335
|
systemRole?: SystemRole;
|
|
334
336
|
betaAccess?: boolean;
|
|
335
337
|
}
|
|
@@ -130,6 +130,9 @@ export function useTaskFormActions(input) {
|
|
|
130
130
|
if (!editingTaskId || attachmentsDirty) {
|
|
131
131
|
body.attachments = attachments;
|
|
132
132
|
}
|
|
133
|
+
if (editingTaskId) {
|
|
134
|
+
body.saveSource = source;
|
|
135
|
+
}
|
|
133
136
|
const res = await fetch(url, {
|
|
134
137
|
method,
|
|
135
138
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -45,6 +45,16 @@ export function useTaskData({ tasks, archivedTasks, setTasks, setArchivedTasks,
|
|
|
45
45
|
const getTaskRevisionKey = useCallback((task) => {
|
|
46
46
|
return `${task.updatedAt || task.createdAt || ''}|${task.status}|${task.priority}|${task.title}`;
|
|
47
47
|
}, []);
|
|
48
|
+
const isExpectedAbortFailure = useCallback((error, signal) => {
|
|
49
|
+
if (!signal.aborted)
|
|
50
|
+
return false;
|
|
51
|
+
if (error === signal.reason)
|
|
52
|
+
return true;
|
|
53
|
+
if (error instanceof DOMException) {
|
|
54
|
+
return error.name === 'AbortError';
|
|
55
|
+
}
|
|
56
|
+
return String(error?.name || '').toLowerCase() === 'aborterror';
|
|
57
|
+
}, []);
|
|
48
58
|
// === SECTION: Recently Changed ===
|
|
49
59
|
const [recentlyChangedTaskIds, setRecentlyChangedTaskIds] = useState([]);
|
|
50
60
|
const highlightTimersRef = useRef(new Map());
|
|
@@ -137,18 +147,16 @@ export function useTaskData({ tasks, archivedTasks, setTasks, setArchivedTasks,
|
|
|
137
147
|
}
|
|
138
148
|
}
|
|
139
149
|
catch (err) {
|
|
140
|
-
|
|
141
|
-
? err.name === 'AbortError'
|
|
142
|
-
: String(err?.name || '').toLowerCase() === 'aborterror';
|
|
143
|
-
if (isAbortError)
|
|
150
|
+
if (isExpectedAbortFailure(err, abortController.signal))
|
|
144
151
|
return;
|
|
145
152
|
console.error('[Taskforce] Failed to fetch tasks:', err);
|
|
146
153
|
}
|
|
147
154
|
finally {
|
|
148
|
-
|
|
155
|
+
const isCurrentRequest = tasksAbortRef.current === abortController;
|
|
156
|
+
if (isCurrentRequest) {
|
|
149
157
|
tasksAbortRef.current = null;
|
|
150
158
|
}
|
|
151
|
-
if (!isSilent)
|
|
159
|
+
if (!isSilent && isCurrentRequest)
|
|
152
160
|
setLoadingTasks(false);
|
|
153
161
|
}
|
|
154
162
|
}, [
|
|
@@ -186,10 +194,7 @@ export function useTaskData({ tasks, archivedTasks, setTasks, setArchivedTasks,
|
|
|
186
194
|
}
|
|
187
195
|
}
|
|
188
196
|
catch (err) {
|
|
189
|
-
|
|
190
|
-
? err.name === 'AbortError'
|
|
191
|
-
: String(err?.name || '').toLowerCase() === 'aborterror';
|
|
192
|
-
if (isAbortError)
|
|
197
|
+
if (isExpectedAbortFailure(err, abortController.signal))
|
|
193
198
|
return;
|
|
194
199
|
console.error('[Taskforce] Failed to fetch archive:', err);
|
|
195
200
|
}
|
|
@@ -198,7 +203,7 @@ export function useTaskData({ tasks, archivedTasks, setTasks, setArchivedTasks,
|
|
|
198
203
|
archiveAbortRef.current = null;
|
|
199
204
|
}
|
|
200
205
|
}
|
|
201
|
-
}, [getCurrentWorkspaceId, setArchivedTasks, shouldDeferProtectedApiCalls, shouldBlockProtectedApiCalls]);
|
|
206
|
+
}, [getCurrentWorkspaceId, isExpectedAbortFailure, setArchivedTasks, shouldDeferProtectedApiCalls, shouldBlockProtectedApiCalls]);
|
|
202
207
|
const refreshTaskCollections = useCallback(async (options) => {
|
|
203
208
|
const isSilent = options?.isSilent !== false;
|
|
204
209
|
const ignoreAuthGuard = options?.ignoreAuthGuard === true;
|
|
@@ -23,6 +23,46 @@ function makeAbortableRequest(payload) {
|
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
+
function makeAbortableRequestRejectingWithReason(payload) {
|
|
27
|
+
let rejectRequest = null;
|
|
28
|
+
let resolveRequest = null;
|
|
29
|
+
const promise = new Promise((resolve, reject) => {
|
|
30
|
+
resolveRequest = resolve;
|
|
31
|
+
rejectRequest = reject;
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
promise,
|
|
35
|
+
attach(signal) {
|
|
36
|
+
signal?.addEventListener('abort', () => {
|
|
37
|
+
rejectRequest?.(signal.reason);
|
|
38
|
+
}, { once: true });
|
|
39
|
+
queueMicrotask(() => {
|
|
40
|
+
if (!signal?.aborted) {
|
|
41
|
+
resolveRequest?.(makeAbortableFetchResponse(payload));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function makeControlledAbortableRequest(payload) {
|
|
48
|
+
let rejectRequest = null;
|
|
49
|
+
let resolveRequest = null;
|
|
50
|
+
const promise = new Promise((resolve, reject) => {
|
|
51
|
+
rejectRequest = reject;
|
|
52
|
+
resolveRequest = () => resolve(makeAbortableFetchResponse(payload));
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
promise,
|
|
56
|
+
resolve() {
|
|
57
|
+
resolveRequest?.();
|
|
58
|
+
},
|
|
59
|
+
attach(signal) {
|
|
60
|
+
signal?.addEventListener('abort', () => {
|
|
61
|
+
rejectRequest?.(new DOMException('Aborted', 'AbortError'));
|
|
62
|
+
}, { once: true });
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
26
66
|
describe('useTaskData', () => {
|
|
27
67
|
beforeEach(() => {
|
|
28
68
|
vi.stubGlobal('fetch', vi.fn());
|
|
@@ -116,4 +156,91 @@ describe('useTaskData', () => {
|
|
|
116
156
|
expect(requestSignals[0]?.aborted).toBe(true);
|
|
117
157
|
expect(requestSignals[1]?.aborted).toBe(false);
|
|
118
158
|
});
|
|
159
|
+
it('does not log an error when an archive request is aborted with a string reason', async () => {
|
|
160
|
+
const firstRequest = makeAbortableRequestRejectingWithReason({ archived: [] });
|
|
161
|
+
const secondRequest = makeAbortableRequestRejectingWithReason({ archived: [] });
|
|
162
|
+
const fetchMock = vi.mocked(fetch);
|
|
163
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
164
|
+
let requestCount = 0;
|
|
165
|
+
fetchMock.mockImplementation((_input, init) => {
|
|
166
|
+
const signal = init?.signal;
|
|
167
|
+
requestCount += 1;
|
|
168
|
+
if (requestCount === 1) {
|
|
169
|
+
firstRequest.attach(signal);
|
|
170
|
+
return firstRequest.promise;
|
|
171
|
+
}
|
|
172
|
+
secondRequest.attach(signal);
|
|
173
|
+
return secondRequest.promise;
|
|
174
|
+
});
|
|
175
|
+
const { result } = renderHook(() => useTaskData({
|
|
176
|
+
tasks: [],
|
|
177
|
+
archivedTasks: [],
|
|
178
|
+
setTasks: vi.fn(),
|
|
179
|
+
setArchivedTasks: vi.fn(),
|
|
180
|
+
setLoadingTasks: vi.fn(),
|
|
181
|
+
getCurrentWorkspaceId: () => 'workspace-1',
|
|
182
|
+
workspaceResetKey: 'workspace-1',
|
|
183
|
+
shouldDeferProtectedApiCalls: false,
|
|
184
|
+
shouldBlockProtectedApiCalls: false,
|
|
185
|
+
handleUnauthorized: vi.fn(),
|
|
186
|
+
authRequiredForApi: false
|
|
187
|
+
}));
|
|
188
|
+
let firstPromise;
|
|
189
|
+
let secondPromise;
|
|
190
|
+
await act(async () => {
|
|
191
|
+
firstPromise = result.current.fetchArchive(true);
|
|
192
|
+
secondPromise = result.current.fetchArchive(true);
|
|
193
|
+
await secondPromise;
|
|
194
|
+
await firstPromise;
|
|
195
|
+
});
|
|
196
|
+
expect(consoleErrorSpy).not.toHaveBeenCalledWith('[Taskforce] Failed to fetch archive:', 'superseded');
|
|
197
|
+
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
198
|
+
});
|
|
199
|
+
it('keeps loading active until the replacement tasks request finishes', async () => {
|
|
200
|
+
const firstRequest = makeControlledAbortableRequest({ tasks: [] });
|
|
201
|
+
const secondRequest = makeControlledAbortableRequest({ tasks: [] });
|
|
202
|
+
const fetchMock = vi.mocked(fetch);
|
|
203
|
+
const setLoadingTasks = vi.fn();
|
|
204
|
+
let requestCount = 0;
|
|
205
|
+
fetchMock.mockImplementation((_input, init) => {
|
|
206
|
+
const signal = init?.signal;
|
|
207
|
+
requestCount += 1;
|
|
208
|
+
if (requestCount === 1) {
|
|
209
|
+
firstRequest.attach(signal);
|
|
210
|
+
return firstRequest.promise;
|
|
211
|
+
}
|
|
212
|
+
secondRequest.attach(signal);
|
|
213
|
+
return secondRequest.promise;
|
|
214
|
+
});
|
|
215
|
+
const { result } = renderHook(() => useTaskData({
|
|
216
|
+
tasks: [],
|
|
217
|
+
archivedTasks: [],
|
|
218
|
+
setTasks: vi.fn(),
|
|
219
|
+
setArchivedTasks: vi.fn(),
|
|
220
|
+
setLoadingTasks,
|
|
221
|
+
getCurrentWorkspaceId: () => 'workspace-1',
|
|
222
|
+
workspaceResetKey: 'workspace-1',
|
|
223
|
+
shouldDeferProtectedApiCalls: false,
|
|
224
|
+
shouldBlockProtectedApiCalls: false,
|
|
225
|
+
handleUnauthorized: vi.fn(),
|
|
226
|
+
authRequiredForApi: false
|
|
227
|
+
}));
|
|
228
|
+
let firstPromise;
|
|
229
|
+
let secondPromise;
|
|
230
|
+
await act(async () => {
|
|
231
|
+
firstPromise = result.current.fetchTasks(false);
|
|
232
|
+
secondPromise = result.current.fetchTasks(false);
|
|
233
|
+
await Promise.resolve();
|
|
234
|
+
});
|
|
235
|
+
expect(setLoadingTasks).toHaveBeenNthCalledWith(1, true);
|
|
236
|
+
expect(setLoadingTasks).toHaveBeenNthCalledWith(2, true);
|
|
237
|
+
expect(setLoadingTasks).toHaveBeenCalledTimes(2);
|
|
238
|
+
await act(async () => {
|
|
239
|
+
secondRequest.resolve();
|
|
240
|
+
await secondPromise;
|
|
241
|
+
await firstPromise;
|
|
242
|
+
});
|
|
243
|
+
expect(setLoadingTasks).toHaveBeenLastCalledWith(false);
|
|
244
|
+
expect(setLoadingTasks).toHaveBeenCalledTimes(3);
|
|
245
|
+
});
|
|
119
246
|
});
|
|
@@ -117,8 +117,10 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
|
|
|
117
117
|
authRequiredForApi: boolean;
|
|
118
118
|
authBlocked: boolean;
|
|
119
119
|
isAuthenticated: boolean;
|
|
120
|
+
authUserId: string;
|
|
120
121
|
authUserEmail: string;
|
|
121
122
|
authUserDisplayName: string;
|
|
123
|
+
authUserAvatarUrl: string;
|
|
122
124
|
authWorkspaceId: string;
|
|
123
125
|
userGlobalSyncStatus: "error" | "idle" | "syncing" | "disconnected";
|
|
124
126
|
workspaceLastPullAt: string | null;
|
|
@@ -250,6 +252,8 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
|
|
|
250
252
|
}>;
|
|
251
253
|
updateCurrentUserProfile: (input: {
|
|
252
254
|
displayName: string;
|
|
255
|
+
avatarDraftId?: string | null;
|
|
256
|
+
clearAvatar?: boolean;
|
|
253
257
|
}) => Promise<{
|
|
254
258
|
success: boolean;
|
|
255
259
|
error?: string;
|
|
@@ -258,6 +262,7 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
|
|
|
258
262
|
userId: string;
|
|
259
263
|
email: string;
|
|
260
264
|
displayName: string | null;
|
|
265
|
+
avatarUrl: string | null;
|
|
261
266
|
};
|
|
262
267
|
}>;
|
|
263
268
|
requestEmailVerification: (email: string) => Promise<{
|
|
@@ -45,7 +45,7 @@ import { cloneDefaultWorkflowDefinitions } from '../shared/workflowDefaults.js';
|
|
|
45
45
|
import { resolveClientDeploymentConfig } from '../config/clientDeployment';
|
|
46
46
|
import { resolveTaskforceMcpBaseUrlFromBaseUrl } from '../config/cloudEnvironment';
|
|
47
47
|
import { createDefaultAssigneeOptions, createWorkspaceAssigneeOptions, mergeAssigneeOptions } from '../utils/assignees';
|
|
48
|
-
import { persistLocalMutationActorUserId } from '../utils/taskforceApiClient';
|
|
48
|
+
import { persistLocalMutationActorUserId, readLocalMutationActorUserId } from '../utils/taskforceApiClient';
|
|
49
49
|
import { useSyncOrchestrator } from './useSyncOrchestrator';
|
|
50
50
|
import { useRealtimeSync } from './useRealtimeSync';
|
|
51
51
|
import { useUiNotice } from './ui/useUiNotice';
|
|
@@ -129,8 +129,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
129
129
|
const workspaceMode = workspaceModeRaw === 'single-local' || workspaceModeRaw === 'multi-cloud'
|
|
130
130
|
? workspaceModeRaw
|
|
131
131
|
: (runtimeModeHint === 'cloud' ? 'multi-cloud' : 'single-local');
|
|
132
|
-
const authSourceRaw = String(cfg.authSource || '').trim().toLowerCase();
|
|
133
|
-
const authSource = authSourceRaw === 'local' ? 'local' : 'cloud';
|
|
134
132
|
setRuntimeConfigOverride({
|
|
135
133
|
cloudEnvironment: typeof cfg.cloudEnvironment === 'string'
|
|
136
134
|
? String(cfg.cloudEnvironment).trim().toLowerCase()
|
|
@@ -142,7 +140,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
142
140
|
cloudAuthBaseUrl: normalizeBaseUrl(cfg.cloudAuthBaseUrl),
|
|
143
141
|
wsBaseUrl: normalizeBaseUrl(cfg.wsBaseUrl),
|
|
144
142
|
cloudAuthViaLocalProxy: Boolean(cfg.cloudAuthViaLocalProxy),
|
|
145
|
-
authSource,
|
|
143
|
+
authSource: 'cloud',
|
|
146
144
|
workspaceMode,
|
|
147
145
|
workspaceSwitchingEnabled: typeof cfg.workspaceSwitchingEnabled === 'boolean'
|
|
148
146
|
? Boolean(cfg.workspaceSwitchingEnabled)
|
|
@@ -182,8 +180,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
182
180
|
|| '';
|
|
183
181
|
const explicitConfiguredCloudAuthBaseUrl = propCloudAuthBaseUrl || envCloudAuthBaseUrl || envBaseUrl;
|
|
184
182
|
const runtimeResolvedCloudAuthBaseUrl = runtimeConfigOverride.cloudAuthBaseUrl || runtimeConfigOverride.baseUrl;
|
|
185
|
-
const
|
|
186
|
-
||
|
|
183
|
+
const preferredCloudAuthBaseUrl = isLoopbackHost
|
|
184
|
+
? (runtimeResolvedCloudAuthBaseUrl || explicitConfiguredCloudAuthBaseUrl)
|
|
185
|
+
: (explicitConfiguredCloudAuthBaseUrl || runtimeResolvedCloudAuthBaseUrl);
|
|
186
|
+
const normalizedCloudAuthBaseUrl = (preferredCloudAuthBaseUrl
|
|
187
187
|
|| normalizedApiBaseUrl
|
|
188
188
|
|| defaultCloudAuthBaseUrl).trim().replace(/\/+$/, '');
|
|
189
189
|
const inferredCloudMcpBaseUrl = normalizeBaseUrl(resolveTaskforceMcpBaseUrlFromBaseUrl(runtimeConfigOverride.cloudMcpBaseUrl
|
|
@@ -208,6 +208,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
208
208
|
const shouldGateProtectedApiCalls = shouldProbeCloudAuth && !authOnlyCloudMode;
|
|
209
209
|
const cloudAuthConfigured = Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl || isCloudHost);
|
|
210
210
|
const initialRuntimeMode = isCloudHost ? 'cloud' : 'local';
|
|
211
|
+
const optimisticLocalAuthUserId = initialRuntimeMode === 'local' && isLoopbackHost && cloudAuthConfigured
|
|
212
|
+
? readLocalMutationActorUserId()
|
|
213
|
+
: '';
|
|
214
|
+
const hasOptimisticLocalAuth = optimisticLocalAuthUserId.length > 0;
|
|
211
215
|
const resolveApiUrl = useCallback((path) => {
|
|
212
216
|
if (!path)
|
|
213
217
|
return path;
|
|
@@ -310,11 +314,12 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
310
314
|
: runtimeContractFallback.workspaceSwitchingEnabled;
|
|
311
315
|
const [authRequiredForApi, setAuthRequiredForApi] = useState(false);
|
|
312
316
|
const [authBlocked, setAuthBlocked] = useState(false);
|
|
313
|
-
const [isAuthenticated, setIsAuthenticated] = useState(
|
|
314
|
-
const [authUserId, setAuthUserId] = useState('anonymous');
|
|
317
|
+
const [isAuthenticated, setIsAuthenticated] = useState(hasOptimisticLocalAuth);
|
|
318
|
+
const [authUserId, setAuthUserId] = useState(hasOptimisticLocalAuth ? optimisticLocalAuthUserId : 'anonymous');
|
|
315
319
|
const [authWorkspaceId, setAuthWorkspaceId] = useState('');
|
|
316
320
|
const [authUserEmail, setAuthUserEmail] = useState('');
|
|
317
321
|
const [authUserDisplayName, setAuthUserDisplayName] = useState('');
|
|
322
|
+
const [authUserAvatarUrl, setAuthUserAvatarUrl] = useState('');
|
|
318
323
|
const [hasBetaAccess, setHasBetaAccess] = useState(true);
|
|
319
324
|
const [hydratedWorkspaceRole, setHydratedWorkspaceRole] = useState(null);
|
|
320
325
|
const [currentWorkspaceId, setCurrentWorkspaceId] = useState(() => {
|
|
@@ -333,13 +338,13 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
333
338
|
const [availableWorkspaces, setAvailableWorkspaces] = useState([]);
|
|
334
339
|
const [assigneeOptions, setAssigneeOptions] = useState(() => createDefaultAssigneeOptions());
|
|
335
340
|
const [workspaceBootstrapPending, setWorkspaceBootstrapPending] = useState(false);
|
|
336
|
-
const [authSessionResolved, setAuthSessionResolved] = useState(
|
|
341
|
+
const [authSessionResolved, setAuthSessionResolved] = useState(hasOptimisticLocalAuth);
|
|
337
342
|
const [bootstrapPhase, setBootstrapPhase] = useState('idle');
|
|
338
343
|
const [bootstrapError, setBootstrapError] = useState(null);
|
|
339
344
|
const [bootstrapStartedAt, setBootstrapStartedAt] = useState(null);
|
|
340
345
|
const authSessionRequestRef = useRef(null);
|
|
341
346
|
const authSessionLastCheckedAtRef = useRef(0);
|
|
342
|
-
const authSessionLastResultRef = useRef(
|
|
347
|
+
const authSessionLastResultRef = useRef(hasOptimisticLocalAuth);
|
|
343
348
|
const currentWorkspaceIdRef = useRef(currentWorkspaceId);
|
|
344
349
|
const explicitWorkspaceSelectionRef = useRef(null);
|
|
345
350
|
const workspaceTransitionEpochRef = useRef(0);
|
|
@@ -622,6 +627,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
622
627
|
setIsAuthenticated(false);
|
|
623
628
|
setAuthUserEmail('');
|
|
624
629
|
setAuthUserDisplayName('');
|
|
630
|
+
setAuthUserAvatarUrl('');
|
|
625
631
|
setError(AUTH_REQUIRED_ERROR);
|
|
626
632
|
}, [AUTH_REQUIRED_ERROR, runtimeMode]);
|
|
627
633
|
// === SECTION: Task Data ===
|
|
@@ -822,6 +828,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
822
828
|
persistLocalMutationActorUserId(null);
|
|
823
829
|
setAuthUserEmail('');
|
|
824
830
|
setAuthUserDisplayName('');
|
|
831
|
+
setAuthUserAvatarUrl('');
|
|
825
832
|
setUserGlobalSyncStatus('disconnected');
|
|
826
833
|
setUserGlobalSyncError(null);
|
|
827
834
|
setHasBetaAccess(true);
|
|
@@ -881,6 +888,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
881
888
|
const userDisplayName = typeof data.displayName === 'string'
|
|
882
889
|
? data.displayName.trim()
|
|
883
890
|
: '';
|
|
891
|
+
const userAvatarUrl = typeof data.avatarUrl === 'string'
|
|
892
|
+
? data.avatarUrl.trim()
|
|
893
|
+
: '';
|
|
884
894
|
const isLocalRuntime = runtimeMode === 'local';
|
|
885
895
|
const keepLocalWorkspaceContext = runtimeMode === 'local' || authOnlyCloudMode;
|
|
886
896
|
setAuthRequiredForApi(authOnlyCloudMode ? false : (isLocalRuntime ? false : required));
|
|
@@ -892,6 +902,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
892
902
|
}
|
|
893
903
|
setAuthUserEmail(authenticated ? userEmail : '');
|
|
894
904
|
setAuthUserDisplayName(authenticated ? userDisplayName : '');
|
|
905
|
+
setAuthUserAvatarUrl(authenticated ? userAvatarUrl : '');
|
|
895
906
|
if (authenticated) {
|
|
896
907
|
setError((prev) => prev === AUTH_REQUIRED_ERROR ? '' : prev);
|
|
897
908
|
}
|
|
@@ -1769,7 +1780,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1769
1780
|
if (currentMemberRole === 'owner' || currentMemberRole === 'admin' || currentMemberRole === 'member' || currentMemberRole === 'read-only') {
|
|
1770
1781
|
setHydratedWorkspaceRole(currentMemberRole);
|
|
1771
1782
|
}
|
|
1772
|
-
else if (!workspaceSwitchingEnabled) {
|
|
1783
|
+
else if (!workspaceSwitchingEnabled && users.length === 0) {
|
|
1773
1784
|
setHydratedWorkspaceRole(null);
|
|
1774
1785
|
}
|
|
1775
1786
|
const workspaceMemberCandidates = users
|
|
@@ -1793,7 +1804,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1793
1804
|
candidate
|
|
1794
1805
|
])).values()).filter((candidate) => String(candidate.userId || '').trim().length > 0);
|
|
1795
1806
|
}
|
|
1796
|
-
else if (!workspaceSwitchingEnabled) {
|
|
1807
|
+
else if (!workspaceSwitchingEnabled && !fallbackCandidates.length) {
|
|
1797
1808
|
setHydratedWorkspaceRole(null);
|
|
1798
1809
|
}
|
|
1799
1810
|
const mergedOptions = mergeAssigneeOptions([
|
|
@@ -2233,6 +2244,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2233
2244
|
return { success: false, error: 'Cloud auth endpoint is not configured.' };
|
|
2234
2245
|
}
|
|
2235
2246
|
const trimmedDisplayName = String(input.displayName || '').trim();
|
|
2247
|
+
const trimmedAvatarDraftId = typeof input.avatarDraftId === 'string' ? input.avatarDraftId.trim() : '';
|
|
2248
|
+
const shouldClearAvatar = input.clearAvatar === true;
|
|
2236
2249
|
if (!trimmedDisplayName) {
|
|
2237
2250
|
return { success: false, error: 'Display name is required.' };
|
|
2238
2251
|
}
|
|
@@ -2241,7 +2254,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2241
2254
|
method: 'POST',
|
|
2242
2255
|
headers: { 'Content-Type': 'application/json' },
|
|
2243
2256
|
credentials: 'include',
|
|
2244
|
-
body: JSON.stringify({
|
|
2257
|
+
body: JSON.stringify({
|
|
2258
|
+
displayName: trimmedDisplayName,
|
|
2259
|
+
...(trimmedAvatarDraftId ? { avatarDraftId: trimmedAvatarDraftId } : {}),
|
|
2260
|
+
...(shouldClearAvatar ? { clearAvatar: true } : {})
|
|
2261
|
+
})
|
|
2245
2262
|
});
|
|
2246
2263
|
const data = await res.json().catch(() => ({}));
|
|
2247
2264
|
if (!res.ok || !data?.success) {
|
|
@@ -2250,9 +2267,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2250
2267
|
const profile = {
|
|
2251
2268
|
userId: typeof data?.profile?.userId === 'string' ? data.profile.userId : authUserId,
|
|
2252
2269
|
email: typeof data?.profile?.email === 'string' ? data.profile.email : authUserEmail,
|
|
2253
|
-
displayName: typeof data?.profile?.displayName === 'string' ? data.profile.displayName : null
|
|
2270
|
+
displayName: typeof data?.profile?.displayName === 'string' ? data.profile.displayName : null,
|
|
2271
|
+
avatarUrl: typeof data?.profile?.avatarUrl === 'string' ? data.profile.avatarUrl : null
|
|
2254
2272
|
};
|
|
2255
2273
|
setAuthUserDisplayName(profile.displayName || '');
|
|
2274
|
+
setAuthUserAvatarUrl(profile.avatarUrl || '');
|
|
2256
2275
|
return { success: true, profile };
|
|
2257
2276
|
}
|
|
2258
2277
|
catch {
|
|
@@ -2505,13 +2524,20 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2505
2524
|
// no-op
|
|
2506
2525
|
}
|
|
2507
2526
|
finally {
|
|
2527
|
+
authSessionRequestRef.current = null;
|
|
2528
|
+
authSessionLastResultRef.current = false;
|
|
2529
|
+
authSessionLastCheckedAtRef.current = Date.now();
|
|
2508
2530
|
setIsAuthenticated(false);
|
|
2531
|
+
setAuthSessionResolved(true);
|
|
2509
2532
|
setAuthUserId('anonymous');
|
|
2533
|
+
setAuthWorkspaceId('');
|
|
2510
2534
|
persistLocalMutationActorUserId(null);
|
|
2511
2535
|
setAuthUserEmail('');
|
|
2512
2536
|
setAuthUserDisplayName('');
|
|
2537
|
+
setAuthUserAvatarUrl('');
|
|
2513
2538
|
setUserGlobalSyncStatus('disconnected');
|
|
2514
2539
|
setUserGlobalSyncError(null);
|
|
2540
|
+
setHasBetaAccess(true);
|
|
2515
2541
|
setAuthBlocked(runtimeMode === 'cloud' && authRequiredForApi);
|
|
2516
2542
|
setCurrentWorkspaceId((prev) => {
|
|
2517
2543
|
const existing = String(prev || '').trim();
|
|
@@ -2527,11 +2553,31 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2527
2553
|
});
|
|
2528
2554
|
setAvailableWorkspaces([]);
|
|
2529
2555
|
workspaceListHydratedRef.current = false;
|
|
2556
|
+
setHydratedWorkspaceRole(null);
|
|
2557
|
+
if (runtimeMode === 'local') {
|
|
2558
|
+
await Promise.allSettled([
|
|
2559
|
+
fetchConfig({ ignoreAuthGuard: true }),
|
|
2560
|
+
fetchTasks(true, { ignoreAuthGuard: true }),
|
|
2561
|
+
fetchPlanningEntities({ ignoreAuthGuard: true }),
|
|
2562
|
+
fetchReferenceData({ ignoreAuthGuard: true, force: true })
|
|
2563
|
+
]);
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2530
2566
|
setTasks([]);
|
|
2531
2567
|
setInitiatives([]);
|
|
2532
2568
|
setWorkstreams([]);
|
|
2533
2569
|
}
|
|
2534
|
-
}, [
|
|
2570
|
+
}, [
|
|
2571
|
+
authRequiredForApi,
|
|
2572
|
+
resolveCloudAuthUrl,
|
|
2573
|
+
cloudAuthConfigured,
|
|
2574
|
+
workspaceSelectionScope,
|
|
2575
|
+
runtimeMode,
|
|
2576
|
+
fetchConfig,
|
|
2577
|
+
fetchPlanningEntities,
|
|
2578
|
+
fetchReferenceData,
|
|
2579
|
+
fetchTasks
|
|
2580
|
+
]);
|
|
2535
2581
|
const createInitiativeFromTemplate = useCallback(async (payload) => {
|
|
2536
2582
|
try {
|
|
2537
2583
|
const res = await fetch('/api/taskforce/initiative-templates/create', {
|
|
@@ -3456,8 +3502,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3456
3502
|
authRequiredForApi,
|
|
3457
3503
|
authBlocked,
|
|
3458
3504
|
isAuthenticated,
|
|
3505
|
+
authUserId,
|
|
3459
3506
|
authUserEmail,
|
|
3460
3507
|
authUserDisplayName,
|
|
3508
|
+
authUserAvatarUrl,
|
|
3461
3509
|
authWorkspaceId,
|
|
3462
3510
|
userGlobalSyncStatus,
|
|
3463
3511
|
workspaceLastPullAt,
|