@taskforcehq/taskforce 0.3.168
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 +241 -0
- package/dist/Taskforce.d.ts +3 -0
- package/dist/Taskforce.js +3 -0
- package/dist/Taskforce.module.css +7587 -0
- package/dist/TaskforceCore.CategoryFallback.test.d.ts +1 -0
- package/dist/TaskforceCore.CategoryFallback.test.js +98 -0
- package/dist/TaskforceCore.d.ts +12 -0
- package/dist/TaskforceCore.js +729 -0
- package/dist/TaskforceCore.test.d.ts +1 -0
- package/dist/TaskforceCore.test.js +2132 -0
- package/dist/TaskforceScreenshot.d.ts +11 -0
- package/dist/TaskforceScreenshot.js +113 -0
- package/dist/TaskforceWidget.d.ts +6 -0
- package/dist/TaskforceWidget.js +101 -0
- package/dist/TaskforceWidget.test.d.ts +1 -0
- package/dist/TaskforceWidget.test.js +92 -0
- package/dist/UnsavedChangesRelaxed.test.d.ts +1 -0
- package/dist/UnsavedChangesRelaxed.test.js +16 -0
- package/dist/__tests__/localizationCatalogs.test.d.ts +1 -0
- package/dist/__tests__/localizationCatalogs.test.js +37 -0
- package/dist/__tests__/planParser.test.d.ts +1 -0
- package/dist/__tests__/planParser.test.js +183 -0
- package/dist/__tests__/releaseVersion.test.d.ts +1 -0
- package/dist/__tests__/releaseVersion.test.js +32 -0
- package/dist/__tests__/structuredCommentValidation.test.d.ts +1 -0
- package/dist/__tests__/structuredCommentValidation.test.js +31 -0
- package/dist/__tests__/structuredDocValidation.test.d.ts +1 -0
- package/dist/__tests__/structuredDocValidation.test.js +30 -0
- package/dist/__tests__/templateEngine.test.d.ts +1 -0
- package/dist/__tests__/templateEngine.test.js +150 -0
- package/dist/__tests__/templates.test.d.ts +1 -0
- package/dist/__tests__/templates.test.js +45 -0
- package/dist/__tests__/workflowDocumentFixtures.test.d.ts +1 -0
- package/dist/__tests__/workflowDocumentFixtures.test.js +78 -0
- package/dist/__tests__/workflowExportMatrix.test.d.ts +1 -0
- package/dist/__tests__/workflowExportMatrix.test.js +113 -0
- package/dist/__tests__/workflowExportSnapshots.test.d.ts +1 -0
- package/dist/__tests__/workflowExportSnapshots.test.js +28 -0
- package/dist/__tests__/workflowGeneration.test.d.ts +1 -0
- package/dist/__tests__/workflowGeneration.test.js +12 -0
- package/dist/__tests__/workflowSimulation.test.d.ts +1 -0
- package/dist/__tests__/workflowSimulation.test.js +113 -0
- package/dist/__tests__/workflowV2Policy.test.d.ts +1 -0
- package/dist/__tests__/workflowV2Policy.test.js +80 -0
- package/dist/api.d.ts +30 -0
- package/dist/api.js +85 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +217 -0
- package/dist/compat/priorityCompat.d.ts +2 -0
- package/dist/compat/priorityCompat.js +24 -0
- package/dist/components/features/DocumentGenerateModal.d.ts +9 -0
- package/dist/components/features/DocumentGenerateModal.js +114 -0
- package/dist/components/features/DocumentIndex.d.ts +19 -0
- package/dist/components/features/DocumentIndex.js +51 -0
- package/dist/components/features/DocumentViewer.d.ts +9 -0
- package/dist/components/features/DocumentViewer.js +253 -0
- package/dist/components/features/DocumentWorkspace.d.ts +21 -0
- package/dist/components/features/DocumentWorkspace.js +88 -0
- package/dist/components/features/TaskSettings.d.ts +209 -0
- package/dist/components/features/TaskSettings.js +500 -0
- package/dist/components/features/TaskSettings.test.d.ts +1 -0
- package/dist/components/features/TaskSettings.test.js +308 -0
- package/dist/components/features/settings/CategoryManager.d.ts +19 -0
- package/dist/components/features/settings/CategoryManager.js +113 -0
- package/dist/components/features/settings/TaxonomyEditor.d.ts +8 -0
- package/dist/components/features/settings/TaxonomyEditor.js +98 -0
- package/dist/components/features/settings/TaxonomyManager 2.d.ts +52 -0
- package/dist/components/features/settings/TaxonomyManager 2.js +75 -0
- package/dist/components/features/settings/TaxonomyManager.d.ts +54 -0
- package/dist/components/features/settings/TaxonomyManager.js +77 -0
- package/dist/components/features/settings/TypeManager.d.ts +10 -0
- package/dist/components/features/settings/TypeManager.js +50 -0
- package/dist/components/features/settings/WorkflowEditorPrototype.d.ts +15 -0
- package/dist/components/features/settings/WorkflowEditorPrototype.js +245 -0
- package/dist/components/task/TaskActionHeader.d.ts +20 -0
- package/dist/components/task/TaskActionHeader.js +13 -0
- package/dist/components/task/TaskCard.d.ts +47 -0
- package/dist/components/task/TaskCard.js +326 -0
- package/dist/components/task/TaskCard.test.d.ts +1 -0
- package/dist/components/task/TaskCard.test.js +95 -0
- package/dist/components/task/TaskContextUpload.d.ts +12 -0
- package/dist/components/task/TaskContextUpload.js +393 -0
- package/dist/components/task/TaskContextUpload.test.d.ts +1 -0
- package/dist/components/task/TaskContextUpload.test.js +189 -0
- package/dist/components/task/TaskForm.d.ts +105 -0
- package/dist/components/task/TaskForm.js +241 -0
- package/dist/components/task/TaskForm.test.d.ts +1 -0
- package/dist/components/task/TaskForm.test.js +288 -0
- package/dist/components/task/TaskKanban 2.d.ts +76 -0
- package/dist/components/task/TaskKanban 2.js +1004 -0
- package/dist/components/task/TaskKanban.d.ts +76 -0
- package/dist/components/task/TaskKanban.empty-column-drop.test.d.ts +1 -0
- package/dist/components/task/TaskKanban.empty-column-drop.test.js +66 -0
- package/dist/components/task/TaskKanban.js +1026 -0
- package/dist/components/task/TaskKanban.test.d.ts +1 -0
- package/dist/components/task/TaskKanban.test.js +46 -0
- package/dist/components/task/TaskList.d.ts +74 -0
- package/dist/components/task/TaskList.js +38 -0
- package/dist/components/task/TaskList.test.d.ts +1 -0
- package/dist/components/task/TaskList.test.js +270 -0
- package/dist/components/task/TaskScreenshotSection.d.ts +15 -0
- package/dist/components/task/TaskScreenshotSection.js +22 -0
- package/dist/components/ui/LevelSelector.d.ts +13 -0
- package/dist/components/ui/LevelSelector.js +25 -0
- package/dist/components/ui/LevelSelector.test.d.ts +1 -0
- package/dist/components/ui/LevelSelector.test.js +55 -0
- package/dist/components/ui/Markdown.d.ts +6 -0
- package/dist/components/ui/Markdown.js +94 -0
- package/dist/components/ui/Markdown.test.d.ts +1 -0
- package/dist/components/ui/Markdown.test.js +38 -0
- package/dist/components/ui/Modal.d.ts +16 -0
- package/dist/components/ui/Modal.js +49 -0
- package/dist/components/ui/MultiSelectFilter.d.ts +10 -0
- package/dist/components/ui/MultiSelectFilter.js +71 -0
- package/dist/components/ui/MultiSelectFilter.test.d.ts +1 -0
- package/dist/components/ui/MultiSelectFilter.test.js +74 -0
- package/dist/components/ui/TaxonomyDropdown.d.ts +17 -0
- package/dist/components/ui/TaxonomyDropdown.js +145 -0
- package/dist/components/ui/TaxonomyDropdown.test.d.ts +1 -0
- package/dist/components/ui/TaxonomyDropdown.test.js +130 -0
- package/dist/components/views/StandaloneLayout 2.d.ts +8 -0
- package/dist/components/views/StandaloneLayout 2.js +1660 -0
- package/dist/components/views/StandaloneLayout.d.ts +8 -0
- package/dist/components/views/StandaloneLayout.js +2117 -0
- package/dist/components/views/WidgetView.d.ts +8 -0
- package/dist/components/views/WidgetView.js +221 -0
- package/dist/config/envSchema.d.ts +28 -0
- package/dist/config/envSchema.js +374 -0
- package/dist/config/envSchema.test.d.ts +1 -0
- package/dist/config/envSchema.test.js +97 -0
- package/dist/config/surfaceBase.d.ts +2 -0
- package/dist/config/surfaceBase.js +8 -0
- package/dist/core/BulkUpdateFieldsPreview.test.d.ts +1 -0
- package/dist/core/BulkUpdateFieldsPreview.test.js +62 -0
- package/dist/core/CompletedAt.test.d.ts +1 -0
- package/dist/core/CompletedAt.test.js +44 -0
- package/dist/core/CreatedBy.test.d.ts +1 -0
- package/dist/core/CreatedBy.test.js +60 -0
- package/dist/core/EntitlementsPolicy.test.d.ts +1 -0
- package/dist/core/EntitlementsPolicy.test.js +145 -0
- package/dist/core/InitiativeTemplates.test.d.ts +1 -0
- package/dist/core/InitiativeTemplates.test.js +50 -0
- package/dist/core/ParentLifecycle.test.d.ts +1 -0
- package/dist/core/ParentLifecycle.test.js +98 -0
- package/dist/core/ScheduleFields.test.d.ts +1 -0
- package/dist/core/ScheduleFields.test.js +197 -0
- package/dist/core/SetupState.test.d.ts +1 -0
- package/dist/core/SetupState.test.js +113 -0
- package/dist/core/SystemAdmin.test.d.ts +1 -0
- package/dist/core/SystemAdmin.test.js +295 -0
- package/dist/core/TaskTaxonomyValidation.test.d.ts +1 -0
- package/dist/core/TaskTaxonomyValidation.test.js +64 -0
- package/dist/core/Taskforce.d.ts +1063 -0
- package/dist/core/Taskforce.ids.test.d.ts +1 -0
- package/dist/core/Taskforce.ids.test.js +168 -0
- package/dist/core/Taskforce.js +5981 -0
- package/dist/core/Taskforce.realtimeMutation.test.d.ts +1 -0
- package/dist/core/Taskforce.realtimeMutation.test.js +76 -0
- package/dist/core/WorkspaceDelete.test.d.ts +1 -0
- package/dist/core/WorkspaceDelete.test.js +140 -0
- package/dist/core/WorkspacePermissions.test.d.ts +1 -0
- package/dist/core/WorkspacePermissions.test.js +151 -0
- package/dist/core/WorkspacePlanMode.test.d.ts +1 -0
- package/dist/core/WorkspacePlanMode.test.js +104 -0
- package/dist/hooks/authGuardPolicy.d.ts +26 -0
- package/dist/hooks/authGuardPolicy.js +32 -0
- package/dist/hooks/authGuardPolicy.test.d.ts +1 -0
- package/dist/hooks/authGuardPolicy.test.js +76 -0
- package/dist/hooks/useDraggable.d.ts +12 -0
- package/dist/hooks/useDraggable.js +65 -0
- package/dist/hooks/useRealtimeSync.d.ts +24 -0
- package/dist/hooks/useRealtimeSync.js +275 -0
- package/dist/hooks/useRealtimeSync.test.d.ts +1 -0
- package/dist/hooks/useRealtimeSync.test.js +284 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +164 -0
- package/dist/hooks/useSyncOrchestrator.js +1427 -0
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +367 -0
- package/dist/hooks/useTaskforce.d.ts +727 -0
- package/dist/hooks/useTaskforce.js +4396 -0
- package/dist/hooks/useTaskforce.priority-filters.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.priority-filters.test.js +33 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +80 -0
- package/dist/hooks/useWorkspaceSyncController.js +598 -0
- package/dist/hooks/useWorkspaceSyncController.test.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.test.js +236 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/localization/index.d.ts +8 -0
- package/dist/localization/index.js +81 -0
- package/dist/localization/locales/en-US.d.ts +151 -0
- package/dist/localization/locales/en-US.js +150 -0
- package/dist/localization/locales/es-419.d.ts +2 -0
- package/dist/localization/locales/es-419.js +150 -0
- package/dist/localization/locales/pt-BR.d.ts +2 -0
- package/dist/localization/locales/pt-BR.js +150 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +33 -0
- package/dist/mcp/optionResolution.d.ts +14 -0
- package/dist/mcp/optionResolution.js +126 -0
- package/dist/mcp/optionResolution.test.d.ts +1 -0
- package/dist/mcp/optionResolution.test.js +39 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +1793 -0
- package/dist/mcp/structuredCommentValidation.d.ts +6 -0
- package/dist/mcp/structuredCommentValidation.js +56 -0
- package/dist/mcp/structuredDocValidation.d.ts +6 -0
- package/dist/mcp/structuredDocValidation.js +75 -0
- package/dist/migrations/billingSchemaParity.test.d.ts +1 -0
- package/dist/migrations/billingSchemaParity.test.js +46 -0
- package/dist/migrations/clientMigrations.d.ts +9 -0
- package/dist/migrations/clientMigrations.js +74 -0
- package/dist/migrations/flags.d.ts +6 -0
- package/dist/migrations/flags.js +8 -0
- package/dist/migrations/taskMigrations.d.ts +71 -0
- package/dist/migrations/taskMigrations.js +236 -0
- package/dist/migrations/taskMigrations.test.d.ts +1 -0
- package/dist/migrations/taskMigrations.test.js +34 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +11 -0
- package/dist/migrations/taskSchemaMigrations.js +453 -0
- package/dist/plugins/index.d.ts +10 -0
- package/dist/plugins/index.js +10 -0
- package/dist/plugins/vite.d.ts +19 -0
- package/dist/plugins/vite.js +271 -0
- package/dist/public/android-chrome-192x192.png +0 -0
- package/dist/public/android-chrome-512x512.png +0 -0
- package/dist/public/apple-touch-icon.png +0 -0
- package/dist/public/favicon-16x16.png +0 -0
- package/dist/public/favicon-32x32.png +0 -0
- package/dist/public/favicon.ico +0 -0
- package/dist/public/favicon.png +0 -0
- package/dist/public/site.webmanifest +1 -0
- package/dist/public/taskforce.png +0 -0
- package/dist/resources/templates/WORKFLOW_V2_POLICY.md +55 -0
- package/dist/resources/templates/environments/aider.yaml +14 -0
- package/dist/resources/templates/environments/antigravity.yaml +10 -0
- package/dist/resources/templates/environments/claude.yaml +10 -0
- package/dist/resources/templates/environments/cline.yaml +10 -0
- package/dist/resources/templates/environments/codex.yaml +13 -0
- package/dist/resources/templates/environments/copilot.yaml +10 -0
- package/dist/resources/templates/environments/cursor.yaml +9 -0
- package/dist/resources/templates/environments/gemini.yaml +9 -0
- package/dist/resources/templates/environments/windsurf.yaml +10 -0
- package/dist/resources/templates/workflow-sources/README.md +9 -0
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +520 -0
- package/dist/resources/templates/workflow-sources/workflowEditorOverrides.json +1 -0
- package/dist/resources/templates/workflows/collaborate.yaml +104 -0
- package/dist/resources/templates/workflows/do.yaml +82 -0
- package/dist/resources/templates/workflows/evaluate.yaml +111 -0
- package/dist/resources/templates/workflows/execute.yaml +142 -0
- package/dist/resources/templates/workflows/plan.yaml +112 -0
- package/dist/resources/templates/workflows/review.yaml +109 -0
- package/dist/runtime/capabilities.d.ts +8 -0
- package/dist/runtime/capabilities.js +16 -0
- package/dist/security/virusScan.d.ts +16 -0
- package/dist/security/virusScan.js +83 -0
- package/dist/server/auth.d.ts +51 -0
- package/dist/server/auth.js +266 -0
- package/dist/server/auth.test.d.ts +1 -0
- package/dist/server/auth.test.js +111 -0
- package/dist/server/buildInfo.d.ts +7 -0
- package/dist/server/buildInfo.js +100 -0
- package/dist/server/buildInfo.test.d.ts +1 -0
- package/dist/server/buildInfo.test.js +29 -0
- package/dist/server/cors.d.ts +7 -0
- package/dist/server/cors.js +76 -0
- package/dist/server/cors.test.d.ts +1 -0
- package/dist/server/cors.test.js +71 -0
- package/dist/server/email.d.ts +28 -0
- package/dist/server/email.js +138 -0
- package/dist/server/email.test.d.ts +1 -0
- package/dist/server/email.test.js +113 -0
- package/dist/server/index.cookieProxy.test.d.ts +1 -0
- package/dist/server/index.cookieProxy.test.js +19 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.js +757 -0
- package/dist/server/index.rateLimit.test.d.ts +1 -0
- package/dist/server/index.rateLimit.test.js +13 -0
- package/dist/server/index.test.d.ts +1 -0
- package/dist/server/index.test.js +19 -0
- package/dist/server/rateLimit.d.ts +29 -0
- package/dist/server/rateLimit.js +99 -0
- package/dist/server/rateLimit.test.d.ts +1 -0
- package/dist/server/rateLimit.test.js +107 -0
- package/dist/server/realtimeSync.auth.test.d.ts +1 -0
- package/dist/server/realtimeSync.auth.test.js +62 -0
- package/dist/server/realtimeSync.replay.test.d.ts +1 -0
- package/dist/server/realtimeSync.replay.test.js +192 -0
- package/dist/server/realtimeSync.workspaceIsolation.test.d.ts +1 -0
- package/dist/server/realtimeSync.workspaceIsolation.test.js +101 -0
- package/dist/server/realtimeSyncWs.d.ts +30 -0
- package/dist/server/realtimeSyncWs.js +257 -0
- package/dist/server/routes.billing.d.ts +15 -0
- package/dist/server/routes.billing.js +991 -0
- package/dist/server/routes.billing.test.d.ts +1 -0
- package/dist/server/routes.billing.test.js +387 -0
- package/dist/server/routes.d.ts +72 -0
- package/dist/server/routes.js +4820 -0
- package/dist/server/routes.sync.d.ts +17 -0
- package/dist/server/routes.sync.integration.test.d.ts +1 -0
- package/dist/server/routes.sync.integration.test.js +470 -0
- package/dist/server/routes.sync.js +1141 -0
- package/dist/server/routes.test.d.ts +1 -0
- package/dist/server/routes.test.js +4945 -0
- package/dist/server/runtimeMode.d.ts +4 -0
- package/dist/server/runtimeMode.js +25 -0
- package/dist/server/runtimeMode.test.d.ts +1 -0
- package/dist/server/runtimeMode.test.js +26 -0
- package/dist/server/securityHeaders.d.ts +3 -0
- package/dist/server/securityHeaders.js +25 -0
- package/dist/server/securityHeaders.test.d.ts +1 -0
- package/dist/server/securityHeaders.test.js +32 -0
- package/dist/server/smtpTransport.d.ts +21 -0
- package/dist/server/smtpTransport.js +11 -0
- package/dist/server/start.d.ts +1 -0
- package/dist/server/start.js +17 -0
- package/dist/services/formatTransformers.d.ts +29 -0
- package/dist/services/formatTransformers.js +56 -0
- package/dist/services/templateEngine.d.ts +46 -0
- package/dist/services/templateEngine.js +148 -0
- package/dist/shared/runtimeContract.d.ts +12 -0
- package/dist/shared/runtimeContract.js +23 -0
- package/dist/shared/runtimeContract.test.d.ts +1 -0
- package/dist/shared/runtimeContract.test.js +23 -0
- package/dist/soak/preflight.cli.d.ts +1 -0
- package/dist/soak/preflight.cli.js +28 -0
- package/dist/soak/preflight.d.ts +26 -0
- package/dist/soak/preflight.js +113 -0
- package/dist/soak/preflight.test.d.ts +1 -0
- package/dist/soak/preflight.test.js +39 -0
- package/dist/soak/syncSoakHarness.d.ts +6 -0
- package/dist/soak/syncSoakHarness.js +10 -0
- package/dist/storage/createDatabaseAdapter.d.ts +2 -0
- package/dist/storage/createDatabaseAdapter.js +9 -0
- package/dist/storage/databaseAdapter.d.ts +22 -0
- package/dist/storage/databaseAdapter.js +1 -0
- package/dist/storage/documentIntegrity.d.ts +28 -0
- package/dist/storage/documentIntegrity.js +103 -0
- package/dist/storage/documentPurge.d.ts +14 -0
- package/dist/storage/documentPurge.js +46 -0
- package/dist/storage/documentRegistry.d.ts +39 -0
- package/dist/storage/documentRegistry.js +115 -0
- package/dist/storage/integritySchedule.d.ts +17 -0
- package/dist/storage/integritySchedule.js +62 -0
- package/dist/storage/objectStorage.d.ts +20 -0
- package/dist/storage/objectStorage.js +15 -0
- package/dist/storage/objectStorageClient.d.ts +18 -0
- package/dist/storage/objectStorageClient.js +301 -0
- package/dist/storage/objectStorageConfig.d.ts +2 -0
- package/dist/storage/objectStorageConfig.js +45 -0
- package/dist/storage/postgresAdapter.d.ts +20 -0
- package/dist/storage/postgresAdapter.js +204 -0
- package/dist/storage/postgresWorker.d.ts +1 -0
- package/dist/storage/postgresWorker.js +110 -0
- package/dist/storage/providerConfig.d.ts +6 -0
- package/dist/storage/providerConfig.js +11 -0
- package/dist/storage/sqliteAdapter.d.ts +14 -0
- package/dist/storage/sqliteAdapter.js +28 -0
- package/dist/storage/taskAssetStore.d.ts +56 -0
- package/dist/storage/taskAssetStore.js +297 -0
- package/dist/sync/cloudSyncApi.d.ts +47 -0
- package/dist/sync/cloudSyncApi.js +138 -0
- package/dist/sync/cloudSyncApi.test.d.ts +1 -0
- package/dist/sync/cloudSyncApi.test.js +54 -0
- package/dist/sync/encryptionService.d.ts +11 -0
- package/dist/sync/encryptionService.js +61 -0
- package/dist/sync/encryptionService.test.d.ts +1 -0
- package/dist/sync/encryptionService.test.js +52 -0
- package/dist/sync/realtimeFeature.d.ts +6 -0
- package/dist/sync/realtimeFeature.js +20 -0
- package/dist/sync/realtimeFeature.test.d.ts +1 -0
- package/dist/sync/realtimeFeature.test.js +30 -0
- package/dist/sync/realtimeReconcile.d.ts +30 -0
- package/dist/sync/realtimeReconcile.js +90 -0
- package/dist/sync/realtimeReconcile.test.d.ts +1 -0
- package/dist/sync/realtimeReconcile.test.js +43 -0
- package/dist/sync/syncService.d.ts +96 -0
- package/dist/sync/syncService.js +423 -0
- package/dist/sync/syncService.test.d.ts +1 -0
- package/dist/sync/syncService.test.js +484 -0
- package/dist/sync/workspaceSyncModel.d.ts +61 -0
- package/dist/sync/workspaceSyncModel.js +136 -0
- package/dist/sync/workspaceSyncModel.test.d.ts +1 -0
- package/dist/sync/workspaceSyncModel.test.js +73 -0
- package/dist/test/setup.d.ts +1 -0
- package/dist/test/setup.js +15 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.js +41 -0
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/index-0W5vyCpn.css +1 -0
- package/dist/ui/assets/index-MCZuNNfn.js +23 -0
- package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
- package/dist/ui/assets/taskforce-BxLPokNB.png +0 -0
- package/dist/ui/assets/vendor-dnd-BI1K77C1.js +5 -0
- package/dist/ui/assets/vendor-icons-ZFG3SGwo.js +1 -0
- package/dist/ui/assets/vendor-markdown-CKYkQHfj.js +29 -0
- package/dist/ui/assets/vendor-react-DdIDsh7f.js +9 -0
- package/dist/ui/assets/vendor-router-BtiNU7Dl.js +3 -0
- package/dist/ui/branding/logos/logo_black_01.png +0 -0
- package/dist/ui/branding/logos/logo_black_02.png +0 -0
- package/dist/ui/branding/logos/logo_white_01.png +0 -0
- package/dist/ui/branding/logos/logo_white_02.png +0 -0
- package/dist/ui/favicon/android-chrome-192x192.png +0 -0
- package/dist/ui/favicon/android-chrome-512x512.png +0 -0
- package/dist/ui/favicon/apple-touch-icon.png +0 -0
- package/dist/ui/favicon/favicon-16x16.png +0 -0
- package/dist/ui/favicon/favicon-32x32.png +0 -0
- package/dist/ui/favicon/favicon.ico +0 -0
- package/dist/ui/favicon/favicon.png +0 -0
- package/dist/ui/favicon/site.webmanifest +1 -0
- package/dist/ui/fonts/RussoOne-Regular.ttf +0 -0
- package/dist/ui/images/taskforce.png +0 -0
- package/dist/ui/index.html +19 -0
- package/dist/utils/checklistMarkdown.d.ts +9 -0
- package/dist/utils/checklistMarkdown.js +46 -0
- package/dist/utils/constants.d.ts +21 -0
- package/dist/utils/constants.icons.test.d.ts +1 -0
- package/dist/utils/constants.icons.test.js +40 -0
- package/dist/utils/constants.js +100 -0
- package/dist/utils/contextFiles.d.ts +8 -0
- package/dist/utils/contextFiles.js +54 -0
- package/dist/utils/formatting.d.ts +5 -0
- package/dist/utils/formatting.js +30 -0
- package/dist/utils/planParser.d.ts +22 -0
- package/dist/utils/planParser.js +151 -0
- package/dist/utils/taskDragZones.d.ts +8 -0
- package/dist/utils/taskDragZones.js +14 -0
- package/dist/utils/taskDragZones.test.d.ts +1 -0
- package/dist/utils/taskDragZones.test.js +14 -0
- package/dist/utils/taskHierarchy.d.ts +4 -0
- package/dist/utils/taskHierarchy.js +69 -0
- package/dist/utils/taskHierarchy.test.d.ts +1 -0
- package/dist/utils/taskHierarchy.test.js +38 -0
- package/dist/utils/taskParenting.d.ts +3 -0
- package/dist/utils/taskParenting.js +24 -0
- package/dist/utils/taskParenting.test.d.ts +1 -0
- package/dist/utils/taskParenting.test.js +21 -0
- package/dist/utils/taskRelations.d.ts +6 -0
- package/dist/utils/taskRelations.js +24 -0
- package/dist/utils/taskRelations.test.d.ts +1 -0
- package/dist/utils/taskRelations.test.js +34 -0
- package/dist/utils/taskUtils.d.ts +2 -0
- package/dist/utils/taskUtils.js +20 -0
- package/dist/utils/taskforceApiClient.d.ts +7 -0
- package/dist/utils/taskforceApiClient.js +42 -0
- package/dist/utils/workspaceIdentity.d.ts +8 -0
- package/dist/utils/workspaceIdentity.js +47 -0
- package/dist/utils/workspaceIdentity.test.d.ts +1 -0
- package/dist/utils/workspaceIdentity.test.js +32 -0
- package/dist/utils/workspaceLifecycle.d.ts +26 -0
- package/dist/utils/workspaceLifecycle.js +69 -0
- package/dist/utils/workspaceLifecycle.test.d.ts +1 -0
- package/dist/utils/workspaceLifecycle.test.js +64 -0
- package/package.json +133 -0
- package/scripts/check-compat-boundaries.js +48 -0
- package/scripts/check-env.ts +24 -0
- package/scripts/check-localization-strings.mjs +49 -0
- package/scripts/check-runtime-boundaries.mjs +44 -0
- package/scripts/debug-global.mjs +34 -0
- package/scripts/debug-server.mjs +36 -0
- package/scripts/export-sqlite-to-postgres.mjs +125 -0
- package/scripts/generate-env-example.ts +8 -0
- package/scripts/generate-workflow-templates.mjs +134 -0
- package/scripts/mcp-stdio-adapter.mjs +160 -0
- package/scripts/postinstall.js +60 -0
- package/scripts/release-version-lib.mjs +88 -0
- package/scripts/release-version.mjs +72 -0
- package/scripts/run-dev-proxy.sh +49 -0
- package/scripts/run-e2e-realtime.sh +16 -0
- package/scripts/run-smoke.sh +23 -0
- package/scripts/run-soak.sh +29 -0
- package/scripts/test-global.js +143 -0
- package/scripts/update-homebrew-formula.sh +38 -0
- package/src/resources/templates/WORKFLOW_V2_POLICY.md +55 -0
- package/src/resources/templates/environments/aider.yaml +14 -0
- package/src/resources/templates/environments/antigravity.yaml +10 -0
- package/src/resources/templates/environments/claude.yaml +10 -0
- package/src/resources/templates/environments/cline.yaml +10 -0
- package/src/resources/templates/environments/codex.yaml +13 -0
- package/src/resources/templates/environments/copilot.yaml +10 -0
- package/src/resources/templates/environments/cursor.yaml +9 -0
- package/src/resources/templates/environments/gemini.yaml +9 -0
- package/src/resources/templates/environments/windsurf.yaml +10 -0
- package/src/resources/templates/workflow-sources/README.md +9 -0
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +520 -0
- package/src/resources/templates/workflow-sources/workflowEditorOverrides.json +1 -0
- package/src/resources/templates/workflows/collaborate.yaml +104 -0
- package/src/resources/templates/workflows/do.yaml +82 -0
- package/src/resources/templates/workflows/evaluate.yaml +111 -0
- package/src/resources/templates/workflows/execute.yaml +142 -0
- package/src/resources/templates/workflows/plan.yaml +112 -0
- package/src/resources/templates/workflows/review.yaml +109 -0
|
@@ -0,0 +1,2117 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { useLocation, useNavigate } from 'react-router-dom';
|
|
5
|
+
import { Settings, Plus, Search, Filter, Layers, User, Maximize2, Minimize2, Check, X, Ban, ChevronLeft, ChevronRight, ArrowUp, ArrowDown, ArrowUpDown, AlertTriangle, Save, Loader2, Folder, FileText, Trash2, LogOut, Cloud, CloudOff, RefreshCw, Users, ChevronsUp, ChevronsDown, EyeOff, Zap, HelpCircle, CalendarDays, PanelRightClose, PanelRightOpen } from 'lucide-react';
|
|
6
|
+
import { STATUS_OPTIONS } from '../../utils/constants';
|
|
7
|
+
import { MultiSelectFilter } from '../ui/MultiSelectFilter';
|
|
8
|
+
import styles from '../../Taskforce.module.css';
|
|
9
|
+
import modalStyles from '../ui/Modal.module.css';
|
|
10
|
+
import { Modal } from '../ui/Modal';
|
|
11
|
+
import { TaskForm } from '../task/TaskForm';
|
|
12
|
+
import { TaskSettings } from '../features/TaskSettings';
|
|
13
|
+
import { DocumentWorkspace } from '../features/DocumentWorkspace';
|
|
14
|
+
import { TaskKanban } from '../task/TaskKanban';
|
|
15
|
+
import { TaskActionHeader } from '../task/TaskActionHeader';
|
|
16
|
+
import { TypeMeta } from '../../utils/constants';
|
|
17
|
+
import { buildHierarchyDepthMap, collectSubtreeTaskIds } from '../../utils/taskHierarchy';
|
|
18
|
+
import { formatDate } from '../../utils/formatting';
|
|
19
|
+
import { getLocale, t } from '../../localization';
|
|
20
|
+
import logo from '../../assets/branding/logos/logo_white_01.png';
|
|
21
|
+
import favicon from '../../assets/favicon/favicon.png';
|
|
22
|
+
import taskforceBg from '../../assets/images/taskforce.png';
|
|
23
|
+
const SCHEDULE_DAY_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
|
|
24
|
+
const SCHEDULE_DAY_LABELS = {
|
|
25
|
+
mon: 'Mon',
|
|
26
|
+
tue: 'Tue',
|
|
27
|
+
wed: 'Wed',
|
|
28
|
+
thu: 'Thu',
|
|
29
|
+
fri: 'Fri',
|
|
30
|
+
sat: 'Sat',
|
|
31
|
+
sun: 'Sun'
|
|
32
|
+
};
|
|
33
|
+
const SYNCING_DISPLAY_DELAY_MS = 220;
|
|
34
|
+
const SYNCING_MIN_VISIBLE_MS = 1200;
|
|
35
|
+
const ERROR_MIN_VISIBLE_MS = 1800;
|
|
36
|
+
function toDateOnlyLocal(date) {
|
|
37
|
+
const y = date.getFullYear();
|
|
38
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
39
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
40
|
+
return `${y}-${m}-${d}`;
|
|
41
|
+
}
|
|
42
|
+
function parseDateOnlyLocal(dateOnly) {
|
|
43
|
+
const match = dateOnly.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
44
|
+
if (!match)
|
|
45
|
+
return null;
|
|
46
|
+
const parsed = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
47
|
+
if (Number.isNaN(parsed.getTime()))
|
|
48
|
+
return null;
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
function startOfWeek(date, weekStartsOn) {
|
|
52
|
+
const day = date.getDay();
|
|
53
|
+
const delta = weekStartsOn === 'sunday'
|
|
54
|
+
? -day
|
|
55
|
+
: (day === 0 ? -6 : 1 - day);
|
|
56
|
+
const result = new Date(date);
|
|
57
|
+
result.setHours(0, 0, 0, 0);
|
|
58
|
+
result.setDate(result.getDate() + delta);
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function getDayOffsetFromWeekStart(dayKey, weekStartsOn) {
|
|
62
|
+
if (weekStartsOn === 'sunday') {
|
|
63
|
+
const sundayFirst = {
|
|
64
|
+
sun: 0,
|
|
65
|
+
mon: 1,
|
|
66
|
+
tue: 2,
|
|
67
|
+
wed: 3,
|
|
68
|
+
thu: 4,
|
|
69
|
+
fri: 5,
|
|
70
|
+
sat: 6
|
|
71
|
+
};
|
|
72
|
+
return sundayFirst[dayKey];
|
|
73
|
+
}
|
|
74
|
+
const mondayFirst = {
|
|
75
|
+
mon: 0,
|
|
76
|
+
tue: 1,
|
|
77
|
+
wed: 2,
|
|
78
|
+
thu: 3,
|
|
79
|
+
fri: 4,
|
|
80
|
+
sat: 5,
|
|
81
|
+
sun: 6
|
|
82
|
+
};
|
|
83
|
+
return mondayFirst[dayKey];
|
|
84
|
+
}
|
|
85
|
+
function addDays(base, days) {
|
|
86
|
+
const next = new Date(base);
|
|
87
|
+
next.setDate(base.getDate() + days);
|
|
88
|
+
return next;
|
|
89
|
+
}
|
|
90
|
+
function dateOnlyToIsoWeekKey(dateOnly) {
|
|
91
|
+
const match = dateOnly.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
92
|
+
if (!match)
|
|
93
|
+
return null;
|
|
94
|
+
const dt = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
|
|
95
|
+
const day = dt.getUTCDay() || 7;
|
|
96
|
+
dt.setUTCDate(dt.getUTCDate() + 4 - day);
|
|
97
|
+
const yearStart = new Date(Date.UTC(dt.getUTCFullYear(), 0, 1));
|
|
98
|
+
const week = Math.ceil((((dt.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
|
99
|
+
return `${dt.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
|
|
100
|
+
}
|
|
101
|
+
export function StandaloneLayout(props) {
|
|
102
|
+
const navigate = useNavigate();
|
|
103
|
+
const location = useLocation();
|
|
104
|
+
const {
|
|
105
|
+
// State
|
|
106
|
+
activeTab, setActiveTab, activeCategories, groupBy, setGroupBy, emptyColumnMode, setEmptyColumnMode, zenMode, setZenMode, filterStatus, setFilterStatus, parentTaskFilterMode, setParentTaskFilterMode,
|
|
107
|
+
// Data & Actions
|
|
108
|
+
tasks, handleEdit, handleSubmit, resetForm, handleUpdateTask,
|
|
109
|
+
// Form State (destructure all needed for TaskForm)
|
|
110
|
+
editingTaskId, loading, error, title, setTitle, description, setDescription, category, setCategory, type, setType, priority, setPriority, complexity, setComplexity, approach, setApproach, assignee, setAssignee, scheduledDate, setScheduledDate, dueDate, setDueDate, parentTaskIdInput, setParentTaskIdInput, addChildTaskIdInput, setAddChildTaskIdInput, manualComplexityEnabled, checklistDropdownEnabled, formTaxonomies, setFormTaxonomies, selectedSpecialists, setSelectedSpecialists, comments, newCommentText, setNewCommentText, screenshots, setScreenshots, descriptionFocused, setDescriptionFocused, showMarkdownHelp, setShowMarkdownHelp, showSpecialists, setShowSpecialists, showChecklist, setShowChecklist, showTaskRelationships, setShowTaskRelationships, showChildTasks, setShowChildTasks, showComments, setShowComments,
|
|
111
|
+
// Form Actions
|
|
112
|
+
handleAddComment, handleSetParentForCurrentTask, handleAddChildToCurrentTask, handleCreateChildForCurrentTask, handleUnlinkChildTask, handleReorderChildTasks, handleClearParentTask, handleOpenTaskById,
|
|
113
|
+
// Other Props for Form
|
|
114
|
+
taxonomies, activeTypes, priorities, approaches, availableSpecialists, copiedId, handleCopyId, handleToggleInProgress, handleToggleComplete, handleToggleReview, handleToggleCancel, handleArchiveTask, handleDelete, handleUnarchive, fetchTasks, searchQuery, setSearchQuery, filterCategories, setFilterCategories, filterTypes, setFilterTypes, filterPriorities, setFilterPriorities, filterAssignees, setFilterAssignees,
|
|
115
|
+
// filterStatus, setFilterStatus, // This was duplicated, removed one instance
|
|
116
|
+
sortBy, setSortBy, clearFilters, // setSearchQuery, // This was duplicated, removed one instance
|
|
117
|
+
// filterCategories, setFilterCategories, // This was duplicated, removed one instance
|
|
118
|
+
// filterTypes, setFilterTypes, // This was duplicated, removed one instance
|
|
119
|
+
// filterPriorities, setFilterPriorities, // This was duplicated, removed one instance
|
|
120
|
+
// filterStatus, setFilterStatus, // This was duplicated, removed one instance
|
|
121
|
+
// sortBy, setSortBy, // This was duplicated, removed one instance
|
|
122
|
+
// clearFilters, // This was duplicated, removed one instance
|
|
123
|
+
// activeTypes, // This was duplicated, removed one instance
|
|
124
|
+
// Settings State & Props
|
|
125
|
+
settingsModel, currentTheme, setCurrentTheme, pathSaved, saveSettings, keyShortcut, setKeyShortcut, globalWeekStartsOn, locale, jsonBackupEnabled, setJsonBackupEnabled, assigneeFilterEnabled, mcpHostRoot, setMcpHostRoot, settingsSection, setSettingsSection, runtimeMode, workspaceSwitchingEnabled, cloudAuthConfigured, authRequiredForApi, authBlocked, isAuthenticated, authUserEmail, authSessionResolved, realtimeSyncEnabled, realtimeSyncFlagSource, workspaceCloudSyncEnabled, realtimeConnectionState, realtimeTelemetry, workspaceSyncSourceOfTruth, workspaceSyncOnboardingCompleted, workspaceSyncReady, workspaceCloudHydrated, saveWorkspaceCloudSyncSettings, userGlobalSyncStatus, userGlobalLastSyncedAt, workspaceLastPullAt, workspaceLastPushAt, workspaceLastErrorAt, workspaceRetryAt, workspaceBootstrapSyncInProgress, workspaceBootstrapSyncPages, workspaceBootstrapSyncAppliedChanges, userGlobalSyncPendingChanges, userGlobalSyncError, workspaceLastErrorMessage, workspaceLastWarningMessage, workspaceSyncFailureCount, workspaceSyncRetryScheduledCount, workspaceSyncAuthFailureCount, workspaceSyncLastFailureSource, workspaceSyncLastFailureStatusCode, retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull, currentWorkspaceId, availableWorkspaces, createWorkspace, switchWorkspace, logout, projectRoot, projectName, mcpScriptPath, serverHostRoot,
|
|
126
|
+
// Browser Props
|
|
127
|
+
showFolderBrowser, setShowFolderBrowser, folders, files, currentBrowsePath, fetchFolders, browserTarget, setBrowserTarget, contextLinkBrowsePath, handleSelectPath, handleAddPath, handleRemovePath,
|
|
128
|
+
// Settings Managers
|
|
129
|
+
handleUpdateCategory, handleRemoveCategory, handleSaveCategory, handleUpdateCategoryIcon, handleUpdateCategoryColor, handleSaveType, handleRemoveType, handleUpdateTaxonomies, handleUpdatePriorities, handleUpdateApproaches, pathValidation,
|
|
130
|
+
// Exports
|
|
131
|
+
exportEnvironment, setExportEnvironment, exportWorkflowsPath, setExportWorkflowsPath, exportSpecialistsPath, setExportSpecialistsPath, exportResult, handleExportResources, exportingResource, availableWorkflows, onExportWorkflows, fetchWorkflows, availableEnvironments,
|
|
132
|
+
// Misc
|
|
133
|
+
successBanner, returnToParentTaskId, returnFromChildTaskId, clearReturnToParentTask, tasksScrollRef, setTasksScrollPos,
|
|
134
|
+
// Refs
|
|
135
|
+
commentsEndRef,
|
|
136
|
+
// Modals - Unsaved Changes
|
|
137
|
+
unsavedModalOpen, setUnsavedModalOpen, pendingNavigation, handleNavigation, currentTask, currentTaskParent, currentTaskChildren,
|
|
138
|
+
// Modals - Cancellation
|
|
139
|
+
cancellationPromptTask, cancellationOpenDescendantIds, cancellationMode, setCancellationMode, confirmCancellation, cancelCancellationPrompt, scheduleWarningPrompt, confirmScheduleWarning, cancelScheduleWarning } = props;
|
|
140
|
+
const resolvedLocale = locale || getLocale();
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const normalizedProject = String(projectName || '').trim();
|
|
143
|
+
document.title = normalizedProject ? `Taskforce - ${normalizedProject}` : 'Taskforce';
|
|
144
|
+
}, [projectName]);
|
|
145
|
+
// Handlers
|
|
146
|
+
const handleDiscard = () => {
|
|
147
|
+
setUnsavedModalOpen(false);
|
|
148
|
+
if (pendingNavigation) {
|
|
149
|
+
if (activeTab === 'add')
|
|
150
|
+
resetForm();
|
|
151
|
+
pendingNavigation();
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
if (activeTab === 'add')
|
|
155
|
+
resetForm();
|
|
156
|
+
setActiveTab('tasks');
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const handleSaveAndNavigate = async () => {
|
|
160
|
+
// Fake event for handleSubmit
|
|
161
|
+
const fakeEvent = { preventDefault: () => { } };
|
|
162
|
+
await handleSubmit(fakeEvent);
|
|
163
|
+
setUnsavedModalOpen(false);
|
|
164
|
+
if (pendingNavigation) {
|
|
165
|
+
pendingNavigation();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
// State for Card Compression
|
|
169
|
+
const [compressedCards, setCompressedCards] = useState(false);
|
|
170
|
+
const [scheduleShowWeekends, setScheduleShowWeekends] = useState(false);
|
|
171
|
+
const [scheduleShowBacklog, setScheduleShowBacklog] = useState(true);
|
|
172
|
+
const [scheduleOnlyExpired, setScheduleOnlyExpired] = useState(false);
|
|
173
|
+
const [scheduleSidebarOpen, setScheduleSidebarOpen] = useState(true);
|
|
174
|
+
const [hierarchySidebarOpen, setHierarchySidebarOpen] = useState(true);
|
|
175
|
+
const [hierarchyRootTaskId, setHierarchyRootTaskId] = useState('');
|
|
176
|
+
const [hierarchyShowUnlinkedColumn, setHierarchyShowUnlinkedColumn] = useState(true);
|
|
177
|
+
const [hierarchyChildOrderMode, setHierarchyChildOrderMode] = useState('manual');
|
|
178
|
+
const [hierarchyRootSearch, setHierarchyRootSearch] = useState('');
|
|
179
|
+
const [scheduleSelectedDate, setScheduleSelectedDate] = useState(toDateOnlyLocal(new Date()));
|
|
180
|
+
const [scheduleCalendarMonth, setScheduleCalendarMonth] = useState(() => {
|
|
181
|
+
const now = new Date();
|
|
182
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
183
|
+
});
|
|
184
|
+
const [scheduleScrollLeft, setScheduleScrollLeft] = useState(0);
|
|
185
|
+
const [showFilters, setShowFilters] = useState(false);
|
|
186
|
+
const [requestedDocPath, setRequestedDocPath] = useState(null);
|
|
187
|
+
const [layoutStateReady, setLayoutStateReady] = useState(false);
|
|
188
|
+
const lastNonDocsGroupByRef = useRef('category');
|
|
189
|
+
const loadedLayoutStateWorkspaceRef = useRef('');
|
|
190
|
+
const workspaceUiStateHeaders = useMemo(() => {
|
|
191
|
+
const headers = {};
|
|
192
|
+
const workspaceIdHeader = String(currentWorkspaceId || '').trim();
|
|
193
|
+
if (runtimeMode === 'cloud' && workspaceIdHeader && workspaceIdHeader !== 'default') {
|
|
194
|
+
headers['x-taskforce-workspace-id'] = workspaceIdHeader;
|
|
195
|
+
}
|
|
196
|
+
return headers;
|
|
197
|
+
}, [runtimeMode, currentWorkspaceId]);
|
|
198
|
+
const layoutStateWorkspaceKey = useMemo(() => `${runtimeMode}:${String(currentWorkspaceId || 'default').trim() || 'default'}`, [runtimeMode, currentWorkspaceId]);
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
const handleOpenMarkdownDocument = (event) => {
|
|
201
|
+
const customEvent = event;
|
|
202
|
+
const fsPath = customEvent.detail?.fsPath;
|
|
203
|
+
if (!fsPath)
|
|
204
|
+
return;
|
|
205
|
+
customEvent.preventDefault();
|
|
206
|
+
setRequestedDocPath(fsPath);
|
|
207
|
+
if (groupBy !== 'docs') {
|
|
208
|
+
setGroupBy('docs');
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
window.addEventListener('taskforce:open-markdown-document', handleOpenMarkdownDocument);
|
|
212
|
+
return () => {
|
|
213
|
+
window.removeEventListener('taskforce:open-markdown-document', handleOpenMarkdownDocument);
|
|
214
|
+
};
|
|
215
|
+
}, [groupBy, setGroupBy]);
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (loadedLayoutStateWorkspaceRef.current === layoutStateWorkspaceKey)
|
|
218
|
+
return;
|
|
219
|
+
loadedLayoutStateWorkspaceRef.current = layoutStateWorkspaceKey;
|
|
220
|
+
setLayoutStateReady(false);
|
|
221
|
+
let cancelled = false;
|
|
222
|
+
const loadLayoutUiState = async () => {
|
|
223
|
+
try {
|
|
224
|
+
const res = await fetch('/api/taskforce/ui-state?key=layout', {
|
|
225
|
+
method: 'GET',
|
|
226
|
+
credentials: 'include',
|
|
227
|
+
headers: workspaceUiStateHeaders
|
|
228
|
+
});
|
|
229
|
+
if (!res.ok)
|
|
230
|
+
return;
|
|
231
|
+
const data = await res.json().catch(() => ({}));
|
|
232
|
+
const state = (data?.state && typeof data.state === 'object')
|
|
233
|
+
? data.state
|
|
234
|
+
: null;
|
|
235
|
+
if (!state || cancelled)
|
|
236
|
+
return;
|
|
237
|
+
if (typeof state.scheduleShowWeekends === 'boolean')
|
|
238
|
+
setScheduleShowWeekends(state.scheduleShowWeekends);
|
|
239
|
+
if (typeof state.scheduleShowBacklog === 'boolean')
|
|
240
|
+
setScheduleShowBacklog(state.scheduleShowBacklog);
|
|
241
|
+
if (typeof state.scheduleOnlyExpired === 'boolean')
|
|
242
|
+
setScheduleOnlyExpired(state.scheduleOnlyExpired);
|
|
243
|
+
if (typeof state.scheduleSidebarOpen === 'boolean')
|
|
244
|
+
setScheduleSidebarOpen(state.scheduleSidebarOpen);
|
|
245
|
+
if (typeof state.hierarchySidebarOpen === 'boolean')
|
|
246
|
+
setHierarchySidebarOpen(state.hierarchySidebarOpen);
|
|
247
|
+
if (typeof state.hierarchyRootTaskId === 'string')
|
|
248
|
+
setHierarchyRootTaskId(state.hierarchyRootTaskId);
|
|
249
|
+
if (typeof state.hierarchyShowUnlinkedColumn === 'boolean')
|
|
250
|
+
setHierarchyShowUnlinkedColumn(state.hierarchyShowUnlinkedColumn);
|
|
251
|
+
if (state.hierarchyChildOrderMode === 'manual' || state.hierarchyChildOrderMode === 'auto') {
|
|
252
|
+
setHierarchyChildOrderMode(state.hierarchyChildOrderMode);
|
|
253
|
+
}
|
|
254
|
+
if (typeof state.scheduleSelectedDate === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(state.scheduleSelectedDate)) {
|
|
255
|
+
setScheduleSelectedDate(state.scheduleSelectedDate);
|
|
256
|
+
}
|
|
257
|
+
if (typeof state.scheduleCalendarMonth === 'string' && /^\d{4}-\d{2}$/.test(state.scheduleCalendarMonth)) {
|
|
258
|
+
setScheduleCalendarMonth(state.scheduleCalendarMonth);
|
|
259
|
+
}
|
|
260
|
+
if (typeof state.scheduleScrollLeft === 'number' && Number.isFinite(state.scheduleScrollLeft) && state.scheduleScrollLeft >= 0) {
|
|
261
|
+
setScheduleScrollLeft(state.scheduleScrollLeft);
|
|
262
|
+
}
|
|
263
|
+
if (typeof state.showFilters === 'boolean')
|
|
264
|
+
setShowFilters(state.showFilters);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// ignore read failure
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
if (!cancelled)
|
|
271
|
+
setLayoutStateReady(true);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
loadLayoutUiState();
|
|
275
|
+
return () => {
|
|
276
|
+
cancelled = true;
|
|
277
|
+
};
|
|
278
|
+
}, [layoutStateWorkspaceKey, workspaceUiStateHeaders]);
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
if (!layoutStateReady)
|
|
281
|
+
return;
|
|
282
|
+
const timeoutId = window.setTimeout(async () => {
|
|
283
|
+
try {
|
|
284
|
+
await fetch('/api/taskforce/ui-state', {
|
|
285
|
+
method: 'POST',
|
|
286
|
+
headers: { 'Content-Type': 'application/json', ...workspaceUiStateHeaders },
|
|
287
|
+
credentials: 'include',
|
|
288
|
+
body: JSON.stringify({
|
|
289
|
+
stateKey: 'layout',
|
|
290
|
+
patch: {
|
|
291
|
+
scheduleShowWeekends,
|
|
292
|
+
scheduleShowBacklog,
|
|
293
|
+
scheduleOnlyExpired,
|
|
294
|
+
scheduleSidebarOpen,
|
|
295
|
+
hierarchySidebarOpen,
|
|
296
|
+
hierarchyRootTaskId,
|
|
297
|
+
hierarchyShowUnlinkedColumn,
|
|
298
|
+
hierarchyChildOrderMode,
|
|
299
|
+
scheduleSelectedDate,
|
|
300
|
+
scheduleCalendarMonth,
|
|
301
|
+
scheduleScrollLeft,
|
|
302
|
+
showFilters
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// ignore write failure
|
|
309
|
+
}
|
|
310
|
+
}, 250);
|
|
311
|
+
return () => window.clearTimeout(timeoutId);
|
|
312
|
+
}, [
|
|
313
|
+
layoutStateReady,
|
|
314
|
+
scheduleShowWeekends,
|
|
315
|
+
scheduleShowBacklog,
|
|
316
|
+
scheduleOnlyExpired,
|
|
317
|
+
scheduleSidebarOpen,
|
|
318
|
+
hierarchySidebarOpen,
|
|
319
|
+
hierarchyRootTaskId,
|
|
320
|
+
hierarchyShowUnlinkedColumn,
|
|
321
|
+
hierarchyChildOrderMode,
|
|
322
|
+
scheduleSelectedDate,
|
|
323
|
+
scheduleCalendarMonth,
|
|
324
|
+
scheduleScrollLeft,
|
|
325
|
+
showFilters,
|
|
326
|
+
workspaceUiStateHeaders
|
|
327
|
+
]);
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
if (groupBy !== 'docs') {
|
|
330
|
+
lastNonDocsGroupByRef.current = groupBy;
|
|
331
|
+
}
|
|
332
|
+
}, [groupBy]);
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
if (groupBy === 'schedule') {
|
|
335
|
+
setScheduleSidebarOpen(true);
|
|
336
|
+
}
|
|
337
|
+
if (groupBy === 'hierarchy') {
|
|
338
|
+
setHierarchySidebarOpen(true);
|
|
339
|
+
}
|
|
340
|
+
}, [groupBy]);
|
|
341
|
+
const { showArchive, setShowArchive, fetchArchive, filteredArchive } = props;
|
|
342
|
+
const kanbanTasks = useMemo(() => {
|
|
343
|
+
if (!showArchive)
|
|
344
|
+
return props.filteredTasks;
|
|
345
|
+
const archivedWithFlag = filteredArchive.map(t => ({ ...t, isArchived: true }));
|
|
346
|
+
return [...props.filteredTasks, ...archivedWithFlag];
|
|
347
|
+
}, [props.filteredTasks, filteredArchive, showArchive]);
|
|
348
|
+
const scheduleBaseTasks = useMemo(() => props.tasks.filter(task => !task.isArchived), [props.tasks]);
|
|
349
|
+
const scheduleFilteredTaskIds = useMemo(() => new Set(props.filteredTasks.map(task => task.id)), [props.filteredTasks]);
|
|
350
|
+
const kanbanAllTasks = useMemo(() => {
|
|
351
|
+
if (!showArchive)
|
|
352
|
+
return props.searchAgnosticTasks;
|
|
353
|
+
return [...props.searchAgnosticTasks, ...filteredArchive];
|
|
354
|
+
}, [props.searchAgnosticTasks, filteredArchive, showArchive]);
|
|
355
|
+
const hierarchyTasks = useMemo(() => {
|
|
356
|
+
const byId = new Map();
|
|
357
|
+
for (const task of props.archivedTasks)
|
|
358
|
+
byId.set(task.id, task);
|
|
359
|
+
for (const task of props.tasks)
|
|
360
|
+
byId.set(task.id, task);
|
|
361
|
+
return Array.from(byId.values());
|
|
362
|
+
}, [props.tasks, props.archivedTasks]);
|
|
363
|
+
const hierarchyParentIdSet = useMemo(() => {
|
|
364
|
+
const set = new Set();
|
|
365
|
+
for (const task of hierarchyTasks) {
|
|
366
|
+
if (task.parentTaskId)
|
|
367
|
+
set.add(task.parentTaskId);
|
|
368
|
+
}
|
|
369
|
+
return set;
|
|
370
|
+
}, [hierarchyTasks]);
|
|
371
|
+
const hierarchyTaskById = useMemo(() => {
|
|
372
|
+
const map = new Map();
|
|
373
|
+
hierarchyTasks.forEach((task) => map.set(task.id, task));
|
|
374
|
+
return map;
|
|
375
|
+
}, [hierarchyTasks]);
|
|
376
|
+
const hierarchyChildrenByParent = useMemo(() => {
|
|
377
|
+
const map = new Map();
|
|
378
|
+
hierarchyTasks.forEach((task) => {
|
|
379
|
+
if (!task.parentTaskId)
|
|
380
|
+
return;
|
|
381
|
+
const list = map.get(task.parentTaskId) || [];
|
|
382
|
+
list.push(task);
|
|
383
|
+
map.set(task.parentTaskId, list);
|
|
384
|
+
});
|
|
385
|
+
return map;
|
|
386
|
+
}, [hierarchyTasks]);
|
|
387
|
+
const hierarchySearchMatches = useMemo(() => {
|
|
388
|
+
const needle = hierarchyRootSearch.trim().toLowerCase();
|
|
389
|
+
if (!needle)
|
|
390
|
+
return [];
|
|
391
|
+
const rankForTask = (task) => {
|
|
392
|
+
const id = task.id.toLowerCase();
|
|
393
|
+
const shortId = (task.id.split('-').slice(-1)[0] || task.id).toLowerCase();
|
|
394
|
+
const title = task.title.toLowerCase();
|
|
395
|
+
if (id === needle)
|
|
396
|
+
return 0;
|
|
397
|
+
if (shortId === needle)
|
|
398
|
+
return 1;
|
|
399
|
+
if (id.startsWith(needle) || shortId.startsWith(needle))
|
|
400
|
+
return 2;
|
|
401
|
+
if (title.startsWith(needle))
|
|
402
|
+
return 3;
|
|
403
|
+
if (title.includes(needle))
|
|
404
|
+
return 4;
|
|
405
|
+
if (id.includes(needle) || shortId.includes(needle))
|
|
406
|
+
return 5;
|
|
407
|
+
return null;
|
|
408
|
+
};
|
|
409
|
+
return hierarchyTasks
|
|
410
|
+
.map((task) => ({ task, rank: rankForTask(task) }))
|
|
411
|
+
.filter((entry) => entry.rank !== null)
|
|
412
|
+
.sort((a, b) => {
|
|
413
|
+
if (a.rank !== b.rank)
|
|
414
|
+
return a.rank - b.rank;
|
|
415
|
+
return a.task.title.localeCompare(b.task.title);
|
|
416
|
+
})
|
|
417
|
+
.map((entry) => entry.task);
|
|
418
|
+
}, [hierarchyTasks, hierarchyRootSearch]);
|
|
419
|
+
const hierarchyDescendantIdSet = useMemo(() => {
|
|
420
|
+
if (!hierarchyRootTaskId)
|
|
421
|
+
return null;
|
|
422
|
+
return collectSubtreeTaskIds(hierarchyTasks, hierarchyRootTaskId);
|
|
423
|
+
}, [hierarchyRootTaskId, hierarchyTasks]);
|
|
424
|
+
const hierarchyVisibleTasks = useMemo(() => {
|
|
425
|
+
if (groupBy === 'schedule')
|
|
426
|
+
return scheduleBaseTasks;
|
|
427
|
+
if (groupBy !== 'hierarchy')
|
|
428
|
+
return kanbanTasks;
|
|
429
|
+
const isUnlinkedTask = (task) => !task.parentTaskId && !hierarchyParentIdSet.has(task.id);
|
|
430
|
+
let base = kanbanTasks;
|
|
431
|
+
if (parentTaskFilterMode === 'parents') {
|
|
432
|
+
base = kanbanTasks.filter(task => hierarchyParentIdSet.has(task.id));
|
|
433
|
+
}
|
|
434
|
+
else if (parentTaskFilterMode === 'linked') {
|
|
435
|
+
base = kanbanTasks.filter(task => !!task.parentTaskId || hierarchyParentIdSet.has(task.id));
|
|
436
|
+
}
|
|
437
|
+
else if (parentTaskFilterMode === 'unlinked') {
|
|
438
|
+
base = kanbanTasks.filter(task => isUnlinkedTask(task));
|
|
439
|
+
}
|
|
440
|
+
if (!hierarchyShowUnlinkedColumn && parentTaskFilterMode !== 'unlinked') {
|
|
441
|
+
base = base.filter((task) => !isUnlinkedTask(task));
|
|
442
|
+
}
|
|
443
|
+
if (hierarchyDescendantIdSet) {
|
|
444
|
+
base = base.filter(task => hierarchyDescendantIdSet.has(task.id)
|
|
445
|
+
|| (hierarchyShowUnlinkedColumn && (parentTaskFilterMode === 'all' || parentTaskFilterMode === 'unlinked') && isUnlinkedTask(task)));
|
|
446
|
+
}
|
|
447
|
+
return base;
|
|
448
|
+
}, [groupBy, parentTaskFilterMode, kanbanTasks, hierarchyParentIdSet, scheduleBaseTasks, hierarchyDescendantIdSet, hierarchyShowUnlinkedColumn]);
|
|
449
|
+
const todayDateOnly = useMemo(() => {
|
|
450
|
+
const now = new Date();
|
|
451
|
+
const y = now.getFullYear();
|
|
452
|
+
const m = String(now.getMonth() + 1).padStart(2, '0');
|
|
453
|
+
const d = String(now.getDate()).padStart(2, '0');
|
|
454
|
+
return `${y}-${m}-${d}`;
|
|
455
|
+
}, []);
|
|
456
|
+
const boardVisibleTasks = useMemo(() => {
|
|
457
|
+
if (groupBy !== 'schedule' || !scheduleOnlyExpired)
|
|
458
|
+
return hierarchyVisibleTasks;
|
|
459
|
+
return hierarchyVisibleTasks.filter((task) => {
|
|
460
|
+
const isTerminal = task.status === 'done' || task.status === 'cancelled';
|
|
461
|
+
if (isTerminal)
|
|
462
|
+
return false;
|
|
463
|
+
const expiredScheduled = !!task.scheduledDate && task.scheduledDate < todayDateOnly;
|
|
464
|
+
const overdueDue = !!task.dueDate && task.dueDate < todayDateOnly;
|
|
465
|
+
return expiredScheduled || overdueDue;
|
|
466
|
+
});
|
|
467
|
+
}, [groupBy, scheduleOnlyExpired, hierarchyVisibleTasks, todayDateOnly]);
|
|
468
|
+
const expiredRecoveryCandidates = useMemo(() => {
|
|
469
|
+
if (groupBy !== 'schedule')
|
|
470
|
+
return [];
|
|
471
|
+
return hierarchyVisibleTasks.filter((task) => {
|
|
472
|
+
const isTerminal = task.status === 'done' || task.status === 'cancelled';
|
|
473
|
+
if (isTerminal)
|
|
474
|
+
return false;
|
|
475
|
+
const expiredScheduled = !!task.scheduledDate && task.scheduledDate < todayDateOnly;
|
|
476
|
+
const overdueDue = !!task.dueDate && task.dueDate < todayDateOnly;
|
|
477
|
+
return expiredScheduled || overdueDue;
|
|
478
|
+
});
|
|
479
|
+
}, [groupBy, hierarchyVisibleTasks, todayDateOnly]);
|
|
480
|
+
const expiredScheduledCount = useMemo(() => {
|
|
481
|
+
if (groupBy !== 'schedule')
|
|
482
|
+
return 0;
|
|
483
|
+
return hierarchyVisibleTasks.filter((task) => {
|
|
484
|
+
const isTerminal = task.status === 'done' || task.status === 'cancelled';
|
|
485
|
+
if (isTerminal)
|
|
486
|
+
return false;
|
|
487
|
+
return !!task.scheduledDate && task.scheduledDate < todayDateOnly;
|
|
488
|
+
}).length;
|
|
489
|
+
}, [groupBy, hierarchyVisibleTasks, todayDateOnly]);
|
|
490
|
+
const overdueDueCount = useMemo(() => {
|
|
491
|
+
if (groupBy !== 'schedule')
|
|
492
|
+
return 0;
|
|
493
|
+
return hierarchyVisibleTasks.filter((task) => {
|
|
494
|
+
const isTerminal = task.status === 'done' || task.status === 'cancelled';
|
|
495
|
+
if (isTerminal)
|
|
496
|
+
return false;
|
|
497
|
+
return !!task.dueDate && task.dueDate < todayDateOnly;
|
|
498
|
+
}).length;
|
|
499
|
+
}, [groupBy, hierarchyVisibleTasks, todayDateOnly]);
|
|
500
|
+
const expiredColumnCandidates = useMemo(() => {
|
|
501
|
+
if (groupBy !== 'schedule')
|
|
502
|
+
return [];
|
|
503
|
+
return hierarchyVisibleTasks.filter((task) => {
|
|
504
|
+
const isTerminal = task.status === 'done' || task.status === 'cancelled';
|
|
505
|
+
if (isTerminal)
|
|
506
|
+
return false;
|
|
507
|
+
return !!task.scheduledDate && task.scheduledDate < todayDateOnly;
|
|
508
|
+
});
|
|
509
|
+
}, [groupBy, hierarchyVisibleTasks, todayDateOnly]);
|
|
510
|
+
const expiredLeafCandidates = useMemo(() => expiredRecoveryCandidates.filter(task => !hierarchyParentIdSet.has(task.id)), [expiredRecoveryCandidates, hierarchyParentIdSet]);
|
|
511
|
+
const [scheduleBulkBusy, setScheduleBulkBusy] = useState(false);
|
|
512
|
+
const [scheduleParentDropBusy, setScheduleParentDropBusy] = useState(false);
|
|
513
|
+
const [scheduleParentDropPrompt, setScheduleParentDropPrompt] = useState(null);
|
|
514
|
+
const hierarchyDepthTasks = useMemo(() => hierarchyVisibleTasks.filter(task => !!task.parentTaskId || hierarchyParentIdSet.has(task.id)), [hierarchyVisibleTasks, hierarchyParentIdSet]);
|
|
515
|
+
const hierarchyGlobalDepthMap = useMemo(() => buildHierarchyDepthMap(hierarchyTasks), [hierarchyTasks]);
|
|
516
|
+
const maxHierarchyDepth = useMemo(() => hierarchyDepthTasks.reduce((maxDepth, task) => {
|
|
517
|
+
const depth = hierarchyGlobalDepthMap.get(task.id) ?? 0;
|
|
518
|
+
return depth > maxDepth ? depth : maxDepth;
|
|
519
|
+
}, 0), [hierarchyDepthTasks, hierarchyGlobalDepthMap]);
|
|
520
|
+
const tryReturnToParent = () => {
|
|
521
|
+
if (activeTab === 'add'
|
|
522
|
+
&& editingTaskId
|
|
523
|
+
&& returnToParentTaskId
|
|
524
|
+
&& returnFromChildTaskId === editingTaskId) {
|
|
525
|
+
const parentTask = tasks.find(t => t.id === returnToParentTaskId);
|
|
526
|
+
clearReturnToParentTask();
|
|
527
|
+
if (parentTask) {
|
|
528
|
+
handleEdit(parentTask);
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return false;
|
|
533
|
+
};
|
|
534
|
+
const handleCloseModal = () => {
|
|
535
|
+
handleNavigation(() => {
|
|
536
|
+
if (tryReturnToParent())
|
|
537
|
+
return;
|
|
538
|
+
if (activeTab === 'add')
|
|
539
|
+
resetForm();
|
|
540
|
+
setActiveTab('tasks');
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
const [localCancellationReason, setLocalCancellationReason] = useState('');
|
|
544
|
+
const handleConfirmCancellation = async () => {
|
|
545
|
+
const success = await confirmCancellation(localCancellationReason, cancellationMode);
|
|
546
|
+
if (success) {
|
|
547
|
+
setLocalCancellationReason('');
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
const handleCancelCancellation = () => {
|
|
551
|
+
cancelCancellationPrompt();
|
|
552
|
+
setLocalCancellationReason('');
|
|
553
|
+
};
|
|
554
|
+
const handleDeleteFromCancellation = async () => {
|
|
555
|
+
if (!cancellationPromptTask)
|
|
556
|
+
return;
|
|
557
|
+
const confirmed = window.confirm('Delete this task permanently? This cannot be undone.');
|
|
558
|
+
if (!confirmed)
|
|
559
|
+
return;
|
|
560
|
+
const deleted = await handleDelete(cancellationPromptTask.id, false, { skipConfirm: true });
|
|
561
|
+
if (deleted) {
|
|
562
|
+
handleCancelCancellation();
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
const handleAddTaskToColumn = useCallback((groupField, value) => {
|
|
566
|
+
resetForm();
|
|
567
|
+
// Handle different grouping fields
|
|
568
|
+
if (groupField === 'category') {
|
|
569
|
+
const cat = activeCategories.find(c => String(c.value) === String(value) || c.label === String(value));
|
|
570
|
+
if (cat)
|
|
571
|
+
setCategory(cat.label);
|
|
572
|
+
}
|
|
573
|
+
else if (groupField === 'type') {
|
|
574
|
+
setType(String(value));
|
|
575
|
+
}
|
|
576
|
+
else if (groupField === 'priority') {
|
|
577
|
+
setPriority(Number(value));
|
|
578
|
+
}
|
|
579
|
+
else if (groupField === 'approach') {
|
|
580
|
+
const app = approaches.find(a => String(a.value) === String(value) || a.label === String(value));
|
|
581
|
+
if (app) {
|
|
582
|
+
setApproach(app.value);
|
|
583
|
+
setFormTaxonomies(prev => ({ ...prev, approach: app.value }));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
else if (groupField === 'status') {
|
|
587
|
+
// When adding to a status column, we just open the form.
|
|
588
|
+
// But we can preset the status flags if we want, or just let users set them.
|
|
589
|
+
// Currently, the TaskForm doesn't expose a simple 'status' dropdown to preset.
|
|
590
|
+
// So for now, we'll just open the form.
|
|
591
|
+
// Ideally, we would preset the flags (inProgress, readyForReview, etc.)
|
|
592
|
+
// implementing basic presetting:
|
|
593
|
+
if (value === 'inProgress') {
|
|
594
|
+
handleToggleInProgress({}); // Hacky: assumes handleToggle works on new task? No.
|
|
595
|
+
// Better: we can't easily preset status flags in the current form setup without modifying resetForm/TaskForm props deeply.
|
|
596
|
+
// For now, simple open.
|
|
597
|
+
// Actually, let's try to pass initial state if possible, but useTaskforce API is limited.
|
|
598
|
+
// We'll leave it as default (Task) for now, or users can toggle after adding.
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
else if (groupField === 'complexity') {
|
|
602
|
+
const direct = Number(value);
|
|
603
|
+
if (Number.isFinite(direct)) {
|
|
604
|
+
setComplexity(direct);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else if (groupField === 'hierarchy') {
|
|
608
|
+
// Hierarchy columns are depth-based and do not map to form defaults.
|
|
609
|
+
}
|
|
610
|
+
setActiveTab('add');
|
|
611
|
+
}, [resetForm, activeCategories, approaches, setCategory, setType, setPriority, setComplexity, setApproach, setFormTaxonomies, setActiveTab]);
|
|
612
|
+
// Ensure 'done' status is shown by default as requested
|
|
613
|
+
React.useEffect(() => {
|
|
614
|
+
if (filterStatus && !filterStatus.includes('done')) {
|
|
615
|
+
setFilterStatus(prev => [...prev, 'done']);
|
|
616
|
+
}
|
|
617
|
+
}, []);
|
|
618
|
+
// Local state for filter toolbar
|
|
619
|
+
const [showHelpModal, setShowHelpModal] = useState(false);
|
|
620
|
+
const [showAccountMenu, setShowAccountMenu] = useState(false);
|
|
621
|
+
const [showAccountHub, setShowAccountHub] = useState(false);
|
|
622
|
+
const [showSyncStatusModal, setShowSyncStatusModal] = useState(false);
|
|
623
|
+
const [workspaceSyncBusy, setWorkspaceSyncBusy] = useState(false);
|
|
624
|
+
const [workspaceSyncError, setWorkspaceSyncError] = useState(null);
|
|
625
|
+
const [workspacePanelOpen, setWorkspacePanelOpen] = useState(false);
|
|
626
|
+
const [workspaceFormName, setWorkspaceFormName] = useState('');
|
|
627
|
+
const [workspaceFormDescription, setWorkspaceFormDescription] = useState('');
|
|
628
|
+
const [workspaceActionBusy, setWorkspaceActionBusy] = useState(false);
|
|
629
|
+
const [workspaceActionError, setWorkspaceActionError] = useState('');
|
|
630
|
+
const [showTeamManagementModal, setShowTeamManagementModal] = useState(false);
|
|
631
|
+
const [teamManagementTab, setTeamManagementTab] = useState('members');
|
|
632
|
+
const [teamPlanMode, setTeamPlanMode] = useState('unknown');
|
|
633
|
+
const [teamMgmtError, setTeamMgmtError] = useState(null);
|
|
634
|
+
const [teamUsersLoading, setTeamUsersLoading] = useState(false);
|
|
635
|
+
const [teamUsers, setTeamUsers] = useState([]);
|
|
636
|
+
const [teamActionBusyUserId, setTeamActionBusyUserId] = useState(null);
|
|
637
|
+
const [teamInviteEmail, setTeamInviteEmail] = useState('');
|
|
638
|
+
const [teamInviteRole, setTeamInviteRole] = useState('member');
|
|
639
|
+
const [teamInvitePermissionMode, setTeamInvitePermissionMode] = useState('read-write');
|
|
640
|
+
const [teamInviteBusy, setTeamInviteBusy] = useState(false);
|
|
641
|
+
const [teamInviteFeedback, setTeamInviteFeedback] = useState(null);
|
|
642
|
+
const [teamAuditPage, setTeamAuditPage] = useState(0);
|
|
643
|
+
const [teamAuditLoading, setTeamAuditLoading] = useState(false);
|
|
644
|
+
const [teamAuditEvents, setTeamAuditEvents] = useState([]);
|
|
645
|
+
const [teamAuditHasMore, setTeamAuditHasMore] = useState(false);
|
|
646
|
+
const [teamAuditPages, setTeamAuditPages] = useState(1);
|
|
647
|
+
const [billingStatus, setBillingStatus] = useState(null);
|
|
648
|
+
const [billingLoading, setBillingLoading] = useState(false);
|
|
649
|
+
const [billingError, setBillingError] = useState(null);
|
|
650
|
+
const [billingActionBusy, setBillingActionBusy] = useState(false);
|
|
651
|
+
const [billingActionError, setBillingActionError] = useState(null);
|
|
652
|
+
const [billingNotice, setBillingNotice] = useState(null);
|
|
653
|
+
const [billingIntervalChoice, setBillingIntervalChoice] = useState('month');
|
|
654
|
+
const TEAM_AUDIT_PAGE_SIZE = 25;
|
|
655
|
+
const accountMenuRef = useRef(null);
|
|
656
|
+
const billingSuccessPollRef = useRef(null);
|
|
657
|
+
// showSearch state removed - search always visible in actions area
|
|
658
|
+
useEffect(() => {
|
|
659
|
+
if (!showAccountMenu)
|
|
660
|
+
return;
|
|
661
|
+
const handlePointerDown = (event) => {
|
|
662
|
+
if (!accountMenuRef.current?.contains(event.target)) {
|
|
663
|
+
setShowAccountMenu(false);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
document.addEventListener('mousedown', handlePointerDown);
|
|
667
|
+
return () => document.removeEventListener('mousedown', handlePointerDown);
|
|
668
|
+
}, [showAccountMenu]);
|
|
669
|
+
useEffect(() => () => {
|
|
670
|
+
if (billingSuccessPollRef.current) {
|
|
671
|
+
window.clearInterval(billingSuccessPollRef.current);
|
|
672
|
+
billingSuccessPollRef.current = null;
|
|
673
|
+
}
|
|
674
|
+
}, []);
|
|
675
|
+
const canManageCloudWorkspaces = runtimeMode === 'cloud' && workspaceSwitchingEnabled && isAuthenticated;
|
|
676
|
+
const canManageWorkspaceSync = runtimeMode === 'local' && isAuthenticated;
|
|
677
|
+
const currentWorkspaceRole = useMemo(() => {
|
|
678
|
+
const current = availableWorkspaces.find((workspace) => workspace.id === currentWorkspaceId);
|
|
679
|
+
if (!current)
|
|
680
|
+
return null;
|
|
681
|
+
const role = String(current.role || '').trim().toLowerCase();
|
|
682
|
+
if (role === 'owner' || role === 'admin' || role === 'member' || role === 'read-only')
|
|
683
|
+
return role;
|
|
684
|
+
return null;
|
|
685
|
+
}, [availableWorkspaces, currentWorkspaceId]);
|
|
686
|
+
const canOpenTeamManagement = canManageCloudWorkspaces
|
|
687
|
+
&& (currentWorkspaceRole === 'owner' || currentWorkspaceRole === 'admin')
|
|
688
|
+
&& teamPlanMode === 'team';
|
|
689
|
+
const authIdentityLabel = String(authUserEmail || '').trim();
|
|
690
|
+
const hasAuthIdentity = authIdentityLabel.length > 0;
|
|
691
|
+
const isAuthBootstrapPending = cloudAuthConfigured && !authSessionResolved;
|
|
692
|
+
const teamAdminHeaders = useCallback(() => {
|
|
693
|
+
const headers = {};
|
|
694
|
+
const workspaceId = String(currentWorkspaceId || '').trim();
|
|
695
|
+
if (workspaceId)
|
|
696
|
+
headers['x-taskforce-workspace-id'] = workspaceId;
|
|
697
|
+
return headers;
|
|
698
|
+
}, [currentWorkspaceId]);
|
|
699
|
+
const loadBillingStatus = useCallback(async () => {
|
|
700
|
+
if (runtimeMode !== 'cloud' || !isAuthenticated) {
|
|
701
|
+
setBillingStatus(null);
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
setBillingLoading(true);
|
|
705
|
+
setBillingError(null);
|
|
706
|
+
try {
|
|
707
|
+
const res = await fetch('/api/taskforce/billing/status', {
|
|
708
|
+
method: 'GET',
|
|
709
|
+
credentials: 'include'
|
|
710
|
+
});
|
|
711
|
+
const payload = await res.json().catch(() => ({}));
|
|
712
|
+
if (!res.ok) {
|
|
713
|
+
setBillingStatus(null);
|
|
714
|
+
setBillingError(String(payload?.error || 'Failed to load billing status.'));
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
setBillingStatus(payload);
|
|
718
|
+
return payload;
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
setBillingStatus(null);
|
|
722
|
+
setBillingError('Failed to load billing status.');
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
finally {
|
|
726
|
+
setBillingLoading(false);
|
|
727
|
+
}
|
|
728
|
+
}, [isAuthenticated, runtimeMode]);
|
|
729
|
+
const handleStartCheckout = useCallback(async () => {
|
|
730
|
+
setBillingActionError(null);
|
|
731
|
+
setBillingNotice(null);
|
|
732
|
+
setBillingActionBusy(true);
|
|
733
|
+
try {
|
|
734
|
+
const res = await fetch('/api/taskforce/billing/checkout-session', {
|
|
735
|
+
method: 'POST',
|
|
736
|
+
credentials: 'include',
|
|
737
|
+
headers: { 'Content-Type': 'application/json' },
|
|
738
|
+
body: JSON.stringify({
|
|
739
|
+
tier: 'team',
|
|
740
|
+
interval: billingIntervalChoice
|
|
741
|
+
})
|
|
742
|
+
});
|
|
743
|
+
const payload = await res.json().catch(() => ({}));
|
|
744
|
+
if (!res.ok) {
|
|
745
|
+
setBillingActionError(String(payload?.error || 'Failed to create checkout session.'));
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const nextUrl = String(payload?.url || '').trim();
|
|
749
|
+
if (!nextUrl) {
|
|
750
|
+
setBillingActionError('Checkout session did not return a redirect URL.');
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
window.location.assign(nextUrl);
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
setBillingActionError('Failed to start checkout.');
|
|
757
|
+
}
|
|
758
|
+
finally {
|
|
759
|
+
setBillingActionBusy(false);
|
|
760
|
+
}
|
|
761
|
+
}, [billingIntervalChoice]);
|
|
762
|
+
const handleOpenBillingPortal = useCallback(async () => {
|
|
763
|
+
setBillingActionError(null);
|
|
764
|
+
setBillingNotice(null);
|
|
765
|
+
setBillingActionBusy(true);
|
|
766
|
+
try {
|
|
767
|
+
const res = await fetch('/api/taskforce/billing/portal-session', {
|
|
768
|
+
method: 'POST',
|
|
769
|
+
credentials: 'include'
|
|
770
|
+
});
|
|
771
|
+
const payload = await res.json().catch(() => ({}));
|
|
772
|
+
if (!res.ok) {
|
|
773
|
+
setBillingActionError(String(payload?.error || 'Failed to create portal session.'));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
const nextUrl = String(payload?.url || '').trim();
|
|
777
|
+
if (!nextUrl) {
|
|
778
|
+
setBillingActionError('Portal session did not return a redirect URL.');
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
window.location.assign(nextUrl);
|
|
782
|
+
}
|
|
783
|
+
catch {
|
|
784
|
+
setBillingActionError('Failed to open billing portal.');
|
|
785
|
+
}
|
|
786
|
+
finally {
|
|
787
|
+
setBillingActionBusy(false);
|
|
788
|
+
}
|
|
789
|
+
}, []);
|
|
790
|
+
const handleSwitchSubscriptionInterval = useCallback(async () => {
|
|
791
|
+
setBillingActionError(null);
|
|
792
|
+
setBillingNotice(null);
|
|
793
|
+
setBillingActionBusy(true);
|
|
794
|
+
try {
|
|
795
|
+
const res = await fetch('/api/taskforce/billing/subscription', {
|
|
796
|
+
method: 'POST',
|
|
797
|
+
credentials: 'include',
|
|
798
|
+
headers: { 'Content-Type': 'application/json' },
|
|
799
|
+
body: JSON.stringify({
|
|
800
|
+
tier: 'team',
|
|
801
|
+
interval: billingIntervalChoice
|
|
802
|
+
})
|
|
803
|
+
});
|
|
804
|
+
const payload = await res.json().catch(() => ({}));
|
|
805
|
+
if (!res.ok) {
|
|
806
|
+
setBillingActionError(String(payload?.error || 'Failed to update subscription interval.'));
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
setBillingNotice('Subscription updated.');
|
|
810
|
+
await loadBillingStatus();
|
|
811
|
+
}
|
|
812
|
+
catch {
|
|
813
|
+
setBillingActionError('Failed to update subscription interval.');
|
|
814
|
+
}
|
|
815
|
+
finally {
|
|
816
|
+
setBillingActionBusy(false);
|
|
817
|
+
}
|
|
818
|
+
}, [billingIntervalChoice, loadBillingStatus]);
|
|
819
|
+
const loadTeamPlanMode = useCallback(async () => {
|
|
820
|
+
if (!canManageCloudWorkspaces || !(currentWorkspaceRole === 'owner' || currentWorkspaceRole === 'admin')) {
|
|
821
|
+
setTeamPlanMode('unknown');
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
try {
|
|
825
|
+
const res = await fetch('/api/taskforce/account/entitlements', {
|
|
826
|
+
method: 'GET',
|
|
827
|
+
credentials: 'include',
|
|
828
|
+
headers: teamAdminHeaders()
|
|
829
|
+
});
|
|
830
|
+
const payload = await res.json().catch(() => ({}));
|
|
831
|
+
if (!res.ok) {
|
|
832
|
+
setTeamPlanMode('unknown');
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const planId = String(payload?.planId || '').trim().toLowerCase();
|
|
836
|
+
const state = String(payload?.state || '').trim().toLowerCase();
|
|
837
|
+
const teamAllowed = payload?.features?.['collaboration.team_management']?.allowed === true;
|
|
838
|
+
const stateAllows = state === 'active' || state === 'trialing' || state === 'grace';
|
|
839
|
+
setTeamPlanMode(planId === 'team' && teamAllowed && stateAllows ? 'team' : 'personal');
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
setTeamPlanMode('unknown');
|
|
843
|
+
}
|
|
844
|
+
}, [canManageCloudWorkspaces, currentWorkspaceRole, teamAdminHeaders]);
|
|
845
|
+
useEffect(() => {
|
|
846
|
+
if (!showAccountMenu && !showTeamManagementModal && !showAccountHub)
|
|
847
|
+
return;
|
|
848
|
+
void loadTeamPlanMode();
|
|
849
|
+
}, [showAccountHub, showAccountMenu, showTeamManagementModal, loadTeamPlanMode]);
|
|
850
|
+
useEffect(() => {
|
|
851
|
+
if (!showAccountHub)
|
|
852
|
+
return;
|
|
853
|
+
void loadBillingStatus();
|
|
854
|
+
}, [loadBillingStatus, showAccountHub]);
|
|
855
|
+
useEffect(() => {
|
|
856
|
+
if (!isAuthenticated)
|
|
857
|
+
return;
|
|
858
|
+
if (location.pathname !== '/billing/success' && location.pathname !== '/billing/cancel')
|
|
859
|
+
return;
|
|
860
|
+
setShowAccountHub(true);
|
|
861
|
+
setShowAccountMenu(false);
|
|
862
|
+
setBillingActionError(null);
|
|
863
|
+
setBillingError(null);
|
|
864
|
+
if (location.pathname === '/billing/cancel') {
|
|
865
|
+
setBillingNotice('Checkout canceled. No billing changes were applied.');
|
|
866
|
+
navigate('/', { replace: true });
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
setBillingNotice('Checkout complete. Waiting for billing confirmation...');
|
|
870
|
+
void loadBillingStatus();
|
|
871
|
+
let attempts = 0;
|
|
872
|
+
const maxAttempts = 30;
|
|
873
|
+
if (billingSuccessPollRef.current) {
|
|
874
|
+
window.clearInterval(billingSuccessPollRef.current);
|
|
875
|
+
billingSuccessPollRef.current = null;
|
|
876
|
+
}
|
|
877
|
+
billingSuccessPollRef.current = window.setInterval(() => {
|
|
878
|
+
attempts += 1;
|
|
879
|
+
void loadBillingStatus().then((payload) => {
|
|
880
|
+
const entitlementState = String(payload?.entitlementState || '').trim().toLowerCase();
|
|
881
|
+
if (entitlementState && entitlementState !== 'free') {
|
|
882
|
+
setBillingNotice('Subscription is active.');
|
|
883
|
+
if (billingSuccessPollRef.current) {
|
|
884
|
+
window.clearInterval(billingSuccessPollRef.current);
|
|
885
|
+
billingSuccessPollRef.current = null;
|
|
886
|
+
}
|
|
887
|
+
navigate('/', { replace: true });
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (attempts >= maxAttempts && billingSuccessPollRef.current) {
|
|
891
|
+
window.clearInterval(billingSuccessPollRef.current);
|
|
892
|
+
billingSuccessPollRef.current = null;
|
|
893
|
+
setBillingNotice('Checkout finished, but entitlement update is still pending. Refresh Billing Status in a few seconds.');
|
|
894
|
+
navigate('/', { replace: true });
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
}, 1500);
|
|
898
|
+
return () => {
|
|
899
|
+
if (billingSuccessPollRef.current) {
|
|
900
|
+
window.clearInterval(billingSuccessPollRef.current);
|
|
901
|
+
billingSuccessPollRef.current = null;
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
}, [isAuthenticated, loadBillingStatus, location.pathname, navigate]);
|
|
905
|
+
const loadTeamUsers = useCallback(async () => {
|
|
906
|
+
if (!canOpenTeamManagement)
|
|
907
|
+
return;
|
|
908
|
+
setTeamUsersLoading(true);
|
|
909
|
+
setTeamMgmtError(null);
|
|
910
|
+
try {
|
|
911
|
+
const res = await fetch('/api/taskforce/admin/users', {
|
|
912
|
+
method: 'GET',
|
|
913
|
+
credentials: 'include',
|
|
914
|
+
headers: teamAdminHeaders()
|
|
915
|
+
});
|
|
916
|
+
const payload = await res.json().catch(() => ({}));
|
|
917
|
+
if (!res.ok) {
|
|
918
|
+
setTeamMgmtError(payload?.error || 'Failed to load workspace users.');
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const users = Array.isArray(payload?.users) ? payload.users : [];
|
|
922
|
+
const roleSortOrder = { owner: 0, admin: 1, member: 2, 'read-only': 3 };
|
|
923
|
+
const mapped = users.map((user) => ({
|
|
924
|
+
userId: String(user?.userId || ''),
|
|
925
|
+
email: String(user?.email || ''),
|
|
926
|
+
displayName: typeof user?.displayName === 'string' ? user.displayName : null,
|
|
927
|
+
role: user?.role === 'owner' || user?.role === 'admin' || user?.role === 'member' || user?.role === 'read-only'
|
|
928
|
+
? user.role
|
|
929
|
+
: 'member',
|
|
930
|
+
permissionMode: user?.permissionMode === 'read-only' ? 'read-only' : 'read-write',
|
|
931
|
+
status: String(user?.status || 'active'),
|
|
932
|
+
disabled: user?.disabled === true
|
|
933
|
+
}));
|
|
934
|
+
mapped.sort((a, b) => {
|
|
935
|
+
const roleDelta = (roleSortOrder[a.role] ?? 99) - (roleSortOrder[b.role] ?? 99);
|
|
936
|
+
if (roleDelta !== 0)
|
|
937
|
+
return roleDelta;
|
|
938
|
+
return (a.displayName || a.email).localeCompare(b.displayName || b.email, undefined, { sensitivity: 'base' });
|
|
939
|
+
});
|
|
940
|
+
setTeamUsers(mapped);
|
|
941
|
+
}
|
|
942
|
+
catch {
|
|
943
|
+
setTeamMgmtError('Failed to load workspace users.');
|
|
944
|
+
}
|
|
945
|
+
finally {
|
|
946
|
+
setTeamUsersLoading(false);
|
|
947
|
+
}
|
|
948
|
+
}, [canOpenTeamManagement, teamAdminHeaders]);
|
|
949
|
+
const loadTeamAuditPage = useCallback(async (page) => {
|
|
950
|
+
if (!canOpenTeamManagement)
|
|
951
|
+
return;
|
|
952
|
+
const safePage = Math.max(0, Math.floor(page));
|
|
953
|
+
setTeamAuditLoading(true);
|
|
954
|
+
setTeamMgmtError(null);
|
|
955
|
+
try {
|
|
956
|
+
const offset = safePage * TEAM_AUDIT_PAGE_SIZE;
|
|
957
|
+
const res = await fetch(`/api/taskforce/admin/audit-logs?limit=${TEAM_AUDIT_PAGE_SIZE}&offset=${offset}`, {
|
|
958
|
+
method: 'GET',
|
|
959
|
+
credentials: 'include',
|
|
960
|
+
headers: teamAdminHeaders()
|
|
961
|
+
});
|
|
962
|
+
const payload = await res.json().catch(() => ({}));
|
|
963
|
+
if (!res.ok) {
|
|
964
|
+
setTeamMgmtError(payload?.error || 'Failed to load workspace audit log.');
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const events = Array.isArray(payload?.events) ? payload.events : [];
|
|
968
|
+
setTeamAuditEvents(events.map((event) => ({
|
|
969
|
+
id: String(event?.id || ''),
|
|
970
|
+
action: String(event?.action || ''),
|
|
971
|
+
actorUserId: String(event?.actorUserId || ''),
|
|
972
|
+
actorRole: String(event?.actorRole || ''),
|
|
973
|
+
createdAt: String(event?.createdAt || event?.ts || '')
|
|
974
|
+
})));
|
|
975
|
+
setTeamAuditPage(safePage);
|
|
976
|
+
const pages = Math.max(1, Number(payload?.pages || 1));
|
|
977
|
+
setTeamAuditPages(pages);
|
|
978
|
+
setTeamAuditHasMore(safePage + 1 < pages);
|
|
979
|
+
}
|
|
980
|
+
catch {
|
|
981
|
+
setTeamMgmtError('Failed to load workspace audit log.');
|
|
982
|
+
}
|
|
983
|
+
finally {
|
|
984
|
+
setTeamAuditLoading(false);
|
|
985
|
+
}
|
|
986
|
+
}, [canOpenTeamManagement, teamAdminHeaders]);
|
|
987
|
+
const reloadTeamManagementData = useCallback(async () => {
|
|
988
|
+
await Promise.all([loadTeamUsers(), loadTeamAuditPage(teamAuditPage)]);
|
|
989
|
+
}, [loadTeamUsers, loadTeamAuditPage, teamAuditPage]);
|
|
990
|
+
const openSharedLoginRoute = useCallback((mode = 'login') => {
|
|
991
|
+
const targetRaw = `${location.pathname}${location.search}${location.hash}`;
|
|
992
|
+
const target = (!targetRaw || targetRaw === '/login' || !targetRaw.startsWith('/')) ? '/' : targetRaw;
|
|
993
|
+
setShowAccountMenu(false);
|
|
994
|
+
setShowAccountHub(false);
|
|
995
|
+
setShowSyncStatusModal(false);
|
|
996
|
+
navigate(`/login?mode=${mode}&next=${encodeURIComponent(target)}`);
|
|
997
|
+
}, [location.hash, location.pathname, location.search, navigate]);
|
|
998
|
+
const openWorkspaceSetup = useCallback(() => {
|
|
999
|
+
setShowAccountMenu(false);
|
|
1000
|
+
setWorkspacePanelOpen(false);
|
|
1001
|
+
navigate('/setup?step=workspace', { replace: true });
|
|
1002
|
+
}, [navigate]);
|
|
1003
|
+
const handleSwitchWorkspace = useCallback(async (workspaceId) => {
|
|
1004
|
+
if (!workspaceId || workspaceActionBusy)
|
|
1005
|
+
return;
|
|
1006
|
+
setWorkspaceActionError('');
|
|
1007
|
+
setWorkspaceActionBusy(true);
|
|
1008
|
+
try {
|
|
1009
|
+
const result = await switchWorkspace(workspaceId);
|
|
1010
|
+
if (!result.success) {
|
|
1011
|
+
if (result.code === 'WORKSPACE_NOT_FOUND' || result.code === 'WORKSPACE_ID_REQUIRED') {
|
|
1012
|
+
openWorkspaceSetup();
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
setWorkspaceActionError(result.error || 'Failed to switch workspace.');
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
await fetchTasks(true);
|
|
1019
|
+
setShowAccountMenu(false);
|
|
1020
|
+
setWorkspacePanelOpen(false);
|
|
1021
|
+
}
|
|
1022
|
+
finally {
|
|
1023
|
+
setWorkspaceActionBusy(false);
|
|
1024
|
+
}
|
|
1025
|
+
}, [switchWorkspace, workspaceActionBusy, fetchTasks, openWorkspaceSetup]);
|
|
1026
|
+
const handleCreateWorkspace = useCallback(async () => {
|
|
1027
|
+
if (workspaceActionBusy)
|
|
1028
|
+
return;
|
|
1029
|
+
const name = workspaceFormName.trim();
|
|
1030
|
+
if (!name) {
|
|
1031
|
+
setWorkspaceActionError('Workspace name is required.');
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
setWorkspaceActionError('');
|
|
1035
|
+
setWorkspaceActionBusy(true);
|
|
1036
|
+
try {
|
|
1037
|
+
const result = await createWorkspace(name, workspaceFormDescription.trim() || undefined);
|
|
1038
|
+
if (!result.success) {
|
|
1039
|
+
if (result.code === 'WORKSPACE_NOT_FOUND' || result.code === 'WORKSPACE_ID_REQUIRED') {
|
|
1040
|
+
openWorkspaceSetup();
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
setWorkspaceActionError(result.error || 'Failed to create workspace.');
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
await fetchTasks(true);
|
|
1047
|
+
setWorkspaceFormName('');
|
|
1048
|
+
setWorkspaceFormDescription('');
|
|
1049
|
+
setWorkspacePanelOpen(false);
|
|
1050
|
+
setShowAccountMenu(false);
|
|
1051
|
+
}
|
|
1052
|
+
finally {
|
|
1053
|
+
setWorkspaceActionBusy(false);
|
|
1054
|
+
}
|
|
1055
|
+
}, [workspaceActionBusy, workspaceFormName, workspaceFormDescription, createWorkspace, fetchTasks, openWorkspaceSetup]);
|
|
1056
|
+
const openTeamManagementModal = useCallback(async () => {
|
|
1057
|
+
setShowAccountMenu(false);
|
|
1058
|
+
setShowTeamManagementModal(true);
|
|
1059
|
+
setTeamManagementTab('members');
|
|
1060
|
+
await loadTeamPlanMode();
|
|
1061
|
+
await loadTeamUsers();
|
|
1062
|
+
await loadTeamAuditPage(0);
|
|
1063
|
+
}, [loadTeamAuditPage, loadTeamPlanMode, loadTeamUsers]);
|
|
1064
|
+
const executeTeamUserAction = useCallback(async (userId, fn) => {
|
|
1065
|
+
setTeamMgmtError(null);
|
|
1066
|
+
setTeamActionBusyUserId(userId);
|
|
1067
|
+
try {
|
|
1068
|
+
const res = await fn();
|
|
1069
|
+
const payload = await res.json().catch(() => ({}));
|
|
1070
|
+
if (!res.ok) {
|
|
1071
|
+
setTeamMgmtError(payload?.error || 'Team management action failed.');
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
await reloadTeamManagementData();
|
|
1075
|
+
}
|
|
1076
|
+
catch {
|
|
1077
|
+
setTeamMgmtError('Team management action failed.');
|
|
1078
|
+
}
|
|
1079
|
+
finally {
|
|
1080
|
+
setTeamActionBusyUserId(null);
|
|
1081
|
+
}
|
|
1082
|
+
}, [reloadTeamManagementData]);
|
|
1083
|
+
const submitTeamInvite = useCallback(async () => {
|
|
1084
|
+
const email = teamInviteEmail.trim();
|
|
1085
|
+
if (!email)
|
|
1086
|
+
return;
|
|
1087
|
+
setTeamInviteBusy(true);
|
|
1088
|
+
setTeamMgmtError(null);
|
|
1089
|
+
setTeamInviteFeedback(null);
|
|
1090
|
+
try {
|
|
1091
|
+
const res = await fetch('/api/taskforce/admin/users/invite', {
|
|
1092
|
+
method: 'POST',
|
|
1093
|
+
headers: {
|
|
1094
|
+
'Content-Type': 'application/json',
|
|
1095
|
+
...teamAdminHeaders()
|
|
1096
|
+
},
|
|
1097
|
+
credentials: 'include',
|
|
1098
|
+
body: JSON.stringify({
|
|
1099
|
+
workspaceId: currentWorkspaceId,
|
|
1100
|
+
email,
|
|
1101
|
+
role: teamInviteRole,
|
|
1102
|
+
permissionMode: teamInviteRole === 'member' ? teamInvitePermissionMode : 'read-write'
|
|
1103
|
+
})
|
|
1104
|
+
});
|
|
1105
|
+
const payload = await res.json().catch(() => ({}));
|
|
1106
|
+
if (!res.ok) {
|
|
1107
|
+
setTeamMgmtError(payload?.error || 'Failed to send invite.');
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
setTeamInviteEmail('');
|
|
1111
|
+
if (payload?.inviteEmailSent === false) {
|
|
1112
|
+
setTeamInviteFeedback(`Invite created, but email delivery failed${payload?.inviteEmailError ? `: ${String(payload.inviteEmailError)}` : '.'}`);
|
|
1113
|
+
}
|
|
1114
|
+
else {
|
|
1115
|
+
setTeamInviteFeedback('Invite sent.');
|
|
1116
|
+
}
|
|
1117
|
+
await reloadTeamManagementData();
|
|
1118
|
+
}
|
|
1119
|
+
catch {
|
|
1120
|
+
setTeamMgmtError('Failed to send invite.');
|
|
1121
|
+
}
|
|
1122
|
+
finally {
|
|
1123
|
+
setTeamInviteBusy(false);
|
|
1124
|
+
}
|
|
1125
|
+
}, [
|
|
1126
|
+
currentWorkspaceId,
|
|
1127
|
+
reloadTeamManagementData,
|
|
1128
|
+
teamAdminHeaders,
|
|
1129
|
+
teamInviteEmail,
|
|
1130
|
+
teamInvitePermissionMode,
|
|
1131
|
+
teamInviteRole
|
|
1132
|
+
]);
|
|
1133
|
+
const hasSearchFilter = useMemo(() => searchQuery.trim().length > 0, [searchQuery]);
|
|
1134
|
+
const pendingInvites = useMemo(() => teamUsers.filter((user) => user.status === 'invited'), [teamUsers]);
|
|
1135
|
+
// Check if any non-search filters are set from the filters dropdown/panel
|
|
1136
|
+
const hasDropdownFilters = useMemo(() => {
|
|
1137
|
+
const hasCategoryFilter = filterCategories.length !== activeCategories.length;
|
|
1138
|
+
const hasTypeFilter = filterTypes.length !== activeTypes.length;
|
|
1139
|
+
const hasPriorityFilter = filterPriorities.length !== priorities.length;
|
|
1140
|
+
const hasStatusFilter = filterStatus.length !== STATUS_OPTIONS.length;
|
|
1141
|
+
const hasAssigneeFilter = assigneeFilterEnabled && filterAssignees.length !== 3;
|
|
1142
|
+
const hasParentsOnlyFilter = parentTaskFilterMode !== 'all';
|
|
1143
|
+
return hasCategoryFilter || hasTypeFilter || hasPriorityFilter || hasStatusFilter || hasAssigneeFilter || hasParentsOnlyFilter;
|
|
1144
|
+
}, [filterCategories, activeCategories, filterTypes, activeTypes, filterPriorities, priorities, filterStatus, assigneeFilterEnabled, filterAssignees, parentTaskFilterMode]);
|
|
1145
|
+
// Check if any filters are set (search + dropdown filters)
|
|
1146
|
+
const isFiltered = hasSearchFilter || hasDropdownFilters;
|
|
1147
|
+
const totalActiveCount = tasks.length;
|
|
1148
|
+
const visibleActiveCount = props.filteredTasks.length;
|
|
1149
|
+
const activeCountLabel = isFiltered ? `${visibleActiveCount}/${totalActiveCount}` : `${totalActiveCount}`;
|
|
1150
|
+
const activeCountTitle = isFiltered
|
|
1151
|
+
? t('standalone.visibleActiveTasksTitle')
|
|
1152
|
+
: t('standalone.totalActiveTasksTitle');
|
|
1153
|
+
const shouldShowSyncStatus = runtimeMode === 'local';
|
|
1154
|
+
const formattedLastSyncTime = useMemo(() => {
|
|
1155
|
+
if (!userGlobalLastSyncedAt)
|
|
1156
|
+
return 'Never';
|
|
1157
|
+
const parsed = new Date(userGlobalLastSyncedAt);
|
|
1158
|
+
if (Number.isNaN(parsed.getTime()))
|
|
1159
|
+
return 'Never';
|
|
1160
|
+
return parsed.toLocaleString();
|
|
1161
|
+
}, [userGlobalLastSyncedAt]);
|
|
1162
|
+
const formatSyncTimestamp = useCallback((value) => {
|
|
1163
|
+
if (!value)
|
|
1164
|
+
return 'Never';
|
|
1165
|
+
const parsed = new Date(value);
|
|
1166
|
+
if (Number.isNaN(parsed.getTime()))
|
|
1167
|
+
return 'Never';
|
|
1168
|
+
return parsed.toLocaleString();
|
|
1169
|
+
}, []);
|
|
1170
|
+
const formattedLastPullTime = useMemo(() => formatSyncTimestamp(workspaceLastPullAt), [workspaceLastPullAt, formatSyncTimestamp]);
|
|
1171
|
+
const formattedLastPushTime = useMemo(() => formatSyncTimestamp(workspaceLastPushAt), [workspaceLastPushAt, formatSyncTimestamp]);
|
|
1172
|
+
const formattedLastErrorTime = useMemo(() => formatSyncTimestamp(workspaceLastErrorAt), [workspaceLastErrorAt, formatSyncTimestamp]);
|
|
1173
|
+
const formattedRetryTime = useMemo(() => formatSyncTimestamp(workspaceRetryAt), [workspaceRetryAt, formatSyncTimestamp]);
|
|
1174
|
+
const [displaySyncState, setDisplaySyncState] = useState(() => (workspaceCloudSyncEnabled ? userGlobalSyncStatus : 'disconnected'));
|
|
1175
|
+
const syncStateSinceRef = useRef(Date.now());
|
|
1176
|
+
const syncStateTimerRef = useRef(null);
|
|
1177
|
+
const rawSyncState = useMemo(() => {
|
|
1178
|
+
if (!workspaceCloudSyncEnabled)
|
|
1179
|
+
return 'disconnected';
|
|
1180
|
+
if (userGlobalSyncStatus === 'error')
|
|
1181
|
+
return 'error';
|
|
1182
|
+
if (realtimeSyncEnabled && realtimeConnectionState === 'degraded-fallback')
|
|
1183
|
+
return 'degraded-fallback';
|
|
1184
|
+
if (realtimeSyncEnabled && realtimeConnectionState === 'reconnecting')
|
|
1185
|
+
return 'reconnecting';
|
|
1186
|
+
return userGlobalSyncStatus;
|
|
1187
|
+
}, [workspaceCloudSyncEnabled, userGlobalSyncStatus, realtimeSyncEnabled, realtimeConnectionState]);
|
|
1188
|
+
useEffect(() => {
|
|
1189
|
+
if (syncStateTimerRef.current !== null) {
|
|
1190
|
+
window.clearTimeout(syncStateTimerRef.current);
|
|
1191
|
+
syncStateTimerRef.current = null;
|
|
1192
|
+
}
|
|
1193
|
+
const applyState = (next) => {
|
|
1194
|
+
setDisplaySyncState(next);
|
|
1195
|
+
syncStateSinceRef.current = Date.now();
|
|
1196
|
+
};
|
|
1197
|
+
if (rawSyncState === displaySyncState)
|
|
1198
|
+
return;
|
|
1199
|
+
// Delay "syncing" to avoid flicker on very fast sync cycles.
|
|
1200
|
+
if (rawSyncState === 'syncing' || rawSyncState === 'reconnecting') {
|
|
1201
|
+
syncStateTimerRef.current = window.setTimeout(() => {
|
|
1202
|
+
applyState(rawSyncState);
|
|
1203
|
+
}, SYNCING_DISPLAY_DELAY_MS);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
// Hold syncing/error briefly so status doesn't flash rapidly.
|
|
1207
|
+
if (displaySyncState === 'syncing' || displaySyncState === 'reconnecting') {
|
|
1208
|
+
const elapsed = Date.now() - syncStateSinceRef.current;
|
|
1209
|
+
const remaining = Math.max(0, SYNCING_MIN_VISIBLE_MS - elapsed);
|
|
1210
|
+
syncStateTimerRef.current = window.setTimeout(() => {
|
|
1211
|
+
applyState(rawSyncState);
|
|
1212
|
+
}, remaining);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (displaySyncState === 'error' && rawSyncState !== 'error') {
|
|
1216
|
+
const elapsed = Date.now() - syncStateSinceRef.current;
|
|
1217
|
+
const remaining = Math.max(0, ERROR_MIN_VISIBLE_MS - elapsed);
|
|
1218
|
+
syncStateTimerRef.current = window.setTimeout(() => {
|
|
1219
|
+
applyState(rawSyncState);
|
|
1220
|
+
}, remaining);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
applyState(rawSyncState);
|
|
1224
|
+
}, [rawSyncState, displaySyncState]);
|
|
1225
|
+
useEffect(() => {
|
|
1226
|
+
return () => {
|
|
1227
|
+
if (syncStateTimerRef.current !== null) {
|
|
1228
|
+
window.clearTimeout(syncStateTimerRef.current);
|
|
1229
|
+
syncStateTimerRef.current = null;
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
}, []);
|
|
1233
|
+
const modalSyncStatusMeta = useMemo(() => {
|
|
1234
|
+
if (!workspaceCloudSyncEnabled) {
|
|
1235
|
+
return {
|
|
1236
|
+
label: 'Sync Off',
|
|
1237
|
+
color: '#94a3b8',
|
|
1238
|
+
border: 'rgba(148, 163, 184, 0.45)',
|
|
1239
|
+
background: 'rgba(148, 163, 184, 0.12)'
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
switch (displaySyncState) {
|
|
1243
|
+
case 'syncing':
|
|
1244
|
+
return {
|
|
1245
|
+
label: 'Syncing',
|
|
1246
|
+
color: '#f59e0b',
|
|
1247
|
+
border: 'rgba(245, 158, 11, 0.45)',
|
|
1248
|
+
background: 'rgba(245, 158, 11, 0.16)'
|
|
1249
|
+
};
|
|
1250
|
+
case 'reconnecting':
|
|
1251
|
+
return {
|
|
1252
|
+
label: 'Reconnecting',
|
|
1253
|
+
color: '#f59e0b',
|
|
1254
|
+
border: 'rgba(245, 158, 11, 0.45)',
|
|
1255
|
+
background: 'rgba(245, 158, 11, 0.16)'
|
|
1256
|
+
};
|
|
1257
|
+
case 'degraded-fallback':
|
|
1258
|
+
return {
|
|
1259
|
+
label: 'Fallback Pull',
|
|
1260
|
+
color: '#fb923c',
|
|
1261
|
+
border: 'rgba(251, 146, 60, 0.45)',
|
|
1262
|
+
background: 'rgba(251, 146, 60, 0.16)'
|
|
1263
|
+
};
|
|
1264
|
+
case 'error':
|
|
1265
|
+
return {
|
|
1266
|
+
label: 'Sync Error',
|
|
1267
|
+
color: '#f87171',
|
|
1268
|
+
border: 'rgba(248, 113, 113, 0.45)',
|
|
1269
|
+
background: 'rgba(248, 113, 113, 0.14)'
|
|
1270
|
+
};
|
|
1271
|
+
case 'disconnected':
|
|
1272
|
+
return {
|
|
1273
|
+
label: 'Disconnected',
|
|
1274
|
+
color: '#94a3b8',
|
|
1275
|
+
border: 'rgba(148, 163, 184, 0.45)',
|
|
1276
|
+
background: 'rgba(148, 163, 184, 0.12)'
|
|
1277
|
+
};
|
|
1278
|
+
case 'idle':
|
|
1279
|
+
default:
|
|
1280
|
+
return {
|
|
1281
|
+
label: 'Synced',
|
|
1282
|
+
color: '#34d399',
|
|
1283
|
+
border: 'rgba(52, 211, 153, 0.45)',
|
|
1284
|
+
background: 'rgba(52, 211, 153, 0.14)'
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
}, [displaySyncState, workspaceCloudSyncEnabled]);
|
|
1288
|
+
const headerSyncStatusMeta = useMemo(() => {
|
|
1289
|
+
if (!workspaceCloudSyncEnabled) {
|
|
1290
|
+
return {
|
|
1291
|
+
label: 'Sync Off',
|
|
1292
|
+
color: '#94a3b8',
|
|
1293
|
+
border: 'rgba(148, 163, 184, 0.45)',
|
|
1294
|
+
background: 'rgba(148, 163, 184, 0.12)',
|
|
1295
|
+
icon: 'off'
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
if (displaySyncState === 'error') {
|
|
1299
|
+
return {
|
|
1300
|
+
label: 'Sync Error',
|
|
1301
|
+
color: '#f87171',
|
|
1302
|
+
border: 'rgba(248, 113, 113, 0.45)',
|
|
1303
|
+
background: 'rgba(248, 113, 113, 0.14)',
|
|
1304
|
+
icon: 'off'
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
if (displaySyncState === 'degraded-fallback') {
|
|
1308
|
+
return {
|
|
1309
|
+
label: 'Fallback Pull',
|
|
1310
|
+
color: '#fb923c',
|
|
1311
|
+
border: 'rgba(251, 146, 60, 0.45)',
|
|
1312
|
+
background: 'rgba(251, 146, 60, 0.16)',
|
|
1313
|
+
icon: 'off'
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
if (displaySyncState === 'reconnecting') {
|
|
1317
|
+
return {
|
|
1318
|
+
label: 'Reconnecting',
|
|
1319
|
+
color: '#f59e0b',
|
|
1320
|
+
border: 'rgba(245, 158, 11, 0.45)',
|
|
1321
|
+
background: 'rgba(245, 158, 11, 0.16)',
|
|
1322
|
+
icon: 'off'
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
if (displaySyncState === 'disconnected') {
|
|
1326
|
+
return {
|
|
1327
|
+
label: 'Disconnected',
|
|
1328
|
+
color: '#94a3b8',
|
|
1329
|
+
border: 'rgba(148, 163, 184, 0.45)',
|
|
1330
|
+
background: 'rgba(148, 163, 184, 0.12)',
|
|
1331
|
+
icon: 'off'
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
return {
|
|
1335
|
+
label: 'Synced',
|
|
1336
|
+
color: '#34d399',
|
|
1337
|
+
border: 'rgba(52, 211, 153, 0.45)',
|
|
1338
|
+
background: 'rgba(52, 211, 153, 0.14)',
|
|
1339
|
+
icon: 'cloud'
|
|
1340
|
+
};
|
|
1341
|
+
}, [displaySyncState, workspaceCloudSyncEnabled]);
|
|
1342
|
+
const parentFilterLabel = hierarchyRootTaskId
|
|
1343
|
+
? 'Subtree'
|
|
1344
|
+
: parentTaskFilterMode === 'parents'
|
|
1345
|
+
? t('standalone.linksParents')
|
|
1346
|
+
: parentTaskFilterMode === 'linked'
|
|
1347
|
+
? t('standalone.linksLinked')
|
|
1348
|
+
: parentTaskFilterMode === 'unlinked'
|
|
1349
|
+
? t('standalone.linksUnlinked')
|
|
1350
|
+
: t('standalone.linksAll');
|
|
1351
|
+
const selectedHierarchyRootTask = hierarchyRootTaskId ? hierarchyTaskById.get(hierarchyRootTaskId) : undefined;
|
|
1352
|
+
const hierarchySubtreeStats = useMemo(() => {
|
|
1353
|
+
if (!hierarchyDescendantIdSet || !selectedHierarchyRootTask)
|
|
1354
|
+
return null;
|
|
1355
|
+
const selectedTasks = hierarchyTasks.filter((task) => hierarchyDescendantIdSet.has(task.id));
|
|
1356
|
+
let active = 0;
|
|
1357
|
+
let terminal = 0;
|
|
1358
|
+
let overdue = 0;
|
|
1359
|
+
selectedTasks.forEach((task) => {
|
|
1360
|
+
const isTerminal = task.status === 'done' || task.status === 'cancelled' || Boolean(task.isArchived);
|
|
1361
|
+
if (isTerminal) {
|
|
1362
|
+
terminal += 1;
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
active += 1;
|
|
1366
|
+
if (task.dueDate && task.dueDate < todayDateOnly)
|
|
1367
|
+
overdue += 1;
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
let depth = 0;
|
|
1371
|
+
const visited = new Set();
|
|
1372
|
+
const stack = [{ id: selectedHierarchyRootTask.id, depth: 0 }];
|
|
1373
|
+
while (stack.length > 0) {
|
|
1374
|
+
const current = stack.pop();
|
|
1375
|
+
if (visited.has(current.id))
|
|
1376
|
+
continue;
|
|
1377
|
+
visited.add(current.id);
|
|
1378
|
+
if (current.depth > depth)
|
|
1379
|
+
depth = current.depth;
|
|
1380
|
+
const children = hierarchyChildrenByParent.get(current.id) || [];
|
|
1381
|
+
children.forEach((child) => {
|
|
1382
|
+
if (!visited.has(child.id) && hierarchyDescendantIdSet.has(child.id)) {
|
|
1383
|
+
stack.push({ id: child.id, depth: current.depth + 1 });
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
return { total: selectedTasks.length, active, terminal, overdue, depth };
|
|
1388
|
+
}, [hierarchyDescendantIdSet, selectedHierarchyRootTask, hierarchyTasks, todayDateOnly, hierarchyChildrenByParent]);
|
|
1389
|
+
const hierarchyVisibleLevelCounts = useMemo(() => {
|
|
1390
|
+
const byLevel = new Map();
|
|
1391
|
+
hierarchyDepthTasks.forEach((task) => {
|
|
1392
|
+
const level = (hierarchyGlobalDepthMap.get(task.id) ?? 0) + 1;
|
|
1393
|
+
byLevel.set(level, (byLevel.get(level) || 0) + 1);
|
|
1394
|
+
});
|
|
1395
|
+
return Array.from(byLevel.entries())
|
|
1396
|
+
.sort((a, b) => a[0] - b[0])
|
|
1397
|
+
.map(([level, count]) => ({ level, count }));
|
|
1398
|
+
}, [hierarchyDepthTasks, hierarchyGlobalDepthMap]);
|
|
1399
|
+
const hierarchyVisibleUnlinkedCount = useMemo(() => hierarchyVisibleTasks.filter((task) => !task.parentTaskId && !hierarchyParentIdSet.has(task.id)).length, [hierarchyVisibleTasks, hierarchyParentIdSet]);
|
|
1400
|
+
const hierarchyVisibleRootCount = useMemo(() => hierarchyVisibleTasks.filter((task) => !task.parentTaskId && hierarchyParentIdSet.has(task.id)).length, [hierarchyVisibleTasks, hierarchyParentIdSet]);
|
|
1401
|
+
const hierarchySubtreeTaskList = useMemo(() => {
|
|
1402
|
+
if (!hierarchyRootTaskId || !hierarchyDescendantIdSet)
|
|
1403
|
+
return [];
|
|
1404
|
+
return hierarchyTasks
|
|
1405
|
+
.filter((task) => hierarchyDescendantIdSet.has(task.id))
|
|
1406
|
+
.sort((a, b) => {
|
|
1407
|
+
const levelA = hierarchyGlobalDepthMap.get(a.id) ?? Number.MAX_SAFE_INTEGER;
|
|
1408
|
+
const levelB = hierarchyGlobalDepthMap.get(b.id) ?? Number.MAX_SAFE_INTEGER;
|
|
1409
|
+
if (levelA !== levelB)
|
|
1410
|
+
return levelA - levelB;
|
|
1411
|
+
return a.title.localeCompare(b.title);
|
|
1412
|
+
});
|
|
1413
|
+
}, [hierarchyRootTaskId, hierarchyDescendantIdSet, hierarchyTasks, hierarchyGlobalDepthMap]);
|
|
1414
|
+
const applyHierarchySearchSelection = useCallback(() => {
|
|
1415
|
+
const bestMatch = hierarchySearchMatches[0];
|
|
1416
|
+
if (!bestMatch)
|
|
1417
|
+
return;
|
|
1418
|
+
setHierarchyRootTaskId(bestMatch.id);
|
|
1419
|
+
setHierarchyRootSearch('');
|
|
1420
|
+
if (parentTaskFilterMode === 'unlinked')
|
|
1421
|
+
setParentTaskFilterMode('all');
|
|
1422
|
+
}, [hierarchySearchMatches, parentTaskFilterMode, setParentTaskFilterMode]);
|
|
1423
|
+
const selectedDateObj = useMemo(() => parseDateOnlyLocal(scheduleSelectedDate) || new Date(), [scheduleSelectedDate]);
|
|
1424
|
+
const scheduleWeekStart = useMemo(() => startOfWeek(selectedDateObj, globalWeekStartsOn), [selectedDateObj, globalWeekStartsOn]);
|
|
1425
|
+
const scheduleDates = useMemo(() => {
|
|
1426
|
+
const map = {
|
|
1427
|
+
mon: '',
|
|
1428
|
+
tue: '',
|
|
1429
|
+
wed: '',
|
|
1430
|
+
thu: '',
|
|
1431
|
+
fri: '',
|
|
1432
|
+
sat: '',
|
|
1433
|
+
sun: ''
|
|
1434
|
+
};
|
|
1435
|
+
SCHEDULE_DAY_KEYS.forEach((key) => {
|
|
1436
|
+
const offset = getDayOffsetFromWeekStart(key, globalWeekStartsOn);
|
|
1437
|
+
map[key] = toDateOnlyLocal(addDays(scheduleWeekStart, offset));
|
|
1438
|
+
});
|
|
1439
|
+
return map;
|
|
1440
|
+
}, [scheduleWeekStart, globalWeekStartsOn]);
|
|
1441
|
+
const mapToSelectedWeekDate = useCallback((sourceDateOnly) => {
|
|
1442
|
+
const parsed = parseDateOnlyLocal(sourceDateOnly);
|
|
1443
|
+
if (!parsed)
|
|
1444
|
+
return scheduleDates.mon;
|
|
1445
|
+
const day = parsed.getDay();
|
|
1446
|
+
const key = day === 1 ? 'mon' :
|
|
1447
|
+
day === 2 ? 'tue' :
|
|
1448
|
+
day === 3 ? 'wed' :
|
|
1449
|
+
day === 4 ? 'thu' :
|
|
1450
|
+
day === 5 ? 'fri' :
|
|
1451
|
+
day === 6 ? (scheduleShowWeekends ? 'sat' : 'fri') :
|
|
1452
|
+
(scheduleShowWeekends ? 'sun' : 'fri');
|
|
1453
|
+
return scheduleDates[key] || scheduleDates.mon;
|
|
1454
|
+
}, [scheduleDates, scheduleShowWeekends]);
|
|
1455
|
+
const handleMoveExpiredToSelectedWeek = useCallback(async () => {
|
|
1456
|
+
if (scheduleBulkBusy || expiredLeafCandidates.length === 0)
|
|
1457
|
+
return;
|
|
1458
|
+
setScheduleBulkBusy(true);
|
|
1459
|
+
try {
|
|
1460
|
+
for (const task of expiredLeafCandidates) {
|
|
1461
|
+
const sourceDate = task.scheduledDate || task.dueDate || todayDateOnly;
|
|
1462
|
+
const nextDate = mapToSelectedWeekDate(sourceDate);
|
|
1463
|
+
await handleUpdateTask(task.id, {
|
|
1464
|
+
scheduledDate: nextDate,
|
|
1465
|
+
scheduledWeekKey: dateOnlyToIsoWeekKey(nextDate),
|
|
1466
|
+
orderInDay: null
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
await fetchTasks(true);
|
|
1470
|
+
}
|
|
1471
|
+
finally {
|
|
1472
|
+
setScheduleBulkBusy(false);
|
|
1473
|
+
}
|
|
1474
|
+
}, [scheduleBulkBusy, expiredLeafCandidates, todayDateOnly, mapToSelectedWeekDate, handleUpdateTask, fetchTasks]);
|
|
1475
|
+
const handleMoveExpiredToBacklog = useCallback(async () => {
|
|
1476
|
+
if (scheduleBulkBusy || expiredRecoveryCandidates.length === 0)
|
|
1477
|
+
return;
|
|
1478
|
+
setScheduleBulkBusy(true);
|
|
1479
|
+
try {
|
|
1480
|
+
for (const task of expiredRecoveryCandidates) {
|
|
1481
|
+
await handleUpdateTask(task.id, {
|
|
1482
|
+
scheduledDate: null,
|
|
1483
|
+
scheduledWeekKey: null,
|
|
1484
|
+
orderInDay: null
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
await fetchTasks(true);
|
|
1488
|
+
}
|
|
1489
|
+
finally {
|
|
1490
|
+
setScheduleBulkBusy(false);
|
|
1491
|
+
}
|
|
1492
|
+
}, [scheduleBulkBusy, expiredRecoveryCandidates, handleUpdateTask, fetchTasks]);
|
|
1493
|
+
const handleScheduleParentDrop = useCallback((payload) => {
|
|
1494
|
+
if (payload.targetDate < todayDateOnly) {
|
|
1495
|
+
const byId = new Map(tasks.map((task) => [task.id, task]));
|
|
1496
|
+
const hasNonTerminalLeaf = payload.toScheduleLeafTaskIds.some((taskId) => {
|
|
1497
|
+
const task = byId.get(taskId);
|
|
1498
|
+
if (!task)
|
|
1499
|
+
return true;
|
|
1500
|
+
return task.status !== 'done' && task.status !== 'cancelled';
|
|
1501
|
+
});
|
|
1502
|
+
if (hasNonTerminalLeaf) {
|
|
1503
|
+
window.alert(t('schedule.completedOnlyPastSchedule'));
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
if (payload.toScheduleLeafTaskIds.length === 0) {
|
|
1508
|
+
window.alert(t('schedule.noUnscheduledLeafTasks', { parentTitle: payload.parentTitle }));
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
setScheduleParentDropPrompt(payload);
|
|
1512
|
+
}, [tasks, todayDateOnly]);
|
|
1513
|
+
const confirmScheduleParentDrop = useCallback(async () => {
|
|
1514
|
+
if (!scheduleParentDropPrompt || scheduleParentDropBusy)
|
|
1515
|
+
return;
|
|
1516
|
+
setScheduleParentDropBusy(true);
|
|
1517
|
+
try {
|
|
1518
|
+
const { targetDate, toScheduleLeafTaskIds } = scheduleParentDropPrompt;
|
|
1519
|
+
const dayTasks = tasks
|
|
1520
|
+
.filter((task) => task.scheduledDate === targetDate && !toScheduleLeafTaskIds.includes(task.id))
|
|
1521
|
+
.sort((a, b) => {
|
|
1522
|
+
const orderA = typeof a.orderInDay === 'number' ? a.orderInDay : Number.MAX_SAFE_INTEGER;
|
|
1523
|
+
const orderB = typeof b.orderInDay === 'number' ? b.orderInDay : Number.MAX_SAFE_INTEGER;
|
|
1524
|
+
if (orderA !== orderB)
|
|
1525
|
+
return orderA - orderB;
|
|
1526
|
+
return String(a.createdAt || '').localeCompare(String(b.createdAt || ''));
|
|
1527
|
+
});
|
|
1528
|
+
let nextOrder = dayTasks.length;
|
|
1529
|
+
for (const taskId of toScheduleLeafTaskIds) {
|
|
1530
|
+
await handleUpdateTask(taskId, {
|
|
1531
|
+
scheduledDate: targetDate,
|
|
1532
|
+
scheduledWeekKey: dateOnlyToIsoWeekKey(targetDate),
|
|
1533
|
+
orderInDay: nextOrder++
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
setScheduleParentDropPrompt(null);
|
|
1537
|
+
await fetchTasks(true);
|
|
1538
|
+
}
|
|
1539
|
+
finally {
|
|
1540
|
+
setScheduleParentDropBusy(false);
|
|
1541
|
+
}
|
|
1542
|
+
}, [scheduleParentDropPrompt, scheduleParentDropBusy, tasks, handleUpdateTask, fetchTasks]);
|
|
1543
|
+
const cancelScheduleParentDrop = useCallback(() => {
|
|
1544
|
+
if (scheduleParentDropBusy)
|
|
1545
|
+
return;
|
|
1546
|
+
setScheduleParentDropPrompt(null);
|
|
1547
|
+
}, [scheduleParentDropBusy]);
|
|
1548
|
+
const scheduleMonthDate = useMemo(() => {
|
|
1549
|
+
const [yearRaw, monthRaw] = scheduleCalendarMonth.split('-');
|
|
1550
|
+
const year = Number(yearRaw);
|
|
1551
|
+
const month = Number(monthRaw);
|
|
1552
|
+
if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12) {
|
|
1553
|
+
return new Date(selectedDateObj.getFullYear(), selectedDateObj.getMonth(), 1);
|
|
1554
|
+
}
|
|
1555
|
+
return new Date(year, month - 1, 1);
|
|
1556
|
+
}, [scheduleCalendarMonth, selectedDateObj]);
|
|
1557
|
+
const scheduleCalendarMonthLabel = useMemo(() => formatDate(scheduleMonthDate, { month: 'long', year: 'numeric' }, resolvedLocale), [scheduleMonthDate, resolvedLocale]);
|
|
1558
|
+
const scheduleWeekLabel = useMemo(() => formatDate(scheduleWeekStart, { month: 'short', day: 'numeric', year: 'numeric' }, resolvedLocale), [scheduleWeekStart, resolvedLocale]);
|
|
1559
|
+
const calendarCells = useMemo(() => {
|
|
1560
|
+
const start = startOfWeek(scheduleMonthDate, globalWeekStartsOn);
|
|
1561
|
+
return Array.from({ length: 42 }, (_, i) => addDays(start, i));
|
|
1562
|
+
}, [scheduleMonthDate, globalWeekStartsOn]);
|
|
1563
|
+
const calendarTaskSignals = useMemo(() => {
|
|
1564
|
+
const byDate = new Map();
|
|
1565
|
+
for (const task of scheduleBaseTasks) {
|
|
1566
|
+
const dateOnly = task.scheduledDate || '';
|
|
1567
|
+
if (!dateOnly)
|
|
1568
|
+
continue;
|
|
1569
|
+
const isTerminal = task.status === 'done' || task.status === 'cancelled';
|
|
1570
|
+
const isExpired = !isTerminal && dateOnly < todayDateOnly;
|
|
1571
|
+
const existing = byDate.get(dateOnly);
|
|
1572
|
+
if (existing) {
|
|
1573
|
+
existing.count += 1;
|
|
1574
|
+
if (isExpired)
|
|
1575
|
+
existing.hasExpired = true;
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1578
|
+
byDate.set(dateOnly, { count: 1, hasExpired: isExpired });
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return byDate;
|
|
1582
|
+
}, [scheduleBaseTasks, todayDateOnly]);
|
|
1583
|
+
const calendarWeekdayLabels = useMemo(() => (globalWeekStartsOn === 'sunday'
|
|
1584
|
+
? ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
1585
|
+
: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']), [globalWeekStartsOn]);
|
|
1586
|
+
// Calculate columns based on grouping
|
|
1587
|
+
const scheduleColumns = useMemo(() => {
|
|
1588
|
+
const orderedKeys = globalWeekStartsOn === 'sunday'
|
|
1589
|
+
? ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
|
|
1590
|
+
: SCHEDULE_DAY_KEYS;
|
|
1591
|
+
const visibleKeys = scheduleShowWeekends
|
|
1592
|
+
? orderedKeys
|
|
1593
|
+
: ['mon', 'tue', 'wed', 'thu', 'fri'];
|
|
1594
|
+
const days = visibleKeys.map((key) => {
|
|
1595
|
+
const date = parseDateOnlyLocal(scheduleDates[key]) || scheduleWeekStart;
|
|
1596
|
+
const dateOnly = scheduleDates[key];
|
|
1597
|
+
const isWeekend = key === 'sat' || key === 'sun';
|
|
1598
|
+
return {
|
|
1599
|
+
value: key,
|
|
1600
|
+
label: `${SCHEDULE_DAY_LABELS[key]} ${formatDate(date, { month: 'short', day: 'numeric' }, resolvedLocale)}`,
|
|
1601
|
+
color: isWeekend ? '#1e3a8a' : '#3b82f6',
|
|
1602
|
+
icon: 'Calendar',
|
|
1603
|
+
date: dateOnly,
|
|
1604
|
+
isPast: dateOnly < todayDateOnly,
|
|
1605
|
+
isSelected: dateOnly === scheduleSelectedDate,
|
|
1606
|
+
isWeekend
|
|
1607
|
+
};
|
|
1608
|
+
});
|
|
1609
|
+
const hasExpiredTasks = expiredColumnCandidates.length > 0;
|
|
1610
|
+
return [
|
|
1611
|
+
...(scheduleShowBacklog ? [{ value: 'backlog', label: 'Backlog', color: '#64748b', icon: 'Inbox' }] : []),
|
|
1612
|
+
...(hasExpiredTasks ? [{ value: 'expired', label: 'Expired', color: '#ef4444', icon: 'AlertTriangle' }] : []),
|
|
1613
|
+
...days
|
|
1614
|
+
];
|
|
1615
|
+
}, [scheduleShowWeekends, scheduleShowBacklog, scheduleDates, scheduleWeekStart, expiredColumnCandidates.length, todayDateOnly, scheduleSelectedDate, globalWeekStartsOn, resolvedLocale]);
|
|
1616
|
+
const kanbanColumns = useMemo(() => {
|
|
1617
|
+
switch (groupBy) {
|
|
1618
|
+
case 'category':
|
|
1619
|
+
return activeCategories;
|
|
1620
|
+
case 'type':
|
|
1621
|
+
return (activeTypes || []).map(t => ({
|
|
1622
|
+
...t,
|
|
1623
|
+
icon: TypeMeta[t.value]?.icon,
|
|
1624
|
+
color: TypeMeta[t.value]?.color
|
|
1625
|
+
}));
|
|
1626
|
+
case 'priority':
|
|
1627
|
+
return priorities;
|
|
1628
|
+
case 'complexity':
|
|
1629
|
+
return [
|
|
1630
|
+
{ value: 1, label: 'Tiny', icon: 'Gauge', color: '#10b981' },
|
|
1631
|
+
{ value: 2, label: 'Low', icon: 'Gauge', color: '#14b8a6' },
|
|
1632
|
+
{ value: 3, label: 'Medium', icon: 'Gauge', color: '#3b82f6' },
|
|
1633
|
+
{ value: 4, label: 'High', icon: 'Gauge', color: '#8b5cf6' },
|
|
1634
|
+
{ value: 5, label: 'Epic', icon: 'Gauge', color: '#d946ef' }
|
|
1635
|
+
];
|
|
1636
|
+
case 'approach':
|
|
1637
|
+
return approaches;
|
|
1638
|
+
case 'assignee':
|
|
1639
|
+
return [
|
|
1640
|
+
{ value: 'agent', label: 'AI', icon: 'Bot', color: '#8b5cf6' },
|
|
1641
|
+
{ value: 'user', label: 'User', icon: 'User', color: '#22c55e' },
|
|
1642
|
+
{ value: 'unassigned', label: 'Unassigned', icon: 'Circle', color: '#64748b' }
|
|
1643
|
+
];
|
|
1644
|
+
case 'status':
|
|
1645
|
+
return [
|
|
1646
|
+
{ value: 'task', label: 'To Do', icon: 'Circle', color: 'blue-200' },
|
|
1647
|
+
{ value: 'on-hold', label: 'Blocked', icon: 'Pause', color: 'amber-500' },
|
|
1648
|
+
{ value: 'in-progress', label: 'In Progress', icon: 'Play', color: 'blue-500' },
|
|
1649
|
+
{ value: 'review', label: 'Ready for Review', icon: 'Sparkles', color: 'violet-500' },
|
|
1650
|
+
{ value: 'completed', label: 'Completed', icon: 'Check', color: 'green-500' }
|
|
1651
|
+
];
|
|
1652
|
+
case 'hierarchy': {
|
|
1653
|
+
if (parentTaskFilterMode === 'unlinked') {
|
|
1654
|
+
return [{
|
|
1655
|
+
value: 'standalone',
|
|
1656
|
+
label: 'Unlinked',
|
|
1657
|
+
icon: 'Circle',
|
|
1658
|
+
color: '#64748b'
|
|
1659
|
+
}];
|
|
1660
|
+
}
|
|
1661
|
+
const levels = Array.from({ length: hierarchyDepthTasks.length > 0 ? maxHierarchyDepth + 1 : 0 }, (_, index) => index + 1);
|
|
1662
|
+
const hierarchyColumns = levels.map((level) => ({
|
|
1663
|
+
value: level,
|
|
1664
|
+
label: `Level ${level}`,
|
|
1665
|
+
icon: 'Layers',
|
|
1666
|
+
color: level === 1 ? '#3b82f6' : '#64748b'
|
|
1667
|
+
}));
|
|
1668
|
+
if (parentTaskFilterMode === 'all' && hierarchyShowUnlinkedColumn) {
|
|
1669
|
+
hierarchyColumns.push({
|
|
1670
|
+
value: 'standalone',
|
|
1671
|
+
label: 'Unlinked',
|
|
1672
|
+
icon: 'Circle',
|
|
1673
|
+
color: '#64748b'
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
return hierarchyColumns;
|
|
1677
|
+
}
|
|
1678
|
+
case 'schedule':
|
|
1679
|
+
return scheduleColumns;
|
|
1680
|
+
default:
|
|
1681
|
+
return activeCategories;
|
|
1682
|
+
}
|
|
1683
|
+
}, [groupBy, activeCategories, activeTypes, priorities, approaches, maxHierarchyDepth, parentTaskFilterMode, scheduleColumns, hierarchyDepthTasks.length, hierarchyShowUnlinkedColumn]);
|
|
1684
|
+
return (_jsxs("div", { className: `${styles.standaloneWrapper} ${zenMode ? styles.zenModeEnabled : ''}`, "data-theme": currentTheme, children: [_jsxs("div", { className: `${styles.header} ${styles.standaloneHeader} `, children: [_jsxs("div", { className: `${styles.headerTitle} ${styles.standaloneTitle}`, children: [_jsx("img", { src: logo, alt: "Taskforce Logo", className: styles.brandIcon, onError: (e) => {
|
|
1685
|
+
const target = e.currentTarget;
|
|
1686
|
+
if (target.src !== favicon) {
|
|
1687
|
+
target.src = favicon;
|
|
1688
|
+
}
|
|
1689
|
+
else {
|
|
1690
|
+
target.style.display = 'none';
|
|
1691
|
+
}
|
|
1692
|
+
} }), _jsx("span", { children: "TaskForce" }), projectName && (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.projectSlash, children: "/" }), _jsx("span", { className: styles.projectName, title: `Workspace ID: ${currentWorkspaceId}`, children: projectName })] })), _jsx("span", { className: styles.taskCountBadge, title: activeCountTitle, children: activeCountLabel }), shouldShowSyncStatus && (_jsx("button", { className: styles.headerActionBtn, onClick: () => setShowSyncStatusModal(true), title: `Cloud sync status: ${headerSyncStatusMeta.label} | Pull: ${formattedLastPullTime} | Push: ${formattedLastPushTime}${realtimeSyncEnabled ? ` | Realtime: ${realtimeConnectionState} (${realtimeSyncFlagSource}) dup=${realtimeTelemetry.duplicateDiscarded} ooo=${realtimeTelemetry.outOfOrderDiscarded} invalid=${realtimeTelemetry.invalidDiscarded}` : ''}`, style: {
|
|
1693
|
+
marginLeft: '6px',
|
|
1694
|
+
height: '24px',
|
|
1695
|
+
width: '24px',
|
|
1696
|
+
padding: 0,
|
|
1697
|
+
borderRadius: '999px',
|
|
1698
|
+
border: `1px solid ${headerSyncStatusMeta.border}`,
|
|
1699
|
+
background: headerSyncStatusMeta.background,
|
|
1700
|
+
color: headerSyncStatusMeta.color,
|
|
1701
|
+
display: 'inline-flex',
|
|
1702
|
+
alignItems: 'center',
|
|
1703
|
+
justifyContent: 'center'
|
|
1704
|
+
}, children: headerSyncStatusMeta.icon === 'off' ? (_jsx(CloudOff, { size: 12 })) : (_jsx(Cloud, { size: 12 })) })), props.loadingTasks && (_jsx("div", { style: { marginLeft: '8px', display: 'flex', alignItems: 'center' }, children: _jsx(Loader2, { size: 14, className: styles.spinner }) })), successBanner && (_jsxs("div", { className: styles.headerSuccess, children: [_jsx(Check, { size: 14 }), _jsx("span", { children: successBanner.message })] }))] }), _jsxs("div", { className: styles.headerActions, style: { gap: '16px' }, children: [_jsxs("div", { className: styles.searchContainer, style: { width: '240px' }, children: [_jsx(Search, { size: 16, className: styles.searchIcon }), _jsx("input", { type: "text", className: `${styles.searchInput} ${hasSearchFilter ? styles.searchActive : ''}`, placeholder: t('standalone.searchPlaceholder'), value: searchQuery, onChange: (e) => setSearchQuery(e.target.value) }), searchQuery && (_jsx("button", { className: styles.clearSearchBtn, onClick: () => setSearchQuery(''), title: t('standalone.clearSearchTitle'), children: _jsx(X, { size: 14 }) }))] }), _jsxs("div", { className: styles.groupByContainer, children: [_jsxs("span", { className: styles.groupByLabel, children: [_jsx(ArrowUpDown, { size: 12 }), " ", t('standalone.sortLabel')] }), _jsxs("select", { className: `${styles.select} ${styles.groupBySelect} `, value: sortBy, onChange: (e) => setSortBy(e.target.value), children: [_jsx("option", { value: "created", children: t('standalone.sortCreated') }), _jsx("option", { value: "updated", children: t('standalone.sortUpdated') }), _jsx("option", { value: "priority", children: t('standalone.sortPriority') }), _jsx("option", { value: "complexity", children: t('standalone.sortComplexity') })] }), _jsx("button", { className: `${styles.headerActionBtn} ${styles.sortDirectionBtn}`, onClick: () => props.toggleSortOrder(), title: props.sortOrder === 'desc' ? t('standalone.sortDirectionDescTitle') : t('standalone.sortDirectionAscTitle'), children: props.sortOrder === 'desc' ? _jsx(ArrowDown, { size: 14 }) : _jsx(ArrowUp, { size: 14 }) })] }), _jsx("div", { className: styles.headerDivider }), groupBy !== 'docs' && (_jsxs("div", { className: styles.groupByContainer, children: [_jsxs("span", { className: styles.groupByLabel, children: [_jsx(Layers, { size: 12 }), " ", t('standalone.groupLabel')] }), _jsxs("select", { className: `${styles.select} ${styles.groupBySelect} `, value: groupBy, onChange: (e) => setGroupBy(e.target.value), children: [_jsx("option", { value: "category", children: t('standalone.groupCategory') }), _jsx("option", { value: "type", children: t('standalone.groupType') }), _jsx("option", { value: "priority", children: t('standalone.groupPriority') }), _jsx("option", { value: "complexity", children: t('standalone.groupComplexity') }), _jsx("option", { value: "approach", children: t('standalone.groupApproach') }), _jsx("option", { value: "assignee", children: t('standalone.groupAssignee') }), _jsx("option", { value: "status", children: t('standalone.groupStatus') }), _jsx("option", { value: "hierarchy", children: t('standalone.groupHierarchy') }), _jsx("option", { value: "schedule", children: t('standalone.groupSchedule') })] })] })), _jsx("button", { className: `${styles.headerActionBtn} ${groupBy === 'docs' ? styles.headerActionBtnActive : ''}`, onClick: () => setGroupBy(groupBy === 'docs' ? lastNonDocsGroupByRef.current : 'docs'), title: groupBy === 'docs' ? 'Exit Docs view' : 'Open Docs view', children: _jsx(FileText, { size: 15 }) }), _jsxs("button", { className: `${styles.headerActionBtn} ${emptyColumnMode !== 'show' ? styles.headerActionBtnActive : ''} `, onClick: () => {
|
|
1705
|
+
setEmptyColumnMode(prev => {
|
|
1706
|
+
if (prev === 'show')
|
|
1707
|
+
return 'collapse';
|
|
1708
|
+
if (prev === 'collapse')
|
|
1709
|
+
return 'hide';
|
|
1710
|
+
return 'show';
|
|
1711
|
+
});
|
|
1712
|
+
}, title: emptyColumnMode === 'show' ? t('standalone.showEmptyColumns') :
|
|
1713
|
+
emptyColumnMode === 'collapse' ? t('standalone.compressEmptyColumns') :
|
|
1714
|
+
t('standalone.hideEmptyColumns'), children: [emptyColumnMode === 'show' && _jsx(Maximize2, { size: 16 }), emptyColumnMode === 'collapse' && _jsx(Minimize2, { size: 16 }), emptyColumnMode === 'hide' && _jsx(EyeOff, { size: 16 })] }), _jsx("button", { className: `${styles.headerActionBtn} ${compressedCards ? styles.headerActionBtnActive : ''}`, onClick: () => setCompressedCards(!compressedCards), title: compressedCards ? t('standalone.expandCards') : t('standalone.compressCards'), children: compressedCards ? _jsx(ChevronsDown, { size: 16 }) : _jsx(ChevronsUp, { size: 16 }) }), _jsx("button", { className: `${styles.headerActionBtn} ${zenMode ? styles.headerActionBtnActive : ''}`, onClick: () => setZenMode(), title: zenMode ? t('standalone.exitZenMode') : t('standalone.enterZenMode'), children: _jsx(Zap, { size: 16, fill: zenMode ? "currentColor" : "none" }) }), _jsx("div", { className: styles.headerDivider }), _jsx("button", { className: `${styles.headerActionBtn} ${showFilters ? styles.headerActionBtnActive : ''} ${hasDropdownFilters ? styles.filterGlow : ''}`, onClick: () => setShowFilters(!showFilters), title: t('standalone.toggleFilters'), children: _jsx(Filter, { size: 16 }) }), _jsxs("button", { className: styles.primaryUpdateBtn, onClick: () => {
|
|
1715
|
+
resetForm();
|
|
1716
|
+
setActiveTab('add');
|
|
1717
|
+
}, children: [_jsx(Plus, { size: 16 }), " ", t('standalone.addTask')] }), _jsxs("div", { className: styles.accountMenuWrap, ref: accountMenuRef, children: [_jsx("button", { className: `${styles.headerActionBtn} ${styles.avatarBtn}`, onClick: () => setShowAccountMenu(prev => !prev), title: isAuthenticated ? 'Account' : 'Account (Not signed in)', "aria-label": isAuthenticated ? 'Account' : 'Account (Not signed in)', "aria-haspopup": "menu", "aria-expanded": showAccountMenu, children: _jsx("span", { className: styles.avatarBadge, "aria-hidden": "true", children: isAuthenticated ? 'U' : '?' }) }), showAccountMenu && (_jsxs("div", { className: styles.accountMenu, role: "menu", "aria-label": "Account menu", children: [_jsxs("div", { className: styles.accountMenuSection, children: [_jsx("div", { className: styles.accountMenuSectionLabel, children: "Account" }), _jsx("div", { className: styles.accountMenuHint, children: isAuthBootstrapPending
|
|
1718
|
+
? 'Checking sign-in status...'
|
|
1719
|
+
: isAuthenticated
|
|
1720
|
+
? (_jsx(_Fragment, { children: hasAuthIdentity
|
|
1721
|
+
? _jsxs(_Fragment, { children: ["Signed in as ", _jsx("span", { className: styles.accountIdentityEmail, children: authIdentityLabel })] })
|
|
1722
|
+
: 'Signed in' }))
|
|
1723
|
+
: 'Not signed in' })] }), canManageCloudWorkspaces && (_jsxs("div", { className: styles.accountMenuSection, children: [_jsx("div", { className: styles.accountMenuSectionLabel, children: "Workspaces (Owned + Invited)" }), availableWorkspaces.length === 0 && (_jsx("div", { className: styles.accountMenuHint, children: "No workspaces found for this account yet." })), availableWorkspaces.map((workspace) => (_jsxs("button", { className: `${styles.accountMenuItem} ${workspace.id === currentWorkspaceId ? styles.accountMenuItemActive : ''}`, onClick: () => handleSwitchWorkspace(workspace.id), role: "menuitem", disabled: workspaceActionBusy || workspace.id === currentWorkspaceId, title: workspace.description || workspace.name, children: [_jsx(Folder, { size: 15 }), _jsxs("span", { style: { display: 'flex', flexDirection: 'column', gap: '2px' }, children: [_jsx("span", { children: workspace.name }), _jsx("span", { className: styles.accountMenuMeta, children: workspace.role })] })] }, workspace.id))), _jsxs("button", { className: styles.accountMenuItem, onClick: () => {
|
|
1724
|
+
setWorkspaceActionError('');
|
|
1725
|
+
setWorkspacePanelOpen((prev) => !prev);
|
|
1726
|
+
}, role: "menuitem", disabled: workspaceActionBusy, children: [_jsx(Plus, { size: 15 }), workspacePanelOpen ? 'Close Workspace Form' : 'Create Workspace'] }), workspacePanelOpen && (_jsxs("div", { className: styles.accountMenuWorkspaceForm, children: [_jsx("input", { className: styles.input, value: workspaceFormName, onChange: (event) => setWorkspaceFormName(event.target.value), placeholder: "Workspace name", disabled: workspaceActionBusy }), _jsx("input", { className: styles.input, value: workspaceFormDescription, onChange: (event) => setWorkspaceFormDescription(event.target.value), placeholder: "Workspace description (optional)", disabled: workspaceActionBusy }), _jsx("button", { className: styles.primaryUpdateBtn, onClick: handleCreateWorkspace, disabled: workspaceActionBusy, children: workspaceActionBusy ? 'Saving...' : 'Save Workspace' })] })), workspaceActionError && (_jsx("div", { className: styles.accountMenuError, children: workspaceActionError }))] })), canOpenTeamManagement && (_jsxs("button", { className: styles.accountMenuItem, onClick: () => { void openTeamManagementModal(); }, role: "menuitem", children: [_jsx(Users, { size: 15 }), "Team Management"] })), _jsxs("button", { className: styles.accountMenuItem, onClick: () => {
|
|
1727
|
+
setShowAccountMenu(false);
|
|
1728
|
+
setActiveTab('settings');
|
|
1729
|
+
setSettingsSection('general');
|
|
1730
|
+
}, role: "menuitem", children: [_jsx(Settings, { size: 15 }), "Workspace Settings"] }), _jsxs("button", { className: styles.accountMenuItem, onClick: () => {
|
|
1731
|
+
setShowAccountMenu(false);
|
|
1732
|
+
setShowAccountHub(true);
|
|
1733
|
+
}, role: "menuitem", children: [_jsx(User, { size: 15 }), "Account Hub"] }), _jsxs("button", { className: styles.accountMenuItem, onClick: () => {
|
|
1734
|
+
setShowAccountMenu(false);
|
|
1735
|
+
setShowHelpModal(true);
|
|
1736
|
+
}, role: "menuitem", children: [_jsx(HelpCircle, { size: 15 }), "Help & Tutorial"] }), runtimeMode === 'local' && !isAuthenticated && !isAuthBootstrapPending && (_jsxs("button", { className: styles.accountMenuItem, onClick: () => {
|
|
1737
|
+
openSharedLoginRoute('login');
|
|
1738
|
+
}, role: "menuitem", children: [_jsx(User, { size: 15 }), "Sign In / Register"] })), isAuthenticated && (_jsxs("button", { className: styles.accountMenuItem, onClick: async () => {
|
|
1739
|
+
setShowAccountMenu(false);
|
|
1740
|
+
await logout();
|
|
1741
|
+
}, role: "menuitem", children: [_jsx(LogOut, { size: 15 }), "Sign Out"] }))] }))] })] })] }), authBlocked && (_jsx("div", { className: styles.authBlockedBanner, children: "Authentication required for this environment. Use the Account menu to sign in." })), showFilters && (_jsxs("div", { className: styles.filterToolbar, style: {
|
|
1742
|
+
padding: '8px 24px',
|
|
1743
|
+
borderBottom: '1px solid var(--border-color)',
|
|
1744
|
+
background: 'var(--bg-secondary)',
|
|
1745
|
+
display: 'flex',
|
|
1746
|
+
alignItems: 'center',
|
|
1747
|
+
gap: '12px',
|
|
1748
|
+
flexWrap: 'wrap',
|
|
1749
|
+
position: 'relative',
|
|
1750
|
+
zIndex: 50
|
|
1751
|
+
}, children: [_jsx("span", { style: { fontSize: '12px', fontWeight: 600, color: 'var(--text-secondary)', marginRight: '4px' }, children: t('standalone.filtersCaps') }), _jsx(MultiSelectFilter, { label: t('standalone.categoryLabel'), options: activeCategories.filter(c => !c.disabled), selected: filterCategories, onChange: (vals) => setFilterCategories(vals), variant: "value" }), _jsx(MultiSelectFilter, { label: t('standalone.typeLabel'), options: activeTypes.map(t => ({ value: t.value, label: t.label })), selected: filterTypes, onChange: (vals) => setFilterTypes(vals), variant: "value" }), _jsx(MultiSelectFilter, { label: t('standalone.priorityLabel'), options: priorities, selected: filterPriorities, onChange: (vals) => setFilterPriorities(vals), variant: "value" }), _jsx(MultiSelectFilter, { label: t('standalone.statusLabel'), options: STATUS_OPTIONS, selected: filterStatus, onChange: (vals) => setFilterStatus(vals), variant: "value" }), assigneeFilterEnabled && (_jsx(MultiSelectFilter, { label: t('standalone.assigneeLabel'), options: [
|
|
1752
|
+
{ value: 'agent', label: 'AI' },
|
|
1753
|
+
{ value: 'user', label: 'User' },
|
|
1754
|
+
{ value: 'unassigned', label: 'Unassigned' }
|
|
1755
|
+
], selected: filterAssignees, onChange: (vals) => setFilterAssignees(vals), variant: "value" })), _jsx("div", { className: styles.headerDivider, style: { height: '16px', margin: '0 8px' } }), _jsxs("div", { className: styles.groupByContainer, children: [_jsxs("span", { className: styles.groupByLabel, children: [_jsx(Layers, { size: 12 }), " ", t('standalone.linksLabel')] }), _jsxs("select", { className: `${styles.select} ${styles.groupBySelect}`, value: parentTaskFilterMode, onChange: (e) => setParentTaskFilterMode(e.target.value), title: t('standalone.chooseLinksTitle'), children: [_jsx("option", { value: "all", children: t('standalone.linksAll') }), _jsx("option", { value: "parents", children: t('standalone.linksParents') }), _jsx("option", { value: "linked", children: t('standalone.linksLinked') }), _jsx("option", { value: "unlinked", children: t('standalone.linksUnlinked') })] })] }), _jsxs("label", { className: styles.archiveToggle, title: t('standalone.includeArchiveTitle'), children: [_jsx("input", { type: "checkbox", checked: showArchive, onChange: (e) => {
|
|
1756
|
+
setShowArchive(e.target.checked);
|
|
1757
|
+
if (e.target.checked)
|
|
1758
|
+
fetchArchive();
|
|
1759
|
+
} }), _jsx("span", { children: t('standalone.includeArchive') })] }), _jsx("div", { style: { flex: 1 } }), _jsxs("button", { onClick: clearFilters, style: {
|
|
1760
|
+
background: 'none',
|
|
1761
|
+
border: 'none',
|
|
1762
|
+
color: 'var(--text-secondary)',
|
|
1763
|
+
cursor: 'pointer',
|
|
1764
|
+
fontSize: '12px',
|
|
1765
|
+
display: 'flex',
|
|
1766
|
+
alignItems: 'center',
|
|
1767
|
+
gap: '4px'
|
|
1768
|
+
}, children: [_jsx(X, { size: 12 }), " ", t('standalone.clearFilters')] })] })), groupBy === 'docs' ? (_jsx("div", { className: `${styles.standalonePage} ${styles.standaloneContent}`, style: { position: 'relative', display: 'flex', flex: 1, minHeight: 0, overflow: 'hidden' }, children: _jsx(DocumentWorkspace, { tasks: tasks, apiBaseUrl: props.config?.apiBaseUrl || '', requestedDocPath: requestedDocPath, onRequestedDocHandled: () => setRequestedDocPath(null) }) })) : (_jsxs("div", { className: `${styles.standalonePage} ${styles.standaloneContent} `, style: {
|
|
1769
|
+
position: 'relative',
|
|
1770
|
+
opacity: props.loadingTasks ? 0.7 : 1,
|
|
1771
|
+
transition: 'opacity 0.2s ease, padding-right 220ms cubic-bezier(0.4, 0, 0.2, 1)',
|
|
1772
|
+
display: 'flex',
|
|
1773
|
+
flex: 1,
|
|
1774
|
+
minHeight: 0,
|
|
1775
|
+
paddingRight: (groupBy === 'schedule' && scheduleSidebarOpen) || (groupBy === 'hierarchy' && hierarchySidebarOpen) ? '332px' : 0
|
|
1776
|
+
}, children: [_jsxs("div", { style: { position: 'relative', width: '100%', minWidth: 0, flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [_jsx("div", { style: {
|
|
1777
|
+
position: 'absolute',
|
|
1778
|
+
top: 0,
|
|
1779
|
+
right: 0,
|
|
1780
|
+
width: '100%',
|
|
1781
|
+
height: '100%',
|
|
1782
|
+
backgroundImage: `url(${taskforceBg})`,
|
|
1783
|
+
backgroundRepeat: 'no-repeat',
|
|
1784
|
+
backgroundPosition: 'top right',
|
|
1785
|
+
backgroundSize: '60%',
|
|
1786
|
+
opacity: 0.2,
|
|
1787
|
+
pointerEvents: 'none',
|
|
1788
|
+
zIndex: 0
|
|
1789
|
+
} }), boardVisibleTasks.length === 0 && groupBy !== 'schedule' ? (_jsxs("div", { className: styles.emptyState, children: [_jsx(Layers, { size: 48 }), _jsx("p", { children: groupBy === 'hierarchy' && hierarchyRootTaskId ? 'No tasks match this subtree with current filters.' : t('standalone.noTasksMatchFilters') }), _jsx("p", { style: { fontSize: '12px', color: 'var(--text-secondary)' }, children: groupBy === 'hierarchy' && hierarchyRootTaskId
|
|
1790
|
+
? `Focused on ${selectedHierarchyRootTask?.title || hierarchyRootTaskId}. Try clearing subtree focus or broadening filters.`
|
|
1791
|
+
: t('standalone.showingWithLinksHint', { parentFilterLabel }) })] })) : (_jsx(TaskKanban, { tasks: boardVisibleTasks, allTasks: kanbanAllTasks, hierarchyTasks: hierarchyTasks, columns: kanbanColumns, groupBy: groupBy, scheduleDates: scheduleDates, searchQuery: searchQuery, filterCategories: filterCategories, filterTypes: filterTypes, filterPriorities: filterPriorities, filterStatus: filterStatus, filterAssignees: filterAssignees, assigneeFilterEnabled: assigneeFilterEnabled, scheduleFilteredTaskIds: Array.from(scheduleFilteredTaskIds), copiedId: copiedId, availableSpecialists: availableSpecialists, taxonomies: taxonomies, priorities: priorities, approaches: approaches, onUpdateTask: handleUpdateTask, onTaskClick: (task) => handleEdit(task), onOpenTaskById: handleOpenTaskById, onCopyId: handleCopyId, onToggleInProgress: handleToggleInProgress, onToggleReview: handleToggleReview, onToggleComplete: handleToggleComplete, onToggleCancel: handleToggleCancel, onArchiveTask: handleArchiveTask, onUnarchive: handleUnarchive, onDelete: handleDelete, onUnlinkChildTask: handleUnlinkChildTask, onClearParentTask: handleClearParentTask, onAddTaskToColumn: handleAddTaskToColumn, onScheduleParentDrop: handleScheduleParentDrop, onScheduleDaySelected: (dateOnly) => {
|
|
1792
|
+
setScheduleSelectedDate(dateOnly);
|
|
1793
|
+
const parsed = parseDateOnlyLocal(dateOnly);
|
|
1794
|
+
if (parsed) {
|
|
1795
|
+
setScheduleCalendarMonth(`${parsed.getFullYear()}-${String(parsed.getMonth() + 1).padStart(2, '0')}`);
|
|
1796
|
+
}
|
|
1797
|
+
}, persistedScrollLeft: groupBy === 'schedule' ? scheduleScrollLeft : undefined, onScrollLeftChange: (scrollLeft) => {
|
|
1798
|
+
if (groupBy !== 'schedule')
|
|
1799
|
+
return;
|
|
1800
|
+
setScheduleScrollLeft(scrollLeft);
|
|
1801
|
+
}, emptyColumnMode: emptyColumnMode, categories: activeCategories, compressed: compressedCards, recentlyChangedTaskIds: props.recentlyChangedTaskIds, hierarchyChildOrderMode: hierarchyChildOrderMode, sortBy: sortBy, sortOrder: props.sortOrder }, `${groupBy}-${kanbanColumns.map(c => String(c.value)).join('|')}-${scheduleDates.mon}`)), groupBy === 'schedule' && !scheduleSidebarOpen && (_jsx("button", { type: "button", className: styles.headerActionBtn, onClick: () => setScheduleSidebarOpen(true), title: "Show Schedule Sidebar", style: {
|
|
1802
|
+
position: 'absolute',
|
|
1803
|
+
top: '10px',
|
|
1804
|
+
right: '10px',
|
|
1805
|
+
zIndex: 20,
|
|
1806
|
+
border: '1px solid var(--border-color)',
|
|
1807
|
+
background: 'var(--bg-secondary)'
|
|
1808
|
+
}, children: _jsx(PanelRightOpen, { size: 16 }) })), groupBy === 'hierarchy' && !hierarchySidebarOpen && (_jsx("button", { type: "button", className: styles.headerActionBtn, onClick: () => setHierarchySidebarOpen(true), title: "Show Hierarchy Sidebar", style: {
|
|
1809
|
+
position: 'absolute',
|
|
1810
|
+
top: '10px',
|
|
1811
|
+
right: '10px',
|
|
1812
|
+
zIndex: 20,
|
|
1813
|
+
border: '1px solid var(--border-color)',
|
|
1814
|
+
background: 'var(--bg-secondary)'
|
|
1815
|
+
}, children: _jsx(PanelRightOpen, { size: 16 }) }))] }), groupBy === 'hierarchy' && (_jsxs("aside", { className: styles.appScrollbar, style: {
|
|
1816
|
+
position: 'absolute',
|
|
1817
|
+
top: 0,
|
|
1818
|
+
right: 0,
|
|
1819
|
+
bottom: 0,
|
|
1820
|
+
width: '320px',
|
|
1821
|
+
minWidth: '320px',
|
|
1822
|
+
borderLeft: '1px solid var(--border-color)',
|
|
1823
|
+
background: 'var(--bg-secondary)',
|
|
1824
|
+
padding: '12px',
|
|
1825
|
+
display: 'flex',
|
|
1826
|
+
flexDirection: 'column',
|
|
1827
|
+
gap: '12px',
|
|
1828
|
+
zIndex: 40,
|
|
1829
|
+
overflowY: 'auto',
|
|
1830
|
+
transform: hierarchySidebarOpen ? 'translateX(0)' : 'translateX(108%)',
|
|
1831
|
+
pointerEvents: hierarchySidebarOpen ? 'auto' : 'none',
|
|
1832
|
+
transition: 'transform 220ms cubic-bezier(0.4, 0, 0.2, 1)',
|
|
1833
|
+
willChange: 'transform'
|
|
1834
|
+
}, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', fontWeight: 600 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx(Layers, { size: 16 }), _jsx("span", { children: "Hierarchy Controls" })] }), _jsx("button", { type: "button", className: styles.headerActionBtn, onClick: () => setHierarchySidebarOpen(false), title: "Hide Hierarchy Sidebar", children: _jsx(PanelRightClose, { size: 16 }) })] }), _jsxs("div", { style: {
|
|
1835
|
+
border: '1px solid var(--border-color)',
|
|
1836
|
+
borderRadius: '10px',
|
|
1837
|
+
padding: '10px',
|
|
1838
|
+
display: 'flex',
|
|
1839
|
+
flexDirection: 'column',
|
|
1840
|
+
gap: '8px'
|
|
1841
|
+
}, children: [_jsx("div", { style: { fontSize: '12px', fontWeight: 600 }, children: "Focus Subtree" }), _jsx("input", { type: "text", className: styles.input, placeholder: "Search parent tasks...", value: hierarchyRootSearch, onChange: (e) => setHierarchyRootSearch(e.target.value), onKeyDown: (e) => {
|
|
1842
|
+
if (e.key === 'Enter') {
|
|
1843
|
+
e.preventDefault();
|
|
1844
|
+
applyHierarchySearchSelection();
|
|
1845
|
+
}
|
|
1846
|
+
} }), _jsx("div", { style: {
|
|
1847
|
+
border: '1px solid var(--border-color)',
|
|
1848
|
+
borderRadius: '8px',
|
|
1849
|
+
padding: '8px',
|
|
1850
|
+
display: 'flex',
|
|
1851
|
+
flexDirection: 'column',
|
|
1852
|
+
gap: '6px',
|
|
1853
|
+
minHeight: '190px'
|
|
1854
|
+
}, children: hierarchyRootSearch.trim().length > 0 ? (_jsxs("div", { className: styles.appScrollbar, style: {
|
|
1855
|
+
maxHeight: '180px',
|
|
1856
|
+
minHeight: '180px',
|
|
1857
|
+
overflowY: 'auto',
|
|
1858
|
+
border: '1px solid var(--border-color)',
|
|
1859
|
+
borderRadius: '8px',
|
|
1860
|
+
padding: '6px',
|
|
1861
|
+
display: 'flex',
|
|
1862
|
+
flexDirection: 'column',
|
|
1863
|
+
gap: '6px'
|
|
1864
|
+
}, children: [hierarchySearchMatches.slice(0, 24).map((task) => (_jsxs("button", { type: "button", className: styles.headerActionBtn, onClick: () => {
|
|
1865
|
+
setHierarchyRootTaskId(task.id);
|
|
1866
|
+
setHierarchyRootSearch('');
|
|
1867
|
+
if (parentTaskFilterMode === 'unlinked') {
|
|
1868
|
+
setParentTaskFilterMode('all');
|
|
1869
|
+
}
|
|
1870
|
+
}, style: { justifyContent: 'space-between', border: '1px solid var(--border-color)' }, title: task.title, children: [_jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: task.title }), _jsx("span", { style: { opacity: 0.75 }, children: task.id.split('-').slice(-1)[0] })] }, task.id))), hierarchySearchMatches.length === 0 && (_jsxs("div", { style: { fontSize: '12px', color: 'var(--text-secondary)', padding: '4px 2px' }, children: ["No matches for \"", hierarchyRootSearch, "\"."] }))] })) : hierarchyRootTaskId ? (_jsxs(_Fragment, { children: [_jsx("div", { style: { fontSize: '13px', fontWeight: 600, color: 'var(--text-primary)' }, title: selectedHierarchyRootTask?.title || hierarchyRootTaskId, children: selectedHierarchyRootTask?.title || hierarchyRootTaskId }), hierarchySubtreeStats && (_jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px', fontSize: '12px', color: 'var(--text-secondary)' }, children: [_jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchySubtreeStats.total }), " total"] }), _jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchySubtreeStats.active }), " active"] }), _jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchySubtreeStats.terminal }), " done/cancelled"] }), _jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: hierarchySubtreeStats.overdue > 0 ? 'var(--color-error, #ef4444)' : 'var(--text-primary)' }, children: hierarchySubtreeStats.overdue }), " overdue"] }), _jsxs("div", { style: { gridColumn: '1 / -1', border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchySubtreeStats.depth }), " levels deep"] })] })), _jsx("button", { className: styles.headerActionBtn, onClick: () => {
|
|
1871
|
+
setHierarchyRootTaskId('');
|
|
1872
|
+
setHierarchyRootSearch('');
|
|
1873
|
+
}, style: { justifyContent: 'center', border: '1px solid var(--border-color)', background: 'rgba(59,130,246,0.12)' }, children: "Clear subtree focus" })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px', fontSize: '12px', color: 'var(--text-secondary)' }, children: [_jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchyVisibleTasks.length }), " visible"] }), _jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchyVisibleRootCount }), " root parents"] }), _jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchyVisibleUnlinkedCount }), " unlinked"] }), _jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchyDepthTasks.length > 0 ? maxHierarchyDepth + 1 : 0 }), " levels"] })] }), _jsx("div", { style: { fontSize: '12px', color: 'var(--text-secondary)' }, children: hierarchyVisibleLevelCounts.length > 0
|
|
1874
|
+
? hierarchyVisibleLevelCounts.map((entry) => `L${entry.level}: ${entry.count}`).join(' • ')
|
|
1875
|
+
: 'No linked hierarchy tasks in current filters.' })] })) })] }), _jsxs("div", { style: {
|
|
1876
|
+
border: '1px solid var(--border-color)',
|
|
1877
|
+
borderRadius: '10px',
|
|
1878
|
+
padding: '10px',
|
|
1879
|
+
display: 'flex',
|
|
1880
|
+
flexDirection: 'column',
|
|
1881
|
+
gap: '8px'
|
|
1882
|
+
}, children: [_jsx("div", { style: { fontSize: '12px', fontWeight: 600 }, children: "Display" }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }, children: [_jsx("input", { type: "checkbox", checked: hierarchyShowUnlinkedColumn, onChange: (e) => {
|
|
1883
|
+
const next = e.target.checked;
|
|
1884
|
+
setHierarchyShowUnlinkedColumn(next);
|
|
1885
|
+
if (!next && parentTaskFilterMode === 'unlinked') {
|
|
1886
|
+
setParentTaskFilterMode('all');
|
|
1887
|
+
}
|
|
1888
|
+
} }), "Show unlinked column"] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }, children: [_jsx("input", { type: "checkbox", checked: hierarchyChildOrderMode === 'manual', onChange: (e) => setHierarchyChildOrderMode(e.target.checked ? 'manual' : 'auto') }), "Sort by parent task order"] }), _jsx("div", { style: { fontSize: '11px', color: 'var(--text-tertiary)' }, children: "Keeps siblings in the order defined on the parent task." })] })] })), groupBy === 'schedule' && (_jsxs("aside", { className: styles.appScrollbar, style: {
|
|
1889
|
+
position: 'absolute',
|
|
1890
|
+
top: 0,
|
|
1891
|
+
right: 0,
|
|
1892
|
+
bottom: 0,
|
|
1893
|
+
width: '320px',
|
|
1894
|
+
minWidth: '320px',
|
|
1895
|
+
borderLeft: '1px solid var(--border-color)',
|
|
1896
|
+
background: 'var(--bg-secondary)',
|
|
1897
|
+
padding: '12px',
|
|
1898
|
+
display: 'flex',
|
|
1899
|
+
flexDirection: 'column',
|
|
1900
|
+
gap: '12px',
|
|
1901
|
+
zIndex: 40,
|
|
1902
|
+
overflowY: 'auto',
|
|
1903
|
+
transform: scheduleSidebarOpen ? 'translateX(0)' : 'translateX(108%)',
|
|
1904
|
+
pointerEvents: scheduleSidebarOpen ? 'auto' : 'none',
|
|
1905
|
+
transition: 'transform 220ms cubic-bezier(0.4, 0, 0.2, 1)',
|
|
1906
|
+
willChange: 'transform'
|
|
1907
|
+
}, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', fontWeight: 600 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx(CalendarDays, { size: 16 }), _jsx("span", { children: "Schedule Controls" })] }), _jsx("button", { type: "button", className: styles.headerActionBtn, onClick: () => setScheduleSidebarOpen(false), title: "Hide Schedule Sidebar", children: _jsx(PanelRightClose, { size: 16 }) })] }), _jsxs("div", { style: { fontSize: '12px', color: 'var(--text-secondary)' }, children: ["Week of ", scheduleWeekLabel] }), _jsxs("div", { style: {
|
|
1908
|
+
border: '1px solid var(--border-color)',
|
|
1909
|
+
borderRadius: '10px',
|
|
1910
|
+
padding: '10px'
|
|
1911
|
+
}, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }, children: [_jsx("button", { className: styles.headerActionBtn, onClick: () => {
|
|
1912
|
+
const prev = new Date(scheduleMonthDate);
|
|
1913
|
+
prev.setMonth(scheduleMonthDate.getMonth() - 1);
|
|
1914
|
+
setScheduleCalendarMonth(`${prev.getFullYear()}-${String(prev.getMonth() + 1).padStart(2, '0')}`);
|
|
1915
|
+
}, title: "Previous month", children: _jsx(ChevronLeft, { size: 14 }) }), _jsx("div", { style: { fontSize: '12px', fontWeight: 600 }, children: scheduleCalendarMonthLabel }), _jsx("button", { className: styles.headerActionBtn, onClick: () => {
|
|
1916
|
+
const next = new Date(scheduleMonthDate);
|
|
1917
|
+
next.setMonth(scheduleMonthDate.getMonth() + 1);
|
|
1918
|
+
setScheduleCalendarMonth(`${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, '0')}`);
|
|
1919
|
+
}, title: "Next month", children: _jsx(ChevronRight, { size: 14 }) })] }), _jsx("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(7, minmax(0, 1fr))', gap: '4px', marginBottom: '4px' }, children: calendarWeekdayLabels.map((label) => (_jsx("div", { style: { fontSize: '11px', color: 'var(--text-secondary)', textAlign: 'center' }, children: label }, label))) }), _jsx("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(7, minmax(0, 1fr))', gap: '4px' }, children: calendarCells.map((date) => {
|
|
1920
|
+
const dateOnly = toDateOnlyLocal(date);
|
|
1921
|
+
const isOutsideMonth = date.getMonth() !== scheduleMonthDate.getMonth();
|
|
1922
|
+
const weekStartForCell = startOfWeek(date, globalWeekStartsOn);
|
|
1923
|
+
const isSelectedWeek = toDateOnlyLocal(weekStartForCell) === toDateOnlyLocal(scheduleWeekStart);
|
|
1924
|
+
const isSelectedDate = dateOnly === scheduleSelectedDate;
|
|
1925
|
+
const isToday = dateOnly === todayDateOnly;
|
|
1926
|
+
const isPast = dateOnly < todayDateOnly;
|
|
1927
|
+
const signal = calendarTaskSignals.get(dateOnly);
|
|
1928
|
+
const hasScheduledTasks = !!signal;
|
|
1929
|
+
const hasExpiredScheduledTasks = !!signal?.hasExpired;
|
|
1930
|
+
return (_jsxs("button", { onClick: () => {
|
|
1931
|
+
setScheduleSelectedDate(dateOnly);
|
|
1932
|
+
setScheduleCalendarMonth(`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`);
|
|
1933
|
+
}, style: {
|
|
1934
|
+
border: isSelectedDate
|
|
1935
|
+
? '1px solid #2563eb'
|
|
1936
|
+
: isToday
|
|
1937
|
+
? '1px solid rgba(245, 158, 11, 0.95)'
|
|
1938
|
+
: '1px solid transparent',
|
|
1939
|
+
borderRadius: '8px',
|
|
1940
|
+
padding: '6px 0',
|
|
1941
|
+
fontSize: '12px',
|
|
1942
|
+
position: 'relative',
|
|
1943
|
+
background: isSelectedDate
|
|
1944
|
+
? 'rgba(59,130,246,0.22)'
|
|
1945
|
+
: isToday
|
|
1946
|
+
? 'rgba(245,158,11,0.16)'
|
|
1947
|
+
: isSelectedWeek
|
|
1948
|
+
? 'rgba(59,130,246,0.18)'
|
|
1949
|
+
: 'transparent',
|
|
1950
|
+
color: isOutsideMonth ? 'var(--text-tertiary)' : 'var(--text-primary)',
|
|
1951
|
+
cursor: 'pointer',
|
|
1952
|
+
fontWeight: isSelectedDate || isToday ? 700 : 500,
|
|
1953
|
+
opacity: isPast ? 0.42 : 1
|
|
1954
|
+
}, title: `${isToday ? `${dateOnly} (Today)` : dateOnly}`
|
|
1955
|
+
+ (hasScheduledTasks ? ` • ${signal?.count} scheduled` : '')
|
|
1956
|
+
+ (hasExpiredScheduledTasks ? ' • includes expired' : ''), children: [date.getDate(), hasScheduledTasks && (_jsx("span", { style: {
|
|
1957
|
+
position: 'absolute',
|
|
1958
|
+
left: '50%',
|
|
1959
|
+
bottom: '3px',
|
|
1960
|
+
transform: 'translateX(-50%)',
|
|
1961
|
+
width: '5px',
|
|
1962
|
+
height: '5px',
|
|
1963
|
+
borderRadius: '999px',
|
|
1964
|
+
background: hasExpiredScheduledTasks ? '#ef4444' : '#3b82f6',
|
|
1965
|
+
opacity: isOutsideMonth ? 0.6 : 0.95
|
|
1966
|
+
} }))] }, dateOnly));
|
|
1967
|
+
}) }), _jsx("div", { style: { display: 'flex', justifyContent: 'space-between', marginTop: '10px' }, children: _jsx("button", { className: styles.headerActionBtn, onClick: () => {
|
|
1968
|
+
const now = new Date();
|
|
1969
|
+
setScheduleSelectedDate(toDateOnlyLocal(now));
|
|
1970
|
+
setScheduleCalendarMonth(`${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`);
|
|
1971
|
+
}, title: "Jump to current week", children: "Today" }) })] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }, children: [_jsx("input", { type: "checkbox", checked: scheduleShowWeekends, onChange: (e) => setScheduleShowWeekends(e.target.checked) }), "Show weekends"] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }, children: [_jsx("input", { type: "checkbox", checked: scheduleShowBacklog, onChange: (e) => setScheduleShowBacklog(e.target.checked) }), "Show backlog"] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }, children: [_jsx("input", { type: "checkbox", checked: scheduleOnlyExpired, onChange: (e) => setScheduleOnlyExpired(e.target.checked) }), "Only expired/overdue"] }), expiredScheduledCount > 0 && (_jsxs("div", { style: {
|
|
1972
|
+
border: '1px solid var(--border-color)',
|
|
1973
|
+
borderRadius: '10px',
|
|
1974
|
+
padding: '10px',
|
|
1975
|
+
display: 'flex',
|
|
1976
|
+
flexDirection: 'column',
|
|
1977
|
+
gap: '8px'
|
|
1978
|
+
}, children: [_jsx("div", { style: { fontSize: '12px', fontWeight: 600 }, children: "Expired Tasks" }), _jsxs("div", { style: { fontSize: '12px', color: 'var(--text-secondary)', display: 'flex', flexDirection: 'column', gap: '2px' }, children: [_jsxs("span", { children: [expiredScheduledCount, " expired"] }), _jsxs("span", { children: [overdueDueCount, " overdue"] })] }), _jsx("button", { className: styles.headerActionBtn, onClick: handleMoveExpiredToSelectedWeek, disabled: scheduleBulkBusy || expiredLeafCandidates.length === 0, title: "Move expired leaf tasks to the selected week while keeping weekday alignment", style: { justifyContent: 'flex-start', border: '1px solid var(--border-color)' }, children: scheduleBulkBusy ? 'Working...' : 'Schedule to selected week' }), _jsx("button", { className: styles.headerActionBtn, onClick: handleMoveExpiredToBacklog, disabled: scheduleBulkBusy || expiredRecoveryCandidates.length === 0, title: "Unschedule expired/overdue tasks back to backlog", style: { justifyContent: 'flex-start', border: '1px solid var(--border-color)' }, children: scheduleBulkBusy ? 'Working...' : 'Unschedule to backlog' })] }))] }))] })), _jsxs(Modal, { isOpen: activeTab === 'add', onClose: handleCloseModal, title: editingTaskId ? 'Edit Task' : 'New Task', size: "md", theme: currentTheme, draggable: true, closeOnOverlayClick: false, children: [_jsx(TaskActionHeader, { editingTaskId: editingTaskId, loading: loading, title: title, currentTask: currentTask, handleSubmit: handleSubmit, resetForm: resetForm, handleCopyId: handleCopyId, copiedId: copiedId, tasks: tasks, handleToggleInProgress: handleToggleInProgress, handleToggleReview: handleToggleReview, handleToggleComplete: handleToggleComplete, handleToggleCancel: handleToggleCancel, handleArchiveTask: handleArchiveTask }), _jsx(TaskForm, { editingTaskId: editingTaskId, error: error, title: title, description: description, category: category, type: type, priority: priority, complexity: complexity, manualComplexityEnabled: manualComplexityEnabled, approach: approach, assignee: assignee, scheduledDate: scheduledDate, dueDate: dueDate, parentTaskIdInput: parentTaskIdInput, addChildTaskIdInput: addChildTaskIdInput, formTaxonomies: formTaxonomies, onTaxonomyChange: (id, val) => setFormTaxonomies(prev => ({ ...prev, [id]: val })), onParentTaskIdInputChange: setParentTaskIdInput, onAddChildTaskIdInputChange: setAddChildTaskIdInput, taxonomies: taxonomies, selectedSpecialists: selectedSpecialists, comments: comments, newCommentText: newCommentText, contextFiles: screenshots, contextLinkBrowsePath: contextLinkBrowsePath, descriptionFocused: descriptionFocused, showMarkdownHelp: showMarkdownHelp, showSpecialists: showSpecialists, showChecklist: showChecklist, checklistEnabled: checklistDropdownEnabled, showTaskRelationships: showTaskRelationships, showChildTasks: showChildTasks, showComments: showComments, categories: activeCategories, types: activeTypes, priorities: priorities, approaches: approaches, availableSpecialists: availableSpecialists, copiedId: copiedId, onTitleChange: setTitle, onDescriptionChange: setDescription, onCategoryChange: setCategory, onTypeChange: setType, onPriorityChange: setPriority, onComplexityChange: setComplexity, onApproachChange: (val) => { setApproach(val); setFormTaxonomies(prev => ({ ...prev, approach: val })); }, onAssigneeChange: setAssignee, onScheduledDateChange: setScheduledDate, onDueDateChange: setDueDate, onSelectedSpecialistsChange: setSelectedSpecialists, onNewCommentTextChange: setNewCommentText, onDescriptionFocusedChange: setDescriptionFocused, onShowMarkdownHelpChange: setShowMarkdownHelp, onShowSpecialistsChange: setShowSpecialists, onShowChecklistChange: setShowChecklist, onShowTaskRelationshipsChange: setShowTaskRelationships, onShowChildTasksChange: setShowChildTasks, onShowCommentsChange: setShowComments, onOpenSettings: (section) => {
|
|
1979
|
+
setSettingsSection(section);
|
|
1980
|
+
setActiveTab('settings');
|
|
1981
|
+
}, onSubmit: handleSubmit, commentsEndRef: commentsEndRef, onAddComment: () => handleAddComment(newCommentText), onSetParentForCurrentTask: handleSetParentForCurrentTask, onAddChildToCurrentTask: handleAddChildToCurrentTask, onCreateChildForCurrentTask: handleCreateChildForCurrentTask, onUnlinkChildTask: handleUnlinkChildTask, onReorderChildTasks: handleReorderChildTasks, onOpenTaskById: handleOpenTaskById, onAddContextFile: (file) => {
|
|
1982
|
+
setScreenshots(prev => [...prev, file]);
|
|
1983
|
+
}, onBrowseContextLinkPath: () => {
|
|
1984
|
+
setBrowserTarget({ type: 'context-link' });
|
|
1985
|
+
fetchFolders('');
|
|
1986
|
+
setShowFolderBrowser(true);
|
|
1987
|
+
}, onRemoveContextFile: async (index) => {
|
|
1988
|
+
const screenshot = screenshots[index];
|
|
1989
|
+
const path = typeof screenshot === 'string' ? screenshot : screenshot.path;
|
|
1990
|
+
setScreenshots(prev => prev.filter((_, i) => i !== index));
|
|
1991
|
+
if (typeof path === 'string' && path.startsWith('/api/taskforce/context/')) {
|
|
1992
|
+
try {
|
|
1993
|
+
await fetch(path, { method: 'DELETE' });
|
|
1994
|
+
}
|
|
1995
|
+
catch { }
|
|
1996
|
+
}
|
|
1997
|
+
}, onUpdateContextCaption: (index, caption) => {
|
|
1998
|
+
setScreenshots(prev => prev.map((s, i) => {
|
|
1999
|
+
if (i !== index)
|
|
2000
|
+
return s;
|
|
2001
|
+
if (typeof s === 'string')
|
|
2002
|
+
return { path: s, caption, timestamp: new Date().toISOString() };
|
|
2003
|
+
return { ...s, caption };
|
|
2004
|
+
}));
|
|
2005
|
+
}, onCopyId: handleCopyId, onToggleInProgress: handleToggleInProgress, onToggleReview: handleToggleReview, onToggleComplete: handleToggleComplete, onToggleCancel: handleToggleCancel, onArchiveTask: handleArchiveTask, onUnarchive: handleUnarchive, currentTask: currentTask, currentTaskParent: currentTaskParent, currentTaskChildren: currentTaskChildren })] }), _jsx(Modal, { isOpen: activeTab === 'settings', onClose: handleCloseModal, title: "Settings", size: "md", theme: currentTheme, draggable: true, isSettings: true, closeOnOverlayClick: false, children: _jsx(TaskSettings, { settingsModel: settingsModel, onSectionChange: setSettingsSection }) }), _jsx(Modal, { isOpen: !!scheduleWarningPrompt, onClose: cancelScheduleWarning, title: "Date Warning", size: "sm", theme: currentTheme, draggable: false, closeOnOverlayClick: false, children: _jsxs("div", { className: styles.form, style: { padding: '16px', gap: '12px' }, children: [_jsx("p", { style: { margin: 0, fontSize: '13px', color: 'var(--text-secondary)' }, children: "Due date is before scheduled date." }), scheduleWarningPrompt && (_jsxs("p", { style: { margin: 0, fontSize: '12px', color: 'var(--text-muted)' }, children: ["Due: ", scheduleWarningPrompt.dueDate, " \u00B7 Scheduled: ", scheduleWarningPrompt.scheduledDate] })), _jsxs("div", { className: styles.formActions, children: [_jsx("button", { className: styles.cancelBtn, onClick: cancelScheduleWarning, children: "Go Back" }), _jsx("button", { className: styles.submitBtn, onClick: confirmScheduleWarning, children: "Save Anyway" })] })] }) }), _jsx(Modal, { isOpen: !!scheduleParentDropPrompt, onClose: cancelScheduleParentDrop, title: "Schedule Descendants", size: "sm", theme: currentTheme, draggable: false, closeOnOverlayClick: false, children: _jsxs("div", { className: styles.form, style: { padding: '16px', gap: '12px' }, children: [scheduleParentDropPrompt && (_jsxs(_Fragment, { children: [_jsxs("p", { style: { margin: 0, fontSize: '13px', color: 'var(--text-secondary)' }, children: ["Schedule unscheduled leaf tasks under ", _jsx("strong", { children: scheduleParentDropPrompt.parentTitle }), " to ", _jsx("strong", { children: scheduleParentDropPrompt.targetDate }), "?"] }), _jsxs("p", { style: { margin: 0, fontSize: '12px', color: 'var(--text-muted)' }, children: [scheduleParentDropPrompt.toScheduleLeafTaskIds.length, " leaf task", scheduleParentDropPrompt.toScheduleLeafTaskIds.length === 1 ? '' : 's', " will be scheduled.", scheduleParentDropPrompt.alreadyScheduledLeafCount > 0 ? ` ${scheduleParentDropPrompt.alreadyScheduledLeafCount} already scheduled leaf task${scheduleParentDropPrompt.alreadyScheduledLeafCount === 1 ? '' : 's'} will be skipped.` : ''] })] })), _jsxs("div", { className: styles.formActions, children: [_jsx("button", { className: styles.cancelBtn, onClick: cancelScheduleParentDrop, disabled: scheduleParentDropBusy, children: "Cancel" }), _jsx("button", { className: styles.submitBtn, onClick: confirmScheduleParentDrop, disabled: scheduleParentDropBusy, children: scheduleParentDropBusy ? 'Scheduling...' : 'Schedule' })] })] }) }), _jsx(Modal, { isOpen: showHelpModal, onClose: () => setShowHelpModal(false), title: "Quick Start", size: "md", theme: currentTheme, draggable: true, children: _jsxs("div", { className: modalStyles.form, style: { paddingTop: '18px' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--text-secondary)' }, children: "Use this quick guide to get productive in Taskforce dashboard mode." }), _jsxs("div", { children: [_jsx("strong", { children: "1. Capture work" }), _jsxs("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: ["Click ", _jsx("code", { children: "Add Task" }), ", write a short title, and save."] })] }), _jsxs("div", { children: [_jsx("strong", { children: "2. Plan the board" }), _jsxs("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: ["Use ", _jsx("code", { children: "Sort" }), ", ", _jsx("code", { children: "Group" }), ", and ", _jsx("code", { children: "Filters" }), " in the header."] })] }), _jsxs("div", { children: [_jsx("strong", { children: "3. Execute and stage" }), _jsxs("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: ["Move tasks through ", _jsx("code", { children: "Task" }), ", ", _jsx("code", { children: "In Progress" }), ", ", _jsx("code", { children: "Review" }), ", and ", _jsx("code", { children: "Done" }), "."] })] }), _jsxs("div", { children: [_jsx("strong", { children: "4. Archive finished work" }), _jsx("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: "Archive completed/cancelled tasks to keep active board signal high." })] }), _jsxs("div", { className: modalStyles.formActions, style: { display: 'flex', justifyContent: 'flex-end', gap: '10px' }, children: [_jsx("button", { className: styles.cancelBtn, onClick: () => {
|
|
2006
|
+
setShowHelpModal(false);
|
|
2007
|
+
setActiveTab('settings');
|
|
2008
|
+
setSettingsSection('general');
|
|
2009
|
+
}, children: "Open Settings" }), _jsx("button", { className: styles.submitBtn, onClick: () => setShowHelpModal(false), children: "Close Tutorial" })] })] }) }), _jsx(Modal, { isOpen: showAccountHub, onClose: () => setShowAccountHub(false), title: "Account Hub", size: "md", theme: currentTheme, draggable: true, children: _jsxs("div", { className: modalStyles.form, style: { paddingTop: '18px' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--text-secondary)' }, children: "Manage sign in, profile, and workspace controls from one place." }), _jsxs("div", { className: styles.accountHubCard, children: [_jsx("strong", { children: "Authentication" }), _jsx("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: runtimeMode === 'cloud'
|
|
2010
|
+
? (authRequiredForApi
|
|
2011
|
+
? (isAuthenticated ? 'Signed in. API access is enabled.' : 'Cloud mode requires sign in before app usage.')
|
|
2012
|
+
: 'Cloud runtime with optional authentication.')
|
|
2013
|
+
: 'Local runtime allows guest usage without sign in.' }), _jsx("p", { style: { margin: '6px 0 0 0', color: 'var(--text-secondary)' }, children: isAuthenticated
|
|
2014
|
+
? (hasAuthIdentity
|
|
2015
|
+
? _jsxs(_Fragment, { children: ["Signed in as ", _jsx("span", { className: styles.accountIdentityEmail, children: authIdentityLabel })] })
|
|
2016
|
+
: 'Signed in')
|
|
2017
|
+
: 'Signed in as: not signed in' }), runtimeMode === 'cloud' && authRequiredForApi && !isAuthenticated && (_jsx("p", { style: { marginTop: '10px', color: 'var(--text-secondary)' }, children: "Use the `/login` screen to sign in." }))] }), _jsxs("div", { className: styles.accountHubCard, children: [_jsx("strong", { children: "Profile & Account" }), _jsx("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: "Profile editing and account preferences will live here." })] }), _jsxs("div", { className: styles.accountHubCard, children: [_jsx("strong", { children: "Billing" }), runtimeMode !== 'cloud' ? (_jsx("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: "Billing actions are available in cloud runtime only." })) : !isAuthenticated ? (_jsx("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: "Sign in to manage subscription billing." })) : (_jsxs(_Fragment, { children: [billingLoading && (_jsx("p", { style: { margin: '6px 0 0 0', color: 'var(--text-secondary)' }, children: "Loading billing status..." })), billingError && (_jsx("p", { className: styles.loginError, style: { marginTop: '8px' }, children: billingError })), billingActionError && (_jsx("p", { className: styles.loginError, style: { marginTop: '8px' }, children: billingActionError })), billingNotice && (_jsx("p", { style: { margin: '8px 0 0 0', color: 'var(--text-secondary)' }, children: billingNotice })), _jsxs("p", { style: { margin: '6px 0 0 0', color: 'var(--text-secondary)' }, children: ["Plan: ", _jsx("code", { children: String(billingStatus?.planId || 'free') }), ' · ', "Entitlement: ", _jsx("code", { children: String(billingStatus?.entitlementState || 'free') })] }), _jsxs("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: ["Stripe status: ", _jsx("code", { children: String(billingStatus?.stripeStatus || 'n/a') }), billingStatus?.effectiveUntil ? ` · Effective until ${new Date(billingStatus.effectiveUntil).toLocaleString()}` : ''] }), _jsx("div", { style: { marginTop: '10px', display: 'flex', gap: '8px', flexWrap: 'wrap' }, children: _jsxs("label", { className: styles.accountMenuMeta, style: { display: 'inline-flex', alignItems: 'center', gap: '8px' }, children: ["Interval", _jsxs("select", { className: styles.input, value: billingIntervalChoice, onChange: (event) => setBillingIntervalChoice(event.target.value === 'year' ? 'year' : 'month'), disabled: billingActionBusy, children: [_jsx("option", { value: "month", children: "Monthly" }), _jsx("option", { value: "year", children: "Yearly" })] })] }) }), _jsxs("div", { style: { marginTop: '10px', display: 'flex', gap: '10px', justifyContent: 'flex-end', flexWrap: 'wrap' }, children: [_jsx("button", { className: styles.cancelBtn, onClick: () => { void loadBillingStatus(); }, disabled: billingActionBusy || billingLoading, children: "Refresh Billing" }), _jsx("button", { className: styles.cancelBtn, onClick: () => { void handleSwitchSubscriptionInterval(); }, disabled: billingActionBusy || !billingStatus?.stripeSubscriptionId, children: "Update Interval" }), _jsx("button", { className: styles.cancelBtn, onClick: () => { void handleOpenBillingPortal(); }, disabled: billingActionBusy || !billingStatus?.stripeCustomerId, children: "Manage Billing" }), _jsx("button", { className: styles.submitBtn, onClick: () => { void handleStartCheckout(); }, disabled: billingActionBusy, children: billingActionBusy ? 'Working...' : 'Start Checkout' })] })] }))] }), _jsxs("div", { className: styles.accountHubCard, children: [_jsx("strong", { children: "Workspace Audit Log" }), _jsx("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: "Audit entries are workspace-scoped for the active workspace, not global account history." }), _jsxs("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: ["Active workspace: ", _jsx("code", { children: currentWorkspaceId })] }), _jsx("div", { style: { marginTop: '10px', display: 'flex', gap: '10px', justifyContent: 'flex-end' }, children: _jsx("button", { className: styles.cancelBtn, onClick: () => {
|
|
2018
|
+
setShowAccountHub(false);
|
|
2019
|
+
void openTeamManagementModal();
|
|
2020
|
+
setTeamManagementTab('audit');
|
|
2021
|
+
}, disabled: !canOpenTeamManagement, children: "Open Workspace Audit" }) })] }), _jsxs("div", { className: styles.accountHubCard, children: [_jsx("strong", { children: "Workspace Cloud Sync" }), _jsx("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: canManageWorkspaceSync
|
|
2022
|
+
? (workspaceCloudSyncEnabled
|
|
2023
|
+
? `Enabled${workspaceSyncSourceOfTruth ? ` · Source: ${workspaceSyncSourceOfTruth}` : ''}`
|
|
2024
|
+
: 'Disabled')
|
|
2025
|
+
: 'Sign in to cloud account to enable workspace sync.' }), workspaceSyncError && (_jsx("p", { className: styles.loginError, style: { marginTop: '8px' }, children: workspaceSyncError })), _jsx("div", { style: { marginTop: '10px', display: 'flex', gap: '10px', justifyContent: 'flex-end' }, children: _jsx("button", { className: styles.cancelBtn, disabled: !canManageWorkspaceSync || workspaceSyncBusy, onClick: async () => {
|
|
2026
|
+
if (!canManageWorkspaceSync)
|
|
2027
|
+
return;
|
|
2028
|
+
setWorkspaceSyncError(null);
|
|
2029
|
+
if (!workspaceCloudSyncEnabled) {
|
|
2030
|
+
setWorkspaceSyncBusy(true);
|
|
2031
|
+
const result = await saveWorkspaceCloudSyncSettings({
|
|
2032
|
+
enabled: true,
|
|
2033
|
+
// I6: start false; the orchestrator marks it true after first successful pull.
|
|
2034
|
+
onboardingCompleted: false
|
|
2035
|
+
});
|
|
2036
|
+
if (!result.success)
|
|
2037
|
+
setWorkspaceSyncError(result.error || 'Failed to enable sync.');
|
|
2038
|
+
setWorkspaceSyncBusy(false);
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
setWorkspaceSyncBusy(true);
|
|
2042
|
+
const result = await saveWorkspaceCloudSyncSettings({
|
|
2043
|
+
enabled: false,
|
|
2044
|
+
sourceOfTruth: workspaceSyncSourceOfTruth,
|
|
2045
|
+
onboardingCompleted: workspaceSyncOnboardingCompleted
|
|
2046
|
+
});
|
|
2047
|
+
if (!result.success)
|
|
2048
|
+
setWorkspaceSyncError(result.error || 'Failed to disable sync.');
|
|
2049
|
+
setWorkspaceSyncBusy(false);
|
|
2050
|
+
}, children: workspaceCloudSyncEnabled ? 'Disable Sync' : 'Enable Sync' }) })] }), _jsxs("div", { className: modalStyles.formActions, style: { display: 'flex', justifyContent: 'flex-end', gap: '10px' }, children: [_jsx("button", { className: styles.cancelBtn, onClick: () => {
|
|
2051
|
+
setShowAccountHub(false);
|
|
2052
|
+
setShowHelpModal(true);
|
|
2053
|
+
}, children: "Help & Tutorial" }), _jsx("button", { className: styles.submitBtn, onClick: () => setShowAccountHub(false), children: "Close" })] })] }) }), _jsx(Modal, { isOpen: showTeamManagementModal, onClose: () => setShowTeamManagementModal(false), title: "Team Management", size: "md", theme: currentTheme, draggable: true, children: _jsxs("div", { className: modalStyles.form, style: { paddingTop: '18px' }, children: [teamPlanMode !== 'team' && (_jsx("p", { className: styles.loginError, children: "Team management is only available in TEAM accounts." })), teamMgmtError && (_jsx("p", { className: styles.loginError, children: teamMgmtError })), _jsxs("div", { className: styles.settingsTabs, children: [_jsx("button", { className: `${styles.settingsTabBtn} ${teamManagementTab === 'members' ? styles.settingsTabBtnActive : ''}`, onClick: () => {
|
|
2054
|
+
setTeamManagementTab('members');
|
|
2055
|
+
void loadTeamUsers();
|
|
2056
|
+
}, children: "Members" }), _jsx("button", { className: `${styles.settingsTabBtn} ${teamManagementTab === 'invites' ? styles.settingsTabBtnActive : ''}`, onClick: () => setTeamManagementTab('invites'), children: "Invites" }), _jsx("button", { className: `${styles.settingsTabBtn} ${teamManagementTab === 'audit' ? styles.settingsTabBtnActive : ''}`, onClick: () => {
|
|
2057
|
+
setTeamManagementTab('audit');
|
|
2058
|
+
void loadTeamAuditPage(teamAuditPage);
|
|
2059
|
+
}, children: "Audit Log" })] }), teamManagementTab === 'members' && (_jsxs("div", { className: styles.accountHubCard, children: [teamUsersLoading && _jsx("p", { style: { margin: 0, color: 'var(--text-secondary)' }, children: "Loading members..." }), !teamUsersLoading && teamUsers.length === 0 && (_jsx("p", { style: { margin: 0, color: 'var(--text-secondary)' }, children: "No users found." })), !teamUsersLoading && teamUsers.map((member) => (_jsxs("div", { style: { borderTop: '1px solid var(--border-color)', paddingTop: '8px', marginTop: '8px' }, children: [_jsx("div", { style: { fontWeight: 600 }, children: member.displayName || member.email }), _jsxs("div", { className: styles.accountMenuMeta, children: [member.email, " \u00B7 ", member.status, member.disabled ? ' · deactivated' : ''] }), _jsxs("div", { style: { marginTop: '6px', display: 'flex', gap: '8px', flexWrap: 'wrap' }, children: [_jsxs("select", { className: styles.input, value: member.role, onChange: (event) => {
|
|
2060
|
+
void executeTeamUserAction(member.userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(member.userId)}/role`, {
|
|
2061
|
+
method: 'PATCH',
|
|
2062
|
+
headers: { 'Content-Type': 'application/json', ...teamAdminHeaders() },
|
|
2063
|
+
credentials: 'include',
|
|
2064
|
+
body: JSON.stringify({
|
|
2065
|
+
workspaceId: currentWorkspaceId,
|
|
2066
|
+
role: event.target.value
|
|
2067
|
+
})
|
|
2068
|
+
}));
|
|
2069
|
+
}, disabled: teamActionBusyUserId === member.userId, style: { maxWidth: '160px' }, children: [_jsx("option", { value: "owner", children: "Owner" }), _jsx("option", { value: "admin", children: "Admin" }), _jsx("option", { value: "member", children: "Member" })] }), member.role === 'member' && (_jsxs("select", { className: styles.input, value: member.permissionMode, onChange: (event) => {
|
|
2070
|
+
void executeTeamUserAction(member.userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(member.userId)}/permission-mode`, {
|
|
2071
|
+
method: 'PATCH',
|
|
2072
|
+
headers: { 'Content-Type': 'application/json', ...teamAdminHeaders() },
|
|
2073
|
+
credentials: 'include',
|
|
2074
|
+
body: JSON.stringify({
|
|
2075
|
+
workspaceId: currentWorkspaceId,
|
|
2076
|
+
mode: event.target.value
|
|
2077
|
+
})
|
|
2078
|
+
}));
|
|
2079
|
+
}, disabled: teamActionBusyUserId === member.userId, style: { maxWidth: '170px' }, children: [_jsx("option", { value: "read-write", children: "Read / Write" }), _jsx("option", { value: "read-only", children: "Read Only" })] })), _jsx("button", { className: styles.cancelBtn, disabled: teamActionBusyUserId === member.userId, onClick: () => {
|
|
2080
|
+
void executeTeamUserAction(member.userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(member.userId)}/disable`, {
|
|
2081
|
+
method: 'PATCH',
|
|
2082
|
+
headers: { 'Content-Type': 'application/json', ...teamAdminHeaders() },
|
|
2083
|
+
credentials: 'include',
|
|
2084
|
+
body: JSON.stringify({
|
|
2085
|
+
workspaceId: currentWorkspaceId,
|
|
2086
|
+
disabled: !member.disabled
|
|
2087
|
+
})
|
|
2088
|
+
}));
|
|
2089
|
+
}, children: member.disabled ? 'Reactivate' : 'Deactivate' }), member.status === 'invited' && (_jsx("button", { className: styles.cancelBtn, disabled: teamActionBusyUserId === member.userId, onClick: () => {
|
|
2090
|
+
void executeTeamUserAction(member.userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(member.userId)}/invite/revoke`, {
|
|
2091
|
+
method: 'POST',
|
|
2092
|
+
headers: { 'Content-Type': 'application/json', ...teamAdminHeaders() },
|
|
2093
|
+
credentials: 'include',
|
|
2094
|
+
body: JSON.stringify({ workspaceId: currentWorkspaceId })
|
|
2095
|
+
}));
|
|
2096
|
+
}, children: "Revoke Invite" })), _jsx("button", { className: styles.cancelBtn, disabled: teamActionBusyUserId === member.userId, onClick: () => {
|
|
2097
|
+
void executeTeamUserAction(member.userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(member.userId)}?workspaceId=${encodeURIComponent(currentWorkspaceId)}`, {
|
|
2098
|
+
method: 'DELETE',
|
|
2099
|
+
headers: teamAdminHeaders(),
|
|
2100
|
+
credentials: 'include'
|
|
2101
|
+
}));
|
|
2102
|
+
}, children: "Remove" })] })] }, member.userId)))] })), teamManagementTab === 'invites' && (_jsxs("div", { className: styles.accountHubCard, children: [teamInviteFeedback && (_jsx("p", { style: { margin: '0 0 8px 0', color: 'var(--text-secondary)' }, children: teamInviteFeedback })), _jsxs("div", { className: styles.pathInputGroup, children: [_jsx("label", { className: styles.label, style: { minWidth: '90px' }, children: "Email" }), _jsx("input", { className: styles.input, value: teamInviteEmail, onChange: (event) => setTeamInviteEmail(event.target.value), placeholder: "name@company.com" })] }), _jsxs("div", { className: styles.pathInputGroup, children: [_jsx("label", { className: styles.label, style: { minWidth: '90px' }, children: "Role" }), _jsxs("select", { className: styles.input, value: teamInviteRole, onChange: (event) => setTeamInviteRole(event.target.value === 'admin' ? 'admin' : 'member'), style: { maxWidth: '150px' }, children: [_jsx("option", { value: "member", children: "Member" }), _jsx("option", { value: "admin", children: "Admin" })] }), teamInviteRole === 'member' && (_jsxs("select", { className: styles.input, value: teamInvitePermissionMode, onChange: (event) => setTeamInvitePermissionMode(event.target.value === 'read-only' ? 'read-only' : 'read-write'), style: { maxWidth: '170px' }, children: [_jsx("option", { value: "read-write", children: "Read / Write" }), _jsx("option", { value: "read-only", children: "Read Only" })] })), _jsx("button", { className: styles.submitBtn, disabled: teamInviteBusy || !teamInviteEmail.trim() || teamPlanMode !== 'team', onClick: () => { void submitTeamInvite(); }, children: teamInviteBusy ? 'Sending...' : 'Send Invite' })] }), _jsxs("div", { style: { marginTop: '12px' }, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: '6px' }, children: "Pending Invites" }), pendingInvites.length === 0 && (_jsx("p", { style: { margin: 0, color: 'var(--text-secondary)' }, children: "No pending invites." })), pendingInvites.map((invite) => (_jsxs("div", { style: { borderTop: '1px solid var(--border-color)', paddingTop: '8px', marginTop: '8px' }, children: [_jsx("div", { style: { fontWeight: 600 }, children: invite.displayName || invite.email }), _jsxs("div", { className: styles.accountMenuMeta, children: [invite.email, " \u00B7 ", invite.role] })] }, invite.userId)))] })] })), teamManagementTab === 'audit' && (_jsxs("div", { className: styles.accountHubCard, children: [teamAuditLoading && _jsx("p", { style: { margin: 0, color: 'var(--text-secondary)' }, children: "Loading audit entries..." }), !teamAuditLoading && teamAuditEvents.length === 0 && (_jsx("p", { style: { margin: 0, color: 'var(--text-secondary)' }, children: "No audit events for this workspace." })), !teamAuditLoading && (_jsx("div", { style: { maxHeight: '320px', overflowY: 'auto', paddingRight: '4px' }, children: teamAuditEvents.map((event) => (_jsxs("div", { style: { borderTop: '1px solid var(--border-color)', paddingTop: '8px', marginTop: '8px' }, children: [_jsx("div", { className: styles.accountMenuMeta, children: new Date(event.createdAt).toLocaleString() }), _jsx("div", { style: { fontWeight: 600 }, children: event.action }), _jsxs("div", { className: styles.accountMenuMeta, children: [event.actorUserId, " (", event.actorRole, ")"] })] }, event.id))) })), _jsxs("div", { style: { marginTop: '10px', display: 'flex', justifyContent: 'space-between', gap: '8px' }, children: [_jsx("button", { className: styles.cancelBtn, disabled: teamAuditLoading || teamAuditPage === 0, onClick: () => { void loadTeamAuditPage(teamAuditPage - 1); }, children: "Previous" }), _jsxs("span", { className: styles.accountMenuMeta, children: ["Page ", teamAuditPage + 1, " of ", teamAuditPages] }), _jsx("button", { className: styles.cancelBtn, disabled: teamAuditLoading || !teamAuditHasMore || teamAuditPage + 1 >= teamAuditPages, onClick: () => { void loadTeamAuditPage(teamAuditPage + 1); }, children: "Next" })] })] })), _jsx("div", { className: modalStyles.formActions, style: { display: 'flex', justifyContent: 'flex-end', gap: '10px' }, children: _jsx("button", { className: styles.cancelBtn, onClick: () => setShowTeamManagementModal(false), children: "Close" }) })] }) }), _jsx(Modal, { isOpen: showSyncStatusModal, onClose: () => {
|
|
2103
|
+
setShowSyncStatusModal(false);
|
|
2104
|
+
}, title: "Cloud Sync Status", size: "md", theme: currentTheme, draggable: true, children: _jsxs("div", { className: `${modalStyles.form} ${styles.syncStatusModal}`, children: [_jsxs("div", { className: styles.syncStatusTop, children: [_jsxs("div", { children: [_jsx("div", { className: styles.syncStatusHeading, children: "Current Status" }), _jsx("div", { className: styles.syncStatusSubtext, children: _jsx("code", { children: currentWorkspaceId }) }), workspaceBootstrapSyncInProgress && (_jsxs("div", { className: styles.syncStatusPausedHint, children: ["Initial sync in progress: ", workspaceBootstrapSyncAppliedChanges, " changes across ", workspaceBootstrapSyncPages, " page", workspaceBootstrapSyncPages === 1 ? '' : 's', "."] })), runtimeMode === 'local' && !isAuthenticated && (_jsx("div", { className: styles.syncStatusPausedHint, children: "Sync is paused. Sign in on cloud to resume." }))] }), _jsx("div", { className: styles.syncStatusBadge, style: {
|
|
2105
|
+
borderColor: modalSyncStatusMeta.border,
|
|
2106
|
+
background: modalSyncStatusMeta.background,
|
|
2107
|
+
color: modalSyncStatusMeta.color
|
|
2108
|
+
}, children: modalSyncStatusMeta.label })] }), _jsxs("div", { className: styles.syncStatusGrid, children: [_jsxs("div", { className: styles.syncStatusItem, children: [_jsx("span", { className: styles.syncStatusLabel, children: "Last successful sync" }), _jsx("span", { className: styles.syncStatusValue, children: formattedLastSyncTime })] }), _jsxs("div", { className: styles.syncStatusItem, children: [_jsx("span", { className: styles.syncStatusLabel, children: "Pending local changes" }), _jsx("span", { className: styles.syncStatusValue, children: userGlobalSyncPendingChanges })] }), _jsxs("div", { className: styles.syncStatusItem, children: [_jsx("span", { className: styles.syncStatusLabel, children: "Last pull from cloud" }), _jsx("span", { className: styles.syncStatusValue, children: formattedLastPullTime })] }), _jsxs("div", { className: styles.syncStatusItem, children: [_jsx("span", { className: styles.syncStatusLabel, children: "Last push to cloud" }), _jsx("span", { className: styles.syncStatusValue, children: formattedLastPushTime })] }), _jsxs("div", { className: styles.syncStatusItem, children: [_jsx("span", { className: styles.syncStatusLabel, children: "Scheduled retry" }), _jsx("span", { className: styles.syncStatusValue, children: workspaceRetryAt ? formattedRetryTime : 'None' })] }), _jsxs("div", { className: styles.syncStatusItem, children: [_jsx("span", { className: styles.syncStatusLabel, children: "Last error time" }), _jsx("span", { className: styles.syncStatusValue, children: workspaceLastErrorAt ? formattedLastErrorTime : 'None' })] })] }), _jsxs("div", { className: styles.syncStatusDetailsGrid, children: [_jsx("div", { className: styles.syncStatusTableWrap, children: _jsx("table", { className: styles.syncStatusTable, children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", { children: "Runtime mode" }), _jsx("td", { children: runtimeMode })] }), _jsxs("tr", { children: [_jsx("th", { children: "Sync source of truth" }), _jsx("td", { children: workspaceSyncSourceOfTruth || 'unset' })] }), _jsxs("tr", { children: [_jsx("th", { children: "Ready" }), _jsx("td", { children: workspaceSyncReady ? 'yes' : 'no' })] }), _jsxs("tr", { children: [_jsx("th", { children: "Hydrated" }), _jsx("td", { children: workspaceCloudHydrated ? 'yes' : 'no' })] }), _jsxs("tr", { children: [_jsx("th", { children: "Failure count" }), _jsx("td", { children: workspaceSyncFailureCount })] })] }) }) }), _jsx("div", { className: styles.syncStatusTableWrap, children: _jsx("table", { className: styles.syncStatusTable, children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", { children: "Auth failures" }), _jsx("td", { children: workspaceSyncAuthFailureCount })] }), _jsxs("tr", { children: [_jsx("th", { children: "Last failure source" }), _jsx("td", { children: workspaceSyncLastFailureSource || 'none' })] }), _jsxs("tr", { children: [_jsx("th", { children: "Last failure status" }), _jsx("td", { children: workspaceSyncLastFailureStatusCode ?? 'none' })] }), _jsxs("tr", { children: [_jsx("th", { children: "Retries scheduled" }), _jsx("td", { children: workspaceSyncRetryScheduledCount })] })] }) }) })] }), _jsx("div", { className: styles.syncStatusTableWrap, children: _jsx("table", { className: styles.syncStatusTable, children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", { className: styles.syncStatusTableNarrowLabel, children: "Last error" }), _jsx("td", { className: styles.syncStatusTableError, style: { color: (workspaceLastErrorMessage || userGlobalSyncError) ? '#fda4af' : 'var(--text-secondary)' }, children: workspaceLastErrorMessage || userGlobalSyncError || 'None' })] }), _jsxs("tr", { children: [_jsx("th", { className: styles.syncStatusTableNarrowLabel, children: "Last warning" }), _jsx("td", { style: { color: workspaceLastWarningMessage ? '#fcd34d' : 'var(--text-secondary)' }, children: workspaceLastWarningMessage || 'None' })] })] }) }) }), _jsxs("div", { className: `${modalStyles.formActions} ${styles.syncStatusActions}`, children: [_jsx("button", { className: styles.cancelBtn, onClick: () => void resetWorkspaceSyncCursorAndPull(), disabled: userGlobalSyncStatus === 'syncing', title: "Clear pull cursor and fetch full cloud state into local workspace", children: "Reset Cursor + Full Pull" }), runtimeMode === 'local' && !isAuthenticated && (_jsx("button", { className: styles.submitBtn, onClick: () => {
|
|
2109
|
+
openSharedLoginRoute('login');
|
|
2110
|
+
}, children: "Sign In / Register" })), _jsx("button", { className: styles.cancelBtn, onClick: () => {
|
|
2111
|
+
setShowSyncStatusModal(false);
|
|
2112
|
+
}, children: "Close" }), _jsx("button", { className: styles.submitBtn, onClick: () => void retryWorkspaceCloudSync(), disabled: userGlobalSyncStatus === 'syncing', children: userGlobalSyncStatus === 'syncing' ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { size: 14, className: styles.spinner }), "Syncing..."] })) : (_jsxs(_Fragment, { children: [_jsx(RefreshCw, { size: 14 }), "Retry Now"] })) })] })] }) }), unsavedModalOpen && createPortal(_jsx("div", { className: `${modalStyles.overlay} ${modalStyles.unsavedOverlay}`, style: { zIndex: 2000 }, children: _jsxs("div", { className: `${modalStyles.modal} ${modalStyles.unsavedModal}`, "data-theme": currentTheme, children: [_jsx("div", { className: `${modalStyles.header} ${modalStyles.unsavedHeader}`, children: _jsxs("div", { className: `${modalStyles.headerTitle} ${modalStyles.unsavedTitle}`, children: [_jsx(AlertTriangle, { size: 20 }), "Unsaved Changes"] }) }), _jsx("div", { className: `${modalStyles.form} ${modalStyles.unsavedContent}`, children: _jsx("p", { className: modalStyles.unsavedText, children: "You have unsaved changes. Would you like to save them?" }) }), _jsxs("div", { className: `${modalStyles.formActions} ${modalStyles.unsavedActions}`, children: [_jsx("button", { className: styles.cancelBtn, onClick: () => setUnsavedModalOpen(false), title: "Close dialog and continue editing", children: "Keep Editing" }), _jsx("button", { className: styles.destructiveBtn, onClick: handleDiscard, title: "Discard unsaved changes and leave", children: "Discard" }), _jsxs("button", { className: styles.submitBtn, onClick: handleSaveAndNavigate, disabled: loading, title: "Save changes and leave", children: [loading ? _jsx(Loader2, { size: 16, className: styles.spinner }) : _jsx(Save, { size: 16 }), "Save"] })] })] }) }), document.body), showFolderBrowser && createPortal(_jsx("div", { className: styles.folderBrowserOverlay, children: _jsxs("div", { className: styles.folderBrowser, children: [_jsxs("div", { className: styles.browserHeader, children: [_jsxs("div", { className: styles.browserPathInfo, children: [_jsx(Folder, { size: 14 }), _jsx("span", { children: currentBrowsePath || 'Project Root' })] }), _jsxs("div", { className: styles.browserActions, children: [currentBrowsePath && (_jsx("button", { className: styles.helpLink, onClick: () => {
|
|
2113
|
+
const parts = currentBrowsePath.split('/').filter(Boolean);
|
|
2114
|
+
parts.pop();
|
|
2115
|
+
fetchFolders(parts.length ? parts.join('/') + '/' : '');
|
|
2116
|
+
}, children: "Back" })), _jsx("button", { className: styles.helpLink, onClick: () => setShowFolderBrowser(false), children: "Close" })] })] }), _jsxs("div", { className: styles.browserList, children: [browserTarget && typeof browserTarget === 'object' && browserTarget.type !== 'context-link' && (_jsxs("div", { className: `${styles.browserItem} ${styles.browserItemFile} ${styles.browserSelectCurrent}`, onClick: () => handleSelectPath(currentBrowsePath), children: [_jsx(Check, { size: 14 }), " Select Current: ./", currentBrowsePath || '(root)'] })), folders.map(folder => (_jsxs("div", { className: styles.browserItem, onClick: () => fetchFolders(currentBrowsePath + folder + '/'), children: [_jsx(Folder, { size: 14 }), " ", folder, "/"] }, folder))), files.map(file => (_jsxs("div", { className: `${styles.browserItem} ${styles.browserItemFile}`, onClick: () => handleSelectPath(currentBrowsePath + file), children: [_jsx(FileText, { size: 14 }), " ", file] }, file))), folders.length === 0 && files.length === 0 && (_jsx("div", { className: `${styles.browserItem} ${styles.browserEmpty}`, children: "No items found" }))] })] }) }), document.body), _jsx(Modal, { isOpen: !!cancellationPromptTask, onClose: handleCancelCancellation, title: "Cancel Task", size: "sm", theme: currentTheme, draggable: true, footer: _jsxs(_Fragment, { children: [_jsx("button", { className: styles.cancelBtn, onClick: handleCancelCancellation, title: "Close and continue working on this task", children: "Back" }), _jsxs("button", { className: `${styles.warningBtn} ${styles.cancelTaskConfirmBtn}`, onClick: handleConfirmCancellation, title: "Keep the task, set status to cancelled, and save a cancellation reason", style: { gap: '8px' }, disabled: cancellationOpenDescendantIds.length > 0 && !cancellationMode, children: [_jsx(Ban, { size: 16 }), " Confirm Cancellation"] }), _jsxs("button", { className: `${styles.destructiveBtn} ${styles.cancelTaskDeleteBtn}`, onClick: handleDeleteFromCancellation, title: "Permanently delete this task and all of its data", style: { gap: '8px' }, children: [_jsx(Trash2, { size: 16 }), " Delete Permanently"] })] }), children: _jsxs("div", { className: styles.form, style: { padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: '8px', overflow: 'hidden' }, children: [cancellationOpenDescendantIds.length > 0 && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: '8px' }, children: [_jsxs("p", { style: { margin: 0, fontSize: '14px', color: 'var(--text-secondary)' }, children: ["This parent has ", cancellationOpenDescendantIds.length, " open descendant task", cancellationOpenDescendantIds.length === 1 ? '' : 's', "."] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx("input", { type: "radio", name: "cancel-mode", checked: cancellationMode === 'cascade', onChange: () => setCancellationMode('cascade') }), "Cancel parent and cancel open descendants"] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx("input", { type: "radio", name: "cancel-mode", checked: cancellationMode === 'unlink', onChange: () => setCancellationMode('unlink') }), "Cancel parent and unlink open descendants"] })] })), _jsx("p", { style: { margin: 0, fontSize: '14px', color: 'var(--text-secondary)' }, children: "Why is this task being cancelled?" }), _jsx("textarea", { className: styles.textarea, placeholder: "Reason for record (e.g., No longer relevant, duplicate, etc.)", value: localCancellationReason, onChange: (e) => setLocalCancellationReason(e.target.value), autoFocus: true, style: { minHeight: '60px', width: '100%', resize: 'none', boxSizing: 'border-box' }, rows: 2 })] }) })] }));
|
|
2117
|
+
}
|