@taskforcehq/taskforce 0.3.305 → 0.3.306
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 +295 -89
- package/dist/TaskforceCore.js +215 -43
- package/dist/TaskforceCore.test.js +433 -28
- package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/assets/fonts/Noir_medium.woff2 +0 -0
- package/dist/assets/fonts/Noir_regular.woff2 +0 -0
- package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/compat/workspaceSyncCompat.js +46 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +549 -0
- package/dist/components/features/AgentsModule.d.ts +10 -1
- package/dist/components/features/AgentsModule.js +260 -15
- package/dist/components/features/AgentsModule.test.js +255 -5
- package/dist/components/features/DocumentViewer.d.ts +5 -1
- package/dist/components/features/DocumentViewer.js +60 -3
- package/dist/components/features/DocumentWorkspace.js +1 -1
- package/dist/components/features/DocumentWorkspace.test.js +33 -0
- package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
- package/dist/components/task/TaskCard.js +19 -28
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskForm.js +85 -90
- package/dist/components/task/TaskForm.test.js +110 -8
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +94 -13
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +70 -14
- package/dist/components/views/PlanComparisonPage.test.js +238 -0
- package/dist/components/views/PlansPage.js +127 -36
- package/dist/components/views/PlansPage.test.js +294 -12
- package/dist/components/views/StandaloneLayout.js +283 -533
- package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
- package/dist/components/views/panels/PlanningDrawer.js +18 -11
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
- package/dist/components/views/planningScope.d.ts +20 -0
- package/dist/components/views/planningScope.js +25 -0
- package/dist/components/views/planningScope.test.d.ts +1 -0
- package/dist/components/views/planningScope.test.js +60 -0
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
- package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
- package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
- package/dist/core/AiProfileService.d.ts +39 -2
- package/dist/core/AiProfileService.js +639 -73
- package/dist/core/AiProfileService.test.js +19 -1
- package/dist/core/AiProfiles.test.js +566 -7
- package/dist/core/AttachmentLinkService.js +57 -15
- package/dist/core/AuthIdentityService.d.ts +107 -0
- package/dist/core/AuthIdentityService.js +284 -0
- package/dist/core/AuthTokenService.d.ts +4 -0
- package/dist/core/AuthTokenService.js +34 -6
- package/dist/core/AuthTokenService.test.js +3 -3
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +115 -6
- package/dist/core/PlanEntitlementService.d.ts +23 -2
- package/dist/core/PlanEntitlementService.js +343 -57
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +35 -2
- package/dist/core/PlanningEntities.test.js +52 -0
- package/dist/core/SignupMonetizationSettings.test.js +199 -27
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +42 -42
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/Taskforce.d.ts +225 -14
- package/dist/core/Taskforce.js +765 -192
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
- package/dist/core/WorkspaceLifecycleService.js +0 -5
- package/dist/core/WorkspacePermissions.test.js +25 -1
- package/dist/core/WorkspacePlanMode.test.js +117 -16
- package/dist/core/shared.d.ts +1 -1
- package/dist/core/shared.js +5 -1
- package/dist/core/types.d.ts +68 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
- package/dist/hooks/sync/bootstrap.js +2 -0
- package/dist/hooks/sync/controlPlane.d.ts +37 -0
- package/dist/hooks/sync/controlPlane.js +78 -0
- package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
- package/dist/hooks/sync/controlPlane.test.js +121 -0
- package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
- package/dist/hooks/sync/lifecyclePolicy.js +93 -0
- package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
- package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
- package/dist/hooks/sync/orchestratorShared.js +68 -47
- package/dist/hooks/sync/orchestratorShared.test.js +50 -1
- package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
- package/dist/hooks/sync/pullLifecycle.js +24 -0
- package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
- package/dist/hooks/sync/pullLifecycle.test.js +80 -0
- package/dist/hooks/sync/recovery.d.ts +16 -2
- package/dist/hooks/sync/recovery.js +171 -53
- package/dist/hooks/sync/recovery.test.d.ts +1 -0
- package/dist/hooks/sync/recovery.test.js +292 -0
- package/dist/hooks/sync/transfers.d.ts +15 -3
- package/dist/hooks/sync/transfers.js +179 -62
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +396 -0
- package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
- package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
- package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
- package/dist/hooks/sync/useSyncStatusActions.js +88 -0
- package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
- package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
- package/dist/hooks/sync/useSyncStatusControls.js +60 -0
- package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
- package/dist/hooks/useSyncOrchestrator.js +359 -178
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskforce.d.ts +60 -2
- package/dist/hooks/useTaskforce.js +549 -240
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +6 -1
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
- package/dist/localization/locales/en-US.d.ts +0 -1
- package/dist/localization/locales/en-US.js +0 -1
- package/dist/localization/locales/es-419.js +0 -1
- package/dist/localization/locales/pt-BR.js +0 -1
- package/dist/mcp/documentAssetRegistrar.js +3 -3
- package/dist/mcp/runtime.d.ts +2 -0
- package/dist/mcp/runtime.js +131 -64
- package/dist/mcp/runtime.test.js +291 -12
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +171 -61
- package/dist/migrations/taskSchemaMigrations.test.js +15 -0
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/dist/resources/templates/workflows/collaborate.yaml +1 -1
- package/dist/resources/templates/workflows/evaluate.yaml +2 -2
- package/dist/resources/templates/workflows/plan.yaml +1 -1
- package/dist/resources/templates/workflows/review.yaml +2 -2
- package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
- package/dist/server/auth/providers/apple.d.ts +33 -0
- package/dist/server/auth/providers/apple.js +82 -0
- package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
- package/dist/server/auth/providers/appleClientSecret.js +65 -0
- package/dist/server/auth/providers/github.d.ts +26 -0
- package/dist/server/auth/providers/github.js +86 -0
- package/dist/server/auth/providers/google.d.ts +21 -0
- package/dist/server/auth/providers/google.js +56 -0
- package/dist/server/auth/providers/types.d.ts +21 -0
- package/dist/server/auth/providers/types.js +1 -0
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.js +6 -3
- package/dist/server/documentReviewRoutes.test.js +36 -3
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +120 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +106 -3
- package/dist/server/routes/admin.d.ts +3 -0
- package/dist/server/routes/admin.js +272 -19
- package/dist/server/routes/auth.d.ts +15 -2
- package/dist/server/routes/auth.js +994 -57
- package/dist/server/routes/authSupport.d.ts +1 -1
- package/dist/server/routes/authSupport.js +1 -1
- package/dist/server/routes/billing.js +361 -35
- package/dist/server/routes/billing.test.js +592 -12
- package/dist/server/routes/documents.d.ts +1 -0
- package/dist/server/routes/documents.js +172 -70
- package/dist/server/routes/shared.d.ts +28 -2
- package/dist/server/routes/shared.js +69 -4
- package/dist/server/routes/sync.integration.test.js +437 -39
- package/dist/server/routes/sync.js +61 -605
- package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
- package/dist/server/routes/syncAuxRoutes.js +91 -0
- package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
- package/dist/server/routes/syncAuxRoutes.test.js +158 -0
- package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
- package/dist/server/routes/syncPullApplyRoutes.js +204 -0
- package/dist/server/routes/syncPushRoutes.d.ts +24 -0
- package/dist/server/routes/syncPushRoutes.js +212 -0
- package/dist/server/routes/syncRouteGuards.d.ts +36 -0
- package/dist/server/routes/syncRouteGuards.js +67 -0
- package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
- package/dist/server/routes/syncRouteGuards.test.js +94 -0
- package/dist/server/routes/syncRouteTypes.d.ts +13 -0
- package/dist/server/routes/syncRouteTypes.js +1 -0
- package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
- package/dist/server/routes/syncSnapshotRoutes.js +180 -0
- package/dist/server/routes/tasks.js +43 -1
- package/dist/server/routes/workspaces.js +36 -5
- package/dist/server/routes.js +60 -3
- package/dist/server/routes.test.js +1824 -171
- package/dist/services/workflowExportSnapshots.test.js +2 -0
- package/dist/shared/aiProfileSeatScope.d.ts +5 -0
- package/dist/shared/aiProfileSeatScope.js +21 -0
- package/dist/shared/passwordPolicy.d.ts +13 -0
- package/dist/shared/passwordPolicy.js +84 -0
- package/dist/shared/passwordPolicy.test.d.ts +1 -0
- package/dist/shared/passwordPolicy.test.js +34 -0
- package/dist/storage/workspaceAssetStore.d.ts +15 -3
- package/dist/storage/workspaceAssetStore.js +132 -44
- package/dist/storage/workspaceAssetStore.test.js +93 -6
- package/dist/styles/fonts.css +70 -0
- package/dist/sync/collaborationSyncPayload.d.ts +6 -1
- package/dist/sync/collaborationSyncPayload.js +17 -11
- package/dist/sync/collaborationSyncPayload.test.js +13 -1
- package/dist/sync/contentSyncState.d.ts +4 -8
- package/dist/sync/contentSyncState.js +17 -70
- package/dist/sync/syncApplyHandlers.d.ts +7 -0
- package/dist/sync/syncApplyHandlers.js +28 -13
- package/dist/sync/syncApplyHandlers.test.js +70 -3
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +44 -0
- package/dist/sync/syncResourceAdapters.js +77 -0
- package/dist/sync/syncService.d.ts +5 -0
- package/dist/sync/syncService.js +52 -0
- package/dist/sync/syncService.test.js +57 -3
- package/dist/sync/taskSyncChangeKey.d.ts +2 -0
- package/dist/sync/taskSyncChangeKey.js +143 -0
- package/dist/sync/taskSyncPayload.js +3 -8
- package/dist/sync/workspacePullFeed.js +4 -3
- package/dist/sync/workspacePullFeed.test.js +43 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +45 -0
- package/dist/sync/workspaceSyncModel.js +183 -28
- package/dist/sync/workspaceSyncModel.test.js +291 -12
- package/dist/sync/workspaceSyncState.d.ts +17 -0
- package/dist/sync/workspaceSyncState.js +2 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +17 -0
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
- package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
- package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
- package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
- package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
- package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
- package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
- package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
- package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
- package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
- package/dist/ui/assets/index-C6k75jr8.js +6 -0
- package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
- package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/ui/fonts/Noir_medium.otf +0 -0
- package/dist/ui/fonts/Noir_medium.woff2 +0 -0
- package/dist/ui/fonts/Noir_regular.otf +0 -0
- package/dist/ui/fonts/Noir_regular.woff2 +0 -0
- package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/assignees.d.ts +2 -0
- package/dist/utils/assignees.js +2 -0
- package/dist/utils/avatarUpload.d.ts +27 -0
- package/dist/utils/avatarUpload.js +132 -0
- package/dist/utils/avatarUpload.test.d.ts +1 -0
- package/dist/utils/avatarUpload.test.js +40 -0
- package/dist/utils/bootstrapTrace.d.ts +7 -0
- package/dist/utils/bootstrapTrace.js +29 -0
- package/dist/utils/commercialLifecyclePresentation.js +3 -0
- package/dist/utils/contextAssetEvents.d.ts +2 -0
- package/dist/utils/syncEventDetails.d.ts +3 -0
- package/dist/utils/syncEventDetails.js +64 -0
- package/dist/utils/syncEventDetails.test.d.ts +1 -0
- package/dist/utils/syncEventDetails.test.js +23 -0
- package/dist/utils/syncEventPresentation.d.ts +15 -0
- package/dist/utils/syncEventPresentation.js +129 -0
- package/dist/utils/syncEventPresentation.test.d.ts +1 -0
- package/dist/utils/syncEventPresentation.test.js +64 -0
- package/dist/utils/syncStatusPresentation.d.ts +72 -0
- package/dist/utils/syncStatusPresentation.js +217 -0
- package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
- package/dist/utils/syncStatusPresentation.test.js +164 -0
- package/dist/utils/taskActivity.d.ts +2 -0
- package/dist/utils/taskActivity.js +228 -22
- package/dist/utils/taskActivity.test.js +119 -8
- package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
- package/dist/utils/workspaceSyncPresentation.js +68 -2
- package/dist/utils/workspaceSyncPresentation.test.js +36 -1
- package/package.json +3 -2
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/run-e2e-realtime.sh +17 -3
- package/scripts/run-smoke.sh +17 -5
- package/scripts/run-soak.sh +17 -5
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/src/resources/templates/workflows/collaborate.yaml +1 -1
- package/src/resources/templates/workflows/evaluate.yaml +2 -2
- package/src/resources/templates/workflows/plan.yaml +1 -1
- package/src/resources/templates/workflows/review.yaml +2 -2
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/index-C2-OXZV7.css +0 -1
- package/dist/ui/assets/index-DcSI5F86.js +0 -6
- package/scripts/migrate-planning-model.ts +0 -233
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* === SECTION: Navigation === Line ~3410 — setActiveTab, handleNavigation
|
|
28
28
|
* === SECTION: Form === Line ~3489 — resetForm, handleEdit, handleSubmit, saveTask
|
|
29
29
|
* === SECTION: Task CRUD === Line ~3691 — handleDelete, handleUpdateTask, handleToggle*
|
|
30
|
-
* === SECTION:
|
|
30
|
+
* === SECTION: Planning Links === Line ~4129 — workstream and initiative link handlers
|
|
31
31
|
* === SECTION: Category/Type Mgmt === Line ~4462 — handleSaveCategory, handleRemoveCategory
|
|
32
32
|
* === SECTION: Settings Model === Line ~4625 — settingsModel object
|
|
33
33
|
* === SECTION: Return Value === Line ~4817 — final return object (public API shape)
|
|
@@ -43,6 +43,7 @@ import { DEFAULT_TYPE_VALUE } from '../shared/taxonomyDefaults.js';
|
|
|
43
43
|
import { cloneDefaultWorkflowDefinitions } from '../shared/workflowDefaults.js';
|
|
44
44
|
import { resolveClientDeploymentConfig } from '../config/clientDeployment';
|
|
45
45
|
import { resolveTaskforceMcpBaseUrlFromBaseUrl } from '../config/cloudEnvironment';
|
|
46
|
+
import { buildBootstrapTraceId, withBootstrapTraceHeader } from '../utils/bootstrapTrace';
|
|
46
47
|
import { createDefaultAssigneeOptions } from '../utils/assignees';
|
|
47
48
|
import { persistLocalMutationActorUserId, readLocalMutationActorUserId } from '../utils/taskforceApiClient';
|
|
48
49
|
import { useSyncOrchestrator } from './useSyncOrchestrator';
|
|
@@ -67,6 +68,13 @@ import { useTaskforceAuthBootstrap } from './auth/useTaskforceAuthBootstrap';
|
|
|
67
68
|
import { deriveAuthGuardPolicy } from './auth/useAuthGuardPolicy';
|
|
68
69
|
import { useTaskforceWorkspaceBootstrap } from './workspace/useTaskforceWorkspaceBootstrap';
|
|
69
70
|
import { useDataVersionRefresh } from './sync/useDataVersionRefresh';
|
|
71
|
+
function upsertEntityById(items, nextItem) {
|
|
72
|
+
const existingIndex = items.findIndex((item) => item.id === nextItem.id);
|
|
73
|
+
if (existingIndex === -1) {
|
|
74
|
+
return [...items, nextItem];
|
|
75
|
+
}
|
|
76
|
+
return items.map((item, index) => (index === existingIndex ? nextItem : item));
|
|
77
|
+
}
|
|
70
78
|
const ENV_DEPLOYMENT = resolveClientDeploymentConfig(import.meta.env);
|
|
71
79
|
const DEFAULT_CLOUD_APP_ORIGIN = (() => {
|
|
72
80
|
const configuredBase = ENV_DEPLOYMENT.baseUrl;
|
|
@@ -82,6 +90,75 @@ const DEFAULT_CLOUD_APP_ORIGIN = (() => {
|
|
|
82
90
|
return 'https://app.taskforcehq.ai';
|
|
83
91
|
})();
|
|
84
92
|
const BOOTSTRAP_REQUEST_TIMEOUT_MS = 12_000;
|
|
93
|
+
const PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS = 20_000;
|
|
94
|
+
const EXPECTED_WORKSPACE_ABORT_REASONS = new Set([
|
|
95
|
+
'external-abort',
|
|
96
|
+
'superseded',
|
|
97
|
+
'workspace-reset',
|
|
98
|
+
'workspace-transition',
|
|
99
|
+
]);
|
|
100
|
+
function hasAbortErrorName(error) {
|
|
101
|
+
if (typeof DOMException !== 'undefined' && error instanceof DOMException) {
|
|
102
|
+
return error.name === 'AbortError';
|
|
103
|
+
}
|
|
104
|
+
return String(error?.name || '').toLowerCase() === 'aborterror';
|
|
105
|
+
}
|
|
106
|
+
function isExpectedWorkspaceAbortReason(reason) {
|
|
107
|
+
return typeof reason === 'string' && EXPECTED_WORKSPACE_ABORT_REASONS.has(reason);
|
|
108
|
+
}
|
|
109
|
+
function isExpectedWorkspaceAbortFailure(error, signal) {
|
|
110
|
+
if (!signal.aborted)
|
|
111
|
+
return false;
|
|
112
|
+
if (error === signal.reason)
|
|
113
|
+
return true;
|
|
114
|
+
if (isExpectedWorkspaceAbortReason(error) || isExpectedWorkspaceAbortReason(signal.reason))
|
|
115
|
+
return true;
|
|
116
|
+
return hasAbortErrorName(error);
|
|
117
|
+
}
|
|
118
|
+
function createBootstrapTimeoutError(timeoutMs) {
|
|
119
|
+
const timeoutError = new Error(`Startup checks timed out after ${timeoutMs}ms.`);
|
|
120
|
+
timeoutError.name = 'BootstrapTimeoutError';
|
|
121
|
+
return timeoutError;
|
|
122
|
+
}
|
|
123
|
+
function sanitizePlanningBootstrapPayload(input) {
|
|
124
|
+
const payload = input && typeof input === 'object' ? input : {};
|
|
125
|
+
const initiatives = Array.isArray(payload.initiatives)
|
|
126
|
+
? payload.initiatives.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
|
|
127
|
+
: [];
|
|
128
|
+
const workstreams = Array.isArray(payload.workstreams)
|
|
129
|
+
? payload.workstreams.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
|
|
130
|
+
: [];
|
|
131
|
+
const workstreamTaskSummaries = Array.isArray(payload.workstreamTaskSummaries)
|
|
132
|
+
? payload.workstreamTaskSummaries.flatMap((entry) => {
|
|
133
|
+
if (!entry || typeof entry !== 'object')
|
|
134
|
+
return [];
|
|
135
|
+
const normalizedWorkstreamId = String(entry.workstreamId || '').trim();
|
|
136
|
+
if (!normalizedWorkstreamId)
|
|
137
|
+
return [];
|
|
138
|
+
const rawTasks = Array.isArray(entry.tasks) ? entry.tasks : [];
|
|
139
|
+
const taskCount = Math.max(0, Number(entry.taskCount) || 0);
|
|
140
|
+
const completedTaskCount = Math.max(0, Number(entry.completedTaskCount) || 0);
|
|
141
|
+
return [{
|
|
142
|
+
workstreamId: normalizedWorkstreamId,
|
|
143
|
+
taskCount,
|
|
144
|
+
completedTaskCount,
|
|
145
|
+
tasks: rawTasks
|
|
146
|
+
.filter((task) => Boolean(task && typeof task === 'object' && typeof task.id === 'string'))
|
|
147
|
+
.map((task) => ({
|
|
148
|
+
id: String(task.id || '').trim(),
|
|
149
|
+
referenceNumber: typeof task.referenceNumber === 'number' ? task.referenceNumber : null,
|
|
150
|
+
title: String(task.title || '').trim() || 'Untitled Task',
|
|
151
|
+
status: typeof task.status === 'string' ? task.status : null,
|
|
152
|
+
})),
|
|
153
|
+
}];
|
|
154
|
+
})
|
|
155
|
+
: [];
|
|
156
|
+
return {
|
|
157
|
+
initiatives,
|
|
158
|
+
workstreams,
|
|
159
|
+
workstreamTaskSummaries,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
85
162
|
export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, onClose }) {
|
|
86
163
|
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
87
164
|
const { categories, types, apiEndpoint, apiBaseUrl, cloudAuthBaseUrl, cloudMcpBaseUrl, wsBaseUrl, shortcut: configShortcut } = mergedConfig;
|
|
@@ -131,6 +208,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
131
208
|
const workspaceMode = workspaceModeRaw === 'single-local' || workspaceModeRaw === 'multi-cloud'
|
|
132
209
|
? workspaceModeRaw
|
|
133
210
|
: (runtimeModeHint === 'cloud' ? 'multi-cloud' : 'single-local');
|
|
211
|
+
setRuntimeMode(runtimeModeHint);
|
|
134
212
|
setRuntimeConfigOverride({
|
|
135
213
|
cloudEnvironment: typeof cfg.cloudEnvironment === 'string'
|
|
136
214
|
? String(cfg.cloudEnvironment).trim().toLowerCase()
|
|
@@ -175,10 +253,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
175
253
|
const propApiBaseUrl = normalizeBaseUrl(apiBaseUrl);
|
|
176
254
|
const propCloudAuthBaseUrl = normalizeBaseUrl(cloudAuthBaseUrl);
|
|
177
255
|
const propCloudMcpBaseUrl = normalizeBaseUrl(cloudMcpBaseUrl);
|
|
178
|
-
const explicitConfiguredApiBaseUrl = propApiBaseUrl || envApiBaseUrl || envBaseUrl;
|
|
179
256
|
const runtimeResolvedApiBaseUrl = runtimeConfigOverride.apiBaseUrl || runtimeConfigOverride.baseUrl;
|
|
180
|
-
const
|
|
257
|
+
const envConfiguredApiBaseUrl = isLoopbackHost ? '' : (envApiBaseUrl || envBaseUrl);
|
|
258
|
+
const normalizedApiBaseUrl = propApiBaseUrl
|
|
181
259
|
|| runtimeResolvedApiBaseUrl
|
|
260
|
+
|| envConfiguredApiBaseUrl
|
|
182
261
|
|| '';
|
|
183
262
|
const explicitConfiguredCloudAuthBaseUrl = propCloudAuthBaseUrl || envCloudAuthBaseUrl || envBaseUrl;
|
|
184
263
|
const runtimeResolvedCloudAuthBaseUrl = runtimeConfigOverride.cloudAuthBaseUrl || runtimeConfigOverride.baseUrl;
|
|
@@ -206,10 +285,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
206
285
|
|| normalizedCloudAuthBaseUrl
|
|
207
286
|
|| defaultCloudAuthBaseUrl).trim().replace(/\/+$/, '');
|
|
208
287
|
const authOnlyCloudMode = !isCloudHost && Boolean(normalizedCloudAuthBaseUrl) && !normalizedApiBaseUrl;
|
|
209
|
-
|
|
288
|
+
// Real cloud hosts must defer protected bootstrap calls immediately, even before
|
|
289
|
+
// runtime-config finishes, or the one-shot initial bootstrap can consume itself
|
|
290
|
+
// on unauthenticated /config, /taxonomy-state, and /planning/bootstrap reads.
|
|
291
|
+
const shouldProbeCloudAuth = isCloudHost || (runtimeConfigReady && Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl));
|
|
210
292
|
const shouldGateProtectedApiCalls = shouldProbeCloudAuth && !authOnlyCloudMode;
|
|
211
293
|
const cloudAuthConfigured = Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl || isCloudHost);
|
|
212
294
|
const initialRuntimeMode = isCloudHost ? 'cloud' : 'local';
|
|
295
|
+
const [runtimeMode, setRuntimeMode] = useState(() => initialRuntimeMode);
|
|
213
296
|
const optimisticLocalAuthUserId = initialRuntimeMode === 'local' && isLoopbackHost && cloudAuthConfigured
|
|
214
297
|
? readLocalMutationActorUserId()
|
|
215
298
|
: '';
|
|
@@ -236,6 +319,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
236
319
|
|| path.startsWith('/api/taskforce/sync/')
|
|
237
320
|
|| path.startsWith('/api/taskforce/settings/mcp/');
|
|
238
321
|
if (!isCloudHost && isCloudScopedPath) {
|
|
322
|
+
if (runtimeMode === 'cloud') {
|
|
323
|
+
return path;
|
|
324
|
+
}
|
|
239
325
|
if (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy) {
|
|
240
326
|
return path;
|
|
241
327
|
}
|
|
@@ -263,7 +349,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
263
349
|
if (!isCloudHost && isCloudScopedPath)
|
|
264
350
|
return path;
|
|
265
351
|
return path;
|
|
266
|
-
}, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, runtimeConfigOverride.workspaceMode, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost]);
|
|
352
|
+
}, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, runtimeConfigOverride.workspaceMode, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost, runtimeMode]);
|
|
267
353
|
const resolveWebSocketUrl = useCallback((path) => {
|
|
268
354
|
const wsPath = String(path || '').trim() || '/taskforce-ws';
|
|
269
355
|
const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`;
|
|
@@ -313,7 +399,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
313
399
|
}), [projectRoot, initialRuntimeMode]);
|
|
314
400
|
const [mcpScriptPath, setMcpScriptPath] = useState('');
|
|
315
401
|
const [tenantId, setTenantId] = useState('');
|
|
316
|
-
const [runtimeMode, setRuntimeMode] = useState(() => initialRuntimeMode);
|
|
317
402
|
const runtimeContractFallback = resolveRuntimeModeContract(runtimeMode);
|
|
318
403
|
const workspaceMode = runtimeConfigOverride.workspaceMode === 'single-local' || runtimeConfigOverride.workspaceMode === 'multi-cloud'
|
|
319
404
|
? runtimeConfigOverride.workspaceMode
|
|
@@ -328,6 +413,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
328
413
|
const [authWorkspaceId, setAuthWorkspaceId] = useState('');
|
|
329
414
|
const [authUserEmail, setAuthUserEmail] = useState('');
|
|
330
415
|
const [authUserDisplayName, setAuthUserDisplayName] = useState('');
|
|
416
|
+
const [availableAuthProviders, setAvailableAuthProviders] = useState([]);
|
|
331
417
|
const [authUserAvatarUrl, setAuthUserAvatarUrl] = useState('');
|
|
332
418
|
const [hasBetaAccess, setHasBetaAccess] = useState(true);
|
|
333
419
|
const [hydratedWorkspaceRole, setHydratedWorkspaceRole] = useState(null);
|
|
@@ -358,6 +444,12 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
358
444
|
const currentWorkspaceIdRef = useRef(currentWorkspaceId);
|
|
359
445
|
const explicitWorkspaceSelectionRef = useRef(null);
|
|
360
446
|
const workspaceTransitionEpochRef = useRef(0);
|
|
447
|
+
const bootstrapTraceSeedRef = useRef((() => {
|
|
448
|
+
const randomPart = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
449
|
+
? crypto.randomUUID()
|
|
450
|
+
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
451
|
+
return randomPart.replace(/[^A-Za-z0-9_-]+/g, '').slice(0, 12) || 'session';
|
|
452
|
+
})());
|
|
361
453
|
const [workspaceTransitionEpoch, setWorkspaceTransitionEpoch] = useState(0);
|
|
362
454
|
const bootstrapConfigAbortRef = useRef(null);
|
|
363
455
|
const referenceDataAbortRef = useRef(null);
|
|
@@ -365,6 +457,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
365
457
|
const assigneeOptionsAbortRef = useRef(null);
|
|
366
458
|
const deletedTasksAbortRef = useRef(null);
|
|
367
459
|
const planningEntitiesAbortRef = useRef(null);
|
|
460
|
+
const planningEntitiesRetryTimerRef = useRef(null);
|
|
368
461
|
const commitCurrentWorkspaceId = useCallback((nextWorkspaceIdRaw, options) => {
|
|
369
462
|
const nextWorkspaceId = String(nextWorkspaceIdRaw || '').trim();
|
|
370
463
|
if (!nextWorkspaceId)
|
|
@@ -409,9 +502,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
409
502
|
return workspaceTransitionEpochRef.current !== requestState.epoch;
|
|
410
503
|
}, []);
|
|
411
504
|
const isAbortError = useCallback((error) => {
|
|
412
|
-
return error
|
|
413
|
-
? error.name === 'AbortError'
|
|
414
|
-
: String(error?.name || '').toLowerCase() === 'aborterror';
|
|
505
|
+
return hasAbortErrorName(error) || isExpectedWorkspaceAbortReason(error);
|
|
415
506
|
}, []);
|
|
416
507
|
const abortWorkspaceScopedRequests = useCallback(() => {
|
|
417
508
|
[
|
|
@@ -425,6 +516,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
425
516
|
ref.current?.abort('workspace-transition');
|
|
426
517
|
ref.current = null;
|
|
427
518
|
});
|
|
519
|
+
if (planningEntitiesRetryTimerRef.current !== null && typeof window !== 'undefined') {
|
|
520
|
+
window.clearTimeout(planningEntitiesRetryTimerRef.current);
|
|
521
|
+
planningEntitiesRetryTimerRef.current = null;
|
|
522
|
+
}
|
|
428
523
|
referenceDataRequestRef.current = null;
|
|
429
524
|
}, []);
|
|
430
525
|
// Local runtime APIs must stay available immediately on app start, even when cloud
|
|
@@ -504,6 +599,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
504
599
|
const [archivedTasks, setArchivedTasks] = useState([]);
|
|
505
600
|
const [initiatives, setInitiatives] = useState([]);
|
|
506
601
|
const [workstreams, setWorkstreams] = useState([]);
|
|
602
|
+
const [planningBootstrapTaskSummaries, setPlanningBootstrapTaskSummaries] = useState([]);
|
|
507
603
|
const [deletedTasks, setDeletedTasks] = useState([]);
|
|
508
604
|
const [loadingTasks, setLoadingTasks] = useState(false);
|
|
509
605
|
const [editingTaskId, setEditingTaskId] = useState(null);
|
|
@@ -545,6 +641,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
545
641
|
const abortController = new AbortController();
|
|
546
642
|
const externalSignal = options?.signal;
|
|
547
643
|
const timeoutLabel = Number(timeoutMs) > 0 ? Number(timeoutMs) : BOOTSTRAP_REQUEST_TIMEOUT_MS;
|
|
644
|
+
const requestState = getWorkspaceRequestState();
|
|
645
|
+
const tracedInit = url.startsWith('/api/taskforce/')
|
|
646
|
+
? withBootstrapTraceHeader(init, buildBootstrapTraceId({
|
|
647
|
+
workspaceId: requestState.workspaceId,
|
|
648
|
+
epoch: requestState.epoch,
|
|
649
|
+
seed: bootstrapTraceSeedRef.current,
|
|
650
|
+
}))
|
|
651
|
+
: init;
|
|
548
652
|
const timerId = typeof window !== 'undefined'
|
|
549
653
|
? window.setTimeout(() => abortController.abort('bootstrap-timeout'), timeoutLabel)
|
|
550
654
|
: null;
|
|
@@ -561,18 +665,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
561
665
|
}
|
|
562
666
|
try {
|
|
563
667
|
return await fetch(url, {
|
|
564
|
-
...(
|
|
668
|
+
...(tracedInit || {}),
|
|
565
669
|
signal: abortController.signal
|
|
566
670
|
});
|
|
567
671
|
}
|
|
568
672
|
catch (error) {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if (
|
|
573
|
-
|
|
574
|
-
timeoutError.name = 'BootstrapTimeoutError';
|
|
575
|
-
throw timeoutError;
|
|
673
|
+
if (externalSignal?.aborted && isExpectedWorkspaceAbortFailure(error, externalSignal)) {
|
|
674
|
+
throw externalSignal.reason || error;
|
|
675
|
+
}
|
|
676
|
+
if (hasAbortErrorName(error) || error === 'bootstrap-timeout') {
|
|
677
|
+
throw createBootstrapTimeoutError(timeoutLabel);
|
|
576
678
|
}
|
|
577
679
|
throw error;
|
|
578
680
|
}
|
|
@@ -584,7 +686,50 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
584
686
|
window.clearTimeout(timerId);
|
|
585
687
|
}
|
|
586
688
|
}
|
|
587
|
-
}, []);
|
|
689
|
+
}, [getWorkspaceRequestState]);
|
|
690
|
+
const bootstrapState = useMemo(() => {
|
|
691
|
+
const authPending = !authSessionResolved;
|
|
692
|
+
const stalled = bootstrapPhase === 'stalled';
|
|
693
|
+
const appPending = authSessionResolved && (!configLoaded || workspaceBootstrapPending || stalled);
|
|
694
|
+
let subtitle = 'Checking account, workspace access, and setup status...';
|
|
695
|
+
if (bootstrapPhase === 'auth') {
|
|
696
|
+
subtitle = 'Verifying session...';
|
|
697
|
+
}
|
|
698
|
+
else if (bootstrapPhase === 'workspace') {
|
|
699
|
+
subtitle = 'Resolving workspace access...';
|
|
700
|
+
}
|
|
701
|
+
else if (bootstrapPhase === 'config') {
|
|
702
|
+
subtitle = 'Loading configuration...';
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
phase: bootstrapPhase,
|
|
706
|
+
auth: {
|
|
707
|
+
resolved: authSessionResolved,
|
|
708
|
+
pending: authPending
|
|
709
|
+
},
|
|
710
|
+
config: {
|
|
711
|
+
loaded: configLoaded
|
|
712
|
+
},
|
|
713
|
+
workspace: {
|
|
714
|
+
pending: workspaceBootstrapPending
|
|
715
|
+
},
|
|
716
|
+
pending: authPending || appPending,
|
|
717
|
+
authPending,
|
|
718
|
+
appPending,
|
|
719
|
+
ready: authSessionResolved && configLoaded && !workspaceBootstrapPending && bootstrapPhase === 'ready',
|
|
720
|
+
stalled,
|
|
721
|
+
subtitle,
|
|
722
|
+
error: bootstrapError,
|
|
723
|
+
startedAt: bootstrapStartedAt
|
|
724
|
+
};
|
|
725
|
+
}, [
|
|
726
|
+
authSessionResolved,
|
|
727
|
+
bootstrapError,
|
|
728
|
+
bootstrapPhase,
|
|
729
|
+
bootstrapStartedAt,
|
|
730
|
+
configLoaded,
|
|
731
|
+
workspaceBootstrapPending
|
|
732
|
+
]);
|
|
588
733
|
const isBootstrapTimeoutError = useCallback((error) => {
|
|
589
734
|
return String(error?.name || '') === 'BootstrapTimeoutError';
|
|
590
735
|
}, []);
|
|
@@ -653,7 +798,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
653
798
|
useEffect(() => {
|
|
654
799
|
if (workspaceTransitionEpoch === 0)
|
|
655
800
|
return;
|
|
656
|
-
abortWorkspaceScopedRequests();
|
|
657
801
|
referenceDataRequestRef.current = null;
|
|
658
802
|
lastLoadedUiStateKeyRef.current = '';
|
|
659
803
|
lastAssigneeOptionsLoadKeyRef.current = '';
|
|
@@ -668,8 +812,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
668
812
|
setDeletedTasks([]);
|
|
669
813
|
setTasks([]);
|
|
670
814
|
setArchivedTasks([]);
|
|
671
|
-
setInitiatives([]);
|
|
672
|
-
setWorkstreams([]);
|
|
673
815
|
setLoadingTasks(false);
|
|
674
816
|
if (cloudRealtimeFollowUpRefreshTimerRef.current !== null && typeof window !== 'undefined') {
|
|
675
817
|
window.clearTimeout(cloudRealtimeFollowUpRefreshTimerRef.current);
|
|
@@ -679,7 +821,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
679
821
|
window.clearTimeout(archiveBootstrapTimerRef.current);
|
|
680
822
|
archiveBootstrapTimerRef.current = null;
|
|
681
823
|
}
|
|
682
|
-
}, [
|
|
824
|
+
}, [setArchivedTasks, setLoadingTasks, setTasks, workspaceTransitionEpoch]);
|
|
683
825
|
const fetchDeletedTasks = useCallback(async (isSilent = false, options) => {
|
|
684
826
|
const ignoreAuthGuard = options?.ignoreAuthGuard === true;
|
|
685
827
|
if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
|
|
@@ -727,7 +869,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
727
869
|
}
|
|
728
870
|
}
|
|
729
871
|
catch (err) {
|
|
730
|
-
if (isAbortError(err))
|
|
872
|
+
if (isExpectedWorkspaceAbortFailure(err, abortController.signal) || isAbortError(err))
|
|
731
873
|
return;
|
|
732
874
|
console.error('[Taskforce] Failed to fetch deleted tasks:', err);
|
|
733
875
|
}
|
|
@@ -751,37 +893,105 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
751
893
|
if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
|
|
752
894
|
return;
|
|
753
895
|
const requestState = getWorkspaceRequestState();
|
|
896
|
+
planningEntitiesAbortRef.current?.abort('superseded');
|
|
897
|
+
if (planningEntitiesRetryTimerRef.current !== null && typeof window !== 'undefined') {
|
|
898
|
+
window.clearTimeout(planningEntitiesRetryTimerRef.current);
|
|
899
|
+
planningEntitiesRetryTimerRef.current = null;
|
|
900
|
+
}
|
|
754
901
|
const abortController = new AbortController();
|
|
755
902
|
planningEntitiesAbortRef.current = abortController;
|
|
756
903
|
try {
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
904
|
+
const planningTraceId = buildBootstrapTraceId({
|
|
905
|
+
workspaceId: requestState.workspaceId,
|
|
906
|
+
epoch: requestState.epoch,
|
|
907
|
+
seed: bootstrapTraceSeedRef.current,
|
|
908
|
+
});
|
|
909
|
+
const fetchPlanningRoute = async (url) => {
|
|
910
|
+
if (typeof window === 'undefined') {
|
|
911
|
+
return fetch(url, withBootstrapTraceHeader({ signal: abortController.signal }, planningTraceId));
|
|
912
|
+
}
|
|
913
|
+
const timeoutController = new AbortController();
|
|
914
|
+
const timerId = window.setTimeout(() => timeoutController.abort('planning-timeout'), PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS);
|
|
915
|
+
const onAbort = () => timeoutController.abort(abortController.signal.reason || 'external-abort');
|
|
916
|
+
if (abortController.signal.aborted) {
|
|
917
|
+
onAbort();
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
abortController.signal.addEventListener('abort', onAbort, { once: true });
|
|
921
|
+
}
|
|
922
|
+
try {
|
|
923
|
+
return await fetch(url, withBootstrapTraceHeader({ signal: timeoutController.signal }, planningTraceId));
|
|
924
|
+
}
|
|
925
|
+
catch (error) {
|
|
926
|
+
if (abortController.signal.aborted)
|
|
927
|
+
throw abortController.signal.reason || error;
|
|
928
|
+
if (hasAbortErrorName(error) || error === 'planning-timeout') {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
throw error;
|
|
932
|
+
}
|
|
933
|
+
finally {
|
|
934
|
+
window.clearTimeout(timerId);
|
|
935
|
+
abortController.signal.removeEventListener('abort', onAbort);
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
let bootstrapPayload = null;
|
|
939
|
+
const bootstrapRes = await fetchPlanningRoute('/api/taskforce/planning/bootstrap');
|
|
940
|
+
if (bootstrapRes?.status === 401) {
|
|
762
941
|
handleUnauthorized();
|
|
763
942
|
setInitiatives([]);
|
|
764
943
|
setWorkstreams([]);
|
|
944
|
+
setPlanningBootstrapTaskSummaries([]);
|
|
765
945
|
return;
|
|
766
946
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
947
|
+
if (bootstrapRes?.ok) {
|
|
948
|
+
bootstrapPayload = sanitizePlanningBootstrapPayload(await bootstrapRes.json().catch(() => ({})));
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
const [initiativesRes, workstreamsRes] = await Promise.all([
|
|
952
|
+
fetchPlanningRoute('/api/taskforce/initiatives'),
|
|
953
|
+
fetchPlanningRoute('/api/taskforce/workstreams'),
|
|
954
|
+
]);
|
|
955
|
+
if (!initiativesRes || !workstreamsRes) {
|
|
956
|
+
throw new Error('Planning endpoints timed out during startup.');
|
|
957
|
+
}
|
|
958
|
+
if (initiativesRes.status === 401 || workstreamsRes.status === 401) {
|
|
959
|
+
handleUnauthorized();
|
|
960
|
+
setInitiatives([]);
|
|
961
|
+
setWorkstreams([]);
|
|
962
|
+
setPlanningBootstrapTaskSummaries([]);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const [initiativesData, workstreamsData] = await Promise.all([
|
|
966
|
+
initiativesRes.ok ? initiativesRes.json().catch(() => []) : [],
|
|
967
|
+
workstreamsRes.ok ? workstreamsRes.json().catch(() => []) : [],
|
|
968
|
+
]);
|
|
969
|
+
bootstrapPayload = sanitizePlanningBootstrapPayload({
|
|
970
|
+
initiatives: initiativesData,
|
|
971
|
+
workstreams: workstreamsData,
|
|
972
|
+
workstreamTaskSummaries: [],
|
|
973
|
+
});
|
|
974
|
+
}
|
|
771
975
|
if (isWorkspaceRequestStale(requestState)) {
|
|
772
976
|
return;
|
|
773
977
|
}
|
|
774
|
-
setInitiatives(
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
setWorkstreams(Array.isArray(workstreamsData)
|
|
778
|
-
? workstreamsData.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
|
|
779
|
-
: []);
|
|
978
|
+
setInitiatives(bootstrapPayload.initiatives);
|
|
979
|
+
setWorkstreams(bootstrapPayload.workstreams);
|
|
980
|
+
setPlanningBootstrapTaskSummaries(bootstrapPayload.workstreamTaskSummaries);
|
|
780
981
|
}
|
|
781
982
|
catch (err) {
|
|
782
|
-
if (isAbortError(err))
|
|
983
|
+
if (isExpectedWorkspaceAbortFailure(err, abortController.signal) || isAbortError(err))
|
|
783
984
|
return;
|
|
784
985
|
console.error('[Taskforce] Failed to fetch planning entities:', err);
|
|
986
|
+
if (typeof window !== 'undefined'
|
|
987
|
+
&& !isWorkspaceRequestStale(requestState)
|
|
988
|
+
&& initiatives.length === 0
|
|
989
|
+
&& workstreams.length === 0) {
|
|
990
|
+
planningEntitiesRetryTimerRef.current = window.setTimeout(() => {
|
|
991
|
+
planningEntitiesRetryTimerRef.current = null;
|
|
992
|
+
void fetchPlanningEntities(options);
|
|
993
|
+
}, 3000);
|
|
994
|
+
}
|
|
785
995
|
}
|
|
786
996
|
finally {
|
|
787
997
|
if (planningEntitiesAbortRef.current === abortController) {
|
|
@@ -789,12 +999,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
789
999
|
}
|
|
790
1000
|
}
|
|
791
1001
|
}, [
|
|
1002
|
+
BOOTSTRAP_REQUEST_TIMEOUT_MS,
|
|
1003
|
+
PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS,
|
|
1004
|
+
initiatives.length,
|
|
792
1005
|
getWorkspaceRequestState,
|
|
793
1006
|
handleUnauthorized,
|
|
794
1007
|
isAbortError,
|
|
795
1008
|
isWorkspaceRequestStale,
|
|
796
1009
|
shouldDeferProtectedApiCalls,
|
|
797
1010
|
shouldBlockProtectedApiCalls,
|
|
1011
|
+
workstreams.length,
|
|
798
1012
|
]);
|
|
799
1013
|
const fetchTasks = useCallback(async (isSilent = false, options) => {
|
|
800
1014
|
await fetchTasksBase(isSilent, options);
|
|
@@ -1323,7 +1537,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1323
1537
|
}
|
|
1324
1538
|
}
|
|
1325
1539
|
}, [fetchWithTimeout, isBootstrapTimeoutError, markBootstrapStalled]);
|
|
1326
|
-
const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks } = useTaskforceWorkspaceBootstrap({
|
|
1540
|
+
const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks, runCloudBootstrap } = useTaskforceWorkspaceBootstrap({
|
|
1327
1541
|
shouldDeferProtectedApiCalls,
|
|
1328
1542
|
shouldBlockProtectedApiCalls,
|
|
1329
1543
|
getWorkspaceRequestState,
|
|
@@ -1363,6 +1577,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1363
1577
|
authSessionResolved,
|
|
1364
1578
|
isAuthenticated,
|
|
1365
1579
|
authUserId,
|
|
1580
|
+
authUserAvatarUrl,
|
|
1366
1581
|
setHydratedWorkspaceRole,
|
|
1367
1582
|
setAssigneeOptions,
|
|
1368
1583
|
setAssigneeOptionsLoaded,
|
|
@@ -1388,6 +1603,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1388
1603
|
showArchive,
|
|
1389
1604
|
taskScope,
|
|
1390
1605
|
settingsSection,
|
|
1606
|
+
workspaceBootstrapPending,
|
|
1391
1607
|
fetchTasks,
|
|
1392
1608
|
fetchArchive,
|
|
1393
1609
|
fetchPlanningEntities,
|
|
@@ -1403,12 +1619,35 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1403
1619
|
useEffect(() => {
|
|
1404
1620
|
if (!configLoaded || workspaceBootstrapPending)
|
|
1405
1621
|
return;
|
|
1406
|
-
const loadKey = `${runtimeMode}:${isAuthenticated ? 'auth' : 'guest'}:${currentWorkspaceId}`;
|
|
1622
|
+
const loadKey = `${runtimeMode}:${isAuthenticated ? 'auth' : 'guest'}:${currentWorkspaceId}:${authUserAvatarUrl || 'no-avatar'}`;
|
|
1407
1623
|
if (assigneeOptionsLoaded && lastAssigneeOptionsLoadKeyRef.current === loadKey)
|
|
1408
1624
|
return;
|
|
1409
1625
|
lastAssigneeOptionsLoadKeyRef.current = loadKey;
|
|
1410
1626
|
void fetchAssigneeOptions();
|
|
1411
|
-
}, [assigneeOptionsLoaded, configLoaded, currentWorkspaceId, fetchAssigneeOptions, isAuthenticated, runtimeMode, workspaceBootstrapPending]);
|
|
1627
|
+
}, [assigneeOptionsLoaded, authUserAvatarUrl, configLoaded, currentWorkspaceId, fetchAssigneeOptions, isAuthenticated, runtimeMode, workspaceBootstrapPending]);
|
|
1628
|
+
useEffect(() => {
|
|
1629
|
+
if (!authSessionResolved || !isAuthenticated)
|
|
1630
|
+
return;
|
|
1631
|
+
const normalizedAuthUserId = String(authUserId || '').trim();
|
|
1632
|
+
if (!normalizedAuthUserId || normalizedAuthUserId === 'anonymous')
|
|
1633
|
+
return;
|
|
1634
|
+
const nextAvatarUrl = String(authUserAvatarUrl || '').trim() || null;
|
|
1635
|
+
setAssigneeOptions((previous) => {
|
|
1636
|
+
let changed = false;
|
|
1637
|
+
const next = previous.map((option) => {
|
|
1638
|
+
if (option.kind !== 'member' || String(option.value || '').trim() !== normalizedAuthUserId)
|
|
1639
|
+
return option;
|
|
1640
|
+
const currentAvatarUrl = typeof option.avatarUrl === 'string' && option.avatarUrl.trim().length > 0
|
|
1641
|
+
? option.avatarUrl.trim()
|
|
1642
|
+
: null;
|
|
1643
|
+
if (currentAvatarUrl === nextAvatarUrl)
|
|
1644
|
+
return option;
|
|
1645
|
+
changed = true;
|
|
1646
|
+
return { ...option, avatarUrl: nextAvatarUrl };
|
|
1647
|
+
});
|
|
1648
|
+
return changed ? next : previous;
|
|
1649
|
+
});
|
|
1650
|
+
}, [authSessionResolved, authUserAvatarUrl, authUserId, isAuthenticated]);
|
|
1412
1651
|
const currentWorkspaceRole = useMemo(() => {
|
|
1413
1652
|
if (workspaceSwitchingEnabled) {
|
|
1414
1653
|
const current = availableWorkspaces.find((workspace) => workspace.id === currentWorkspaceId);
|
|
@@ -1423,44 +1662,86 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1423
1662
|
}, [isAuthenticated]);
|
|
1424
1663
|
const continueAfterCommercialOnboarding = useCallback(async () => {
|
|
1425
1664
|
setBootstrapError(null);
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1665
|
+
const result = await runCloudBootstrap({
|
|
1666
|
+
reason: 'post-plans',
|
|
1667
|
+
ignoreAuthGuard: true,
|
|
1668
|
+
tasksSilent: true,
|
|
1669
|
+
includeDeferredArchive: false
|
|
1670
|
+
});
|
|
1671
|
+
if (!result.authenticated) {
|
|
1672
|
+
return { success: false, destination: 'login', error: result.error || 'Sign in required.', code: result.code || 'AUTH_REQUIRED' };
|
|
1431
1673
|
}
|
|
1432
|
-
if (
|
|
1433
|
-
|
|
1674
|
+
if (!result.success) {
|
|
1675
|
+
return {
|
|
1676
|
+
success: false,
|
|
1677
|
+
destination: 'app',
|
|
1678
|
+
error: result.error || 'Unable to resolve workspace after plan selection.',
|
|
1679
|
+
code: result.code
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
if (result.workspaceSetupRequired) {
|
|
1683
|
+
return { success: true, destination: 'setup' };
|
|
1684
|
+
}
|
|
1685
|
+
return { success: true, destination: 'app' };
|
|
1686
|
+
}, [
|
|
1687
|
+
runCloudBootstrap
|
|
1688
|
+
]);
|
|
1689
|
+
const resolvePostAuthCloudEntry = useCallback(async (options) => {
|
|
1690
|
+
const reason = options?.reason || 'retry';
|
|
1691
|
+
const preferredWorkspaceId = typeof options?.preferredWorkspaceId === 'string' ? options.preferredWorkspaceId.trim() : '';
|
|
1692
|
+
const allowCommercialGate = options?.allowCommercialGate === true;
|
|
1693
|
+
if (preferredWorkspaceId && workspaceSwitchingEnabled) {
|
|
1694
|
+
const authenticated = await checkAuthSession({ force: true });
|
|
1695
|
+
if (!authenticated) {
|
|
1696
|
+
await fetchConfig({ ignoreAuthGuard: true });
|
|
1697
|
+
return { success: false, error: 'Sign in required.', code: 'AUTH_REQUIRED' };
|
|
1698
|
+
}
|
|
1699
|
+
const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
|
|
1434
1700
|
if (!workspaceResolution.success) {
|
|
1435
1701
|
await fetchConfig({ ignoreAuthGuard: true });
|
|
1436
1702
|
return {
|
|
1437
1703
|
success: false,
|
|
1438
|
-
|
|
1439
|
-
error: workspaceResolution.error || 'Unable to resolve workspace after plan selection.',
|
|
1704
|
+
error: workspaceResolution.error || 'Unable to resolve workspace access.',
|
|
1440
1705
|
code: workspaceResolution.code
|
|
1441
1706
|
};
|
|
1442
1707
|
}
|
|
1443
1708
|
if (workspaceResolution.workspaceSetupRequired) {
|
|
1444
1709
|
await fetchConfig({ ignoreAuthGuard: true });
|
|
1445
|
-
return { success: true,
|
|
1710
|
+
return { success: true, workspaceSetupRequired: true };
|
|
1446
1711
|
}
|
|
1447
1712
|
}
|
|
1448
|
-
await
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1713
|
+
const result = await runCloudBootstrap({
|
|
1714
|
+
reason,
|
|
1715
|
+
ignoreAuthGuard: true,
|
|
1716
|
+
tasksSilent: true,
|
|
1717
|
+
includeDeferredArchive: false
|
|
1718
|
+
});
|
|
1719
|
+
if (!result.authenticated) {
|
|
1720
|
+
return { success: false, error: result.error || 'Sign in required.', code: result.code || 'AUTH_REQUIRED' };
|
|
1721
|
+
}
|
|
1722
|
+
if (!result.success) {
|
|
1723
|
+
return {
|
|
1724
|
+
success: false,
|
|
1725
|
+
error: result.error || 'Unable to resolve workspace access.',
|
|
1726
|
+
code: result.code
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
if (result.workspaceSetupRequired) {
|
|
1730
|
+
if (allowCommercialGate) {
|
|
1731
|
+
return { success: true, workspaceSetupRequired: false };
|
|
1732
|
+
}
|
|
1733
|
+
return { success: true, workspaceSetupRequired: true };
|
|
1734
|
+
}
|
|
1735
|
+
if (!allowCommercialGate) {
|
|
1736
|
+
void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
|
|
1737
|
+
}
|
|
1738
|
+
return { success: true, workspaceSetupRequired: false };
|
|
1455
1739
|
}, [
|
|
1456
1740
|
checkAuthSession,
|
|
1457
|
-
fetchBootstrapConfig,
|
|
1458
1741
|
fetchConfig,
|
|
1459
|
-
fetchPlanningEntities,
|
|
1460
|
-
fetchReferenceData,
|
|
1461
|
-
fetchTasks,
|
|
1462
|
-
markBootstrapPhase,
|
|
1463
1742
|
resolveWorkspaceAfterAuth,
|
|
1743
|
+
runCloudBootstrap,
|
|
1744
|
+
syncUserGlobalSettings,
|
|
1464
1745
|
workspaceSwitchingEnabled
|
|
1465
1746
|
]);
|
|
1466
1747
|
const createWorkspace = useCallback(async (name, description) => {
|
|
@@ -1621,30 +1902,34 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1621
1902
|
return { success: false, error: data?.error || 'Sign in failed.', code: data?.code };
|
|
1622
1903
|
}
|
|
1623
1904
|
setError('');
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
return {
|
|
1628
|
-
success: false,
|
|
1629
|
-
error: workspaceResolution.error || 'Unable to resolve workspace after sign in.',
|
|
1630
|
-
code: workspaceResolution.code
|
|
1631
|
-
};
|
|
1905
|
+
const entry = await resolvePostAuthCloudEntry();
|
|
1906
|
+
if (!entry.success) {
|
|
1907
|
+
return { success: false, error: entry.error || 'Unable to resolve workspace after sign in.', code: entry.code };
|
|
1632
1908
|
}
|
|
1633
|
-
|
|
1634
|
-
await fetchConfig({ ignoreAuthGuard: true });
|
|
1635
|
-
return { success: true, workspaceSetupRequired: true };
|
|
1636
|
-
}
|
|
1637
|
-
await Promise.all([
|
|
1638
|
-
fetchConfig({ ignoreAuthGuard: true }),
|
|
1639
|
-
fetchTasks(true, { ignoreAuthGuard: true })
|
|
1640
|
-
]);
|
|
1641
|
-
void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
|
|
1642
|
-
return { success: true };
|
|
1909
|
+
return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
|
|
1643
1910
|
}
|
|
1644
1911
|
catch {
|
|
1645
1912
|
return { success: false, error: 'Sign in failed.' };
|
|
1646
1913
|
}
|
|
1647
|
-
}, [
|
|
1914
|
+
}, [resolveCloudAuthUrl, cloudAuthConfigured, resolvePostAuthCloudEntry]);
|
|
1915
|
+
/**
|
|
1916
|
+
* Initiates an OAuth login (or invite-claim) flow by redirecting the browser
|
|
1917
|
+
* to the provider's authorisation URL via the server-side start route.
|
|
1918
|
+
*
|
|
1919
|
+
* @param provider - 'google' | 'github' | 'apple'
|
|
1920
|
+
* @param returnTo - Optional relative path to redirect back to after auth (defaults to '/').
|
|
1921
|
+
* @param inviteToken - Optional invite token; when provided the flow uses `invite_claim` mode.
|
|
1922
|
+
*/
|
|
1923
|
+
const beginOAuthLogin = useCallback((provider, returnTo, inviteToken) => {
|
|
1924
|
+
const sanitizedReturnTo = returnTo && /^\/[A-Za-z0-9_\-./?=&%]*$/.test(returnTo)
|
|
1925
|
+
? returnTo
|
|
1926
|
+
: '/';
|
|
1927
|
+
let path = `/api/taskforce/auth/oauth/${encodeURIComponent(provider)}/start?return_to=${encodeURIComponent(sanitizedReturnTo)}`;
|
|
1928
|
+
if (inviteToken) {
|
|
1929
|
+
path += `&invite_token=${encodeURIComponent(inviteToken)}`;
|
|
1930
|
+
}
|
|
1931
|
+
window.location.href = resolveCloudAuthUrl(path);
|
|
1932
|
+
}, [resolveCloudAuthUrl]);
|
|
1648
1933
|
const registerWithCredentials = useCallback(async (email, password, options) => {
|
|
1649
1934
|
if (!cloudAuthConfigured) {
|
|
1650
1935
|
return { success: false, error: 'Cloud auth endpoint is not configured.' };
|
|
@@ -1694,32 +1979,42 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1694
1979
|
const verificationRequired = Boolean(data?.verificationRequired);
|
|
1695
1980
|
if (!verificationRequired) {
|
|
1696
1981
|
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
1982
|
const hasCommercialOnboardingGate = Boolean(planSelectionRequired
|
|
1707
1983
|
|| checkoutPending
|
|
1708
1984
|
|| commercialState === 'pending_plan_selection'
|
|
1709
1985
|
|| commercialState === 'checkout_pending');
|
|
1710
|
-
if (
|
|
1711
|
-
await
|
|
1986
|
+
if (hasCommercialOnboardingGate) {
|
|
1987
|
+
const authenticated = await checkAuthSession({ force: true });
|
|
1988
|
+
if (!authenticated) {
|
|
1989
|
+
return {
|
|
1990
|
+
success: false,
|
|
1991
|
+
error: 'Unable to resolve workspace after registration.',
|
|
1992
|
+
code: 'AUTH_REQUIRED'
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
return {
|
|
1996
|
+
success: true,
|
|
1997
|
+
workspaceSetupRequired: false,
|
|
1998
|
+
planSelectionRequired,
|
|
1999
|
+
checkoutPending,
|
|
2000
|
+
commercialState,
|
|
2001
|
+
verificationRequired,
|
|
2002
|
+
verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
|
|
2003
|
+
emailSent: data?.emailSent !== false,
|
|
2004
|
+
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
2005
|
+
};
|
|
1712
2006
|
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
2007
|
+
const entry = await resolvePostAuthCloudEntry({ allowCommercialGate: false });
|
|
2008
|
+
if (!entry.success) {
|
|
2009
|
+
return {
|
|
2010
|
+
success: false,
|
|
2011
|
+
error: entry.error || 'Unable to resolve workspace after registration.',
|
|
2012
|
+
code: entry.code
|
|
2013
|
+
};
|
|
1719
2014
|
}
|
|
1720
2015
|
return {
|
|
1721
2016
|
success: true,
|
|
1722
|
-
workspaceSetupRequired:
|
|
2017
|
+
workspaceSetupRequired: entry.workspaceSetupRequired === true,
|
|
1723
2018
|
planSelectionRequired,
|
|
1724
2019
|
checkoutPending,
|
|
1725
2020
|
commercialState,
|
|
@@ -1744,7 +2039,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1744
2039
|
catch {
|
|
1745
2040
|
return { success: false, error: 'Create account failed.' };
|
|
1746
2041
|
}
|
|
1747
|
-
}, [checkAuthSession,
|
|
2042
|
+
}, [checkAuthSession, resolveCloudAuthUrl, cloudAuthConfigured, resolvePostAuthCloudEntry]);
|
|
1748
2043
|
const updateCurrentUserProfile = useCallback(async (input) => {
|
|
1749
2044
|
if (!cloudAuthConfigured) {
|
|
1750
2045
|
return { success: false, error: 'Cloud auth endpoint is not configured.' };
|
|
@@ -1925,7 +2220,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1925
2220
|
email: typeof data?.email === 'string' ? data.email : undefined,
|
|
1926
2221
|
workspaceId: typeof data?.workspaceId === 'string' ? data.workspaceId : undefined,
|
|
1927
2222
|
inviteeKind: data?.inviteeKind === 'existing_user' ? 'existing_user' : 'new_user',
|
|
1928
|
-
passwordRequired: data?.passwordRequired === true
|
|
2223
|
+
passwordRequired: data?.passwordRequired === true,
|
|
2224
|
+
inviteeState: data?.inviteeState === 'existing_account' ? 'existing_account' : 'pending_setup',
|
|
2225
|
+
availableMethods: Array.isArray(data?.availableMethods) ? data.availableMethods : []
|
|
1929
2226
|
};
|
|
1930
2227
|
}
|
|
1931
2228
|
catch {
|
|
@@ -1953,30 +2250,30 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1953
2250
|
return { success: false, error: data?.error || 'Invite acceptance failed.', code: data?.code };
|
|
1954
2251
|
}
|
|
1955
2252
|
setError('');
|
|
1956
|
-
await checkAuthSession({ force: true });
|
|
1957
2253
|
const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2254
|
+
if (preferredWorkspaceId && workspaceSwitchingEnabled) {
|
|
2255
|
+
const switched = await switchWorkspace(preferredWorkspaceId, { hydrate: false });
|
|
2256
|
+
if (!switched.success) {
|
|
2257
|
+
return {
|
|
2258
|
+
success: false,
|
|
2259
|
+
error: switched.error || 'Unable to resolve workspace after invite acceptance.',
|
|
2260
|
+
code: switched.code
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
1965
2263
|
}
|
|
1966
|
-
|
|
1967
|
-
|
|
2264
|
+
const entry = await resolvePostAuthCloudEntry();
|
|
2265
|
+
if (!entry.success) {
|
|
2266
|
+
return { success: false, error: entry.error || 'Unable to resolve workspace after invite acceptance.', code: entry.code };
|
|
1968
2267
|
}
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
|
|
1974
|
-
return { success: true };
|
|
2268
|
+
if (preferredWorkspaceId && currentWorkspaceIdRef.current !== preferredWorkspaceId) {
|
|
2269
|
+
commitCurrentWorkspaceId(preferredWorkspaceId, { clearExplicitSelection: false });
|
|
2270
|
+
}
|
|
2271
|
+
return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
|
|
1975
2272
|
}
|
|
1976
2273
|
catch {
|
|
1977
2274
|
return { success: false, error: 'Invite acceptance failed.' };
|
|
1978
2275
|
}
|
|
1979
|
-
}, [
|
|
2276
|
+
}, [commitCurrentWorkspaceId, resolveCloudAuthUrl, cloudAuthConfigured, currentWorkspaceIdRef, resolvePostAuthCloudEntry, switchWorkspace, workspaceSwitchingEnabled]);
|
|
1980
2277
|
const joinInviteWithToken = useCallback(async (token) => {
|
|
1981
2278
|
if (!cloudAuthConfigured) {
|
|
1982
2279
|
return { success: false, error: 'Cloud auth endpoint is not configured.' };
|
|
@@ -1997,30 +2294,93 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1997
2294
|
return { success: false, error: data?.error || 'Invite join failed.', code: data?.code };
|
|
1998
2295
|
}
|
|
1999
2296
|
setError('');
|
|
2000
|
-
await checkAuthSession({ force: true });
|
|
2001
2297
|
const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2298
|
+
if (preferredWorkspaceId && workspaceSwitchingEnabled) {
|
|
2299
|
+
const switched = await switchWorkspace(preferredWorkspaceId, { hydrate: false });
|
|
2300
|
+
if (!switched.success) {
|
|
2301
|
+
return {
|
|
2302
|
+
success: false,
|
|
2303
|
+
error: switched.error || 'Unable to resolve workspace after invite join.',
|
|
2304
|
+
code: switched.code
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2009
2307
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2308
|
+
const entry = await resolvePostAuthCloudEntry();
|
|
2309
|
+
if (!entry.success) {
|
|
2310
|
+
return { success: false, error: entry.error || 'Unable to resolve workspace after invite join.', code: entry.code };
|
|
2012
2311
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
|
|
2018
|
-
return { success: true };
|
|
2312
|
+
if (preferredWorkspaceId && currentWorkspaceIdRef.current !== preferredWorkspaceId) {
|
|
2313
|
+
commitCurrentWorkspaceId(preferredWorkspaceId, { clearExplicitSelection: false });
|
|
2314
|
+
}
|
|
2315
|
+
return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
|
|
2019
2316
|
}
|
|
2020
2317
|
catch {
|
|
2021
2318
|
return { success: false, error: 'Invite join failed.' };
|
|
2022
2319
|
}
|
|
2023
|
-
}, [
|
|
2320
|
+
}, [commitCurrentWorkspaceId, resolveCloudAuthUrl, cloudAuthConfigured, currentWorkspaceIdRef, resolvePostAuthCloudEntry, switchWorkspace, workspaceSwitchingEnabled]);
|
|
2321
|
+
// ─── Login method management ────────────────────────────────────────────────
|
|
2322
|
+
const fetchLoginMethods = useCallback(async () => {
|
|
2323
|
+
if (!cloudAuthConfigured)
|
|
2324
|
+
return { success: false, error: 'Cloud auth not configured.' };
|
|
2325
|
+
try {
|
|
2326
|
+
const res = await fetch(resolveCloudAuthUrl('/api/taskforce/account/login-methods'), {
|
|
2327
|
+
credentials: 'include'
|
|
2328
|
+
});
|
|
2329
|
+
const data = await res.json().catch(() => ({}));
|
|
2330
|
+
if (!res.ok || !data?.success) {
|
|
2331
|
+
return { success: false, error: data?.error || 'Failed to fetch login methods.' };
|
|
2332
|
+
}
|
|
2333
|
+
return { success: true, methods: Array.isArray(data?.methods) ? data.methods : [] };
|
|
2334
|
+
}
|
|
2335
|
+
catch {
|
|
2336
|
+
return { success: false, error: 'Failed to fetch login methods.' };
|
|
2337
|
+
}
|
|
2338
|
+
}, [resolveCloudAuthUrl, cloudAuthConfigured]);
|
|
2339
|
+
const unlinkLoginMethod = useCallback(async (provider) => {
|
|
2340
|
+
if (!cloudAuthConfigured)
|
|
2341
|
+
return { success: false, error: 'Cloud auth not configured.' };
|
|
2342
|
+
try {
|
|
2343
|
+
const res = await fetch(resolveCloudAuthUrl(`/api/taskforce/account/login-methods/${encodeURIComponent(provider)}`), {
|
|
2344
|
+
method: 'DELETE',
|
|
2345
|
+
credentials: 'include'
|
|
2346
|
+
});
|
|
2347
|
+
const data = await res.json().catch(() => ({}));
|
|
2348
|
+
if (!res.ok || !data?.success) {
|
|
2349
|
+
return { success: false, error: data?.error || 'Unlink failed.', code: data?.code };
|
|
2350
|
+
}
|
|
2351
|
+
return { success: true };
|
|
2352
|
+
}
|
|
2353
|
+
catch {
|
|
2354
|
+
return { success: false, error: 'Unlink failed.' };
|
|
2355
|
+
}
|
|
2356
|
+
}, [resolveCloudAuthUrl, cloudAuthConfigured]);
|
|
2357
|
+
const addPasswordToAccount = useCallback(async (password) => {
|
|
2358
|
+
if (!cloudAuthConfigured)
|
|
2359
|
+
return { success: false, error: 'Cloud auth not configured.' };
|
|
2360
|
+
try {
|
|
2361
|
+
const res = await fetch(resolveCloudAuthUrl('/api/taskforce/account/login-methods/password'), {
|
|
2362
|
+
method: 'POST',
|
|
2363
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2364
|
+
credentials: 'include',
|
|
2365
|
+
body: JSON.stringify({ password })
|
|
2366
|
+
});
|
|
2367
|
+
const data = await res.json().catch(() => ({}));
|
|
2368
|
+
if (!res.ok || !data?.success) {
|
|
2369
|
+
return { success: false, error: data?.error || 'Failed to add password.', code: data?.code };
|
|
2370
|
+
}
|
|
2371
|
+
return { success: true };
|
|
2372
|
+
}
|
|
2373
|
+
catch {
|
|
2374
|
+
return { success: false, error: 'Failed to add password.' };
|
|
2375
|
+
}
|
|
2376
|
+
}, [resolveCloudAuthUrl, cloudAuthConfigured]);
|
|
2377
|
+
const beginOAuthLink = useCallback((provider, returnTo) => {
|
|
2378
|
+
const sanitizedReturnTo = returnTo && /^\/[A-Za-z0-9_\-./?=&%]*$/.test(returnTo)
|
|
2379
|
+
? returnTo
|
|
2380
|
+
: '/';
|
|
2381
|
+
const path = `/api/taskforce/auth/oauth/${encodeURIComponent(provider)}/start?flow=link&return_to=${encodeURIComponent(sanitizedReturnTo)}`;
|
|
2382
|
+
window.location.href = resolveCloudAuthUrl(path);
|
|
2383
|
+
}, [resolveCloudAuthUrl]);
|
|
2024
2384
|
const resetCloudLogoutClientState = useCallback(() => {
|
|
2025
2385
|
abortWorkspaceScopedRequests();
|
|
2026
2386
|
referenceDataRequestRef.current = null;
|
|
@@ -2039,6 +2399,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2039
2399
|
setArchivedTasks([]);
|
|
2040
2400
|
setInitiatives([]);
|
|
2041
2401
|
setWorkstreams([]);
|
|
2402
|
+
setPlanningBootstrapTaskSummaries([]);
|
|
2042
2403
|
setLoadingTasks(false);
|
|
2043
2404
|
if (cloudRealtimeFollowUpRefreshTimerRef.current !== null && typeof window !== 'undefined') {
|
|
2044
2405
|
window.clearTimeout(cloudRealtimeFollowUpRefreshTimerRef.current);
|
|
@@ -2149,6 +2510,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2149
2510
|
throw new Error(data?.error || `Failed to create initiative (${res.status})`);
|
|
2150
2511
|
}
|
|
2151
2512
|
await fetchPlanningEntities();
|
|
2513
|
+
setInitiatives((current) => upsertEntityById(current, data));
|
|
2152
2514
|
return data;
|
|
2153
2515
|
}, [fetchPlanningEntities]);
|
|
2154
2516
|
const updateInitiative = useCallback(async (id, payload) => {
|
|
@@ -2162,6 +2524,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2162
2524
|
throw new Error(data?.error || `Failed to update initiative (${res.status})`);
|
|
2163
2525
|
}
|
|
2164
2526
|
await fetchPlanningEntities();
|
|
2527
|
+
setInitiatives((current) => upsertEntityById(current, data));
|
|
2165
2528
|
return data;
|
|
2166
2529
|
}, [fetchPlanningEntities]);
|
|
2167
2530
|
const archiveInitiative = useCallback(async (id) => {
|
|
@@ -2173,6 +2536,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2173
2536
|
throw new Error(data?.error || `Failed to archive initiative (${res.status})`);
|
|
2174
2537
|
}
|
|
2175
2538
|
await fetchPlanningEntities();
|
|
2539
|
+
setInitiatives((current) => upsertEntityById(current, data));
|
|
2176
2540
|
return data;
|
|
2177
2541
|
}, [fetchPlanningEntities]);
|
|
2178
2542
|
const unarchiveInitiative = useCallback(async (id) => {
|
|
@@ -2184,6 +2548,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2184
2548
|
throw new Error(data?.error || `Failed to unarchive initiative (${res.status})`);
|
|
2185
2549
|
}
|
|
2186
2550
|
await fetchPlanningEntities();
|
|
2551
|
+
setInitiatives((current) => upsertEntityById(current, data));
|
|
2187
2552
|
return data;
|
|
2188
2553
|
}, [fetchPlanningEntities]);
|
|
2189
2554
|
const createWorkstream = useCallback(async (payload) => {
|
|
@@ -2197,6 +2562,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2197
2562
|
throw new Error(data?.error || `Failed to create workstream (${res.status})`);
|
|
2198
2563
|
}
|
|
2199
2564
|
await fetchPlanningEntities();
|
|
2565
|
+
setWorkstreams((current) => upsertEntityById(current, data));
|
|
2200
2566
|
return data;
|
|
2201
2567
|
}, [fetchPlanningEntities]);
|
|
2202
2568
|
const updateWorkstream = useCallback(async (id, payload) => {
|
|
@@ -2210,6 +2576,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2210
2576
|
throw new Error(data?.error || `Failed to update workstream (${res.status})`);
|
|
2211
2577
|
}
|
|
2212
2578
|
await fetchPlanningEntities();
|
|
2579
|
+
setWorkstreams((current) => upsertEntityById(current, data));
|
|
2213
2580
|
return data;
|
|
2214
2581
|
}, [fetchPlanningEntities]);
|
|
2215
2582
|
const archiveWorkstream = useCallback(async (id) => {
|
|
@@ -2221,6 +2588,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2221
2588
|
throw new Error(data?.error || `Failed to archive workstream (${res.status})`);
|
|
2222
2589
|
}
|
|
2223
2590
|
await fetchPlanningEntities();
|
|
2591
|
+
setWorkstreams((current) => upsertEntityById(current, data));
|
|
2224
2592
|
return data;
|
|
2225
2593
|
}, [fetchPlanningEntities]);
|
|
2226
2594
|
const unarchiveWorkstream = useCallback(async (id) => {
|
|
@@ -2232,6 +2600,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2232
2600
|
throw new Error(data?.error || `Failed to unarchive workstream (${res.status})`);
|
|
2233
2601
|
}
|
|
2234
2602
|
await fetchPlanningEntities();
|
|
2603
|
+
setWorkstreams((current) => upsertEntityById(current, data));
|
|
2235
2604
|
return data;
|
|
2236
2605
|
}, [fetchPlanningEntities]);
|
|
2237
2606
|
// Initialize auth once, then bootstrap protected data once auth gates are settled.
|
|
@@ -2248,6 +2617,38 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2248
2617
|
return;
|
|
2249
2618
|
void checkAuthSession();
|
|
2250
2619
|
}, [runtimeConfigReady, checkAuthSession]);
|
|
2620
|
+
// Fetch available OAuth providers once we know the auth endpoint.
|
|
2621
|
+
// Uses a short timeout so an unreachable auth server doesn't hold open
|
|
2622
|
+
// browser connections and slow down subsequent requests on the same origin.
|
|
2623
|
+
useEffect(() => {
|
|
2624
|
+
if (!cloudAuthConfigured)
|
|
2625
|
+
return;
|
|
2626
|
+
let cancelled = false;
|
|
2627
|
+
const controller = new AbortController();
|
|
2628
|
+
const timerId = typeof window !== 'undefined'
|
|
2629
|
+
? window.setTimeout(() => controller.abort(), 5000)
|
|
2630
|
+
: null;
|
|
2631
|
+
fetch(resolveCloudAuthUrl('/api/taskforce/auth/providers'), {
|
|
2632
|
+
credentials: 'include',
|
|
2633
|
+
signal: controller.signal
|
|
2634
|
+
})
|
|
2635
|
+
.then(r => r.ok ? r.json() : null)
|
|
2636
|
+
.then(data => {
|
|
2637
|
+
if (!cancelled && Array.isArray(data?.providers)) {
|
|
2638
|
+
setAvailableAuthProviders(data.providers);
|
|
2639
|
+
}
|
|
2640
|
+
})
|
|
2641
|
+
.catch(() => { })
|
|
2642
|
+
.finally(() => {
|
|
2643
|
+
if (timerId !== null && typeof window !== 'undefined') {
|
|
2644
|
+
window.clearTimeout(timerId);
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2647
|
+
return () => {
|
|
2648
|
+
cancelled = true;
|
|
2649
|
+
controller.abort();
|
|
2650
|
+
};
|
|
2651
|
+
}, [cloudAuthConfigured, resolveCloudAuthUrl]);
|
|
2251
2652
|
// Keep local runtime auth state aligned with cloud session changes
|
|
2252
2653
|
// (e.g. user signs in/out in another tab or cloud-hosted app).
|
|
2253
2654
|
useEffect(() => {
|
|
@@ -2271,109 +2672,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2271
2672
|
window.clearInterval(intervalId);
|
|
2272
2673
|
};
|
|
2273
2674
|
}, [runtimeMode, shouldProbeCloudAuth, checkAuthSession]);
|
|
2274
|
-
useEffect(() => {
|
|
2275
|
-
if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
|
|
2276
|
-
return;
|
|
2277
|
-
if (hasBootstrappedDataRef.current)
|
|
2278
|
-
return;
|
|
2279
|
-
hasBootstrappedDataRef.current = true;
|
|
2280
|
-
void (async () => {
|
|
2281
|
-
if (runtimeMode === 'cloud' && authSessionResolved && isAuthenticated) {
|
|
2282
|
-
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
2283
|
-
if (!workspaceResolution.success || workspaceResolution.workspaceSetupRequired) {
|
|
2284
|
-
await fetchBootstrapConfig();
|
|
2285
|
-
void fetchReferenceData();
|
|
2286
|
-
return;
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
await fetchBootstrapConfig();
|
|
2290
|
-
await Promise.all([
|
|
2291
|
-
(async () => {
|
|
2292
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
2293
|
-
await fetchTasks(false);
|
|
2294
|
-
logBootstrapDebug('bootstrap_tasks_loaded', {
|
|
2295
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
2296
|
-
});
|
|
2297
|
-
})(),
|
|
2298
|
-
fetchReferenceData(),
|
|
2299
|
-
fetchPlanningEntities()
|
|
2300
|
-
]);
|
|
2301
|
-
if (typeof window !== 'undefined') {
|
|
2302
|
-
if (archiveBootstrapTimerRef.current !== null) {
|
|
2303
|
-
window.clearTimeout(archiveBootstrapTimerRef.current);
|
|
2304
|
-
}
|
|
2305
|
-
archiveBootstrapTimerRef.current = window.setTimeout(() => {
|
|
2306
|
-
archiveBootstrapTimerRef.current = null;
|
|
2307
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
2308
|
-
void fetchArchive(true).then(() => {
|
|
2309
|
-
logBootstrapDebug('bootstrap_archive_loaded_deferred', {
|
|
2310
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
2311
|
-
});
|
|
2312
|
-
});
|
|
2313
|
-
}, 1500);
|
|
2314
|
-
}
|
|
2315
|
-
})();
|
|
2316
|
-
}, [
|
|
2317
|
-
shouldDeferProtectedApiCalls,
|
|
2318
|
-
shouldBlockProtectedApiCalls,
|
|
2319
|
-
runtimeMode,
|
|
2320
|
-
authSessionResolved,
|
|
2321
|
-
isAuthenticated,
|
|
2322
|
-
resolveWorkspaceAfterAuth,
|
|
2323
|
-
fetchBootstrapConfig,
|
|
2324
|
-
fetchTasks,
|
|
2325
|
-
fetchArchive,
|
|
2326
|
-
fetchReferenceData,
|
|
2327
|
-
fetchPlanningEntities
|
|
2328
|
-
]);
|
|
2329
|
-
useEffect(() => {
|
|
2330
|
-
if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
|
|
2331
|
-
return;
|
|
2332
|
-
if (workspaceBootstrapPending)
|
|
2333
|
-
return;
|
|
2334
|
-
if (workspaceSelectionScope === WORKSPACE_BOOTSTRAP_SCOPE)
|
|
2335
|
-
return;
|
|
2336
|
-
if (!hasBootstrappedDataRef.current)
|
|
2337
|
-
return;
|
|
2338
|
-
const loadKey = `${runtimeMode}:${currentWorkspaceId}:${workspaceSelectionScope}`;
|
|
2339
|
-
if (lastConfigRefreshKeyRef.current === loadKey)
|
|
2340
|
-
return;
|
|
2341
|
-
lastConfigRefreshKeyRef.current = loadKey;
|
|
2342
|
-
void fetchConfig();
|
|
2343
|
-
}, [
|
|
2344
|
-
currentWorkspaceId,
|
|
2345
|
-
shouldDeferProtectedApiCalls,
|
|
2346
|
-
shouldBlockProtectedApiCalls,
|
|
2347
|
-
workspaceBootstrapPending,
|
|
2348
|
-
runtimeMode,
|
|
2349
|
-
workspaceSelectionScope,
|
|
2350
|
-
fetchConfig
|
|
2351
|
-
]);
|
|
2352
|
-
useEffect(() => {
|
|
2353
|
-
if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
|
|
2354
|
-
return;
|
|
2355
|
-
if (workspaceBootstrapPending)
|
|
2356
|
-
return;
|
|
2357
|
-
const loadKey = currentWorkspaceId;
|
|
2358
|
-
if (lastWorkspaceSyncStateLoadKeyRef.current === loadKey)
|
|
2359
|
-
return;
|
|
2360
|
-
lastWorkspaceSyncStateLoadKeyRef.current = loadKey;
|
|
2361
|
-
void loadWorkspaceSyncState();
|
|
2362
|
-
}, [shouldDeferProtectedApiCalls, shouldBlockProtectedApiCalls, currentWorkspaceId, loadWorkspaceSyncState, workspaceBootstrapPending]);
|
|
2363
|
-
useEffect(() => {
|
|
2364
|
-
if (runtimeMode !== 'cloud' || !authSessionResolved || !isAuthenticated) {
|
|
2365
|
-
if (runtimeMode !== 'cloud') {
|
|
2366
|
-
setAvailableWorkspaces([]);
|
|
2367
|
-
}
|
|
2368
|
-
workspaceListHydratedRef.current = false;
|
|
2369
|
-
return;
|
|
2370
|
-
}
|
|
2371
|
-
if (workspaceBootstrapPending)
|
|
2372
|
-
return;
|
|
2373
|
-
if (!hasBootstrappedDataRef.current || workspaceListHydratedRef.current)
|
|
2374
|
-
return;
|
|
2375
|
-
fetchWorkspaces();
|
|
2376
|
-
}, [runtimeMode, authSessionResolved, isAuthenticated, fetchWorkspaces, workspaceBootstrapPending]);
|
|
2377
2675
|
// Report task count to parent
|
|
2378
2676
|
useEffect(() => {
|
|
2379
2677
|
if (onTaskCountChange) {
|
|
@@ -2539,6 +2837,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2539
2837
|
return;
|
|
2540
2838
|
lastTaskSurfaceLoadKeyRef.current = loadKey;
|
|
2541
2839
|
fetchTasks();
|
|
2840
|
+
fetchPlanningEntities();
|
|
2542
2841
|
if (showArchive || taskScope === 'archived') {
|
|
2543
2842
|
fetchArchive(true);
|
|
2544
2843
|
}
|
|
@@ -2565,6 +2864,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2565
2864
|
showArchive,
|
|
2566
2865
|
taskScope,
|
|
2567
2866
|
fetchTasks,
|
|
2867
|
+
fetchPlanningEntities,
|
|
2568
2868
|
fetchArchive,
|
|
2569
2869
|
fetchDeletedTasks,
|
|
2570
2870
|
fetchConfig,
|
|
@@ -3023,6 +3323,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3023
3323
|
workspaceCloudSyncEnabled,
|
|
3024
3324
|
saveWorkspaceCloudSyncSettings,
|
|
3025
3325
|
applyWorkspaceSyncStateSnapshot,
|
|
3326
|
+
bootstrapState,
|
|
3026
3327
|
authSessionResolved,
|
|
3027
3328
|
workspaceBootstrapPending,
|
|
3028
3329
|
bootstrapPhase,
|
|
@@ -3039,6 +3340,12 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3039
3340
|
deleteWorkspace,
|
|
3040
3341
|
switchWorkspace,
|
|
3041
3342
|
loginWithCredentials,
|
|
3343
|
+
beginOAuthLogin,
|
|
3344
|
+
availableAuthProviders,
|
|
3345
|
+
fetchLoginMethods,
|
|
3346
|
+
unlinkLoginMethod,
|
|
3347
|
+
addPasswordToAccount,
|
|
3348
|
+
beginOAuthLink,
|
|
3042
3349
|
registerWithCredentials,
|
|
3043
3350
|
updateCurrentUserProfile,
|
|
3044
3351
|
requestEmailVerification,
|
|
@@ -3071,6 +3378,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3071
3378
|
archivedTasks,
|
|
3072
3379
|
initiatives,
|
|
3073
3380
|
workstreams,
|
|
3381
|
+
planningBootstrapTaskSummaries,
|
|
3074
3382
|
deletedTasks,
|
|
3075
3383
|
activeCategories,
|
|
3076
3384
|
activeTypes,
|
|
@@ -3093,6 +3401,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3093
3401
|
setFilterAssignees,
|
|
3094
3402
|
filterTaxonomies,
|
|
3095
3403
|
setFilterTaxonomies,
|
|
3404
|
+
hasInitedFilters,
|
|
3096
3405
|
sortBy,
|
|
3097
3406
|
setSortBy,
|
|
3098
3407
|
sortOrder,
|