@taskforcehq/taskforce 0.3.301 → 0.3.303
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/dist/Taskforce.module.css +37 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +72 -33
- package/dist/TaskforceCore.test.js +848 -130
- 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/task/TaskKanban.test.js +17 -0
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.d.ts +3 -1
- package/dist/components/views/PlansPage.js +109 -49
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +100 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +473 -256
- package/dist/components/views/panels/PlanningDrawer.js +18 -1
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -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/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +29 -5
- package/dist/core/PlanEntitlementService.js +297 -219
- package/dist/core/SignupMonetizationSettings.test.js +173 -49
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +100 -10
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +459 -127
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +13 -12
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useSyncOrchestrator.js +17 -3
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
- package/dist/hooks/useTaskData.js +3 -2
- package/dist/hooks/useTaskData.test.js +66 -0
- package/dist/hooks/useTaskforce.d.ts +8 -0
- package/dist/hooks/useTaskforce.js +72 -19
- 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 +131 -48
- package/dist/mcp/runtime.test.js +321 -83
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +68 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +2 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +17 -1
- package/dist/server/index.test.js +15 -1
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +105 -45
- package/dist/server/routes/auth.d.ts +6 -0
- package/dist/server/routes/auth.js +505 -30
- package/dist/server/routes/billing.js +517 -24
- package/dist/server/routes/billing.test.js +967 -45
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +16 -6
- package/dist/server/routes/sync.integration.test.js +290 -0
- package/dist/server/routes/sync.js +234 -1922
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +29 -22
- package/dist/server/routes.test.js +639 -82
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- 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 +98 -0
- package/dist/sync/contentSyncState.js +1004 -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/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +21 -8
- 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 +38 -0
- package/dist/sync/workspacePullFeed.js +349 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +278 -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-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
- package/dist/ui/assets/index-CLIMgR6g.js +6 -0
- package/dist/ui/assets/index-DneETxcu.css +1 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -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 -3
- 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-B6BDECSi.js +0 -1
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
- package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
- package/dist/ui/assets/index-xBWLPOEb.js +0 -6
|
@@ -44,6 +44,25 @@ function makeAbortableRequestRejectingWithReason(payload) {
|
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
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
|
+
}
|
|
47
66
|
describe('useTaskData', () => {
|
|
48
67
|
beforeEach(() => {
|
|
49
68
|
vi.stubGlobal('fetch', vi.fn());
|
|
@@ -177,4 +196,51 @@ describe('useTaskData', () => {
|
|
|
177
196
|
expect(consoleErrorSpy).not.toHaveBeenCalledWith('[Taskforce] Failed to fetch archive:', 'superseded');
|
|
178
197
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
179
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
|
+
});
|
|
180
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;
|
|
@@ -247,9 +249,14 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
|
|
|
247
249
|
emailSent?: boolean;
|
|
248
250
|
emailError?: string;
|
|
249
251
|
workspaceSetupRequired?: boolean;
|
|
252
|
+
planSelectionRequired?: boolean;
|
|
253
|
+
checkoutPending?: boolean;
|
|
254
|
+
commercialState?: string | null;
|
|
250
255
|
}>;
|
|
251
256
|
updateCurrentUserProfile: (input: {
|
|
252
257
|
displayName: string;
|
|
258
|
+
avatarDraftId?: string | null;
|
|
259
|
+
clearAvatar?: boolean;
|
|
253
260
|
}) => Promise<{
|
|
254
261
|
success: boolean;
|
|
255
262
|
error?: string;
|
|
@@ -258,6 +265,7 @@ export declare function useTaskforce({ config, initialTaskId, onTaskCountChange,
|
|
|
258
265
|
userId: string;
|
|
259
266
|
email: string;
|
|
260
267
|
displayName: string | null;
|
|
268
|
+
avatarUrl: string | null;
|
|
261
269
|
};
|
|
262
270
|
}>;
|
|
263
271
|
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;
|
|
@@ -225,6 +229,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
225
229
|
if (!path.startsWith('/'))
|
|
226
230
|
return path;
|
|
227
231
|
const isCloudScopedPath = path.startsWith('/api/taskforce/auth/')
|
|
232
|
+
|| path.startsWith('/api/taskforce/account/')
|
|
233
|
+
|| path.startsWith('/api/taskforce/billing/')
|
|
228
234
|
|| path.startsWith('/api/taskforce/sync/')
|
|
229
235
|
|| path.startsWith('/api/taskforce/settings/mcp/');
|
|
230
236
|
if (!isCloudHost && isCloudScopedPath && (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy)) {
|
|
@@ -310,11 +316,12 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
310
316
|
: runtimeContractFallback.workspaceSwitchingEnabled;
|
|
311
317
|
const [authRequiredForApi, setAuthRequiredForApi] = useState(false);
|
|
312
318
|
const [authBlocked, setAuthBlocked] = useState(false);
|
|
313
|
-
const [isAuthenticated, setIsAuthenticated] = useState(
|
|
314
|
-
const [authUserId, setAuthUserId] = useState('anonymous');
|
|
319
|
+
const [isAuthenticated, setIsAuthenticated] = useState(hasOptimisticLocalAuth);
|
|
320
|
+
const [authUserId, setAuthUserId] = useState(hasOptimisticLocalAuth ? optimisticLocalAuthUserId : 'anonymous');
|
|
315
321
|
const [authWorkspaceId, setAuthWorkspaceId] = useState('');
|
|
316
322
|
const [authUserEmail, setAuthUserEmail] = useState('');
|
|
317
323
|
const [authUserDisplayName, setAuthUserDisplayName] = useState('');
|
|
324
|
+
const [authUserAvatarUrl, setAuthUserAvatarUrl] = useState('');
|
|
318
325
|
const [hasBetaAccess, setHasBetaAccess] = useState(true);
|
|
319
326
|
const [hydratedWorkspaceRole, setHydratedWorkspaceRole] = useState(null);
|
|
320
327
|
const [currentWorkspaceId, setCurrentWorkspaceId] = useState(() => {
|
|
@@ -333,13 +340,13 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
333
340
|
const [availableWorkspaces, setAvailableWorkspaces] = useState([]);
|
|
334
341
|
const [assigneeOptions, setAssigneeOptions] = useState(() => createDefaultAssigneeOptions());
|
|
335
342
|
const [workspaceBootstrapPending, setWorkspaceBootstrapPending] = useState(false);
|
|
336
|
-
const [authSessionResolved, setAuthSessionResolved] = useState(
|
|
343
|
+
const [authSessionResolved, setAuthSessionResolved] = useState(hasOptimisticLocalAuth);
|
|
337
344
|
const [bootstrapPhase, setBootstrapPhase] = useState('idle');
|
|
338
345
|
const [bootstrapError, setBootstrapError] = useState(null);
|
|
339
346
|
const [bootstrapStartedAt, setBootstrapStartedAt] = useState(null);
|
|
340
347
|
const authSessionRequestRef = useRef(null);
|
|
341
348
|
const authSessionLastCheckedAtRef = useRef(0);
|
|
342
|
-
const authSessionLastResultRef = useRef(
|
|
349
|
+
const authSessionLastResultRef = useRef(hasOptimisticLocalAuth);
|
|
343
350
|
const currentWorkspaceIdRef = useRef(currentWorkspaceId);
|
|
344
351
|
const explicitWorkspaceSelectionRef = useRef(null);
|
|
345
352
|
const workspaceTransitionEpochRef = useRef(0);
|
|
@@ -622,6 +629,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
622
629
|
setIsAuthenticated(false);
|
|
623
630
|
setAuthUserEmail('');
|
|
624
631
|
setAuthUserDisplayName('');
|
|
632
|
+
setAuthUserAvatarUrl('');
|
|
625
633
|
setError(AUTH_REQUIRED_ERROR);
|
|
626
634
|
}, [AUTH_REQUIRED_ERROR, runtimeMode]);
|
|
627
635
|
// === SECTION: Task Data ===
|
|
@@ -822,6 +830,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
822
830
|
persistLocalMutationActorUserId(null);
|
|
823
831
|
setAuthUserEmail('');
|
|
824
832
|
setAuthUserDisplayName('');
|
|
833
|
+
setAuthUserAvatarUrl('');
|
|
825
834
|
setUserGlobalSyncStatus('disconnected');
|
|
826
835
|
setUserGlobalSyncError(null);
|
|
827
836
|
setHasBetaAccess(true);
|
|
@@ -881,6 +890,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
881
890
|
const userDisplayName = typeof data.displayName === 'string'
|
|
882
891
|
? data.displayName.trim()
|
|
883
892
|
: '';
|
|
893
|
+
const userAvatarUrl = typeof data.avatarUrl === 'string'
|
|
894
|
+
? data.avatarUrl.trim()
|
|
895
|
+
: '';
|
|
884
896
|
const isLocalRuntime = runtimeMode === 'local';
|
|
885
897
|
const keepLocalWorkspaceContext = runtimeMode === 'local' || authOnlyCloudMode;
|
|
886
898
|
setAuthRequiredForApi(authOnlyCloudMode ? false : (isLocalRuntime ? false : required));
|
|
@@ -892,6 +904,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
892
904
|
}
|
|
893
905
|
setAuthUserEmail(authenticated ? userEmail : '');
|
|
894
906
|
setAuthUserDisplayName(authenticated ? userDisplayName : '');
|
|
907
|
+
setAuthUserAvatarUrl(authenticated ? userAvatarUrl : '');
|
|
895
908
|
if (authenticated) {
|
|
896
909
|
setError((prev) => prev === AUTH_REQUIRED_ERROR ? '' : prev);
|
|
897
910
|
}
|
|
@@ -1769,7 +1782,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1769
1782
|
if (currentMemberRole === 'owner' || currentMemberRole === 'admin' || currentMemberRole === 'member' || currentMemberRole === 'read-only') {
|
|
1770
1783
|
setHydratedWorkspaceRole(currentMemberRole);
|
|
1771
1784
|
}
|
|
1772
|
-
else if (!workspaceSwitchingEnabled) {
|
|
1785
|
+
else if (!workspaceSwitchingEnabled && users.length === 0) {
|
|
1773
1786
|
setHydratedWorkspaceRole(null);
|
|
1774
1787
|
}
|
|
1775
1788
|
const workspaceMemberCandidates = users
|
|
@@ -1793,7 +1806,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1793
1806
|
candidate
|
|
1794
1807
|
])).values()).filter((candidate) => String(candidate.userId || '').trim().length > 0);
|
|
1795
1808
|
}
|
|
1796
|
-
else if (!workspaceSwitchingEnabled) {
|
|
1809
|
+
else if (!workspaceSwitchingEnabled && !fallbackCandidates.length) {
|
|
1797
1810
|
setHydratedWorkspaceRole(null);
|
|
1798
1811
|
}
|
|
1799
1812
|
const mergedOptions = mergeAssigneeOptions([
|
|
@@ -1988,7 +2001,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1988
2001
|
const retryBootstrapChecks = useCallback(async () => {
|
|
1989
2002
|
setBootstrapError(null);
|
|
1990
2003
|
markBootstrapPhase('auth');
|
|
1991
|
-
const authenticated = await checkAuthSession();
|
|
2004
|
+
const authenticated = await checkAuthSession({ force: true });
|
|
1992
2005
|
if (workspaceSwitchingEnabled && authenticated) {
|
|
1993
2006
|
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
1994
2007
|
if (!workspaceResolution.success || workspaceResolution.workspaceSetupRequired) {
|
|
@@ -2055,7 +2068,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2055
2068
|
if (!res.ok || data?.success === false) {
|
|
2056
2069
|
return { success: false, error: data?.error || `Failed to delete workspace (${res.status})`, code: data?.code };
|
|
2057
2070
|
}
|
|
2058
|
-
await checkAuthSession();
|
|
2071
|
+
await checkAuthSession({ force: true });
|
|
2059
2072
|
await Promise.all([fetchConfig(), fetchWorkspaces()]);
|
|
2060
2073
|
const nextWorkspaceId = typeof data?.nextWorkspaceId === 'string' ? data.nextWorkspaceId.trim() : '';
|
|
2061
2074
|
if (nextWorkspaceId) {
|
|
@@ -2157,7 +2170,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2157
2170
|
return { success: false, error: data?.error || 'Sign in failed.', code: data?.code };
|
|
2158
2171
|
}
|
|
2159
2172
|
setError('');
|
|
2160
|
-
await checkAuthSession();
|
|
2173
|
+
await checkAuthSession({ force: true });
|
|
2161
2174
|
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
2162
2175
|
if (!workspaceResolution.success) {
|
|
2163
2176
|
return {
|
|
@@ -2218,6 +2231,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2218
2231
|
return {
|
|
2219
2232
|
success: true,
|
|
2220
2233
|
workspaceSetupRequired: Boolean(data?.workspaceSetupRequired),
|
|
2234
|
+
planSelectionRequired: data?.planSelectionRequired === true,
|
|
2235
|
+
checkoutPending: data?.checkoutPending === true,
|
|
2236
|
+
commercialState: typeof data?.commercialState === 'string' ? data.commercialState : null,
|
|
2221
2237
|
verificationRequired: Boolean(data?.verificationRequired),
|
|
2222
2238
|
verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
|
|
2223
2239
|
emailSent: data?.emailSent !== false,
|
|
@@ -2233,6 +2249,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2233
2249
|
return { success: false, error: 'Cloud auth endpoint is not configured.' };
|
|
2234
2250
|
}
|
|
2235
2251
|
const trimmedDisplayName = String(input.displayName || '').trim();
|
|
2252
|
+
const trimmedAvatarDraftId = typeof input.avatarDraftId === 'string' ? input.avatarDraftId.trim() : '';
|
|
2253
|
+
const shouldClearAvatar = input.clearAvatar === true;
|
|
2236
2254
|
if (!trimmedDisplayName) {
|
|
2237
2255
|
return { success: false, error: 'Display name is required.' };
|
|
2238
2256
|
}
|
|
@@ -2241,7 +2259,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2241
2259
|
method: 'POST',
|
|
2242
2260
|
headers: { 'Content-Type': 'application/json' },
|
|
2243
2261
|
credentials: 'include',
|
|
2244
|
-
body: JSON.stringify({
|
|
2262
|
+
body: JSON.stringify({
|
|
2263
|
+
displayName: trimmedDisplayName,
|
|
2264
|
+
...(trimmedAvatarDraftId ? { avatarDraftId: trimmedAvatarDraftId } : {}),
|
|
2265
|
+
...(shouldClearAvatar ? { clearAvatar: true } : {})
|
|
2266
|
+
})
|
|
2245
2267
|
});
|
|
2246
2268
|
const data = await res.json().catch(() => ({}));
|
|
2247
2269
|
if (!res.ok || !data?.success) {
|
|
@@ -2250,9 +2272,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2250
2272
|
const profile = {
|
|
2251
2273
|
userId: typeof data?.profile?.userId === 'string' ? data.profile.userId : authUserId,
|
|
2252
2274
|
email: typeof data?.profile?.email === 'string' ? data.profile.email : authUserEmail,
|
|
2253
|
-
displayName: typeof data?.profile?.displayName === 'string' ? data.profile.displayName : null
|
|
2275
|
+
displayName: typeof data?.profile?.displayName === 'string' ? data.profile.displayName : null,
|
|
2276
|
+
avatarUrl: typeof data?.profile?.avatarUrl === 'string' ? data.profile.avatarUrl : null
|
|
2254
2277
|
};
|
|
2255
2278
|
setAuthUserDisplayName(profile.displayName || '');
|
|
2279
|
+
setAuthUserAvatarUrl(profile.avatarUrl || '');
|
|
2256
2280
|
return { success: true, profile };
|
|
2257
2281
|
}
|
|
2258
2282
|
catch {
|
|
@@ -2424,7 +2448,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2424
2448
|
return { success: false, error: data?.error || 'Invite acceptance failed.', code: data?.code };
|
|
2425
2449
|
}
|
|
2426
2450
|
setError('');
|
|
2427
|
-
await checkAuthSession();
|
|
2451
|
+
await checkAuthSession({ force: true });
|
|
2428
2452
|
const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
|
|
2429
2453
|
const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
|
|
2430
2454
|
if (!workspaceResolution.success) {
|
|
@@ -2505,13 +2529,20 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2505
2529
|
// no-op
|
|
2506
2530
|
}
|
|
2507
2531
|
finally {
|
|
2532
|
+
authSessionRequestRef.current = null;
|
|
2533
|
+
authSessionLastResultRef.current = false;
|
|
2534
|
+
authSessionLastCheckedAtRef.current = Date.now();
|
|
2508
2535
|
setIsAuthenticated(false);
|
|
2536
|
+
setAuthSessionResolved(true);
|
|
2509
2537
|
setAuthUserId('anonymous');
|
|
2538
|
+
setAuthWorkspaceId('');
|
|
2510
2539
|
persistLocalMutationActorUserId(null);
|
|
2511
2540
|
setAuthUserEmail('');
|
|
2512
2541
|
setAuthUserDisplayName('');
|
|
2542
|
+
setAuthUserAvatarUrl('');
|
|
2513
2543
|
setUserGlobalSyncStatus('disconnected');
|
|
2514
2544
|
setUserGlobalSyncError(null);
|
|
2545
|
+
setHasBetaAccess(true);
|
|
2515
2546
|
setAuthBlocked(runtimeMode === 'cloud' && authRequiredForApi);
|
|
2516
2547
|
setCurrentWorkspaceId((prev) => {
|
|
2517
2548
|
const existing = String(prev || '').trim();
|
|
@@ -2527,11 +2558,31 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2527
2558
|
});
|
|
2528
2559
|
setAvailableWorkspaces([]);
|
|
2529
2560
|
workspaceListHydratedRef.current = false;
|
|
2561
|
+
setHydratedWorkspaceRole(null);
|
|
2562
|
+
if (runtimeMode === 'local') {
|
|
2563
|
+
await Promise.allSettled([
|
|
2564
|
+
fetchConfig({ ignoreAuthGuard: true }),
|
|
2565
|
+
fetchTasks(true, { ignoreAuthGuard: true }),
|
|
2566
|
+
fetchPlanningEntities({ ignoreAuthGuard: true }),
|
|
2567
|
+
fetchReferenceData({ ignoreAuthGuard: true, force: true })
|
|
2568
|
+
]);
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2530
2571
|
setTasks([]);
|
|
2531
2572
|
setInitiatives([]);
|
|
2532
2573
|
setWorkstreams([]);
|
|
2533
2574
|
}
|
|
2534
|
-
}, [
|
|
2575
|
+
}, [
|
|
2576
|
+
authRequiredForApi,
|
|
2577
|
+
resolveCloudAuthUrl,
|
|
2578
|
+
cloudAuthConfigured,
|
|
2579
|
+
workspaceSelectionScope,
|
|
2580
|
+
runtimeMode,
|
|
2581
|
+
fetchConfig,
|
|
2582
|
+
fetchPlanningEntities,
|
|
2583
|
+
fetchReferenceData,
|
|
2584
|
+
fetchTasks
|
|
2585
|
+
]);
|
|
2535
2586
|
const createInitiativeFromTemplate = useCallback(async (payload) => {
|
|
2536
2587
|
try {
|
|
2537
2588
|
const res = await fetch('/api/taskforce/initiative-templates/create', {
|
|
@@ -3456,8 +3507,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3456
3507
|
authRequiredForApi,
|
|
3457
3508
|
authBlocked,
|
|
3458
3509
|
isAuthenticated,
|
|
3510
|
+
authUserId,
|
|
3459
3511
|
authUserEmail,
|
|
3460
3512
|
authUserDisplayName,
|
|
3513
|
+
authUserAvatarUrl,
|
|
3461
3514
|
authWorkspaceId,
|
|
3462
3515
|
userGlobalSyncStatus,
|
|
3463
3516
|
workspaceLastPullAt,
|
|
@@ -87,6 +87,7 @@ const archivedTaskRecord = (id, title) => ({
|
|
|
87
87
|
describe('useTaskforce sync behavior', () => {
|
|
88
88
|
beforeEach(() => {
|
|
89
89
|
localStorage.clear();
|
|
90
|
+
sessionStorage.clear();
|
|
90
91
|
});
|
|
91
92
|
afterEach(() => {
|
|
92
93
|
vi.useRealTimers();
|
|
@@ -97,7 +98,7 @@ describe('useTaskforce sync behavior', () => {
|
|
|
97
98
|
it('merges a server-returned task into the active collection immediately', async () => {
|
|
98
99
|
vi.stubGlobal('fetch', vi.fn((url, init) => {
|
|
99
100
|
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
100
|
-
return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: '
|
|
101
|
+
return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
|
|
101
102
|
}
|
|
102
103
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
103
104
|
return jsonResponse({
|
|
@@ -188,7 +189,7 @@ describe('useTaskforce sync behavior', () => {
|
|
|
188
189
|
it('keeps the dawn theme when settings re-fetch configuration', async () => {
|
|
189
190
|
const fetchMock = vi.fn((url) => {
|
|
190
191
|
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
191
|
-
return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: '
|
|
192
|
+
return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
|
|
192
193
|
}
|
|
193
194
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
194
195
|
return jsonResponse({
|
|
@@ -261,7 +262,7 @@ describe('useTaskforce sync behavior', () => {
|
|
|
261
262
|
it('merges a server-returned archived task immediately instead of dropping it between collections', async () => {
|
|
262
263
|
vi.stubGlobal('fetch', vi.fn((url, init) => {
|
|
263
264
|
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
264
|
-
return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: '
|
|
265
|
+
return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
|
|
265
266
|
}
|
|
266
267
|
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
267
268
|
return jsonResponse({ success: true, state: {} });
|
|
@@ -339,7 +340,7 @@ describe('useTaskforce sync behavior', () => {
|
|
|
339
340
|
it('merges a server-returned completed task immediately when toggling done', async () => {
|
|
340
341
|
vi.stubGlobal('fetch', vi.fn((url, init) => {
|
|
341
342
|
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
342
|
-
return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: '
|
|
343
|
+
return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
|
|
343
344
|
}
|
|
344
345
|
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
345
346
|
return jsonResponse({ success: true, state: {} });
|
|
@@ -416,7 +417,7 @@ describe('useTaskforce sync behavior', () => {
|
|
|
416
417
|
vi.stubGlobal('confirm', vi.fn(() => true));
|
|
417
418
|
vi.stubGlobal('fetch', vi.fn((url, init) => {
|
|
418
419
|
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
419
|
-
return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: '
|
|
420
|
+
return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
|
|
420
421
|
}
|
|
421
422
|
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
422
423
|
return jsonResponse({ success: true, state: {} });
|
|
@@ -503,7 +504,7 @@ describe('useTaskforce sync behavior', () => {
|
|
|
503
504
|
config: {
|
|
504
505
|
runtimeMode: 'local',
|
|
505
506
|
workspaceMode: 'single-local',
|
|
506
|
-
authSource: '
|
|
507
|
+
authSource: 'cloud',
|
|
507
508
|
baseUrl: 'http://localhost:5174',
|
|
508
509
|
apiBaseUrl: 'http://localhost:5174',
|
|
509
510
|
cloudAuthBaseUrl: 'http://localhost:5174'
|
|
@@ -918,6 +919,184 @@ describe('useTaskforce sync behavior', () => {
|
|
|
918
919
|
expect(result.current.assigneeOptions.some((option) => option.value === 'ai-profile-codex' && option.label === 'Codex')).toBe(true);
|
|
919
920
|
});
|
|
920
921
|
});
|
|
922
|
+
it('keeps the local hydrated workspace role when workspace-members temporarily fails', async () => {
|
|
923
|
+
let workspaceMembersCalls = 0;
|
|
924
|
+
vi.stubGlobal('fetch', vi.fn((url) => {
|
|
925
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
926
|
+
return jsonResponse({
|
|
927
|
+
config: {
|
|
928
|
+
runtimeMode: 'local',
|
|
929
|
+
workspaceMode: 'single-local',
|
|
930
|
+
authSource: 'cloud',
|
|
931
|
+
workspaceSwitchingEnabled: false
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
936
|
+
return jsonResponse({
|
|
937
|
+
authenticated: true,
|
|
938
|
+
authRequiredForApi: true,
|
|
939
|
+
betaAccess: true,
|
|
940
|
+
userId: 'user-1',
|
|
941
|
+
email: 'user@example.com',
|
|
942
|
+
workspaceId: 'workspace-cloud-1'
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
if (url.includes('/api/taskforce/ui-state?key=app'))
|
|
946
|
+
return jsonResponse({ success: true, state: {} });
|
|
947
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
948
|
+
return jsonResponse({
|
|
949
|
+
success: true,
|
|
950
|
+
state: { version: 2, enabled: false, phase: 'idle', pullCursor: null }
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
if (url.includes('/api/taskforce/config')) {
|
|
954
|
+
return jsonResponse({
|
|
955
|
+
projectName: 'Local Project',
|
|
956
|
+
workspaceId: 'workspace-cloud-1',
|
|
957
|
+
runtimeMode: 'local',
|
|
958
|
+
authRequiredForApi: true,
|
|
959
|
+
userId: 'user-1',
|
|
960
|
+
setupState: {
|
|
961
|
+
globalSetupState: 'complete',
|
|
962
|
+
workspaceSetupState: 'complete',
|
|
963
|
+
workspaceId: 'workspace-cloud-1'
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
if (url.includes('/api/taskforce/global-settings'))
|
|
968
|
+
return jsonResponse({});
|
|
969
|
+
if (url.includes('/api/taskforce/categories'))
|
|
970
|
+
return jsonResponse({ categories: [{ value: 'general', label: 'General' }] });
|
|
971
|
+
if (url.includes('/api/taskforce/types'))
|
|
972
|
+
return jsonResponse({ types: [] });
|
|
973
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
974
|
+
return jsonResponse({ priorities: [] });
|
|
975
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
976
|
+
return jsonResponse({ approaches: [] });
|
|
977
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
978
|
+
return jsonResponse({ specialists: [] });
|
|
979
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
980
|
+
return jsonResponse({ tasks: [activeTask] });
|
|
981
|
+
if (url.includes('/api/taskforce/archive'))
|
|
982
|
+
return jsonResponse({ archived: [] });
|
|
983
|
+
if (url.includes('/api/taskforce/workspace/assignee-options')) {
|
|
984
|
+
return jsonResponse({
|
|
985
|
+
assignees: [
|
|
986
|
+
{ value: 'member-1', label: 'Member One', icon: 'User', color: '#22c55e', kind: 'member', userId: 'member-1', email: 'member1@example.com' }
|
|
987
|
+
]
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
if (url.includes('/api/taskforce/auth/workspace-members')) {
|
|
991
|
+
workspaceMembersCalls += 1;
|
|
992
|
+
if (workspaceMembersCalls === 1) {
|
|
993
|
+
return jsonResponse({
|
|
994
|
+
users: [
|
|
995
|
+
{
|
|
996
|
+
userId: 'user-1',
|
|
997
|
+
email: 'user@example.com',
|
|
998
|
+
displayName: 'Signed In Owner',
|
|
999
|
+
role: 'owner',
|
|
1000
|
+
status: 'active',
|
|
1001
|
+
disabled: false
|
|
1002
|
+
}
|
|
1003
|
+
]
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
return Promise.reject(new Error('temporary workspace-members failure'));
|
|
1007
|
+
}
|
|
1008
|
+
if (url.includes('/api/taskforce/data-version'))
|
|
1009
|
+
return jsonResponse({ dataVersion: 1 });
|
|
1010
|
+
return jsonResponse({});
|
|
1011
|
+
}));
|
|
1012
|
+
const { result } = renderHook(() => useTaskforce({ config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }), { wrapper });
|
|
1013
|
+
await waitFor(() => {
|
|
1014
|
+
expect(result.current.currentWorkspaceRole).toBe('owner');
|
|
1015
|
+
});
|
|
1016
|
+
await act(async () => {
|
|
1017
|
+
await result.current.fetchAssigneeOptions();
|
|
1018
|
+
});
|
|
1019
|
+
expect(result.current.currentWorkspaceRole).toBe('owner');
|
|
1020
|
+
});
|
|
1021
|
+
it('seeds local auth state from the persisted loopback actor while localhost session bootstrap is in flight', async () => {
|
|
1022
|
+
sessionStorage.setItem('taskforce.localMutationActorUserId.v1', 'user-1');
|
|
1023
|
+
const fetchMock = vi.fn((url) => {
|
|
1024
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
1025
|
+
return jsonResponse({
|
|
1026
|
+
config: {
|
|
1027
|
+
runtimeMode: 'local',
|
|
1028
|
+
workspaceMode: 'single-local',
|
|
1029
|
+
authSource: 'cloud',
|
|
1030
|
+
workspaceSwitchingEnabled: false,
|
|
1031
|
+
cloudAuthBaseUrl: 'https://app.taskforcehq.ai',
|
|
1032
|
+
cloudAuthViaLocalProxy: true
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1037
|
+
return jsonResponse({
|
|
1038
|
+
authenticated: true,
|
|
1039
|
+
authRequiredForApi: true,
|
|
1040
|
+
betaAccess: true,
|
|
1041
|
+
userId: 'user-1',
|
|
1042
|
+
email: 'user@example.com',
|
|
1043
|
+
workspaceId: 'workspace-cloud-1'
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
if (url.includes('/api/taskforce/ui-state?key=app'))
|
|
1047
|
+
return jsonResponse({ success: true, state: {} });
|
|
1048
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
1049
|
+
return jsonResponse({
|
|
1050
|
+
success: true,
|
|
1051
|
+
state: { version: 2, enabled: false, phase: 'idle', pullCursor: null }
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1055
|
+
return jsonResponse({
|
|
1056
|
+
projectName: 'Local Project',
|
|
1057
|
+
workspaceId: 'workspace-cloud-1',
|
|
1058
|
+
runtimeMode: 'local',
|
|
1059
|
+
setupState: {
|
|
1060
|
+
globalSetupState: 'complete',
|
|
1061
|
+
workspaceSetupState: 'complete',
|
|
1062
|
+
workspaceId: 'workspace-cloud-1'
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
if (url.includes('/api/taskforce/global-settings'))
|
|
1067
|
+
return jsonResponse({});
|
|
1068
|
+
if (url.includes('/api/taskforce/categories'))
|
|
1069
|
+
return jsonResponse({ categories: [{ value: 'general', label: 'General' }] });
|
|
1070
|
+
if (url.includes('/api/taskforce/types'))
|
|
1071
|
+
return jsonResponse({ types: [] });
|
|
1072
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
1073
|
+
return jsonResponse({ priorities: [] });
|
|
1074
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
1075
|
+
return jsonResponse({ approaches: [] });
|
|
1076
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
1077
|
+
return jsonResponse({ specialists: [] });
|
|
1078
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
1079
|
+
return jsonResponse({ tasks: [activeTask] });
|
|
1080
|
+
if (url.includes('/api/taskforce/archive'))
|
|
1081
|
+
return jsonResponse({ archived: [] });
|
|
1082
|
+
if (url.includes('/api/taskforce/workspace/assignee-options'))
|
|
1083
|
+
return jsonResponse({ assignees: [] });
|
|
1084
|
+
if (url.includes('/api/taskforce/data-version'))
|
|
1085
|
+
return jsonResponse({ dataVersion: 1 });
|
|
1086
|
+
return jsonResponse({});
|
|
1087
|
+
});
|
|
1088
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1089
|
+
const { result } = renderHook(() => useTaskforce({ config: { cloudAuthBaseUrl: 'https://app.taskforcehq.ai' } }), { wrapper });
|
|
1090
|
+
expect(result.current.isAuthenticated).toBe(true);
|
|
1091
|
+
expect(result.current.authSessionResolved).toBe(true);
|
|
1092
|
+
await waitFor(() => {
|
|
1093
|
+
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/auth/session'))).toBe(true);
|
|
1094
|
+
});
|
|
1095
|
+
await waitFor(() => {
|
|
1096
|
+
expect(result.current.isAuthenticated).toBe(true);
|
|
1097
|
+
expect(result.current.authSessionResolved).toBe(true);
|
|
1098
|
+
});
|
|
1099
|
+
});
|
|
921
1100
|
it('enables cloud realtime refresh for authenticated sessions and refreshes open task comments from signals', async () => {
|
|
922
1101
|
vi.stubGlobal('WebSocket', MockWebSocket);
|
|
923
1102
|
let invalidationTaskFetchCount = 0;
|