@taskforcehq/taskforce 0.3.303 → 0.3.305
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -247
- package/dist/Taskforce.module.css +102 -5
- package/dist/TaskforceCore.js +635 -238
- package/dist/TaskforceCore.test.js +2243 -495
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/compat/workspaceSyncCompat.js +6 -0
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +1 -1
- package/dist/components/features/DocumentViewer.js +32 -9
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +193 -6
- package/dist/components/features/TaskSettings.js +87 -41
- package/dist/components/features/TaskSettings.test.js +180 -40
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +9 -8
- package/dist/components/task/TaskForm.js +374 -219
- package/dist/components/task/TaskForm.test.js +482 -361
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +33 -161
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +9 -10
- package/dist/components/views/PlanComparisonPage.test.js +55 -41
- package/dist/components/views/PlansPage.d.ts +5 -1
- package/dist/components/views/PlansPage.js +150 -81
- package/dist/components/views/PlansPage.test.js +367 -41
- package/dist/components/views/StandaloneLayout.js +135 -54
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +6 -1
- package/dist/core/AiProfileService.js +161 -16
- package/dist/core/AiProfileService.test.js +3 -1
- package/dist/core/AiProfiles.test.js +200 -0
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +75 -13
- package/dist/core/PlanEntitlementService.d.ts +9 -1
- package/dist/core/PlanEntitlementService.js +144 -19
- package/dist/core/PlanVersionPolicy.test.js +19 -26
- package/dist/core/SignupMonetizationSettings.test.js +75 -15
- package/dist/core/SystemAdmin.test.js +212 -56
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +18 -11
- package/dist/core/Taskforce.js +93 -12
- package/dist/core/WorkspacePlanMode.test.js +73 -8
- package/dist/core/shared.d.ts +2 -0
- package/dist/core/shared.js +11 -0
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +63 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +123 -0
- package/dist/hooks/sync/transfers.js +435 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
- package/dist/hooks/useSyncOrchestrator.js +388 -879
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +57 -7
- package/dist/hooks/useTaskMutations.test.js +176 -0
- package/dist/hooks/useTaskforce.d.ts +13 -4
- package/dist/hooks/useTaskforce.js +302 -817
- package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
- package/dist/hooks/useWorkspaceSyncController.js +33 -11
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +2455 -3925
- package/dist/mcp/runtime.test.js +240 -8
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +46 -3
- package/dist/server/index.test.js +12 -1
- package/dist/server/routes/admin.d.ts +1 -5
- package/dist/server/routes/admin.js +151 -80
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +1 -4
- package/dist/server/routes/auth.js +59 -64
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +122 -255
- package/dist/server/routes/billing.test.js +257 -86
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +4 -2
- package/dist/server/routes/documents.js +73 -7
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +31 -1
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +111 -238
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +50 -8
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +1 -1
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +84 -181
- package/dist/server/routes.test.js +912 -32
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.js +28 -17
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +379 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +44 -2
- package/dist/sync/syncService.test.js +62 -0
- package/dist/sync/workspacePullFeed.d.ts +1 -0
- package/dist/sync/workspacePullFeed.js +66 -59
- package/dist/sync/workspacePullFeed.test.js +72 -4
- package/dist/sync/workspaceSyncModel.d.ts +25 -1
- package/dist/sync/workspaceSyncModel.js +77 -3
- package/dist/sync/workspaceSyncModel.test.js +153 -0
- package/dist/sync/workspaceSyncState.d.ts +2 -0
- package/dist/sync/workspaceSyncState.js +1 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
- package/dist/ui/assets/index-C2-OXZV7.css +1 -0
- package/dist/ui/assets/index-DcSI5F86.js +6 -0
- package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +196 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +10 -1
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/playwright-auth.sh +9 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
- package/dist/ui/assets/index-CLIMgR6g.js +0 -6
- package/dist/ui/assets/index-DneETxcu.css +0 -1
|
@@ -109,6 +109,17 @@ describe('TaskSettings Component', () => {
|
|
|
109
109
|
};
|
|
110
110
|
return render(_jsx(TaskSettings, { ...props }));
|
|
111
111
|
};
|
|
112
|
+
const openMcpSettings = async (user) => {
|
|
113
|
+
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
114
|
+
};
|
|
115
|
+
const openCloudMcpSettings = async (user) => {
|
|
116
|
+
await openMcpSettings(user);
|
|
117
|
+
const cloudToggle = screen.queryByRole('button', { name: 'Cloud MCP' });
|
|
118
|
+
if (cloudToggle) {
|
|
119
|
+
await user.click(cloudToggle);
|
|
120
|
+
}
|
|
121
|
+
await screen.findByText('Cloud MCP Configuration');
|
|
122
|
+
};
|
|
112
123
|
const originalFetch = global.fetch;
|
|
113
124
|
it('4.1 Open settings - Renders correctly', () => {
|
|
114
125
|
renderComponent();
|
|
@@ -859,7 +870,7 @@ describe('TaskSettings Component', () => {
|
|
|
859
870
|
credentials: 'include'
|
|
860
871
|
}));
|
|
861
872
|
}, 10000);
|
|
862
|
-
it('4.21 Local authenticated runtime defaults MCP tab to
|
|
873
|
+
it('4.21 Local authenticated runtime defaults MCP tab to local with local-first toggle', async () => {
|
|
863
874
|
const fetchCloudAuthApi = vi.fn(async () => ({
|
|
864
875
|
ok: true,
|
|
865
876
|
json: async () => ({ items: [] })
|
|
@@ -875,15 +886,17 @@ describe('TaskSettings Component', () => {
|
|
|
875
886
|
}
|
|
876
887
|
});
|
|
877
888
|
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
expect(
|
|
881
|
-
expect(screen.
|
|
882
|
-
expect(screen.queryByText('MCP
|
|
883
|
-
await user.click(screen.getByRole('button', { name: 'Local MCP' }));
|
|
889
|
+
const localButton = await screen.findByRole('button', { name: 'Local MCP' });
|
|
890
|
+
const cloudButton = screen.getByRole('button', { name: 'Cloud MCP' });
|
|
891
|
+
expect(localButton.compareDocumentPosition(cloudButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
892
|
+
expect(screen.getByText('Local MCP Configuration')).toBeInTheDocument();
|
|
893
|
+
expect(screen.queryByText('Cloud MCP Configuration')).not.toBeInTheDocument();
|
|
884
894
|
expect(screen.queryByText('MCP Path Mapping (Host Root)')).not.toBeInTheDocument();
|
|
885
895
|
await user.click(screen.getByRole('button', { name: 'Advanced' }));
|
|
886
896
|
expect(screen.getByText('MCP Path Mapping (Host Root)')).toBeInTheDocument();
|
|
897
|
+
await user.click(cloudButton);
|
|
898
|
+
expect(screen.getByText('Cloud MCP Configuration')).toBeInTheDocument();
|
|
899
|
+
expect(screen.getAllByText(/https:\/\/mcp\.taskforcehq\.ai\/mcp/).length).toBeGreaterThan(0);
|
|
887
900
|
expect(fetchCloudAuthApi).toHaveBeenCalledWith('/api/taskforce/settings/mcp/tokens', expect.objectContaining({
|
|
888
901
|
method: 'GET',
|
|
889
902
|
credentials: 'include'
|
|
@@ -900,7 +913,7 @@ describe('TaskSettings Component', () => {
|
|
|
900
913
|
id: 'mcp-token-1',
|
|
901
914
|
name: 'Workspace Alpha MCP',
|
|
902
915
|
tokenPrefix: 'tfmcp_example',
|
|
903
|
-
scopes: ['tasks:read', 'tasks:write'],
|
|
916
|
+
scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
|
|
904
917
|
status: 'active',
|
|
905
918
|
createdAt: new Date().toISOString()
|
|
906
919
|
}
|
|
@@ -921,12 +934,19 @@ describe('TaskSettings Component', () => {
|
|
|
921
934
|
fetchCloudAuthApi
|
|
922
935
|
}
|
|
923
936
|
});
|
|
924
|
-
await user
|
|
925
|
-
await user.click(
|
|
937
|
+
await openCloudMcpSettings(user);
|
|
938
|
+
await user.click(screen.getByRole('button', { name: 'Generate Token' }));
|
|
926
939
|
expect(fetchCloudAuthApi).toHaveBeenCalledWith('/api/taskforce/settings/mcp/tokens', expect.objectContaining({
|
|
927
940
|
method: 'POST',
|
|
928
941
|
credentials: 'include'
|
|
929
942
|
}));
|
|
943
|
+
expect(fetchCloudAuthApi).toHaveBeenCalledWith('/api/taskforce/settings/mcp/tokens', expect.objectContaining({
|
|
944
|
+
body: JSON.stringify({
|
|
945
|
+
name: 'Workspace Alpha MCP',
|
|
946
|
+
scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
|
|
947
|
+
expiresAt: null
|
|
948
|
+
})
|
|
949
|
+
}));
|
|
930
950
|
expect((await screen.findAllByText('tfmcp_example_secret')).length).toBeGreaterThan(0);
|
|
931
951
|
}, 10000);
|
|
932
952
|
it('4.23c MCP token name defaults to the workspace name', async () => {
|
|
@@ -938,7 +958,7 @@ describe('TaskSettings Component', () => {
|
|
|
938
958
|
currentWorkspaceName: 'Workspace Alpha'
|
|
939
959
|
}
|
|
940
960
|
});
|
|
941
|
-
await user
|
|
961
|
+
await openCloudMcpSettings(user);
|
|
942
962
|
expect(await screen.findByDisplayValue('Workspace Alpha MCP')).toBeInTheDocument();
|
|
943
963
|
});
|
|
944
964
|
it('4.23d MCP test connection uses the provided cloud auth helper', async () => {
|
|
@@ -950,7 +970,8 @@ describe('TaskSettings Component', () => {
|
|
|
950
970
|
result: {
|
|
951
971
|
workspaceName: 'Workspace Alpha',
|
|
952
972
|
workspaceId: 'workspace-alpha',
|
|
953
|
-
scopes: ['tasks:read', 'tasks:write']
|
|
973
|
+
scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
|
|
974
|
+
endpoint: 'https://performance-mcp.taskforcehq.ai/mcp'
|
|
954
975
|
}
|
|
955
976
|
})
|
|
956
977
|
};
|
|
@@ -969,7 +990,7 @@ describe('TaskSettings Component', () => {
|
|
|
969
990
|
fetchCloudAuthApi
|
|
970
991
|
}
|
|
971
992
|
});
|
|
972
|
-
await user
|
|
993
|
+
await openCloudMcpSettings(user);
|
|
973
994
|
await user.click(screen.getByRole('button', { name: 'Test Connection' }));
|
|
974
995
|
await user.type(await screen.findByPlaceholderText('tfmcp_xxxxx_replace-me'), 'tfmcp_example_secret');
|
|
975
996
|
await user.click(screen.getAllByRole('button', { name: 'Test Connection' })[1]);
|
|
@@ -978,7 +999,47 @@ describe('TaskSettings Component', () => {
|
|
|
978
999
|
credentials: 'include'
|
|
979
1000
|
}));
|
|
980
1001
|
expect(await screen.findByText('Connection looks good for this workspace.')).toBeInTheDocument();
|
|
981
|
-
expect(screen.getByText(/Granted scopes: tasks:read, tasks:write/i)).toBeInTheDocument();
|
|
1002
|
+
expect(screen.getByText(/Granted scopes: tasks:read, tasks:write, workspace:read/i)).toBeInTheDocument();
|
|
1003
|
+
expect(screen.getByText(/Endpoint: https:\/\/performance-mcp\.taskforcehq\.ai\/mcp/i)).toBeInTheDocument();
|
|
1004
|
+
}, 10000);
|
|
1005
|
+
it('4.23d2 MCP test connection shows a dedicated-host incident message when the endpoint returns a gateway failure', async () => {
|
|
1006
|
+
const fetchCloudAuthApi = vi.fn(async (path) => {
|
|
1007
|
+
if (path === '/api/taskforce/settings/mcp/test-connection') {
|
|
1008
|
+
return {
|
|
1009
|
+
ok: false,
|
|
1010
|
+
json: async () => ({
|
|
1011
|
+
code: 'MCP_ENDPOINT_GATEWAY_ERROR',
|
|
1012
|
+
error: 'Cloud MCP endpoint https://performance-mcp.taskforcehq.ai/mcp returned 502.',
|
|
1013
|
+
details: {
|
|
1014
|
+
endpoint: 'https://performance-mcp.taskforcehq.ai/mcp',
|
|
1015
|
+
upstreamStatus: 502
|
|
1016
|
+
}
|
|
1017
|
+
})
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
return {
|
|
1021
|
+
ok: true,
|
|
1022
|
+
json: async () => ({ items: [] })
|
|
1023
|
+
};
|
|
1024
|
+
});
|
|
1025
|
+
const user = userEvent.setup();
|
|
1026
|
+
renderComponent({
|
|
1027
|
+
settingsModel: {
|
|
1028
|
+
runtimeMode: 'local',
|
|
1029
|
+
isAuthenticated: true,
|
|
1030
|
+
currentWorkspaceName: 'Workspace Alpha',
|
|
1031
|
+
cloudMcpBaseUrl: 'https://performance-mcp.taskforcehq.ai',
|
|
1032
|
+
fetchCloudAuthApi
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
await openCloudMcpSettings(user);
|
|
1036
|
+
await user.click(screen.getByRole('button', { name: 'Test Connection' }));
|
|
1037
|
+
await user.type(await screen.findByPlaceholderText('tfmcp_xxxxx_replace-me'), 'tfmcp_example_secret');
|
|
1038
|
+
await user.click(screen.getAllByRole('button', { name: 'Test Connection' })[1]);
|
|
1039
|
+
expect(await screen.findByText('The dedicated MCP host is responding with a gateway error.')).toBeInTheDocument();
|
|
1040
|
+
expect(screen.getByText(/Endpoint: https:\/\/performance-mcp\.taskforcehq\.ai\/mcp/i)).toBeInTheDocument();
|
|
1041
|
+
expect(screen.getByText(/Error code: MCP_ENDPOINT_GATEWAY_ERROR/i)).toBeInTheDocument();
|
|
1042
|
+
expect(screen.getByText(/This usually means the cloud MCP service is degraded/i)).toBeInTheDocument();
|
|
982
1043
|
}, 10000);
|
|
983
1044
|
it('4.23e Antigravity uses serverUrl instead of transport in the cloud snippet', async () => {
|
|
984
1045
|
const fetchCloudAuthApi = vi.fn(async () => ({
|
|
@@ -995,7 +1056,7 @@ describe('TaskSettings Component', () => {
|
|
|
995
1056
|
fetchCloudAuthApi
|
|
996
1057
|
}
|
|
997
1058
|
});
|
|
998
|
-
await user
|
|
1059
|
+
await openCloudMcpSettings(user);
|
|
999
1060
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'antigravity');
|
|
1000
1061
|
const snippetMatches = await screen.findAllByText(/serverUrl/);
|
|
1001
1062
|
expect(snippetMatches.length).toBeGreaterThan(0);
|
|
@@ -1018,7 +1079,7 @@ describe('TaskSettings Component', () => {
|
|
|
1018
1079
|
fetchCloudAuthApi
|
|
1019
1080
|
}
|
|
1020
1081
|
});
|
|
1021
|
-
await user
|
|
1082
|
+
await openCloudMcpSettings(user);
|
|
1022
1083
|
expect(await screen.findByText(/https:\/\/performance-mcp\.taskforcehq\.ai\/mcp/)).toBeInTheDocument();
|
|
1023
1084
|
expect(screen.queryByText(/https:\/\/performance-app\.taskforcehq\.ai\/mcp/)).not.toBeInTheDocument();
|
|
1024
1085
|
}, 10000);
|
|
@@ -1038,7 +1099,7 @@ describe('TaskSettings Component', () => {
|
|
|
1038
1099
|
fetchCloudAuthApi
|
|
1039
1100
|
}
|
|
1040
1101
|
});
|
|
1041
|
-
await user
|
|
1102
|
+
await openCloudMcpSettings(user);
|
|
1042
1103
|
expect(await screen.findByText(/https:\/\/performance-mcp\.taskforcehq\.ai\/mcp/)).toBeInTheDocument();
|
|
1043
1104
|
expect(screen.queryByText(/https:\/\/performance-app\.taskforcehq\.ai\/mcp/)).not.toBeInTheDocument();
|
|
1044
1105
|
}, 10000);
|
|
@@ -1057,7 +1118,7 @@ describe('TaskSettings Component', () => {
|
|
|
1057
1118
|
fetchCloudAuthApi
|
|
1058
1119
|
}
|
|
1059
1120
|
});
|
|
1060
|
-
await user
|
|
1121
|
+
await openCloudMcpSettings(user);
|
|
1061
1122
|
const urlMatches = await screen.findAllByText(/"url"/);
|
|
1062
1123
|
expect(urlMatches.length).toBeGreaterThan(0);
|
|
1063
1124
|
expect(screen.getAllByText(/https:\/\/mcp\.taskforcehq\.ai\/mcp/).length).toBeGreaterThan(0);
|
|
@@ -1079,7 +1140,7 @@ describe('TaskSettings Component', () => {
|
|
|
1079
1140
|
fetchCloudAuthApi
|
|
1080
1141
|
}
|
|
1081
1142
|
});
|
|
1082
|
-
await user
|
|
1143
|
+
await openCloudMcpSettings(user);
|
|
1083
1144
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'cline');
|
|
1084
1145
|
expect(await screen.findByText(/Cline Configuration Instructions/i)).toBeInTheDocument();
|
|
1085
1146
|
expect(screen.getByText(/"type"/)).toBeInTheDocument();
|
|
@@ -1101,7 +1162,7 @@ describe('TaskSettings Component', () => {
|
|
|
1101
1162
|
fetchCloudAuthApi
|
|
1102
1163
|
}
|
|
1103
1164
|
});
|
|
1104
|
-
await user
|
|
1165
|
+
await openCloudMcpSettings(user);
|
|
1105
1166
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'vscode');
|
|
1106
1167
|
expect(await screen.findByText(/VS Code Configuration Instructions/i)).toBeInTheDocument();
|
|
1107
1168
|
expect(screen.getByText(/"servers"/)).toBeInTheDocument();
|
|
@@ -1123,7 +1184,7 @@ describe('TaskSettings Component', () => {
|
|
|
1123
1184
|
fetchCloudAuthApi
|
|
1124
1185
|
}
|
|
1125
1186
|
});
|
|
1126
|
-
await user
|
|
1187
|
+
await openCloudMcpSettings(user);
|
|
1127
1188
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
|
|
1128
1189
|
expect((await screen.findAllByText(/\.codex\/config\.toml/i)).length).toBeGreaterThan(0);
|
|
1129
1190
|
expect(screen.getByText(/\[mcp_servers\./)).toBeInTheDocument();
|
|
@@ -1145,7 +1206,7 @@ describe('TaskSettings Component', () => {
|
|
|
1145
1206
|
fetchCloudAuthApi
|
|
1146
1207
|
}
|
|
1147
1208
|
});
|
|
1148
|
-
await user
|
|
1209
|
+
await openCloudMcpSettings(user);
|
|
1149
1210
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'claude-code');
|
|
1150
1211
|
expect(await screen.findByText(/Claude Code Configuration Instructions/i)).toBeInTheDocument();
|
|
1151
1212
|
expect(screen.getByText(/claude mcp add --transport http/i)).toBeInTheDocument();
|
|
@@ -1167,7 +1228,7 @@ describe('TaskSettings Component', () => {
|
|
|
1167
1228
|
fetchCloudAuthApi
|
|
1168
1229
|
}
|
|
1169
1230
|
});
|
|
1170
|
-
await user
|
|
1231
|
+
await openCloudMcpSettings(user);
|
|
1171
1232
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'gemini-cli');
|
|
1172
1233
|
expect(await screen.findByText(/Gemini CLI Configuration Instructions/i)).toBeInTheDocument();
|
|
1173
1234
|
expect(screen.getByText(/gemini mcp add taskforce-/i)).toBeInTheDocument();
|
|
@@ -1190,7 +1251,7 @@ describe('TaskSettings Component', () => {
|
|
|
1190
1251
|
fetchCloudAuthApi
|
|
1191
1252
|
}
|
|
1192
1253
|
});
|
|
1193
|
-
await user
|
|
1254
|
+
await openCloudMcpSettings(user);
|
|
1194
1255
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'openclaw');
|
|
1195
1256
|
expect(await screen.findByText(/OpenClaw Configuration Instructions/i)).toBeInTheDocument();
|
|
1196
1257
|
expect((await screen.findAllByText(/~\/\.openclaw\/openclaw\.json/i)).length).toBeGreaterThan(0);
|
|
@@ -1217,10 +1278,10 @@ describe('TaskSettings Component', () => {
|
|
|
1217
1278
|
fetchCloudAuthApi
|
|
1218
1279
|
}
|
|
1219
1280
|
});
|
|
1220
|
-
await user
|
|
1281
|
+
await openCloudMcpSettings(user);
|
|
1221
1282
|
expect(screen.queryByRole('option', { name: /Claude Desktop/i })).not.toBeInTheDocument();
|
|
1222
1283
|
expect(screen.queryByRole('option', { name: /ChatGPT Desktop/i })).not.toBeInTheDocument();
|
|
1223
|
-
expect(await screen.findByText('
|
|
1284
|
+
expect(await screen.findByText('General MCP Configuration Instructions')).toBeInTheDocument();
|
|
1224
1285
|
}, 10000);
|
|
1225
1286
|
it('4.23e4b Integrations tab shows ChatGPT and Claude connector management separately from PAT tokens', async () => {
|
|
1226
1287
|
const fetchCloudAuthApi = vi.fn(async (path) => {
|
|
@@ -1294,8 +1355,7 @@ describe('TaskSettings Component', () => {
|
|
|
1294
1355
|
fetchCloudAuthApi
|
|
1295
1356
|
}
|
|
1296
1357
|
});
|
|
1297
|
-
await user
|
|
1298
|
-
expect(await screen.findByText('Cloud MCP Configuration')).toBeInTheDocument();
|
|
1358
|
+
await openCloudMcpSettings(user);
|
|
1299
1359
|
expect(screen.queryByText('ChatGPT Connector')).not.toBeInTheDocument();
|
|
1300
1360
|
expect(screen.queryByText(/^Connectors$/)).not.toBeInTheDocument();
|
|
1301
1361
|
}, 10000);
|
|
@@ -1309,8 +1369,7 @@ describe('TaskSettings Component', () => {
|
|
|
1309
1369
|
cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
|
|
1310
1370
|
}
|
|
1311
1371
|
});
|
|
1312
|
-
await user
|
|
1313
|
-
expect(await screen.findByText('Cloud MCP Configuration')).toBeInTheDocument();
|
|
1372
|
+
await openCloudMcpSettings(user);
|
|
1314
1373
|
expect(screen.getAllByRole('button', { name: /copy/i }).length).toBeGreaterThan(0);
|
|
1315
1374
|
}, 10000);
|
|
1316
1375
|
it('4.23g1 Cloud MCP copy warns before a token is generated', async () => {
|
|
@@ -1323,8 +1382,7 @@ describe('TaskSettings Component', () => {
|
|
|
1323
1382
|
cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
|
|
1324
1383
|
}
|
|
1325
1384
|
});
|
|
1326
|
-
await user
|
|
1327
|
-
await screen.findByText('Cloud MCP Configuration');
|
|
1385
|
+
await openCloudMcpSettings(user);
|
|
1328
1386
|
await user.click(screen.getByTitle('Copy full configuration file'));
|
|
1329
1387
|
expect(screen.getByText('Generate a token before copying the Cloud MCP configuration.')).toBeInTheDocument();
|
|
1330
1388
|
}, 10000);
|
|
@@ -1338,8 +1396,7 @@ describe('TaskSettings Component', () => {
|
|
|
1338
1396
|
cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
|
|
1339
1397
|
}
|
|
1340
1398
|
});
|
|
1341
|
-
await user
|
|
1342
|
-
await screen.findByText('Cloud MCP Configuration');
|
|
1399
|
+
await openCloudMcpSettings(user);
|
|
1343
1400
|
expect(screen.queryByText('Automation Identity (Optional)')).not.toBeInTheDocument();
|
|
1344
1401
|
expect(screen.queryByRole('combobox', { name: 'Use existing AI profile' })).not.toBeInTheDocument();
|
|
1345
1402
|
}, 10000);
|
|
@@ -1353,8 +1410,7 @@ describe('TaskSettings Component', () => {
|
|
|
1353
1410
|
cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
|
|
1354
1411
|
}
|
|
1355
1412
|
});
|
|
1356
|
-
await user
|
|
1357
|
-
await screen.findByText('Cloud MCP Configuration');
|
|
1413
|
+
await openCloudMcpSettings(user);
|
|
1358
1414
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
|
|
1359
1415
|
expect(screen.queryByText(/X-Taskforce-AI-Name/i)).not.toBeInTheDocument();
|
|
1360
1416
|
expect(screen.queryByText(/X-Taskforce-AI-Profile-Token/i)).not.toBeInTheDocument();
|
|
@@ -1394,7 +1450,7 @@ describe('TaskSettings Component', () => {
|
|
|
1394
1450
|
fetchCloudAuthApi
|
|
1395
1451
|
}
|
|
1396
1452
|
});
|
|
1397
|
-
await user
|
|
1453
|
+
await openCloudMcpSettings(user);
|
|
1398
1454
|
expect(await screen.findByText('Current Token')).toBeInTheDocument();
|
|
1399
1455
|
expect(screen.getByText('Old Token')).toBeInTheDocument();
|
|
1400
1456
|
const tokenLabels = screen.getAllByText(/^(Current Token|Old Token)$/);
|
|
@@ -1414,8 +1470,7 @@ describe('TaskSettings Component', () => {
|
|
|
1414
1470
|
cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
|
|
1415
1471
|
}
|
|
1416
1472
|
});
|
|
1417
|
-
await user
|
|
1418
|
-
await screen.findByText('Cloud MCP Configuration');
|
|
1473
|
+
await openCloudMcpSettings(user);
|
|
1419
1474
|
const codeNodes = Array.from(document.querySelectorAll('code'));
|
|
1420
1475
|
const snippetNode = codeNodes.find((node) => node.textContent?.includes('"mcpServers"'));
|
|
1421
1476
|
expect(snippetNode).toBeTruthy();
|
|
@@ -1435,11 +1490,96 @@ describe('TaskSettings Component', () => {
|
|
|
1435
1490
|
});
|
|
1436
1491
|
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
1437
1492
|
expect(screen.getByText('Local MCP')).toBeInTheDocument();
|
|
1438
|
-
expect(screen.
|
|
1493
|
+
expect(screen.getByRole('combobox', { name: 'MCP client' })).toHaveValue('general');
|
|
1494
|
+
expect(screen.getByText('General MCP Configuration Instructions')).toBeInTheDocument();
|
|
1439
1495
|
expect(screen.queryByText('MCP Path Mapping (Host Root)')).not.toBeInTheDocument();
|
|
1440
1496
|
await user.click(screen.getByRole('button', { name: 'Advanced' }));
|
|
1441
1497
|
expect(screen.getByText('MCP Path Mapping (Host Root)')).toBeInTheDocument();
|
|
1442
1498
|
expect(screen.queryByText('Cloud MCP Configuration')).not.toBeInTheDocument();
|
|
1443
1499
|
expect(screen.queryByRole('button', { name: 'Cloud MCP' })).not.toBeInTheDocument();
|
|
1444
1500
|
});
|
|
1501
|
+
it('4.24a Local MCP selector offers General first and keeps the rest alphabetical', async () => {
|
|
1502
|
+
const user = userEvent.setup();
|
|
1503
|
+
renderComponent({
|
|
1504
|
+
settingsModel: {
|
|
1505
|
+
runtimeMode: 'local',
|
|
1506
|
+
isAuthenticated: false
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
1510
|
+
const options = screen.getAllByRole('option').map((option) => option.textContent);
|
|
1511
|
+
expect(options).toEqual([
|
|
1512
|
+
'General',
|
|
1513
|
+
'Antigravity',
|
|
1514
|
+
'Claude Code CLI',
|
|
1515
|
+
'Cline',
|
|
1516
|
+
'Codex',
|
|
1517
|
+
'Cursor',
|
|
1518
|
+
'Gemini CLI',
|
|
1519
|
+
'OpenClaw',
|
|
1520
|
+
'VS Code / Copilot',
|
|
1521
|
+
'Windsurf'
|
|
1522
|
+
]);
|
|
1523
|
+
});
|
|
1524
|
+
it('4.24b Local Codex shows config.toml MCP output without token headers', async () => {
|
|
1525
|
+
const user = userEvent.setup();
|
|
1526
|
+
renderComponent({
|
|
1527
|
+
settingsModel: {
|
|
1528
|
+
runtimeMode: 'local',
|
|
1529
|
+
isAuthenticated: false,
|
|
1530
|
+
projectRoot: '/Users/rossburke/projects/taskforce',
|
|
1531
|
+
projectName: 'taskforce',
|
|
1532
|
+
tenantId: 'tenant-alpha',
|
|
1533
|
+
workspaceId: 'workspace-alpha',
|
|
1534
|
+
mcpScriptPath: '/usr/local/bin/taskforce'
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
1538
|
+
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
|
|
1539
|
+
expect(await screen.findByText(/Codex Configuration Instructions/i)).toBeInTheDocument();
|
|
1540
|
+
expect((await screen.findAllByText(/\.codex\/config\.toml/i)).length).toBeGreaterThan(0);
|
|
1541
|
+
expect(screen.getByText(/\[mcp_servers\./)).toBeInTheDocument();
|
|
1542
|
+
expect(screen.getByText(/command = "taskforce"/i)).toBeInTheDocument();
|
|
1543
|
+
expect(screen.queryByText(/Authorization/i)).not.toBeInTheDocument();
|
|
1544
|
+
});
|
|
1545
|
+
it('4.24c Local VS Code uses stdio config under servers', async () => {
|
|
1546
|
+
const user = userEvent.setup();
|
|
1547
|
+
renderComponent({
|
|
1548
|
+
settingsModel: {
|
|
1549
|
+
runtimeMode: 'local',
|
|
1550
|
+
isAuthenticated: false,
|
|
1551
|
+
projectRoot: '/Users/rossburke/projects/taskforce',
|
|
1552
|
+
projectName: 'taskforce',
|
|
1553
|
+
tenantId: 'tenant-alpha',
|
|
1554
|
+
workspaceId: 'workspace-alpha',
|
|
1555
|
+
mcpScriptPath: '/usr/local/bin/taskforce'
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
1559
|
+
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'vscode');
|
|
1560
|
+
expect(await screen.findByText(/VS Code Configuration Instructions/i)).toBeInTheDocument();
|
|
1561
|
+
expect(screen.getByText(/"servers"/)).toBeInTheDocument();
|
|
1562
|
+
expect(screen.getByText(/"type"/)).toBeInTheDocument();
|
|
1563
|
+
expect(screen.getByText(/"stdio"/)).toBeInTheDocument();
|
|
1564
|
+
});
|
|
1565
|
+
it('4.24d Local Claude Code shows a stdio add command', async () => {
|
|
1566
|
+
const user = userEvent.setup();
|
|
1567
|
+
renderComponent({
|
|
1568
|
+
settingsModel: {
|
|
1569
|
+
runtimeMode: 'local',
|
|
1570
|
+
isAuthenticated: false,
|
|
1571
|
+
projectRoot: '/Users/rossburke/projects/taskforce',
|
|
1572
|
+
projectName: 'taskforce',
|
|
1573
|
+
tenantId: 'tenant-alpha',
|
|
1574
|
+
workspaceId: 'workspace-alpha',
|
|
1575
|
+
mcpScriptPath: '/usr/local/bin/taskforce'
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
1579
|
+
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'claude-code');
|
|
1580
|
+
expect(await screen.findByText(/Claude Code Configuration Instructions/i)).toBeInTheDocument();
|
|
1581
|
+
expect(screen.getByText(/claude mcp add taskforce-/i)).toBeInTheDocument();
|
|
1582
|
+
expect(screen.getByText(/--env TASKFORCE_TENANT_ID=tenant-alpha/i)).toBeInTheDocument();
|
|
1583
|
+
expect(screen.getByText(/-- taskforce mcp/i)).toBeInTheDocument();
|
|
1584
|
+
});
|
|
1445
1585
|
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DocumentAttachmentFilter, DocumentTypeFilter } from './documentWorkspaceFilters';
|
|
2
|
+
export interface DocMeta {
|
|
3
|
+
id: string;
|
|
4
|
+
assetId?: string;
|
|
5
|
+
referenceNumber?: number | null;
|
|
6
|
+
referenceLabel?: string | null;
|
|
7
|
+
fsPath: string;
|
|
8
|
+
apiUrl: string;
|
|
9
|
+
taskId: string | null;
|
|
10
|
+
displayName: string;
|
|
11
|
+
originalFilename?: string | null;
|
|
12
|
+
title: string;
|
|
13
|
+
docType: 'implementation-plan' | 'review' | 'walkthrough' | 'planning' | 'other';
|
|
14
|
+
sizeBytes: number;
|
|
15
|
+
updatedAt: string | null;
|
|
16
|
+
editable?: boolean;
|
|
17
|
+
attachmentCount?: number;
|
|
18
|
+
attachmentState?: 'attached' | 'unattached' | 'standalone';
|
|
19
|
+
}
|
|
20
|
+
export declare function normalizeDocPath(value: string): string;
|
|
21
|
+
export declare function docPathMatches(candidate: string, requested: string): boolean;
|
|
22
|
+
export declare function extractAssetIdFromPath(value: string | null | undefined): string | null;
|
|
23
|
+
export declare function getEffectiveAttachmentState(doc: Pick<DocMeta, 'attachmentState' | 'attachmentCount' | 'taskId'>): 'attached' | 'unattached';
|
|
24
|
+
export declare function filterDocuments(documents: DocMeta[], searchQuery: string, typeFilters: DocumentTypeFilter[], attachmentFilters: DocumentAttachmentFilter[]): DocMeta[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getReadableDocumentName } from '../../utils/documentNames';
|
|
2
|
+
export function normalizeDocPath(value) {
|
|
3
|
+
let decoded = value;
|
|
4
|
+
try {
|
|
5
|
+
decoded = decodeURIComponent(value);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
decoded = value;
|
|
9
|
+
}
|
|
10
|
+
const normalized = decoded
|
|
11
|
+
.trim()
|
|
12
|
+
.replace(/\\/g, '/')
|
|
13
|
+
.replace(/^\/+/, '')
|
|
14
|
+
.replace(/^\.\//, '')
|
|
15
|
+
.replace(/^\.taskforce\//, '');
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
export function docPathMatches(candidate, requested) {
|
|
19
|
+
if (candidate === requested)
|
|
20
|
+
return true;
|
|
21
|
+
if (candidate.endsWith(`/${requested}`))
|
|
22
|
+
return true;
|
|
23
|
+
if (requested.endsWith(`/${candidate}`))
|
|
24
|
+
return true;
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
export function extractAssetIdFromPath(value) {
|
|
28
|
+
const normalized = normalizeDocPath(String(value || ''));
|
|
29
|
+
if (!normalized)
|
|
30
|
+
return null;
|
|
31
|
+
const match = normalized.match(/(^|\/)(asset-[a-f0-9]{32})(?=[^/]*$)/i);
|
|
32
|
+
return match?.[2] ? match[2].toLowerCase() : null;
|
|
33
|
+
}
|
|
34
|
+
export function getEffectiveAttachmentState(doc) {
|
|
35
|
+
if (doc.attachmentState === 'attached' || doc.attachmentState === 'unattached') {
|
|
36
|
+
return doc.attachmentState;
|
|
37
|
+
}
|
|
38
|
+
const attachmentCount = typeof doc.attachmentCount === 'number' && Number.isFinite(doc.attachmentCount)
|
|
39
|
+
? doc.attachmentCount
|
|
40
|
+
: 0;
|
|
41
|
+
if (attachmentCount > 0 || !!doc.taskId) {
|
|
42
|
+
return 'attached';
|
|
43
|
+
}
|
|
44
|
+
return 'unattached';
|
|
45
|
+
}
|
|
46
|
+
export function filterDocuments(documents, searchQuery, typeFilters, attachmentFilters) {
|
|
47
|
+
const normalizedQuery = searchQuery.trim().toLowerCase();
|
|
48
|
+
return documents.filter((doc) => {
|
|
49
|
+
const readableName = getReadableDocumentName(doc);
|
|
50
|
+
const haystack = `${readableName} ${doc.originalFilename || ''}`.toLowerCase();
|
|
51
|
+
const matchesSearch = normalizedQuery.length === 0 || haystack.includes(normalizedQuery);
|
|
52
|
+
const matchesType = typeFilters.length === 0 || typeFilters.includes(doc.docType);
|
|
53
|
+
const attachmentState = getEffectiveAttachmentState(doc);
|
|
54
|
+
const matchesAttachment = attachmentFilters.length === 0 || attachmentFilters.includes(attachmentState);
|
|
55
|
+
return matchesSearch && matchesType && matchesAttachment;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type DocumentAttachmentFilter, type DocumentTypeFilter } from './documentWorkspaceFilters';
|
|
2
|
+
import { type DocMeta } from './documentWorkspaceModel';
|
|
3
|
+
interface Options {
|
|
4
|
+
runtimeMode?: 'local' | 'cloud';
|
|
5
|
+
apiBaseUrl?: string;
|
|
6
|
+
cloudAuthBaseUrl?: string;
|
|
7
|
+
typeFilters?: DocumentTypeFilter[];
|
|
8
|
+
attachmentFilters?: DocumentAttachmentFilter[];
|
|
9
|
+
onTypeFiltersChange?: (values: DocumentTypeFilter[]) => void;
|
|
10
|
+
onAttachmentFiltersChange?: (values: DocumentAttachmentFilter[]) => void;
|
|
11
|
+
requestedDocPath?: string | null;
|
|
12
|
+
requestedDocAssetId?: string | null;
|
|
13
|
+
onRequestedDocHandled?: () => void;
|
|
14
|
+
}
|
|
15
|
+
interface Result {
|
|
16
|
+
documentApiBaseUrl: string;
|
|
17
|
+
documents: DocMeta[];
|
|
18
|
+
filteredDocs: DocMeta[];
|
|
19
|
+
loading: boolean;
|
|
20
|
+
error: string | null;
|
|
21
|
+
selectedDoc: DocMeta | null;
|
|
22
|
+
searchQuery: string;
|
|
23
|
+
typeFilters: DocumentTypeFilter[];
|
|
24
|
+
attachmentFilters: DocumentAttachmentFilter[];
|
|
25
|
+
hasActiveFilters: boolean;
|
|
26
|
+
setSearchQuery: (value: string) => void;
|
|
27
|
+
handleTypeFiltersChange: (values: DocumentTypeFilter[]) => void;
|
|
28
|
+
handleAttachmentFiltersChange: (values: DocumentAttachmentFilter[]) => void;
|
|
29
|
+
fetchDocuments: () => Promise<void>;
|
|
30
|
+
selectDoc: (doc: DocMeta | null) => void;
|
|
31
|
+
handleDeleteSelected: () => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export declare function useDocumentWorkspaceController({ runtimeMode, apiBaseUrl, cloudAuthBaseUrl, typeFilters: controlledTypeFilters, attachmentFilters: controlledAttachmentFilters, onTypeFiltersChange, onAttachmentFiltersChange, requestedDocPath, requestedDocAssetId, onRequestedDocHandled, }: Options): Result;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { fetchTaskforceApi } from '../../utils/taskforceApiClient';
|
|
3
|
+
import { logDocumentManagerDebug, resolveDocumentManagerApiBase } from '../../utils/documentManagerApi';
|
|
4
|
+
import { DOCUMENT_ATTACHMENT_FILTER_OPTIONS, DOCUMENT_TYPE_FILTER_OPTIONS, } from './documentWorkspaceFilters';
|
|
5
|
+
import { docPathMatches, extractAssetIdFromPath, filterDocuments, normalizeDocPath, } from './documentWorkspaceModel';
|
|
6
|
+
export function useDocumentWorkspaceController({ runtimeMode = 'local', apiBaseUrl = '', cloudAuthBaseUrl = '', typeFilters: controlledTypeFilters, attachmentFilters: controlledAttachmentFilters, onTypeFiltersChange, onAttachmentFiltersChange, requestedDocPath = null, requestedDocAssetId = null, onRequestedDocHandled, }) {
|
|
7
|
+
const [documents, setDocuments] = useState([]);
|
|
8
|
+
const [loading, setLoading] = useState(true);
|
|
9
|
+
const [error, setError] = useState(null);
|
|
10
|
+
const [selectedDocId, setSelectedDocId] = useState(null);
|
|
11
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
12
|
+
const [internalTypeFilters, setInternalTypeFilters] = useState(() => DOCUMENT_TYPE_FILTER_OPTIONS.map((option) => option.value));
|
|
13
|
+
const [internalAttachmentFilters, setInternalAttachmentFilters] = useState(() => DOCUMENT_ATTACHMENT_FILTER_OPTIONS.map((option) => option.value));
|
|
14
|
+
const documentApiBaseUrl = resolveDocumentManagerApiBase({
|
|
15
|
+
runtimeMode,
|
|
16
|
+
apiBaseUrl,
|
|
17
|
+
cloudAuthBaseUrl,
|
|
18
|
+
});
|
|
19
|
+
const typeFilters = controlledTypeFilters ?? internalTypeFilters;
|
|
20
|
+
const attachmentFilters = controlledAttachmentFilters ?? internalAttachmentFilters;
|
|
21
|
+
const handleTypeFiltersChange = onTypeFiltersChange ?? setInternalTypeFilters;
|
|
22
|
+
const handleAttachmentFiltersChange = onAttachmentFiltersChange ?? setInternalAttachmentFilters;
|
|
23
|
+
const fetchDocuments = useCallback(async () => {
|
|
24
|
+
setLoading(true);
|
|
25
|
+
setError(null);
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetchTaskforceApi('/api/taskforce/documents', {
|
|
28
|
+
credentials: 'include',
|
|
29
|
+
}, documentApiBaseUrl);
|
|
30
|
+
if (!res.ok)
|
|
31
|
+
throw new Error(`Failed to load documents (${res.status})`);
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
const nextDocuments = Array.isArray(data.documents) ? data.documents : [];
|
|
34
|
+
logDocumentManagerDebug('list.loaded', {
|
|
35
|
+
runtimeMode,
|
|
36
|
+
apiBaseUrl: documentApiBaseUrl || '(same-origin)',
|
|
37
|
+
count: nextDocuments.length,
|
|
38
|
+
});
|
|
39
|
+
setDocuments(nextDocuments);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
setError(e?.message || 'Failed to load documents');
|
|
43
|
+
logDocumentManagerDebug('list.error', {
|
|
44
|
+
runtimeMode,
|
|
45
|
+
apiBaseUrl: documentApiBaseUrl || '(same-origin)',
|
|
46
|
+
message: e?.message || 'Failed to load documents',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
setLoading(false);
|
|
51
|
+
}
|
|
52
|
+
}, [documentApiBaseUrl, runtimeMode]);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
void fetchDocuments();
|
|
55
|
+
}, [fetchDocuments]);
|
|
56
|
+
const filteredDocs = useMemo(() => filterDocuments(documents, searchQuery, typeFilters, attachmentFilters), [attachmentFilters, documents, searchQuery, typeFilters]);
|
|
57
|
+
const selectedDoc = useMemo(() => (selectedDocId ? documents.find((doc) => doc.id === selectedDocId) ?? null : null), [documents, selectedDocId]);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if ((!requestedDocPath && !requestedDocAssetId) || documents.length === 0)
|
|
60
|
+
return;
|
|
61
|
+
const requested = requestedDocPath ? normalizeDocPath(requestedDocPath) : '';
|
|
62
|
+
const normalizedRequestedAssetId = String(requestedDocAssetId || '').trim().toLowerCase()
|
|
63
|
+
|| extractAssetIdFromPath(requestedDocPath);
|
|
64
|
+
const matched = documents.find((doc) => {
|
|
65
|
+
const candidateAssetId = extractAssetIdFromPath(doc.fsPath) || String(doc.assetId || '').trim().toLowerCase() || null;
|
|
66
|
+
if (normalizedRequestedAssetId && candidateAssetId && candidateAssetId === normalizedRequestedAssetId)
|
|
67
|
+
return true;
|
|
68
|
+
if (!requested)
|
|
69
|
+
return false;
|
|
70
|
+
return docPathMatches(normalizeDocPath(doc.fsPath), requested);
|
|
71
|
+
});
|
|
72
|
+
if (!matched)
|
|
73
|
+
return;
|
|
74
|
+
setSelectedDocId(matched.id);
|
|
75
|
+
onRequestedDocHandled?.();
|
|
76
|
+
}, [documents, onRequestedDocHandled, requestedDocAssetId, requestedDocPath]);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if ((requestedDocPath || requestedDocAssetId) && !selectedDoc)
|
|
79
|
+
return;
|
|
80
|
+
if (filteredDocs.length === 0) {
|
|
81
|
+
if (selectedDocId !== null) {
|
|
82
|
+
setSelectedDocId(null);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (selectedDoc && filteredDocs.some((doc) => doc.id === selectedDoc.id))
|
|
87
|
+
return;
|
|
88
|
+
setSelectedDocId(filteredDocs[0].id);
|
|
89
|
+
}, [filteredDocs, requestedDocAssetId, requestedDocPath, selectedDoc, selectedDocId]);
|
|
90
|
+
const handleDeleteSelected = useCallback(async () => {
|
|
91
|
+
setSelectedDocId(null);
|
|
92
|
+
await fetchDocuments();
|
|
93
|
+
}, [fetchDocuments]);
|
|
94
|
+
const hasActiveFilters = searchQuery.trim().length > 0 || typeFilters.length > 0 || attachmentFilters.length > 0;
|
|
95
|
+
return {
|
|
96
|
+
documentApiBaseUrl,
|
|
97
|
+
documents,
|
|
98
|
+
filteredDocs,
|
|
99
|
+
loading,
|
|
100
|
+
error,
|
|
101
|
+
selectedDoc,
|
|
102
|
+
searchQuery,
|
|
103
|
+
typeFilters,
|
|
104
|
+
attachmentFilters,
|
|
105
|
+
hasActiveFilters,
|
|
106
|
+
setSearchQuery,
|
|
107
|
+
handleTypeFiltersChange,
|
|
108
|
+
handleAttachmentFiltersChange,
|
|
109
|
+
fetchDocuments,
|
|
110
|
+
selectDoc: (doc) => setSelectedDocId(doc?.id ?? null),
|
|
111
|
+
handleDeleteSelected,
|
|
112
|
+
};
|
|
113
|
+
}
|