@taskforcehq/taskforce 0.3.303 → 0.3.305
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 +140 -247
- package/dist/Taskforce.module.css +102 -5
- package/dist/TaskforceCore.js +635 -238
- package/dist/TaskforceCore.test.js +2243 -495
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/compat/workspaceSyncCompat.js +6 -0
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +1 -1
- package/dist/components/features/DocumentViewer.js +32 -9
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +193 -6
- package/dist/components/features/TaskSettings.js +87 -41
- package/dist/components/features/TaskSettings.test.js +180 -40
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +9 -8
- package/dist/components/task/TaskForm.js +374 -219
- package/dist/components/task/TaskForm.test.js +482 -361
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +33 -161
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +9 -10
- package/dist/components/views/PlanComparisonPage.test.js +55 -41
- package/dist/components/views/PlansPage.d.ts +5 -1
- package/dist/components/views/PlansPage.js +150 -81
- package/dist/components/views/PlansPage.test.js +367 -41
- package/dist/components/views/StandaloneLayout.js +135 -54
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +6 -1
- package/dist/core/AiProfileService.js +161 -16
- package/dist/core/AiProfileService.test.js +3 -1
- package/dist/core/AiProfiles.test.js +200 -0
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +75 -13
- package/dist/core/PlanEntitlementService.d.ts +9 -1
- package/dist/core/PlanEntitlementService.js +144 -19
- package/dist/core/PlanVersionPolicy.test.js +19 -26
- package/dist/core/SignupMonetizationSettings.test.js +75 -15
- package/dist/core/SystemAdmin.test.js +212 -56
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +18 -11
- package/dist/core/Taskforce.js +93 -12
- package/dist/core/WorkspacePlanMode.test.js +73 -8
- package/dist/core/shared.d.ts +2 -0
- package/dist/core/shared.js +11 -0
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +63 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +123 -0
- package/dist/hooks/sync/transfers.js +435 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
- package/dist/hooks/useSyncOrchestrator.js +388 -879
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +57 -7
- package/dist/hooks/useTaskMutations.test.js +176 -0
- package/dist/hooks/useTaskforce.d.ts +13 -4
- package/dist/hooks/useTaskforce.js +302 -817
- package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
- package/dist/hooks/useWorkspaceSyncController.js +33 -11
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +2455 -3925
- package/dist/mcp/runtime.test.js +240 -8
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +46 -3
- package/dist/server/index.test.js +12 -1
- package/dist/server/routes/admin.d.ts +1 -5
- package/dist/server/routes/admin.js +151 -80
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +1 -4
- package/dist/server/routes/auth.js +59 -64
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +122 -255
- package/dist/server/routes/billing.test.js +257 -86
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +4 -2
- package/dist/server/routes/documents.js +73 -7
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +31 -1
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +111 -238
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +50 -8
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +1 -1
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +84 -181
- package/dist/server/routes.test.js +912 -32
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.js +28 -17
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +379 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +44 -2
- package/dist/sync/syncService.test.js +62 -0
- package/dist/sync/workspacePullFeed.d.ts +1 -0
- package/dist/sync/workspacePullFeed.js +66 -59
- package/dist/sync/workspacePullFeed.test.js +72 -4
- package/dist/sync/workspaceSyncModel.d.ts +25 -1
- package/dist/sync/workspaceSyncModel.js +77 -3
- package/dist/sync/workspaceSyncModel.test.js +153 -0
- package/dist/sync/workspaceSyncState.d.ts +2 -0
- package/dist/sync/workspaceSyncState.js +1 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
- package/dist/ui/assets/index-C2-OXZV7.css +1 -0
- package/dist/ui/assets/index-DcSI5F86.js +6 -0
- package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +196 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +10 -1
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/playwright-auth.sh +9 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
- package/dist/ui/assets/index-CLIMgR6g.js +0 -6
- package/dist/ui/assets/index-DneETxcu.css +0 -1
|
@@ -39,12 +39,11 @@ import { normalizeTaskFromApi } from '../utils/taskNormalization.js';
|
|
|
39
39
|
import { getSupportedLocales, initializeLocale } from '../localization';
|
|
40
40
|
import { WORKSPACE_BOOTSTRAP_SCOPE, isReservedWorkspaceId, resolveWorkspaceSelectionScopeForRuntime, resolveWorkspaceUiStateScopeForRuntime } from '../utils/workspaceIdentity';
|
|
41
41
|
import { resolveRuntimeModeContract } from '../shared/runtimeContract';
|
|
42
|
-
import { hasWorkspaceTaxonomyStatePayload, normalizeWorkspaceTaxonomyStatePayload } from '../shared/taxonomyState.js';
|
|
43
42
|
import { DEFAULT_TYPE_VALUE } from '../shared/taxonomyDefaults.js';
|
|
44
43
|
import { cloneDefaultWorkflowDefinitions } from '../shared/workflowDefaults.js';
|
|
45
44
|
import { resolveClientDeploymentConfig } from '../config/clientDeployment';
|
|
46
45
|
import { resolveTaskforceMcpBaseUrlFromBaseUrl } from '../config/cloudEnvironment';
|
|
47
|
-
import { createDefaultAssigneeOptions
|
|
46
|
+
import { createDefaultAssigneeOptions } from '../utils/assignees';
|
|
48
47
|
import { persistLocalMutationActorUserId, readLocalMutationActorUserId } from '../utils/taskforceApiClient';
|
|
49
48
|
import { useSyncOrchestrator } from './useSyncOrchestrator';
|
|
50
49
|
import { useRealtimeSync } from './useRealtimeSync';
|
|
@@ -62,9 +61,12 @@ import { useTaskFilters } from './tasks/useTaskFilters';
|
|
|
62
61
|
import { useTaskFormActions } from './tasks/useTaskFormActions';
|
|
63
62
|
import { useTaskRelationships } from './tasks/useTaskRelationships';
|
|
64
63
|
import { useTaskActions } from './tasks/useTaskActions';
|
|
65
|
-
import { logBootstrapDebug, readPersistedAppUiState, readPersistedWorkspaceId, writePersistedAppUiState, writePersistedWorkspaceId } from './workspace/workspaceLocalState';
|
|
64
|
+
import { clearPersistedWorkspaceId, logBootstrapDebug, persistRemoteUiState, readPersistedAppUiState, readPersistedWorkspaceId, writePersistedAppUiState, writePersistedWorkspaceId } from './workspace/workspaceLocalState';
|
|
66
65
|
import { useTaskforceApiRouting } from './workspace/useTaskforceApiRouting';
|
|
67
|
-
import {
|
|
66
|
+
import { useTaskforceAuthBootstrap } from './auth/useTaskforceAuthBootstrap';
|
|
67
|
+
import { deriveAuthGuardPolicy } from './auth/useAuthGuardPolicy';
|
|
68
|
+
import { useTaskforceWorkspaceBootstrap } from './workspace/useTaskforceWorkspaceBootstrap';
|
|
69
|
+
import { useDataVersionRefresh } from './sync/useDataVersionRefresh';
|
|
68
70
|
const ENV_DEPLOYMENT = resolveClientDeploymentConfig(import.meta.env);
|
|
69
71
|
const DEFAULT_CLOUD_APP_ORIGIN = (() => {
|
|
70
72
|
const configuredBase = ENV_DEPLOYMENT.baseUrl;
|
|
@@ -203,7 +205,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
203
205
|
|| envCloudBaseUrl
|
|
204
206
|
|| normalizedCloudAuthBaseUrl
|
|
205
207
|
|| defaultCloudAuthBaseUrl).trim().replace(/\/+$/, '');
|
|
206
|
-
const authOnlyCloudMode = Boolean(normalizedCloudAuthBaseUrl) && !normalizedApiBaseUrl;
|
|
208
|
+
const authOnlyCloudMode = !isCloudHost && Boolean(normalizedCloudAuthBaseUrl) && !normalizedApiBaseUrl;
|
|
207
209
|
const shouldProbeCloudAuth = runtimeConfigReady && (Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl) || isCloudHost);
|
|
208
210
|
const shouldGateProtectedApiCalls = shouldProbeCloudAuth && !authOnlyCloudMode;
|
|
209
211
|
const cloudAuthConfigured = Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl || isCloudHost);
|
|
@@ -233,8 +235,13 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
233
235
|
|| path.startsWith('/api/taskforce/billing/')
|
|
234
236
|
|| path.startsWith('/api/taskforce/sync/')
|
|
235
237
|
|| path.startsWith('/api/taskforce/settings/mcp/');
|
|
236
|
-
if (!isCloudHost && isCloudScopedPath
|
|
237
|
-
|
|
238
|
+
if (!isCloudHost && isCloudScopedPath) {
|
|
239
|
+
if (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy) {
|
|
240
|
+
return path;
|
|
241
|
+
}
|
|
242
|
+
if (runtimeConfigOverride.workspaceMode === 'multi-cloud') {
|
|
243
|
+
return path;
|
|
244
|
+
}
|
|
238
245
|
}
|
|
239
246
|
const base = normalizedCloudAuthBaseUrl || normalizedApiBaseUrl;
|
|
240
247
|
if (base) {
|
|
@@ -256,7 +263,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
256
263
|
if (!isCloudHost && isCloudScopedPath)
|
|
257
264
|
return path;
|
|
258
265
|
return path;
|
|
259
|
-
}, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost]);
|
|
266
|
+
}, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, runtimeConfigOverride.workspaceMode, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost]);
|
|
260
267
|
const resolveWebSocketUrl = useCallback((path) => {
|
|
261
268
|
const wsPath = String(path || '').trim() || '/taskforce-ws';
|
|
262
269
|
const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`;
|
|
@@ -345,6 +352,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
345
352
|
const [bootstrapError, setBootstrapError] = useState(null);
|
|
346
353
|
const [bootstrapStartedAt, setBootstrapStartedAt] = useState(null);
|
|
347
354
|
const authSessionRequestRef = useRef(null);
|
|
355
|
+
const authSessionEpochRef = useRef(0);
|
|
348
356
|
const authSessionLastCheckedAtRef = useRef(0);
|
|
349
357
|
const authSessionLastResultRef = useRef(hasOptimisticLocalAuth);
|
|
350
358
|
const currentWorkspaceIdRef = useRef(currentWorkspaceId);
|
|
@@ -375,25 +383,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
375
383
|
useEffect(() => {
|
|
376
384
|
currentWorkspaceIdRef.current = currentWorkspaceId;
|
|
377
385
|
}, [currentWorkspaceId]);
|
|
378
|
-
const applyResolvedWorkspaceId = useCallback((nextWorkspaceIdRaw
|
|
386
|
+
const applyResolvedWorkspaceId = useCallback((nextWorkspaceIdRaw) => {
|
|
379
387
|
const nextWorkspaceId = String(nextWorkspaceIdRaw || '').trim();
|
|
380
388
|
if (!nextWorkspaceId)
|
|
381
389
|
return;
|
|
382
|
-
setCurrentWorkspaceId((
|
|
390
|
+
setCurrentWorkspaceId(() => {
|
|
383
391
|
const explicitWorkspaceId = String(explicitWorkspaceSelectionRef.current || '').trim();
|
|
384
392
|
let resolvedWorkspaceId = nextWorkspaceId;
|
|
385
393
|
if (explicitWorkspaceId && explicitWorkspaceId !== nextWorkspaceId) {
|
|
386
394
|
resolvedWorkspaceId = explicitWorkspaceId;
|
|
387
395
|
}
|
|
388
|
-
else {
|
|
389
|
-
const prevValue = String(prev || '').trim();
|
|
390
|
-
if (options?.preserveNonDefaultDefault
|
|
391
|
-
&& nextWorkspaceId === 'default'
|
|
392
|
-
&& prevValue
|
|
393
|
-
&& prevValue.toLowerCase() !== 'default') {
|
|
394
|
-
resolvedWorkspaceId = prevValue;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
396
|
currentWorkspaceIdRef.current = resolvedWorkspaceId;
|
|
398
397
|
return resolvedWorkspaceId;
|
|
399
398
|
});
|
|
@@ -477,7 +476,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
477
476
|
shouldBlockProtectedApiCalls
|
|
478
477
|
});
|
|
479
478
|
const { zenMode, setZenModeState, toggleZenMode } = useZenMode();
|
|
480
|
-
const { exportEnvironment, setExportEnvironment, availableEnvironments, loadAvailableEnvironments, exportWorkflowsPath, setExportWorkflowsPath, exportingResource, exportResult, handleExportWorkflows } = useResourceExport({
|
|
479
|
+
const { exportEnvironment, setExportEnvironment, availableEnvironments, loadAvailableEnvironments, exportWorkflowsPath, setExportWorkflowsPath, exportingResource, exportResult, handleExportWorkflows } = useResourceExport({
|
|
480
|
+
storagePath,
|
|
481
|
+
shouldDeferProtectedApiCalls,
|
|
482
|
+
shouldBlockProtectedApiCalls
|
|
483
|
+
});
|
|
481
484
|
// Unsaved Changes Modal State
|
|
482
485
|
const [unsavedModalOpen, setUnsavedModalOpen] = useState(false);
|
|
483
486
|
const [pendingNavigation, setPendingNavigation] = useState(null);
|
|
@@ -813,159 +816,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
813
816
|
await fetchDeletedTasks(true);
|
|
814
817
|
await fetchPlanningEntities();
|
|
815
818
|
}, [fetchDeletedTasks, fetchPlanningEntities, refreshTaskCollectionsFromInvalidationBase]);
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
// This avoids transient "signed out" UI flicker on refresh.
|
|
822
|
-
return authSessionLastResultRef.current;
|
|
823
|
-
}
|
|
824
|
-
if (!shouldProbeCloudAuth) {
|
|
825
|
-
setRuntimeMode('local');
|
|
826
|
-
setAuthRequiredForApi(false);
|
|
827
|
-
setIsAuthenticated(false);
|
|
828
|
-
setAuthUserId('anonymous');
|
|
829
|
-
setAuthWorkspaceId('');
|
|
830
|
-
persistLocalMutationActorUserId(null);
|
|
831
|
-
setAuthUserEmail('');
|
|
832
|
-
setAuthUserDisplayName('');
|
|
833
|
-
setAuthUserAvatarUrl('');
|
|
834
|
-
setUserGlobalSyncStatus('disconnected');
|
|
835
|
-
setUserGlobalSyncError(null);
|
|
836
|
-
setHasBetaAccess(true);
|
|
837
|
-
setAvailableWorkspaces([]);
|
|
838
|
-
setHydratedWorkspaceRole(null);
|
|
839
|
-
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
840
|
-
setAuthBlocked(false);
|
|
841
|
-
setAuthSessionResolved(true);
|
|
842
|
-
authSessionLastResultRef.current = false;
|
|
843
|
-
authSessionLastCheckedAtRef.current = Date.now();
|
|
844
|
-
return false;
|
|
845
|
-
}
|
|
846
|
-
const sessionUrl = resolveCloudAuthUrl('/api/taskforce/auth/session');
|
|
847
|
-
const now = Date.now();
|
|
848
|
-
if (!force && authSessionRequestRef.current)
|
|
849
|
-
return authSessionRequestRef.current;
|
|
850
|
-
if (!force && authSessionResolved && (now - authSessionLastCheckedAtRef.current) < 1500) {
|
|
851
|
-
return authSessionLastResultRef.current;
|
|
852
|
-
}
|
|
853
|
-
const request = (async () => {
|
|
854
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
855
|
-
const abortController = new AbortController();
|
|
856
|
-
const timeoutId = typeof window !== 'undefined'
|
|
857
|
-
? window.setTimeout(() => abortController.abort(), 8000)
|
|
858
|
-
: null;
|
|
859
|
-
try {
|
|
860
|
-
const res = await fetch(sessionUrl, {
|
|
861
|
-
method: 'GET',
|
|
862
|
-
credentials: 'include',
|
|
863
|
-
mode: 'cors',
|
|
864
|
-
signal: abortController.signal
|
|
865
|
-
});
|
|
866
|
-
if (res.status === 429) {
|
|
867
|
-
setAuthSessionResolved(true);
|
|
868
|
-
return authSessionLastResultRef.current;
|
|
869
|
-
}
|
|
870
|
-
if (!res.ok) {
|
|
871
|
-
if (isLoopbackHost)
|
|
872
|
-
persistLocalMutationActorUserId(null);
|
|
873
|
-
setAuthSessionResolved(true);
|
|
874
|
-
authSessionLastResultRef.current = false;
|
|
875
|
-
return false;
|
|
876
|
-
}
|
|
877
|
-
const data = await res.json();
|
|
878
|
-
const required = Boolean(data.authRequiredForApi);
|
|
879
|
-
const authenticated = Boolean(data.authenticated);
|
|
880
|
-
const betaAccess = authenticated ? (data.betaAccess !== false) : true;
|
|
881
|
-
const workspaceId = typeof data.workspaceId === 'string' && data.workspaceId.trim().length > 0
|
|
882
|
-
? data.workspaceId.trim()
|
|
883
|
-
: '';
|
|
884
|
-
const userId = typeof data.userId === 'string' && data.userId.trim().length > 0
|
|
885
|
-
? data.userId.trim()
|
|
886
|
-
: 'anonymous';
|
|
887
|
-
const userEmail = typeof data.email === 'string' && data.email.trim().length > 0
|
|
888
|
-
? data.email.trim().toLowerCase()
|
|
889
|
-
: '';
|
|
890
|
-
const userDisplayName = typeof data.displayName === 'string'
|
|
891
|
-
? data.displayName.trim()
|
|
892
|
-
: '';
|
|
893
|
-
const userAvatarUrl = typeof data.avatarUrl === 'string'
|
|
894
|
-
? data.avatarUrl.trim()
|
|
895
|
-
: '';
|
|
896
|
-
const isLocalRuntime = runtimeMode === 'local';
|
|
897
|
-
const keepLocalWorkspaceContext = runtimeMode === 'local' || authOnlyCloudMode;
|
|
898
|
-
setAuthRequiredForApi(authOnlyCloudMode ? false : (isLocalRuntime ? false : required));
|
|
899
|
-
setIsAuthenticated(authenticated);
|
|
900
|
-
setAuthUserId(authenticated ? userId : 'anonymous');
|
|
901
|
-
setAuthWorkspaceId(authenticated ? workspaceId : '');
|
|
902
|
-
if (isLoopbackHost) {
|
|
903
|
-
persistLocalMutationActorUserId(authenticated ? userId : null);
|
|
904
|
-
}
|
|
905
|
-
setAuthUserEmail(authenticated ? userEmail : '');
|
|
906
|
-
setAuthUserDisplayName(authenticated ? userDisplayName : '');
|
|
907
|
-
setAuthUserAvatarUrl(authenticated ? userAvatarUrl : '');
|
|
908
|
-
if (authenticated) {
|
|
909
|
-
setError((prev) => prev === AUTH_REQUIRED_ERROR ? '' : prev);
|
|
910
|
-
}
|
|
911
|
-
setUserGlobalSyncStatus(authenticated ? 'idle' : 'disconnected');
|
|
912
|
-
setUserGlobalSyncError(null);
|
|
913
|
-
setHasBetaAccess(betaAccess);
|
|
914
|
-
if (keepLocalWorkspaceContext) {
|
|
915
|
-
// Local runtime resolves workspace identity from local DB via /api/taskforce/config.
|
|
916
|
-
// Do not override it from cloud auth session payloads.
|
|
917
|
-
}
|
|
918
|
-
else {
|
|
919
|
-
applyResolvedWorkspaceId(workspaceId || readPersistedWorkspaceId(workspaceSelectionScope) || 'default');
|
|
920
|
-
}
|
|
921
|
-
setAuthBlocked(authOnlyCloudMode ? false : (isLocalRuntime ? false : (required && !authenticated)));
|
|
922
|
-
// Runtime mode should be sourced from /api/taskforce/config, not
|
|
923
|
-
// from auth-session responses which may come from a separate cloud-auth origin.
|
|
924
|
-
setAuthSessionResolved(true);
|
|
925
|
-
authSessionLastResultRef.current = authenticated;
|
|
926
|
-
logBootstrapDebug('auth_session_resolved', {
|
|
927
|
-
authenticated,
|
|
928
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
929
|
-
});
|
|
930
|
-
if (authenticated)
|
|
931
|
-
markBootstrapPhase('ready');
|
|
932
|
-
return authenticated;
|
|
933
|
-
}
|
|
934
|
-
catch {
|
|
935
|
-
if (isLoopbackHost)
|
|
936
|
-
persistLocalMutationActorUserId(null);
|
|
937
|
-
setAuthSessionResolved(true);
|
|
938
|
-
authSessionLastResultRef.current = false;
|
|
939
|
-
logBootstrapDebug('auth_session_failed', {
|
|
940
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
941
|
-
});
|
|
942
|
-
markBootstrapStalled('Unable to verify your session.');
|
|
943
|
-
return authSessionLastResultRef.current;
|
|
944
|
-
}
|
|
945
|
-
finally {
|
|
946
|
-
if (timeoutId !== null && typeof window !== 'undefined') {
|
|
947
|
-
window.clearTimeout(timeoutId);
|
|
948
|
-
}
|
|
949
|
-
authSessionLastCheckedAtRef.current = Date.now();
|
|
950
|
-
authSessionRequestRef.current = null;
|
|
951
|
-
}
|
|
952
|
-
})();
|
|
953
|
-
authSessionRequestRef.current = request;
|
|
954
|
-
return request;
|
|
955
|
-
}, [
|
|
956
|
-
runtimeConfigReady,
|
|
957
|
-
shouldProbeCloudAuth,
|
|
958
|
-
authSessionResolved,
|
|
959
|
-
resolveCloudAuthUrl,
|
|
960
|
-
authOnlyCloudMode,
|
|
961
|
-
AUTH_REQUIRED_ERROR,
|
|
962
|
-
runtimeMode,
|
|
963
|
-
applyResolvedWorkspaceId,
|
|
964
|
-
workspaceSelectionScope,
|
|
965
|
-
markBootstrapPhase,
|
|
966
|
-
markBootstrapStalled
|
|
967
|
-
]);
|
|
968
|
-
const { userGlobalSyncStatus, setUserGlobalSyncStatus, userGlobalSyncError, setUserGlobalSyncError, workspaceLastPullAt, workspaceLastPushAt, workspaceLastErrorAt, workspaceLastErrorMessage, workspaceLastSuccessfulSyncAt, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceSyncStatus, workspaceSyncSummary, workspaceSyncRecommendedAction, workspaceSyncBusy, workspaceSyncPendingChanges, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspacePullCursorRef, clearWorkspaceRetry, persistWorkspaceSyncPatch, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, syncUserGlobalSettings, buildWorkspaceSyncSignature, pushWorkspaceChangesToCloud, saveWorkspaceCloudSyncSettings, retryUserGlobalSettingsSync, retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull, getWorkspaceSyncDiagnostics } = useSyncOrchestrator({
|
|
819
|
+
const setUserGlobalSyncStatusRef = useRef(() => undefined);
|
|
820
|
+
const setUserGlobalSyncErrorRef = useRef(() => undefined);
|
|
821
|
+
const checkAuthSessionImplRef = useRef(async () => false);
|
|
822
|
+
const checkAuthSession = useCallback((options) => checkAuthSessionImplRef.current(options), []);
|
|
823
|
+
const { userGlobalSyncStatus, setUserGlobalSyncStatus, userGlobalSyncError, setUserGlobalSyncError, workspaceLastPullAt, workspaceLastPushAt, workspaceLastErrorAt, workspaceLastErrorMessage, workspaceLastSuccessfulSyncAt, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceSyncSetupIntent, workspaceSyncStatus, workspaceSyncSummary, workspaceSyncRecommendedAction, workspaceSyncBusy, workspaceSyncPendingChanges, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, workspacePullCursorRef, clearWorkspaceRetry, persistWorkspaceSyncPatch, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, syncUserGlobalSettings, buildWorkspaceSyncSignature, pushWorkspaceChangesToCloud, saveWorkspaceCloudSyncSettings, retryUserGlobalSettingsSync, retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull, getWorkspaceSyncDiagnostics } = useSyncOrchestrator({
|
|
969
824
|
currentWorkspaceId,
|
|
970
825
|
cloudAuthConfigured,
|
|
971
826
|
runtimeMode,
|
|
@@ -978,6 +833,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
978
833
|
realtimeSyncEnabled,
|
|
979
834
|
tasks,
|
|
980
835
|
archivedTasks,
|
|
836
|
+
deletedTasks,
|
|
981
837
|
initiatives,
|
|
982
838
|
workstreams,
|
|
983
839
|
taxonomies,
|
|
@@ -1046,6 +902,50 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1046
902
|
assigneeOptionsLoaded,
|
|
1047
903
|
assigneeOptions
|
|
1048
904
|
});
|
|
905
|
+
setUserGlobalSyncStatusRef.current = setUserGlobalSyncStatus;
|
|
906
|
+
setUserGlobalSyncErrorRef.current = setUserGlobalSyncError;
|
|
907
|
+
const { checkAuthSession: checkAuthSessionImpl } = useTaskforceAuthBootstrap({
|
|
908
|
+
runtimeConfigReady,
|
|
909
|
+
shouldProbeCloudAuth,
|
|
910
|
+
authSessionResolved,
|
|
911
|
+
resolveCloudAuthUrl,
|
|
912
|
+
authOnlyCloudMode,
|
|
913
|
+
authRequiredError: AUTH_REQUIRED_ERROR,
|
|
914
|
+
runtimeMode,
|
|
915
|
+
workspaceSelectionScope,
|
|
916
|
+
markBootstrapPhase,
|
|
917
|
+
markBootstrapStalled,
|
|
918
|
+
setRuntimeMode,
|
|
919
|
+
setAuthRequiredForApi,
|
|
920
|
+
setIsAuthenticated,
|
|
921
|
+
setAuthUserId,
|
|
922
|
+
setAuthWorkspaceId,
|
|
923
|
+
setAuthUserEmail,
|
|
924
|
+
setAuthUserDisplayName,
|
|
925
|
+
setAuthUserAvatarUrl,
|
|
926
|
+
setUserGlobalSyncStatus: (value) => setUserGlobalSyncStatusRef.current(value),
|
|
927
|
+
setUserGlobalSyncError: (value) => setUserGlobalSyncErrorRef.current(value),
|
|
928
|
+
setHasBetaAccess,
|
|
929
|
+
setAvailableWorkspaces,
|
|
930
|
+
setHydratedWorkspaceRole,
|
|
931
|
+
setAssigneeOptions,
|
|
932
|
+
setAuthBlocked,
|
|
933
|
+
setAuthSessionResolved,
|
|
934
|
+
setError,
|
|
935
|
+
applyResolvedWorkspaceId,
|
|
936
|
+
readPersistedWorkspaceId,
|
|
937
|
+
authSessionRequestRef,
|
|
938
|
+
authSessionEpochRef,
|
|
939
|
+
authSessionLastCheckedAtRef,
|
|
940
|
+
authSessionLastResultRef,
|
|
941
|
+
isLoopbackHost,
|
|
942
|
+
setAuthStateReadyRefs: (authenticated, resolved, required) => {
|
|
943
|
+
authSessionResolvedRef.current = resolved;
|
|
944
|
+
authRequiredForApiRef.current = required;
|
|
945
|
+
isAuthenticatedRef.current = authenticated;
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
checkAuthSessionImplRef.current = checkAuthSessionImpl;
|
|
1049
949
|
const [activeWorkspaceModule, setActiveWorkspaceModule] = useState('tasks');
|
|
1050
950
|
const [showComments, setShowComments] = useState(false);
|
|
1051
951
|
const [descriptionFocused, setDescriptionFocused] = useState(false);
|
|
@@ -1069,6 +969,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1069
969
|
const skipNextUiStatePersistRef = useRef(false);
|
|
1070
970
|
const referenceDataRequestRef = useRef(null);
|
|
1071
971
|
const workspaceListHydratedRef = useRef(false);
|
|
972
|
+
const hasBootstrappedDataRef = useRef(false);
|
|
1072
973
|
const [uiStateReady, setUiStateReady] = useState(false);
|
|
1073
974
|
const { handleSaveSettings, handleSaveTheme, handleSaveGlobalTheme, handleJsonBackupEnabledChange, handleSaveGlobalJsonBackupEnabled, handleSaveGlobalWeekStartsOn, handleSaveLocale, handleManualComplexityEnabledChange, handleChecklistDropdownEnabledChange, handleShowTaskCardStatusLabelChange, handleResetProjectToGlobal } = useSettingsPersistence({
|
|
1074
975
|
keyShortcut,
|
|
@@ -1236,15 +1137,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1236
1137
|
writePersistedAppUiState(uiStateSnapshot, workspaceUiStateScope);
|
|
1237
1138
|
const timeoutId = window.setTimeout(async () => {
|
|
1238
1139
|
try {
|
|
1239
|
-
await
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
body: JSON.stringify({
|
|
1244
|
-
stateKey: 'app',
|
|
1245
|
-
workspaceId: currentWorkspaceIdRef.current,
|
|
1246
|
-
patch: uiStateSnapshot
|
|
1247
|
-
})
|
|
1140
|
+
await persistRemoteUiState({
|
|
1141
|
+
stateKey: 'app',
|
|
1142
|
+
workspaceId: currentWorkspaceIdRef.current,
|
|
1143
|
+
patch: uiStateSnapshot
|
|
1248
1144
|
});
|
|
1249
1145
|
}
|
|
1250
1146
|
catch {
|
|
@@ -1281,13 +1177,15 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1281
1177
|
? data.userId.trim()
|
|
1282
1178
|
: 'anonymous';
|
|
1283
1179
|
const serverAuthenticated = nextRuntimeMode === 'cloud' && nextUserId !== 'anonymous';
|
|
1180
|
+
const knownAuthenticated = serverAuthenticated || isAuthenticatedRef.current;
|
|
1181
|
+
const authResolved = authSessionResolvedRef.current;
|
|
1284
1182
|
setRuntimeMode(nextRuntimeMode);
|
|
1285
1183
|
setAuthRequiredForApi(nextAuthRequiredForApi);
|
|
1286
1184
|
if (serverAuthenticated) {
|
|
1287
1185
|
setIsAuthenticated(true);
|
|
1288
1186
|
setAuthUserId(nextUserId);
|
|
1289
1187
|
setAuthBlocked(false);
|
|
1290
|
-
if (!
|
|
1188
|
+
if (!authResolved) {
|
|
1291
1189
|
setAuthSessionResolved(true);
|
|
1292
1190
|
}
|
|
1293
1191
|
}
|
|
@@ -1297,11 +1195,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1297
1195
|
commitCurrentWorkspaceId(nextWorkspaceId);
|
|
1298
1196
|
}
|
|
1299
1197
|
else {
|
|
1300
|
-
applyResolvedWorkspaceId(nextWorkspaceId
|
|
1198
|
+
applyResolvedWorkspaceId(nextWorkspaceId);
|
|
1301
1199
|
}
|
|
1302
1200
|
}
|
|
1303
1201
|
if (nextRuntimeMode === 'cloud' && nextAuthRequiredForApi) {
|
|
1304
|
-
setAuthBlocked(!
|
|
1202
|
+
setAuthBlocked(!knownAuthenticated);
|
|
1305
1203
|
}
|
|
1306
1204
|
else {
|
|
1307
1205
|
setAuthBlocked(false);
|
|
@@ -1399,7 +1297,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1399
1297
|
else {
|
|
1400
1298
|
setShowTaskCardStatusLabel(true);
|
|
1401
1299
|
}
|
|
1402
|
-
}, [applyResolvedWorkspaceId,
|
|
1300
|
+
}, [applyResolvedWorkspaceId, commitCurrentWorkspaceId]);
|
|
1403
1301
|
const fetchBuildInfo = useCallback(async () => {
|
|
1404
1302
|
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1405
1303
|
try {
|
|
@@ -1425,417 +1323,83 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1425
1323
|
}
|
|
1426
1324
|
}
|
|
1427
1325
|
}, [fetchWithTimeout, isBootstrapTimeoutError, markBootstrapStalled]);
|
|
1428
|
-
const fetchReferenceData
|
|
1429
|
-
const ignoreAuthGuard = options?.ignoreAuthGuard === true;
|
|
1430
|
-
const force = options?.force === true;
|
|
1431
|
-
if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
|
|
1432
|
-
return;
|
|
1433
|
-
const requestState = getWorkspaceRequestState();
|
|
1434
|
-
if (!force && referenceDataRequestRef.current?.workspaceId === requestState.workspaceId) {
|
|
1435
|
-
await referenceDataRequestRef.current.promise;
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
|
-
setReferenceDataLoaded(false);
|
|
1439
|
-
const abortController = new AbortController();
|
|
1440
|
-
referenceDataAbortRef.current = abortController;
|
|
1441
|
-
const request = (async () => {
|
|
1442
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1443
|
-
await Promise.allSettled([
|
|
1444
|
-
(async () => {
|
|
1445
|
-
const taxonomyRes = await fetch('/api/taskforce/taxonomy-state', { signal: abortController.signal });
|
|
1446
|
-
if (!taxonomyRes.ok)
|
|
1447
|
-
return;
|
|
1448
|
-
const taxonomyData = await taxonomyRes.json().catch(() => ({}));
|
|
1449
|
-
const hasUnifiedPayload = hasWorkspaceTaxonomyStatePayload(taxonomyData);
|
|
1450
|
-
if (!hasUnifiedPayload) {
|
|
1451
|
-
const [catRes, typeRes, priorityRes, taxRes] = await Promise.all([
|
|
1452
|
-
fetch('/api/taskforce/categories', { signal: abortController.signal }),
|
|
1453
|
-
fetch('/api/taskforce/types', { signal: abortController.signal }),
|
|
1454
|
-
fetch('/api/taskforce/priorities', { signal: abortController.signal }),
|
|
1455
|
-
fetch('/api/taskforce/taxonomies', { signal: abortController.signal })
|
|
1456
|
-
]);
|
|
1457
|
-
const [catData, typeData, priorityData, taxData] = await Promise.all([
|
|
1458
|
-
catRes.ok ? catRes.json().catch(() => ({})) : Promise.resolve({}),
|
|
1459
|
-
typeRes.ok ? typeRes.json().catch(() => ({})) : Promise.resolve({}),
|
|
1460
|
-
priorityRes.ok ? priorityRes.json().catch(() => ({})) : Promise.resolve({}),
|
|
1461
|
-
taxRes.ok ? taxRes.json().catch(() => ({})) : Promise.resolve({})
|
|
1462
|
-
]);
|
|
1463
|
-
taxonomyData.categories = catData.categories;
|
|
1464
|
-
taxonomyData.types = typeData.types;
|
|
1465
|
-
taxonomyData.priorities = priorityData.priorities;
|
|
1466
|
-
taxonomyData.taxonomies = taxData.taxonomies;
|
|
1467
|
-
}
|
|
1468
|
-
const normalizedTaxonomyState = normalizeWorkspaceTaxonomyStatePayload(taxonomyData);
|
|
1469
|
-
if (isWorkspaceRequestStale(requestState))
|
|
1470
|
-
return;
|
|
1471
|
-
if (normalizedTaxonomyState.categories.length > 0)
|
|
1472
|
-
setCustomCategories(normalizedTaxonomyState.categories);
|
|
1473
|
-
if (normalizedTaxonomyState.types.length > 0)
|
|
1474
|
-
setCustomTypes(normalizedTaxonomyState.types);
|
|
1475
|
-
if (normalizedTaxonomyState.priorities.length > 0)
|
|
1476
|
-
setPriorities(normalizedTaxonomyState.priorities);
|
|
1477
|
-
setTaxonomies(normalizedTaxonomyState.taxonomies);
|
|
1478
|
-
setTaxonomyDisplayLabels(normalizedTaxonomyState.displayLabels || {});
|
|
1479
|
-
})(),
|
|
1480
|
-
(async () => {
|
|
1481
|
-
await fetchWorkflows();
|
|
1482
|
-
})(),
|
|
1483
|
-
(async () => {
|
|
1484
|
-
await fetchInitiativeTemplates();
|
|
1485
|
-
})(),
|
|
1486
|
-
(async () => {
|
|
1487
|
-
await loadAvailableEnvironments();
|
|
1488
|
-
})(),
|
|
1489
|
-
]);
|
|
1490
|
-
if (isWorkspaceRequestStale(requestState))
|
|
1491
|
-
return;
|
|
1492
|
-
setReferenceDataLoaded(true);
|
|
1493
|
-
logBootstrapDebug('reference_data_loaded', {
|
|
1494
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1495
|
-
});
|
|
1496
|
-
})();
|
|
1497
|
-
referenceDataRequestRef.current = { workspaceId: requestState.workspaceId, promise: request };
|
|
1498
|
-
try {
|
|
1499
|
-
await request;
|
|
1500
|
-
}
|
|
1501
|
-
catch (error) {
|
|
1502
|
-
if (!isAbortError(error))
|
|
1503
|
-
throw error;
|
|
1504
|
-
}
|
|
1505
|
-
finally {
|
|
1506
|
-
if (referenceDataRequestRef.current?.promise === request) {
|
|
1507
|
-
referenceDataRequestRef.current = null;
|
|
1508
|
-
}
|
|
1509
|
-
if (referenceDataAbortRef.current === abortController) {
|
|
1510
|
-
referenceDataAbortRef.current = null;
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
}, [
|
|
1514
|
-
fetchWorkflows,
|
|
1515
|
-
fetchInitiativeTemplates,
|
|
1516
|
-
getWorkspaceRequestState,
|
|
1517
|
-
isAbortError,
|
|
1518
|
-
isWorkspaceRequestStale,
|
|
1519
|
-
loadAvailableEnvironments,
|
|
1326
|
+
const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks } = useTaskforceWorkspaceBootstrap({
|
|
1520
1327
|
shouldDeferProtectedApiCalls,
|
|
1521
|
-
shouldBlockProtectedApiCalls
|
|
1522
|
-
]);
|
|
1523
|
-
const fetchBootstrapConfig = useCallback(async (options) => {
|
|
1524
|
-
const ignoreAuthGuard = options?.ignoreAuthGuard === true;
|
|
1525
|
-
if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
|
|
1526
|
-
return;
|
|
1527
|
-
markBootstrapPhase('config');
|
|
1528
|
-
const requestState = getWorkspaceRequestState();
|
|
1529
|
-
const abortController = new AbortController();
|
|
1530
|
-
bootstrapConfigAbortRef.current = abortController;
|
|
1531
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1532
|
-
try {
|
|
1533
|
-
const res = await fetchWithTimeout('/api/taskforce/config', undefined, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
|
|
1534
|
-
signal: abortController.signal
|
|
1535
|
-
});
|
|
1536
|
-
if (isWorkspaceEpochStale(requestState))
|
|
1537
|
-
return;
|
|
1538
|
-
if (res.status === 401) {
|
|
1539
|
-
handleUnauthorized();
|
|
1540
|
-
setConfigLoaded(true);
|
|
1541
|
-
return;
|
|
1542
|
-
}
|
|
1543
|
-
let configResolvedWorkspaceId = '';
|
|
1544
|
-
if (res.ok) {
|
|
1545
|
-
const data = await res.json();
|
|
1546
|
-
if (isWorkspaceEpochStale(requestState))
|
|
1547
|
-
return;
|
|
1548
|
-
configResolvedWorkspaceId = typeof data?.workspaceId === 'string' && data.workspaceId.trim().length > 0
|
|
1549
|
-
? data.workspaceId.trim()
|
|
1550
|
-
: '';
|
|
1551
|
-
applyBootstrapConfig(data);
|
|
1552
|
-
}
|
|
1553
|
-
if (isWorkspaceEpochStale(requestState)) {
|
|
1554
|
-
const liveWorkspaceId = String(currentWorkspaceIdRef.current || 'default').trim() || 'default';
|
|
1555
|
-
if (!configResolvedWorkspaceId || liveWorkspaceId !== configResolvedWorkspaceId) {
|
|
1556
|
-
return;
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
if (!hasLoadedUiStateRef.current) {
|
|
1560
|
-
const uiStateWorkspaceId = configResolvedWorkspaceId || requestState.workspaceId;
|
|
1561
|
-
hasLoadedUiStateRef.current = true;
|
|
1562
|
-
await loadPersistedUiState(uiStateWorkspaceId);
|
|
1563
|
-
}
|
|
1564
|
-
if (isWorkspaceEpochStale(requestState)) {
|
|
1565
|
-
const liveWorkspaceId = String(currentWorkspaceIdRef.current || 'default').trim() || 'default';
|
|
1566
|
-
if (!configResolvedWorkspaceId || liveWorkspaceId !== configResolvedWorkspaceId) {
|
|
1567
|
-
return;
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
setConfigLoaded(true);
|
|
1571
|
-
markBootstrapPhase('ready');
|
|
1572
|
-
logBootstrapDebug('bootstrap_config_loaded', {
|
|
1573
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1574
|
-
});
|
|
1575
|
-
void fetchBuildInfo();
|
|
1576
|
-
}
|
|
1577
|
-
catch (error) {
|
|
1578
|
-
if (isAbortError(error))
|
|
1579
|
-
return;
|
|
1580
|
-
if (isWorkspaceEpochStale(requestState))
|
|
1581
|
-
return;
|
|
1582
|
-
if (!hasLoadedUiStateRef.current) {
|
|
1583
|
-
hasLoadedUiStateRef.current = true;
|
|
1584
|
-
await loadPersistedUiState(requestState.workspaceId);
|
|
1585
|
-
}
|
|
1586
|
-
if (isBootstrapTimeoutError(error)) {
|
|
1587
|
-
markBootstrapStalled('Startup checks timed out.');
|
|
1588
|
-
}
|
|
1589
|
-
else {
|
|
1590
|
-
markBootstrapStalled('Unable to finish startup checks.');
|
|
1591
|
-
}
|
|
1592
|
-
setConfigLoaded(true);
|
|
1593
|
-
}
|
|
1594
|
-
finally {
|
|
1595
|
-
if (bootstrapConfigAbortRef.current === abortController) {
|
|
1596
|
-
bootstrapConfigAbortRef.current = null;
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
}, [
|
|
1600
|
-
applyBootstrapConfig,
|
|
1601
|
-
fetchBuildInfo,
|
|
1602
|
-
fetchWithTimeout,
|
|
1328
|
+
shouldBlockProtectedApiCalls,
|
|
1603
1329
|
getWorkspaceRequestState,
|
|
1604
|
-
|
|
1330
|
+
isWorkspaceRequestStale,
|
|
1331
|
+
isWorkspaceEpochStale,
|
|
1605
1332
|
isAbortError,
|
|
1606
1333
|
isBootstrapTimeoutError,
|
|
1607
|
-
|
|
1608
|
-
|
|
1334
|
+
fetchWithTimeout,
|
|
1335
|
+
handleUnauthorized,
|
|
1336
|
+
applyBootstrapConfig,
|
|
1609
1337
|
loadPersistedUiState,
|
|
1338
|
+
fetchBuildInfo,
|
|
1610
1339
|
markBootstrapPhase,
|
|
1611
1340
|
markBootstrapStalled,
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1341
|
+
fetchWorkflows,
|
|
1342
|
+
fetchInitiativeTemplates,
|
|
1343
|
+
loadAvailableEnvironments,
|
|
1344
|
+
setReferenceDataLoaded,
|
|
1345
|
+
setCustomCategories,
|
|
1346
|
+
setCustomTypes,
|
|
1347
|
+
setPriorities,
|
|
1348
|
+
setTaxonomies,
|
|
1349
|
+
setTaxonomyDisplayLabels,
|
|
1350
|
+
setConfigLoaded,
|
|
1351
|
+
hasLoadedUiStateRef,
|
|
1352
|
+
currentWorkspaceIdRef,
|
|
1353
|
+
referenceDataRequestRef,
|
|
1354
|
+
referenceDataAbortRef,
|
|
1355
|
+
bootstrapConfigAbortRef,
|
|
1356
|
+
workspaceListAbortRef,
|
|
1357
|
+
assigneeOptionsAbortRef,
|
|
1358
|
+
workspaceListHydratedRef,
|
|
1359
|
+
setAvailableWorkspaces,
|
|
1360
|
+
runtimeMode,
|
|
1361
|
+
applyResolvedWorkspaceId,
|
|
1362
|
+
workspaceSwitchingEnabled,
|
|
1363
|
+
authSessionResolved,
|
|
1364
|
+
isAuthenticated,
|
|
1365
|
+
authUserId,
|
|
1366
|
+
setHydratedWorkspaceRole,
|
|
1367
|
+
setAssigneeOptions,
|
|
1368
|
+
setAssigneeOptionsLoaded,
|
|
1369
|
+
commitCurrentWorkspaceId: (workspaceId, options) => {
|
|
1370
|
+
explicitWorkspaceSelectionRef.current = workspaceId;
|
|
1371
|
+
commitCurrentWorkspaceId(workspaceId, options);
|
|
1372
|
+
if (options?.clearExplicitSelection !== false) {
|
|
1373
|
+
explicitWorkspaceSelectionRef.current = null;
|
|
1374
|
+
}
|
|
1375
|
+
},
|
|
1376
|
+
currentWorkspaceId,
|
|
1377
|
+
workspaceSelectionScope,
|
|
1378
|
+
abortWorkspaceScopedRequests,
|
|
1379
|
+
setWorkspaceBootstrapPending,
|
|
1380
|
+
checkAuthSession,
|
|
1381
|
+
lastConfigRefreshKeyRef,
|
|
1382
|
+
lastAssigneeOptionsLoadKeyRef,
|
|
1383
|
+
lastWorkspaceSyncStateLoadKeyRef,
|
|
1384
|
+
lastTaskSurfaceLoadKeyRef,
|
|
1385
|
+
lastSettingsSurfaceLoadKeyRef,
|
|
1386
|
+
isOpen,
|
|
1387
|
+
activeTab,
|
|
1388
|
+
showArchive,
|
|
1389
|
+
taskScope,
|
|
1390
|
+
settingsSection,
|
|
1391
|
+
fetchTasks,
|
|
1392
|
+
fetchArchive,
|
|
1393
|
+
fetchPlanningEntities,
|
|
1394
|
+
refreshTaskCollections,
|
|
1395
|
+
loadWorkspaceSyncState,
|
|
1396
|
+
archiveBootstrapTimerRef,
|
|
1397
|
+
hasBootstrappedDataRef
|
|
1398
|
+
});
|
|
1619
1399
|
const refreshSetupContext = useCallback(async () => {
|
|
1620
1400
|
await checkAuthSession();
|
|
1621
1401
|
await fetchConfig();
|
|
1622
1402
|
}, [checkAuthSession, fetchConfig]);
|
|
1623
|
-
const fetchWorkspaces = useCallback(async () => {
|
|
1624
|
-
if (!workspaceSwitchingEnabled) {
|
|
1625
|
-
setAvailableWorkspaces([]);
|
|
1626
|
-
workspaceListHydratedRef.current = false;
|
|
1627
|
-
return { success: true, workspaces: [] };
|
|
1628
|
-
}
|
|
1629
|
-
const requestState = getWorkspaceRequestState();
|
|
1630
|
-
const abortController = new AbortController();
|
|
1631
|
-
workspaceListAbortRef.current = abortController;
|
|
1632
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1633
|
-
try {
|
|
1634
|
-
const res = await fetchWithTimeout('/api/taskforce/workspaces', {
|
|
1635
|
-
method: 'GET',
|
|
1636
|
-
credentials: 'include'
|
|
1637
|
-
}, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
|
|
1638
|
-
signal: abortController.signal
|
|
1639
|
-
});
|
|
1640
|
-
if (isWorkspaceEpochStale(requestState)) {
|
|
1641
|
-
return { success: false, error: 'Workspace changed while loading workspaces.' };
|
|
1642
|
-
}
|
|
1643
|
-
if (res.status === 401) {
|
|
1644
|
-
setAvailableWorkspaces([]);
|
|
1645
|
-
workspaceListHydratedRef.current = false;
|
|
1646
|
-
return { success: false, error: 'Authentication required.' };
|
|
1647
|
-
}
|
|
1648
|
-
const data = await res.json().catch(() => ({}));
|
|
1649
|
-
if (isWorkspaceEpochStale(requestState)) {
|
|
1650
|
-
return { success: false, error: 'Workspace changed while loading workspaces.' };
|
|
1651
|
-
}
|
|
1652
|
-
if (!res.ok || data?.success === false) {
|
|
1653
|
-
setAvailableWorkspaces([]);
|
|
1654
|
-
workspaceListHydratedRef.current = false;
|
|
1655
|
-
return { success: false, error: data?.error || `Failed to load workspaces (${res.status})` };
|
|
1656
|
-
}
|
|
1657
|
-
const workspaces = Array.isArray(data?.workspaces) ? data.workspaces : [];
|
|
1658
|
-
setAvailableWorkspaces(workspaces);
|
|
1659
|
-
workspaceListHydratedRef.current = true;
|
|
1660
|
-
if (runtimeMode !== 'local' && typeof data?.currentWorkspaceId === 'string' && data.currentWorkspaceId.trim().length > 0) {
|
|
1661
|
-
applyResolvedWorkspaceId(data.currentWorkspaceId.trim(), { preserveNonDefaultDefault: true });
|
|
1662
|
-
}
|
|
1663
|
-
logBootstrapDebug('workspace_list_loaded', {
|
|
1664
|
-
workspaceCount: workspaces.length,
|
|
1665
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1666
|
-
});
|
|
1667
|
-
return { success: true, workspaces };
|
|
1668
|
-
}
|
|
1669
|
-
catch (error) {
|
|
1670
|
-
if (isAbortError(error)) {
|
|
1671
|
-
return { success: false, error: 'Workspace list request aborted.' };
|
|
1672
|
-
}
|
|
1673
|
-
if (isBootstrapTimeoutError(error)) {
|
|
1674
|
-
markBootstrapStalled('Startup checks timed out.');
|
|
1675
|
-
}
|
|
1676
|
-
setAvailableWorkspaces([]);
|
|
1677
|
-
workspaceListHydratedRef.current = false;
|
|
1678
|
-
return { success: false, error: 'Failed to load workspaces.' };
|
|
1679
|
-
}
|
|
1680
|
-
finally {
|
|
1681
|
-
if (workspaceListAbortRef.current === abortController) {
|
|
1682
|
-
workspaceListAbortRef.current = null;
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
}, [
|
|
1686
|
-
applyResolvedWorkspaceId,
|
|
1687
|
-
fetchWithTimeout,
|
|
1688
|
-
getWorkspaceRequestState,
|
|
1689
|
-
isAbortError,
|
|
1690
|
-
isWorkspaceEpochStale,
|
|
1691
|
-
isBootstrapTimeoutError,
|
|
1692
|
-
isWorkspaceRequestStale,
|
|
1693
|
-
markBootstrapStalled,
|
|
1694
|
-
runtimeMode,
|
|
1695
|
-
workspaceSwitchingEnabled
|
|
1696
|
-
]);
|
|
1697
|
-
const fetchAssigneeOptions = useCallback(async () => {
|
|
1698
|
-
const requestState = getWorkspaceRequestState();
|
|
1699
|
-
const abortController = new AbortController();
|
|
1700
|
-
assigneeOptionsAbortRef.current = abortController;
|
|
1701
|
-
setAssigneeOptionsLoaded(false);
|
|
1702
|
-
try {
|
|
1703
|
-
const [assigneeResult, workspaceMembersResult] = await Promise.all([
|
|
1704
|
-
fetchWithTimeout('/api/taskforce/workspace/assignee-options', {
|
|
1705
|
-
method: 'GET',
|
|
1706
|
-
credentials: 'include'
|
|
1707
|
-
}, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
|
|
1708
|
-
signal: abortController.signal
|
|
1709
|
-
}),
|
|
1710
|
-
(authSessionResolved && isAuthenticated)
|
|
1711
|
-
? fetchWithTimeout('/api/taskforce/auth/workspace-members', {
|
|
1712
|
-
method: 'GET',
|
|
1713
|
-
credentials: 'include'
|
|
1714
|
-
}, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
|
|
1715
|
-
signal: abortController.signal
|
|
1716
|
-
}).catch(() => null)
|
|
1717
|
-
: Promise.resolve(null)
|
|
1718
|
-
]);
|
|
1719
|
-
if (isWorkspaceRequestStale(requestState)) {
|
|
1720
|
-
return { success: false, error: 'Workspace changed while loading assignee options.' };
|
|
1721
|
-
}
|
|
1722
|
-
if (assigneeResult.status === 401) {
|
|
1723
|
-
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
1724
|
-
setAssigneeOptionsLoaded(true);
|
|
1725
|
-
return { success: false, error: 'Authentication required.' };
|
|
1726
|
-
}
|
|
1727
|
-
const data = await assigneeResult.json().catch(() => ({}));
|
|
1728
|
-
if (isWorkspaceRequestStale(requestState)) {
|
|
1729
|
-
return { success: false, error: 'Workspace changed while loading assignee options.' };
|
|
1730
|
-
}
|
|
1731
|
-
if (!assigneeResult.ok) {
|
|
1732
|
-
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
1733
|
-
setAssigneeOptionsLoaded(true);
|
|
1734
|
-
return { success: false, error: data?.error || `Failed to load assignee options (${assigneeResult.status})` };
|
|
1735
|
-
}
|
|
1736
|
-
const assignees = Array.isArray(data?.assignees) ? data.assignees : [];
|
|
1737
|
-
const directOptions = assignees
|
|
1738
|
-
.map((entry) => {
|
|
1739
|
-
const value = String(entry?.value || '').trim();
|
|
1740
|
-
const kindRaw = String(entry?.kind || '').trim().toLowerCase();
|
|
1741
|
-
if (!value)
|
|
1742
|
-
return null;
|
|
1743
|
-
if (kindRaw === 'agent' || kindRaw === 'ai') {
|
|
1744
|
-
return {
|
|
1745
|
-
value,
|
|
1746
|
-
label: String(entry?.label || value).trim() || value,
|
|
1747
|
-
icon: String(entry?.icon || 'Bot'),
|
|
1748
|
-
color: String(entry?.color || '#8b5cf6'),
|
|
1749
|
-
kind: 'agent'
|
|
1750
|
-
};
|
|
1751
|
-
}
|
|
1752
|
-
if (kindRaw === 'member' || kindRaw === 'human') {
|
|
1753
|
-
return {
|
|
1754
|
-
value,
|
|
1755
|
-
label: String(entry?.label || value).trim() || value,
|
|
1756
|
-
icon: String(entry?.icon || 'User'),
|
|
1757
|
-
color: String(entry?.color || '#22c55e'),
|
|
1758
|
-
kind: 'member'
|
|
1759
|
-
};
|
|
1760
|
-
}
|
|
1761
|
-
return null;
|
|
1762
|
-
})
|
|
1763
|
-
.filter((candidate) => Boolean(candidate));
|
|
1764
|
-
const fallbackCandidates = assignees
|
|
1765
|
-
.map((candidate) => ({
|
|
1766
|
-
userId: String(candidate?.userId || '').trim(),
|
|
1767
|
-
email: String(candidate?.email || '').trim().toLowerCase(),
|
|
1768
|
-
displayName: typeof candidate?.displayName === 'string' ? candidate.displayName : null,
|
|
1769
|
-
status: typeof candidate?.status === 'string' ? candidate.status : null,
|
|
1770
|
-
disabled: candidate?.disabled === true,
|
|
1771
|
-
}))
|
|
1772
|
-
.filter((candidate) => candidate.userId
|
|
1773
|
-
&& String(candidate.status || 'active').trim().toLowerCase() === 'active'
|
|
1774
|
-
&& candidate.disabled !== true);
|
|
1775
|
-
let memberCandidates = fallbackCandidates;
|
|
1776
|
-
if (workspaceMembersResult?.ok) {
|
|
1777
|
-
const workspaceMembersData = await workspaceMembersResult.json().catch(() => ({}));
|
|
1778
|
-
const users = Array.isArray(workspaceMembersData?.users) ? workspaceMembersData.users : [];
|
|
1779
|
-
const normalizedAuthUserId = String(authUserId || '').trim();
|
|
1780
|
-
const currentMember = users.find((candidate) => String(candidate?.userId || '').trim() === normalizedAuthUserId);
|
|
1781
|
-
const currentMemberRole = String(currentMember?.role || '').trim().toLowerCase();
|
|
1782
|
-
if (currentMemberRole === 'owner' || currentMemberRole === 'admin' || currentMemberRole === 'member' || currentMemberRole === 'read-only') {
|
|
1783
|
-
setHydratedWorkspaceRole(currentMemberRole);
|
|
1784
|
-
}
|
|
1785
|
-
else if (!workspaceSwitchingEnabled && users.length === 0) {
|
|
1786
|
-
setHydratedWorkspaceRole(null);
|
|
1787
|
-
}
|
|
1788
|
-
const workspaceMemberCandidates = users
|
|
1789
|
-
.map((candidate) => ({
|
|
1790
|
-
userId: String(candidate?.userId || '').trim(),
|
|
1791
|
-
email: String(candidate?.email || '').trim().toLowerCase(),
|
|
1792
|
-
displayName: typeof candidate?.displayName === 'string' ? candidate.displayName : null,
|
|
1793
|
-
status: typeof candidate?.status === 'string' ? candidate.status : null,
|
|
1794
|
-
disabled: candidate?.disabled === true,
|
|
1795
|
-
}))
|
|
1796
|
-
.filter((candidate) => candidate.userId
|
|
1797
|
-
&& String(candidate.status || 'active').trim().toLowerCase() === 'active'
|
|
1798
|
-
&& candidate.disabled !== true)
|
|
1799
|
-
.map((candidate) => ({
|
|
1800
|
-
userId: candidate.userId,
|
|
1801
|
-
email: candidate.email,
|
|
1802
|
-
displayName: candidate.displayName ?? null
|
|
1803
|
-
}));
|
|
1804
|
-
memberCandidates = Array.from(new Map([...fallbackCandidates, ...workspaceMemberCandidates].map((candidate) => [
|
|
1805
|
-
String(candidate.userId || '').trim(),
|
|
1806
|
-
candidate
|
|
1807
|
-
])).values()).filter((candidate) => String(candidate.userId || '').trim().length > 0);
|
|
1808
|
-
}
|
|
1809
|
-
else if (!workspaceSwitchingEnabled && !fallbackCandidates.length) {
|
|
1810
|
-
setHydratedWorkspaceRole(null);
|
|
1811
|
-
}
|
|
1812
|
-
const mergedOptions = mergeAssigneeOptions([
|
|
1813
|
-
...directOptions,
|
|
1814
|
-
...createWorkspaceAssigneeOptions(memberCandidates).filter((option) => option.kind === 'member')
|
|
1815
|
-
]);
|
|
1816
|
-
if (isWorkspaceRequestStale(requestState)) {
|
|
1817
|
-
return { success: false, error: 'Workspace changed while loading assignee options.' };
|
|
1818
|
-
}
|
|
1819
|
-
setAssigneeOptions(mergedOptions);
|
|
1820
|
-
setAssigneeOptionsLoaded(true);
|
|
1821
|
-
return { success: true };
|
|
1822
|
-
}
|
|
1823
|
-
catch (error) {
|
|
1824
|
-
if (isAbortError(error)) {
|
|
1825
|
-
return { success: false, error: 'Assignee options request aborted.' };
|
|
1826
|
-
}
|
|
1827
|
-
if (!isWorkspaceRequestStale(requestState)) {
|
|
1828
|
-
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
1829
|
-
setAssigneeOptionsLoaded(true);
|
|
1830
|
-
}
|
|
1831
|
-
return { success: false, error: 'Failed to load assignee options.' };
|
|
1832
|
-
}
|
|
1833
|
-
finally {
|
|
1834
|
-
if (assigneeOptionsAbortRef.current === abortController) {
|
|
1835
|
-
assigneeOptionsAbortRef.current = null;
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
}, [authSessionResolved, authUserId, fetchWithTimeout, getWorkspaceRequestState, isAbortError, isAuthenticated, isWorkspaceRequestStale, workspaceSwitchingEnabled]);
|
|
1839
1403
|
useEffect(() => {
|
|
1840
1404
|
if (!configLoaded || workspaceBootstrapPending)
|
|
1841
1405
|
return;
|
|
@@ -1857,162 +1421,44 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1857
1421
|
setHydratedWorkspaceRole(null);
|
|
1858
1422
|
}
|
|
1859
1423
|
}, [isAuthenticated]);
|
|
1860
|
-
const
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
return { success: false, error: '
|
|
1867
|
-
try {
|
|
1868
|
-
const res = await fetch('/api/taskforce/session/workspace', {
|
|
1869
|
-
method: 'POST',
|
|
1870
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1871
|
-
credentials: 'include',
|
|
1872
|
-
body: JSON.stringify({ workspaceId: normalizedWorkspaceId })
|
|
1873
|
-
});
|
|
1874
|
-
const data = await res.json().catch(() => ({}));
|
|
1875
|
-
if (!res.ok || data?.success === false) {
|
|
1876
|
-
return { success: false, error: data?.error || `Failed to switch workspace (${res.status})`, code: data?.code };
|
|
1877
|
-
}
|
|
1878
|
-
explicitWorkspaceSelectionRef.current = normalizedWorkspaceId;
|
|
1879
|
-
commitCurrentWorkspaceId(normalizedWorkspaceId, { clearExplicitSelection: false });
|
|
1880
|
-
abortWorkspaceScopedRequests();
|
|
1881
|
-
setWorkspaceBootstrapPending(true);
|
|
1882
|
-
markBootstrapPhase('workspace');
|
|
1883
|
-
const authenticated = await checkAuthSession({ force: true });
|
|
1884
|
-
if (options?.hydrate === false) {
|
|
1885
|
-
await fetchWorkspaces();
|
|
1886
|
-
}
|
|
1887
|
-
else {
|
|
1888
|
-
lastConfigRefreshKeyRef.current = `${runtimeMode}:${normalizedWorkspaceId}:${workspaceSelectionScope}`;
|
|
1889
|
-
lastAssigneeOptionsLoadKeyRef.current = `${runtimeMode}:${authenticated ? 'auth' : 'guest'}:${normalizedWorkspaceId}`;
|
|
1890
|
-
lastWorkspaceSyncStateLoadKeyRef.current = normalizedWorkspaceId;
|
|
1891
|
-
if (isOpen && activeTab === 'tasks') {
|
|
1892
|
-
lastTaskSurfaceLoadKeyRef.current = `${normalizedWorkspaceId}:${activeTab}:${showArchive ? 'archive' : 'active'}:${taskScope}`;
|
|
1893
|
-
}
|
|
1894
|
-
if (isOpen && activeTab === 'settings') {
|
|
1895
|
-
lastSettingsSurfaceLoadKeyRef.current = `${normalizedWorkspaceId}:${activeTab}:${settingsSection}`;
|
|
1896
|
-
}
|
|
1897
|
-
await Promise.all([
|
|
1898
|
-
fetchConfig(),
|
|
1899
|
-
fetchWorkspaces(),
|
|
1900
|
-
refreshTaskCollections({ isSilent: false }),
|
|
1901
|
-
fetchAssigneeOptions(),
|
|
1902
|
-
loadWorkspaceSyncState()
|
|
1903
|
-
]);
|
|
1904
|
-
}
|
|
1905
|
-
if (explicitWorkspaceSelectionRef.current === normalizedWorkspaceId) {
|
|
1906
|
-
explicitWorkspaceSelectionRef.current = null;
|
|
1907
|
-
}
|
|
1908
|
-
return { success: true };
|
|
1909
|
-
}
|
|
1910
|
-
catch {
|
|
1911
|
-
if (explicitWorkspaceSelectionRef.current === normalizedWorkspaceId) {
|
|
1912
|
-
explicitWorkspaceSelectionRef.current = null;
|
|
1913
|
-
}
|
|
1914
|
-
return { success: false, error: 'Failed to switch workspace.' };
|
|
1915
|
-
}
|
|
1916
|
-
finally {
|
|
1917
|
-
setWorkspaceBootstrapPending(false);
|
|
1424
|
+
const continueAfterCommercialOnboarding = useCallback(async () => {
|
|
1425
|
+
setBootstrapError(null);
|
|
1426
|
+
markBootstrapPhase('auth');
|
|
1427
|
+
const authenticated = await checkAuthSession({ force: true });
|
|
1428
|
+
if (!authenticated) {
|
|
1429
|
+
await fetchConfig({ ignoreAuthGuard: true });
|
|
1430
|
+
return { success: false, destination: 'login', error: 'Sign in required.', code: 'AUTH_REQUIRED' };
|
|
1918
1431
|
}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1924
|
-
try {
|
|
1925
|
-
if (!workspaceSwitchingEnabled) {
|
|
1926
|
-
const localWorkspaceId = String(currentWorkspaceId || '').trim() || 'default';
|
|
1927
|
-
commitCurrentWorkspaceId(localWorkspaceId);
|
|
1928
|
-
logBootstrapDebug('workspace_resolved', {
|
|
1929
|
-
workspaceId: localWorkspaceId,
|
|
1930
|
-
workspaceSetupRequired: false,
|
|
1931
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1932
|
-
});
|
|
1933
|
-
return { success: true, workspaceSetupRequired: false, workspaceId: localWorkspaceId };
|
|
1934
|
-
}
|
|
1935
|
-
const preferredWorkspaceId = String(options?.preferredWorkspaceId || '').trim();
|
|
1936
|
-
const persistedWorkspaceId = readPersistedWorkspaceId(workspaceSelectionScope);
|
|
1937
|
-
const workspacesResult = await fetchWorkspaces();
|
|
1938
|
-
if (!workspacesResult.success) {
|
|
1432
|
+
if (workspaceSwitchingEnabled) {
|
|
1433
|
+
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
1434
|
+
if (!workspaceResolution.success) {
|
|
1435
|
+
await fetchConfig({ ignoreAuthGuard: true });
|
|
1939
1436
|
return {
|
|
1940
1437
|
success: false,
|
|
1941
|
-
|
|
1942
|
-
error:
|
|
1438
|
+
destination: 'app',
|
|
1439
|
+
error: workspaceResolution.error || 'Unable to resolve workspace after plan selection.',
|
|
1440
|
+
code: workspaceResolution.code
|
|
1943
1441
|
};
|
|
1944
1442
|
}
|
|
1945
|
-
|
|
1946
|
-
if (workspaces.length === 0) {
|
|
1947
|
-
commitCurrentWorkspaceId('default');
|
|
1948
|
-
logBootstrapDebug('workspace_resolved', {
|
|
1949
|
-
workspaceId: 'default',
|
|
1950
|
-
workspaceSetupRequired: true,
|
|
1951
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1952
|
-
});
|
|
1953
|
-
return { success: true, workspaceSetupRequired: true };
|
|
1954
|
-
}
|
|
1955
|
-
const ids = new Set(workspaces.map((workspace) => String(workspace.id || '').trim()).filter(Boolean));
|
|
1956
|
-
const selectedWorkspaceId = ((preferredWorkspaceId && ids.has(preferredWorkspaceId) ? preferredWorkspaceId : '')
|
|
1957
|
-
|| (persistedWorkspaceId && ids.has(persistedWorkspaceId) ? persistedWorkspaceId : '')
|
|
1958
|
-
|| String(workspaces[0]?.id || '').trim());
|
|
1959
|
-
if (!selectedWorkspaceId) {
|
|
1960
|
-
commitCurrentWorkspaceId('default');
|
|
1961
|
-
return { success: true, workspaceSetupRequired: true };
|
|
1962
|
-
}
|
|
1963
|
-
const current = String(currentWorkspaceIdRef.current || '').trim();
|
|
1964
|
-
if (current !== selectedWorkspaceId) {
|
|
1965
|
-
const switched = await switchWorkspace(selectedWorkspaceId, { hydrate: false });
|
|
1966
|
-
if (!switched.success) {
|
|
1967
|
-
return {
|
|
1968
|
-
success: false,
|
|
1969
|
-
workspaceSetupRequired: false,
|
|
1970
|
-
error: switched.error || 'Failed to set workspace after authentication.',
|
|
1971
|
-
code: switched.code
|
|
1972
|
-
};
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
else {
|
|
1976
|
-
commitCurrentWorkspaceId(selectedWorkspaceId);
|
|
1977
|
-
}
|
|
1978
|
-
logBootstrapDebug('workspace_resolved', {
|
|
1979
|
-
workspaceId: selectedWorkspaceId,
|
|
1980
|
-
workspaceSetupRequired: false,
|
|
1981
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1982
|
-
});
|
|
1983
|
-
return { success: true, workspaceSetupRequired: false, workspaceId: selectedWorkspaceId };
|
|
1984
|
-
}
|
|
1985
|
-
catch {
|
|
1986
|
-
markBootstrapStalled('Unable to resolve workspace access.');
|
|
1987
|
-
return { success: false, workspaceSetupRequired: false, error: 'Failed to resolve workspace access.' };
|
|
1988
|
-
}
|
|
1989
|
-
finally {
|
|
1990
|
-
setWorkspaceBootstrapPending(false);
|
|
1991
|
-
}
|
|
1992
|
-
}, [
|
|
1993
|
-
commitCurrentWorkspaceId,
|
|
1994
|
-
fetchWorkspaces,
|
|
1995
|
-
markBootstrapPhase,
|
|
1996
|
-
markBootstrapStalled,
|
|
1997
|
-
workspaceSelectionScope,
|
|
1998
|
-
switchWorkspace,
|
|
1999
|
-
workspaceSwitchingEnabled
|
|
2000
|
-
]);
|
|
2001
|
-
const retryBootstrapChecks = useCallback(async () => {
|
|
2002
|
-
setBootstrapError(null);
|
|
2003
|
-
markBootstrapPhase('auth');
|
|
2004
|
-
const authenticated = await checkAuthSession({ force: true });
|
|
2005
|
-
if (workspaceSwitchingEnabled && authenticated) {
|
|
2006
|
-
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
2007
|
-
if (!workspaceResolution.success || workspaceResolution.workspaceSetupRequired) {
|
|
1443
|
+
if (workspaceResolution.workspaceSetupRequired) {
|
|
2008
1444
|
await fetchConfig({ ignoreAuthGuard: true });
|
|
2009
|
-
return;
|
|
1445
|
+
return { success: true, destination: 'setup' };
|
|
2010
1446
|
}
|
|
2011
1447
|
}
|
|
2012
|
-
await
|
|
1448
|
+
await fetchBootstrapConfig();
|
|
1449
|
+
await Promise.all([
|
|
1450
|
+
fetchTasks(true, { ignoreAuthGuard: true }),
|
|
1451
|
+
fetchReferenceData(),
|
|
1452
|
+
fetchPlanningEntities()
|
|
1453
|
+
]);
|
|
1454
|
+
return { success: true, destination: 'app' };
|
|
2013
1455
|
}, [
|
|
2014
1456
|
checkAuthSession,
|
|
1457
|
+
fetchBootstrapConfig,
|
|
2015
1458
|
fetchConfig,
|
|
1459
|
+
fetchPlanningEntities,
|
|
1460
|
+
fetchReferenceData,
|
|
1461
|
+
fetchTasks,
|
|
2016
1462
|
markBootstrapPhase,
|
|
2017
1463
|
resolveWorkspaceAfterAuth,
|
|
2018
1464
|
workspaceSwitchingEnabled
|
|
@@ -2106,6 +1552,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2106
1552
|
}, [refreshSetupContext]);
|
|
2107
1553
|
const saveWorkspaceProfile = useCallback(async (input) => {
|
|
2108
1554
|
const name = String(input?.name || '').trim();
|
|
1555
|
+
const requestedWorkspaceId = typeof input?.workspaceId === 'string'
|
|
1556
|
+
? input.workspaceId.trim()
|
|
1557
|
+
: '';
|
|
2109
1558
|
if (!name)
|
|
2110
1559
|
return { success: false, error: 'Workspace name is required.', code: 'WORKSPACE_NAME_REQUIRED' };
|
|
2111
1560
|
try {
|
|
@@ -2117,7 +1566,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2117
1566
|
},
|
|
2118
1567
|
credentials: 'include',
|
|
2119
1568
|
body: JSON.stringify({
|
|
2120
|
-
workspaceId:
|
|
1569
|
+
workspaceId: requestedWorkspaceId && requestedWorkspaceId !== 'default'
|
|
1570
|
+
? requestedWorkspaceId
|
|
1571
|
+
: undefined,
|
|
2121
1572
|
name,
|
|
2122
1573
|
description: typeof input.description === 'string' ? input.description : undefined,
|
|
2123
1574
|
allowCreate: true
|
|
@@ -2180,6 +1631,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2180
1631
|
};
|
|
2181
1632
|
}
|
|
2182
1633
|
if (workspaceResolution.workspaceSetupRequired) {
|
|
1634
|
+
await fetchConfig({ ignoreAuthGuard: true });
|
|
2183
1635
|
return { success: true, workspaceSetupRequired: true };
|
|
2184
1636
|
}
|
|
2185
1637
|
await Promise.all([
|
|
@@ -2226,15 +1678,64 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2226
1678
|
});
|
|
2227
1679
|
const data = await res.json().catch(() => ({}));
|
|
2228
1680
|
if (!res.ok || !data?.success) {
|
|
2229
|
-
return {
|
|
1681
|
+
return {
|
|
1682
|
+
success: false,
|
|
1683
|
+
error: data?.error || 'Create account failed.',
|
|
1684
|
+
code: data?.code,
|
|
1685
|
+
verificationRequired: Boolean(data?.verificationRequired),
|
|
1686
|
+
verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
|
|
1687
|
+
emailSent: data?.emailSent !== false,
|
|
1688
|
+
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
const planSelectionRequired = data?.planSelectionRequired === true;
|
|
1692
|
+
const checkoutPending = data?.checkoutPending === true;
|
|
1693
|
+
const commercialState = typeof data?.commercialState === 'string' ? data.commercialState : null;
|
|
1694
|
+
const verificationRequired = Boolean(data?.verificationRequired);
|
|
1695
|
+
if (!verificationRequired) {
|
|
1696
|
+
setError('');
|
|
1697
|
+
await checkAuthSession({ force: true });
|
|
1698
|
+
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
1699
|
+
if (!workspaceResolution.success) {
|
|
1700
|
+
return {
|
|
1701
|
+
success: false,
|
|
1702
|
+
error: workspaceResolution.error || 'Unable to resolve workspace after registration.',
|
|
1703
|
+
code: workspaceResolution.code
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
const hasCommercialOnboardingGate = Boolean(planSelectionRequired
|
|
1707
|
+
|| checkoutPending
|
|
1708
|
+
|| commercialState === 'pending_plan_selection'
|
|
1709
|
+
|| commercialState === 'checkout_pending');
|
|
1710
|
+
if (workspaceResolution.workspaceSetupRequired || hasCommercialOnboardingGate) {
|
|
1711
|
+
await fetchConfig({ ignoreAuthGuard: true });
|
|
1712
|
+
}
|
|
1713
|
+
else {
|
|
1714
|
+
await Promise.all([
|
|
1715
|
+
fetchConfig({ ignoreAuthGuard: true }),
|
|
1716
|
+
fetchTasks(true, { ignoreAuthGuard: true })
|
|
1717
|
+
]);
|
|
1718
|
+
void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
|
|
1719
|
+
}
|
|
1720
|
+
return {
|
|
1721
|
+
success: true,
|
|
1722
|
+
workspaceSetupRequired: workspaceResolution.workspaceSetupRequired,
|
|
1723
|
+
planSelectionRequired,
|
|
1724
|
+
checkoutPending,
|
|
1725
|
+
commercialState,
|
|
1726
|
+
verificationRequired,
|
|
1727
|
+
verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
|
|
1728
|
+
emailSent: data?.emailSent !== false,
|
|
1729
|
+
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
1730
|
+
};
|
|
2230
1731
|
}
|
|
2231
1732
|
return {
|
|
2232
1733
|
success: true,
|
|
2233
1734
|
workspaceSetupRequired: Boolean(data?.workspaceSetupRequired),
|
|
2234
|
-
planSelectionRequired
|
|
2235
|
-
checkoutPending
|
|
2236
|
-
commercialState
|
|
2237
|
-
verificationRequired
|
|
1735
|
+
planSelectionRequired,
|
|
1736
|
+
checkoutPending,
|
|
1737
|
+
commercialState,
|
|
1738
|
+
verificationRequired,
|
|
2238
1739
|
verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
|
|
2239
1740
|
emailSent: data?.emailSent !== false,
|
|
2240
1741
|
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
@@ -2243,7 +1744,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2243
1744
|
catch {
|
|
2244
1745
|
return { success: false, error: 'Create account failed.' };
|
|
2245
1746
|
}
|
|
2246
|
-
}, [resolveCloudAuthUrl, cloudAuthConfigured]);
|
|
1747
|
+
}, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
|
|
2247
1748
|
const updateCurrentUserProfile = useCallback(async (input) => {
|
|
2248
1749
|
if (!cloudAuthConfigured) {
|
|
2249
1750
|
return { success: false, error: 'Cloud auth endpoint is not configured.' };
|
|
@@ -2305,6 +1806,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2305
1806
|
success: true,
|
|
2306
1807
|
verificationToken: data?.verificationToken ?? null,
|
|
2307
1808
|
emailSent: data?.emailSent !== false,
|
|
1809
|
+
deliveryAttempted: data?.deliveryAttempted === true
|
|
1810
|
+
|| (typeof data?.verificationToken === 'string' && data.verificationToken.trim().length > 0),
|
|
2308
1811
|
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
2309
1812
|
};
|
|
2310
1813
|
}
|
|
@@ -2358,6 +1861,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2358
1861
|
success: true,
|
|
2359
1862
|
resetToken: data?.resetToken ?? null,
|
|
2360
1863
|
emailSent: data?.emailSent !== false,
|
|
1864
|
+
deliveryAttempted: data?.deliveryAttempted === true
|
|
1865
|
+
|| (typeof data?.resetToken === 'string' && data.resetToken.trim().length > 0),
|
|
2361
1866
|
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
2362
1867
|
};
|
|
2363
1868
|
}
|
|
@@ -2492,7 +1997,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2492
1997
|
return { success: false, error: data?.error || 'Invite join failed.', code: data?.code };
|
|
2493
1998
|
}
|
|
2494
1999
|
setError('');
|
|
2495
|
-
await checkAuthSession();
|
|
2000
|
+
await checkAuthSession({ force: true });
|
|
2496
2001
|
const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
|
|
2497
2002
|
const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
|
|
2498
2003
|
if (!workspaceResolution.success) {
|
|
@@ -2516,6 +2021,34 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2516
2021
|
return { success: false, error: 'Invite join failed.' };
|
|
2517
2022
|
}
|
|
2518
2023
|
}, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
|
|
2024
|
+
const resetCloudLogoutClientState = useCallback(() => {
|
|
2025
|
+
abortWorkspaceScopedRequests();
|
|
2026
|
+
referenceDataRequestRef.current = null;
|
|
2027
|
+
lastLoadedUiStateKeyRef.current = '';
|
|
2028
|
+
lastAssigneeOptionsLoadKeyRef.current = '';
|
|
2029
|
+
lastConfigRefreshKeyRef.current = '';
|
|
2030
|
+
lastWorkspaceSyncStateLoadKeyRef.current = '';
|
|
2031
|
+
lastTaskSurfaceLoadKeyRef.current = '';
|
|
2032
|
+
lastSettingsSurfaceLoadKeyRef.current = '';
|
|
2033
|
+
setUiStateReady(false);
|
|
2034
|
+
setReferenceDataLoaded(false);
|
|
2035
|
+
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
2036
|
+
setAssigneeOptionsLoaded(false);
|
|
2037
|
+
setDeletedTasks([]);
|
|
2038
|
+
setTasks([]);
|
|
2039
|
+
setArchivedTasks([]);
|
|
2040
|
+
setInitiatives([]);
|
|
2041
|
+
setWorkstreams([]);
|
|
2042
|
+
setLoadingTasks(false);
|
|
2043
|
+
if (cloudRealtimeFollowUpRefreshTimerRef.current !== null && typeof window !== 'undefined') {
|
|
2044
|
+
window.clearTimeout(cloudRealtimeFollowUpRefreshTimerRef.current);
|
|
2045
|
+
cloudRealtimeFollowUpRefreshTimerRef.current = null;
|
|
2046
|
+
}
|
|
2047
|
+
if (archiveBootstrapTimerRef.current !== null && typeof window !== 'undefined') {
|
|
2048
|
+
window.clearTimeout(archiveBootstrapTimerRef.current);
|
|
2049
|
+
archiveBootstrapTimerRef.current = null;
|
|
2050
|
+
}
|
|
2051
|
+
}, [abortWorkspaceScopedRequests, setArchivedTasks, setTasks]);
|
|
2519
2052
|
const logout = useCallback(async () => {
|
|
2520
2053
|
try {
|
|
2521
2054
|
if (cloudAuthConfigured) {
|
|
@@ -2529,6 +2062,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2529
2062
|
// no-op
|
|
2530
2063
|
}
|
|
2531
2064
|
finally {
|
|
2065
|
+
abortWorkspaceScopedRequests();
|
|
2066
|
+
authSessionEpochRef.current += 1;
|
|
2532
2067
|
authSessionRequestRef.current = null;
|
|
2533
2068
|
authSessionLastResultRef.current = false;
|
|
2534
2069
|
authSessionLastCheckedAtRef.current = Date.now();
|
|
@@ -2543,19 +2078,18 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2543
2078
|
setUserGlobalSyncStatus('disconnected');
|
|
2544
2079
|
setUserGlobalSyncError(null);
|
|
2545
2080
|
setHasBetaAccess(true);
|
|
2081
|
+
setProjectName('');
|
|
2082
|
+
setConfigLoaded(false);
|
|
2083
|
+
setReferenceDataLoaded(false);
|
|
2084
|
+
setSetupState(null);
|
|
2085
|
+
setBootstrapPhase('idle');
|
|
2086
|
+
setBootstrapError(null);
|
|
2087
|
+
hasLoadedUiStateRef.current = false;
|
|
2088
|
+
if (runtimeMode !== 'local') {
|
|
2089
|
+
clearPersistedWorkspaceId(workspaceSelectionScope);
|
|
2090
|
+
}
|
|
2546
2091
|
setAuthBlocked(runtimeMode === 'cloud' && authRequiredForApi);
|
|
2547
|
-
|
|
2548
|
-
const existing = String(prev || '').trim();
|
|
2549
|
-
let resolvedWorkspaceId = 'default';
|
|
2550
|
-
if (existing && existing.toLowerCase() !== 'default') {
|
|
2551
|
-
resolvedWorkspaceId = existing;
|
|
2552
|
-
}
|
|
2553
|
-
else if (runtimeMode !== 'local') {
|
|
2554
|
-
resolvedWorkspaceId = readPersistedWorkspaceId(workspaceSelectionScope) || 'default';
|
|
2555
|
-
}
|
|
2556
|
-
currentWorkspaceIdRef.current = resolvedWorkspaceId;
|
|
2557
|
-
return resolvedWorkspaceId;
|
|
2558
|
-
});
|
|
2092
|
+
commitCurrentWorkspaceId('default');
|
|
2559
2093
|
setAvailableWorkspaces([]);
|
|
2560
2094
|
workspaceListHydratedRef.current = false;
|
|
2561
2095
|
setHydratedWorkspaceRole(null);
|
|
@@ -2568,12 +2102,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2568
2102
|
]);
|
|
2569
2103
|
return;
|
|
2570
2104
|
}
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
setWorkstreams([]);
|
|
2105
|
+
hasBootstrappedDataRef.current = false;
|
|
2106
|
+
resetCloudLogoutClientState();
|
|
2574
2107
|
}
|
|
2575
2108
|
}, [
|
|
2109
|
+
abortWorkspaceScopedRequests,
|
|
2576
2110
|
authRequiredForApi,
|
|
2111
|
+
clearPersistedWorkspaceId,
|
|
2112
|
+
commitCurrentWorkspaceId,
|
|
2577
2113
|
resolveCloudAuthUrl,
|
|
2578
2114
|
cloudAuthConfigured,
|
|
2579
2115
|
workspaceSelectionScope,
|
|
@@ -2581,7 +2117,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2581
2117
|
fetchConfig,
|
|
2582
2118
|
fetchPlanningEntities,
|
|
2583
2119
|
fetchReferenceData,
|
|
2584
|
-
fetchTasks
|
|
2120
|
+
fetchTasks,
|
|
2121
|
+
resetCloudLogoutClientState
|
|
2585
2122
|
]);
|
|
2586
2123
|
const createInitiativeFromTemplate = useCallback(async (payload) => {
|
|
2587
2124
|
try {
|
|
@@ -2699,7 +2236,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2699
2236
|
}, [fetchPlanningEntities]);
|
|
2700
2237
|
// Initialize auth once, then bootstrap protected data once auth gates are settled.
|
|
2701
2238
|
const hasInitializedAuthRef = useRef(false);
|
|
2702
|
-
const hasBootstrappedDataRef = useRef(false);
|
|
2703
2239
|
useEffect(() => {
|
|
2704
2240
|
setIsOpen(true); // Always open in standalone-first
|
|
2705
2241
|
if (hasInitializedAuthRef.current)
|
|
@@ -2958,73 +2494,19 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2958
2494
|
userId: normalizedRealtimeUserId || undefined
|
|
2959
2495
|
});
|
|
2960
2496
|
// Fallback sync for cross-process writes (e.g., MCP tools) that may miss watcher/websocket events.
|
|
2961
|
-
|
|
2962
|
-
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
2963
|
-
return;
|
|
2964
|
-
if (shouldSkipDataVersionPolling({
|
|
2965
|
-
isOpen,
|
|
2966
|
-
authBlocked,
|
|
2967
|
-
authRequiredForApi,
|
|
2968
|
-
isAuthenticated,
|
|
2969
|
-
canCallProtectedApi: authGuardPolicy.canCallProtectedApi
|
|
2970
|
-
}))
|
|
2971
|
-
return;
|
|
2972
|
-
let cancelled = false;
|
|
2973
|
-
const checkDataVersion = async () => {
|
|
2974
|
-
if (cancelled || document.visibilityState !== 'visible')
|
|
2975
|
-
return;
|
|
2976
|
-
if (shouldSkipDataVersionTick({
|
|
2977
|
-
shouldGateProtectedApiCalls,
|
|
2978
|
-
authSessionResolved: authSessionResolvedRef.current,
|
|
2979
|
-
authRequiredForApi: authRequiredForApiRef.current,
|
|
2980
|
-
isAuthenticated: isAuthenticatedRef.current
|
|
2981
|
-
}))
|
|
2982
|
-
return;
|
|
2983
|
-
try {
|
|
2984
|
-
const res = await fetch('/api/taskforce/data-version');
|
|
2985
|
-
if (!res.ok)
|
|
2986
|
-
return;
|
|
2987
|
-
const data = await res.json();
|
|
2988
|
-
const version = Number(data?.dataVersion);
|
|
2989
|
-
if (!Number.isFinite(version))
|
|
2990
|
-
return;
|
|
2991
|
-
if (dataVersionRef.current === null) {
|
|
2992
|
-
dataVersionRef.current = version;
|
|
2993
|
-
return;
|
|
2994
|
-
}
|
|
2995
|
-
if (version !== dataVersionRef.current) {
|
|
2996
|
-
dataVersionRef.current = version;
|
|
2997
|
-
await refreshTaskCollectionsForDataVersion();
|
|
2998
|
-
}
|
|
2999
|
-
}
|
|
3000
|
-
catch {
|
|
3001
|
-
// Keep polling best-effort and silent.
|
|
3002
|
-
}
|
|
3003
|
-
};
|
|
3004
|
-
const onVisibilityChange = () => {
|
|
3005
|
-
if (document.visibilityState === 'visible') {
|
|
3006
|
-
checkDataVersion();
|
|
3007
|
-
}
|
|
3008
|
-
};
|
|
3009
|
-
const intervalId = window.setInterval(checkDataVersion, 3000);
|
|
3010
|
-
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
3011
|
-
checkDataVersion();
|
|
3012
|
-
return () => {
|
|
3013
|
-
cancelled = true;
|
|
3014
|
-
window.clearInterval(intervalId);
|
|
3015
|
-
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
3016
|
-
};
|
|
3017
|
-
}, [
|
|
2497
|
+
useDataVersionRefresh({
|
|
3018
2498
|
isOpen,
|
|
3019
|
-
fetchTasks,
|
|
3020
|
-
fetchArchive,
|
|
3021
2499
|
authBlocked,
|
|
3022
2500
|
authRequiredForApi,
|
|
3023
2501
|
isAuthenticated,
|
|
3024
|
-
authGuardPolicy,
|
|
2502
|
+
canCallProtectedApi: authGuardPolicy.canCallProtectedApi,
|
|
3025
2503
|
shouldGateProtectedApiCalls,
|
|
2504
|
+
authSessionResolvedRef,
|
|
2505
|
+
authRequiredForApiRef,
|
|
2506
|
+
isAuthenticatedRef,
|
|
2507
|
+
dataVersionRef,
|
|
3026
2508
|
refreshTaskCollectionsForDataVersion
|
|
3027
|
-
|
|
2509
|
+
});
|
|
3028
2510
|
const relationshipTasks = useMemo(() => {
|
|
3029
2511
|
const byId = new Map();
|
|
3030
2512
|
for (const task of archivedTasks)
|
|
@@ -3251,6 +2733,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3251
2733
|
pushWorkspaceChangesToCloud,
|
|
3252
2734
|
workspacePendingSignatureRef,
|
|
3253
2735
|
workspaceDeletedTaskIdsRef,
|
|
2736
|
+
workspaceDeletedTaskWatermarksRef,
|
|
3254
2737
|
});
|
|
3255
2738
|
const { autoSaveState, flushAutoSave, scheduleWarningPrompt, handleSubmit, confirmScheduleWarning, cancelScheduleWarning, handleAddComment } = useTaskFormActions({
|
|
3256
2739
|
activeCategories,
|
|
@@ -3520,6 +3003,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3520
3003
|
workspaceLastErrorMessage,
|
|
3521
3004
|
workspaceLastSuccessfulSyncAt,
|
|
3522
3005
|
workspaceSyncPhase,
|
|
3006
|
+
workspaceSyncSetupIntent,
|
|
3523
3007
|
workspaceSyncStatus,
|
|
3524
3008
|
workspaceSyncSummary,
|
|
3525
3009
|
workspaceSyncRecommendedAction,
|
|
@@ -3548,6 +3032,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3548
3032
|
runtimeCapabilities,
|
|
3549
3033
|
refreshSetupContext,
|
|
3550
3034
|
retryBootstrapChecks,
|
|
3035
|
+
continueAfterCommercialOnboarding,
|
|
3551
3036
|
saveWorkspaceProfile,
|
|
3552
3037
|
fetchWorkspaces,
|
|
3553
3038
|
createWorkspace,
|