@taskforcehq/taskforce 0.3.303 → 0.3.304
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Taskforce.module.css +102 -5
- package/dist/TaskforceCore.js +514 -212
- package/dist/TaskforceCore.test.js +1172 -133
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/TaskSettings.js +58 -38
- package/dist/components/features/TaskSettings.test.js +139 -40
- 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 +2 -1
- package/dist/components/task/TaskForm.js +22 -4
- package/dist/components/task/TaskForm.test.js +46 -0
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +31 -160
- 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 +4 -2
- package/dist/components/views/PlanComparisonPage.test.js +4 -0
- package/dist/components/views/PlansPage.d.ts +5 -1
- package/dist/components/views/PlansPage.js +39 -12
- package/dist/components/views/PlansPage.test.js +59 -2
- package/dist/components/views/StandaloneLayout.js +113 -38
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/PlanEntitlementService.d.ts +3 -1
- package/dist/core/PlanEntitlementService.js +61 -11
- package/dist/core/SignupMonetizationSettings.test.js +37 -2
- 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/Taskforce.d.ts +15 -11
- package/dist/core/Taskforce.js +43 -2
- package/dist/core/WorkspacePlanMode.test.js +65 -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 +61 -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 +122 -0
- package/dist/hooks/sync/transfers.js +431 -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 +5 -4
- package/dist/hooks/useSyncOrchestrator.js +314 -873
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +8 -2
- package/dist/hooks/useTaskforce.js +285 -812
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +22 -10
- 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/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/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +174 -476
- package/dist/mcp/runtime.test.js +81 -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/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.js +13 -19
- package/dist/server/routes/auth.js +37 -2
- package/dist/server/routes/billing.js +18 -67
- package/dist/server/routes/billing.test.js +43 -0
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +110 -237
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +8 -19
- package/dist/server/routes.test.js +243 -5
- 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 +12 -10
- 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 +377 -0
- package/dist/sync/syncService.d.ts +8 -1
- package/dist/sync/syncService.js +36 -2
- package/dist/sync/syncService.test.js +48 -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 +22 -1
- package/dist/sync/workspaceSyncModel.js +65 -0
- package/dist/sync/workspaceSyncModel.test.js +119 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-BjR6iBf9.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-DnFqdUME.js} +1 -1
- package/dist/ui/assets/index-DXUdtH1B.js +6 -0
- package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
- package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +3 -1
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +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,7 @@ 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']
|
|
954
974
|
}
|
|
955
975
|
})
|
|
956
976
|
};
|
|
@@ -969,7 +989,7 @@ describe('TaskSettings Component', () => {
|
|
|
969
989
|
fetchCloudAuthApi
|
|
970
990
|
}
|
|
971
991
|
});
|
|
972
|
-
await user
|
|
992
|
+
await openCloudMcpSettings(user);
|
|
973
993
|
await user.click(screen.getByRole('button', { name: 'Test Connection' }));
|
|
974
994
|
await user.type(await screen.findByPlaceholderText('tfmcp_xxxxx_replace-me'), 'tfmcp_example_secret');
|
|
975
995
|
await user.click(screen.getAllByRole('button', { name: 'Test Connection' })[1]);
|
|
@@ -978,7 +998,7 @@ describe('TaskSettings Component', () => {
|
|
|
978
998
|
credentials: 'include'
|
|
979
999
|
}));
|
|
980
1000
|
expect(await screen.findByText('Connection looks good for this workspace.')).toBeInTheDocument();
|
|
981
|
-
expect(screen.getByText(/Granted scopes: tasks:read, tasks:write/i)).toBeInTheDocument();
|
|
1001
|
+
expect(screen.getByText(/Granted scopes: tasks:read, tasks:write, workspace:read/i)).toBeInTheDocument();
|
|
982
1002
|
}, 10000);
|
|
983
1003
|
it('4.23e Antigravity uses serverUrl instead of transport in the cloud snippet', async () => {
|
|
984
1004
|
const fetchCloudAuthApi = vi.fn(async () => ({
|
|
@@ -995,7 +1015,7 @@ describe('TaskSettings Component', () => {
|
|
|
995
1015
|
fetchCloudAuthApi
|
|
996
1016
|
}
|
|
997
1017
|
});
|
|
998
|
-
await user
|
|
1018
|
+
await openCloudMcpSettings(user);
|
|
999
1019
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'antigravity');
|
|
1000
1020
|
const snippetMatches = await screen.findAllByText(/serverUrl/);
|
|
1001
1021
|
expect(snippetMatches.length).toBeGreaterThan(0);
|
|
@@ -1018,7 +1038,7 @@ describe('TaskSettings Component', () => {
|
|
|
1018
1038
|
fetchCloudAuthApi
|
|
1019
1039
|
}
|
|
1020
1040
|
});
|
|
1021
|
-
await user
|
|
1041
|
+
await openCloudMcpSettings(user);
|
|
1022
1042
|
expect(await screen.findByText(/https:\/\/performance-mcp\.taskforcehq\.ai\/mcp/)).toBeInTheDocument();
|
|
1023
1043
|
expect(screen.queryByText(/https:\/\/performance-app\.taskforcehq\.ai\/mcp/)).not.toBeInTheDocument();
|
|
1024
1044
|
}, 10000);
|
|
@@ -1038,7 +1058,7 @@ describe('TaskSettings Component', () => {
|
|
|
1038
1058
|
fetchCloudAuthApi
|
|
1039
1059
|
}
|
|
1040
1060
|
});
|
|
1041
|
-
await user
|
|
1061
|
+
await openCloudMcpSettings(user);
|
|
1042
1062
|
expect(await screen.findByText(/https:\/\/performance-mcp\.taskforcehq\.ai\/mcp/)).toBeInTheDocument();
|
|
1043
1063
|
expect(screen.queryByText(/https:\/\/performance-app\.taskforcehq\.ai\/mcp/)).not.toBeInTheDocument();
|
|
1044
1064
|
}, 10000);
|
|
@@ -1057,7 +1077,7 @@ describe('TaskSettings Component', () => {
|
|
|
1057
1077
|
fetchCloudAuthApi
|
|
1058
1078
|
}
|
|
1059
1079
|
});
|
|
1060
|
-
await user
|
|
1080
|
+
await openCloudMcpSettings(user);
|
|
1061
1081
|
const urlMatches = await screen.findAllByText(/"url"/);
|
|
1062
1082
|
expect(urlMatches.length).toBeGreaterThan(0);
|
|
1063
1083
|
expect(screen.getAllByText(/https:\/\/mcp\.taskforcehq\.ai\/mcp/).length).toBeGreaterThan(0);
|
|
@@ -1079,7 +1099,7 @@ describe('TaskSettings Component', () => {
|
|
|
1079
1099
|
fetchCloudAuthApi
|
|
1080
1100
|
}
|
|
1081
1101
|
});
|
|
1082
|
-
await user
|
|
1102
|
+
await openCloudMcpSettings(user);
|
|
1083
1103
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'cline');
|
|
1084
1104
|
expect(await screen.findByText(/Cline Configuration Instructions/i)).toBeInTheDocument();
|
|
1085
1105
|
expect(screen.getByText(/"type"/)).toBeInTheDocument();
|
|
@@ -1101,7 +1121,7 @@ describe('TaskSettings Component', () => {
|
|
|
1101
1121
|
fetchCloudAuthApi
|
|
1102
1122
|
}
|
|
1103
1123
|
});
|
|
1104
|
-
await user
|
|
1124
|
+
await openCloudMcpSettings(user);
|
|
1105
1125
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'vscode');
|
|
1106
1126
|
expect(await screen.findByText(/VS Code Configuration Instructions/i)).toBeInTheDocument();
|
|
1107
1127
|
expect(screen.getByText(/"servers"/)).toBeInTheDocument();
|
|
@@ -1123,7 +1143,7 @@ describe('TaskSettings Component', () => {
|
|
|
1123
1143
|
fetchCloudAuthApi
|
|
1124
1144
|
}
|
|
1125
1145
|
});
|
|
1126
|
-
await user
|
|
1146
|
+
await openCloudMcpSettings(user);
|
|
1127
1147
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
|
|
1128
1148
|
expect((await screen.findAllByText(/\.codex\/config\.toml/i)).length).toBeGreaterThan(0);
|
|
1129
1149
|
expect(screen.getByText(/\[mcp_servers\./)).toBeInTheDocument();
|
|
@@ -1145,7 +1165,7 @@ describe('TaskSettings Component', () => {
|
|
|
1145
1165
|
fetchCloudAuthApi
|
|
1146
1166
|
}
|
|
1147
1167
|
});
|
|
1148
|
-
await user
|
|
1168
|
+
await openCloudMcpSettings(user);
|
|
1149
1169
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'claude-code');
|
|
1150
1170
|
expect(await screen.findByText(/Claude Code Configuration Instructions/i)).toBeInTheDocument();
|
|
1151
1171
|
expect(screen.getByText(/claude mcp add --transport http/i)).toBeInTheDocument();
|
|
@@ -1167,7 +1187,7 @@ describe('TaskSettings Component', () => {
|
|
|
1167
1187
|
fetchCloudAuthApi
|
|
1168
1188
|
}
|
|
1169
1189
|
});
|
|
1170
|
-
await user
|
|
1190
|
+
await openCloudMcpSettings(user);
|
|
1171
1191
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'gemini-cli');
|
|
1172
1192
|
expect(await screen.findByText(/Gemini CLI Configuration Instructions/i)).toBeInTheDocument();
|
|
1173
1193
|
expect(screen.getByText(/gemini mcp add taskforce-/i)).toBeInTheDocument();
|
|
@@ -1190,7 +1210,7 @@ describe('TaskSettings Component', () => {
|
|
|
1190
1210
|
fetchCloudAuthApi
|
|
1191
1211
|
}
|
|
1192
1212
|
});
|
|
1193
|
-
await user
|
|
1213
|
+
await openCloudMcpSettings(user);
|
|
1194
1214
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'openclaw');
|
|
1195
1215
|
expect(await screen.findByText(/OpenClaw Configuration Instructions/i)).toBeInTheDocument();
|
|
1196
1216
|
expect((await screen.findAllByText(/~\/\.openclaw\/openclaw\.json/i)).length).toBeGreaterThan(0);
|
|
@@ -1217,10 +1237,10 @@ describe('TaskSettings Component', () => {
|
|
|
1217
1237
|
fetchCloudAuthApi
|
|
1218
1238
|
}
|
|
1219
1239
|
});
|
|
1220
|
-
await user
|
|
1240
|
+
await openCloudMcpSettings(user);
|
|
1221
1241
|
expect(screen.queryByRole('option', { name: /Claude Desktop/i })).not.toBeInTheDocument();
|
|
1222
1242
|
expect(screen.queryByRole('option', { name: /ChatGPT Desktop/i })).not.toBeInTheDocument();
|
|
1223
|
-
expect(await screen.findByText('
|
|
1243
|
+
expect(await screen.findByText('General MCP Configuration Instructions')).toBeInTheDocument();
|
|
1224
1244
|
}, 10000);
|
|
1225
1245
|
it('4.23e4b Integrations tab shows ChatGPT and Claude connector management separately from PAT tokens', async () => {
|
|
1226
1246
|
const fetchCloudAuthApi = vi.fn(async (path) => {
|
|
@@ -1294,8 +1314,7 @@ describe('TaskSettings Component', () => {
|
|
|
1294
1314
|
fetchCloudAuthApi
|
|
1295
1315
|
}
|
|
1296
1316
|
});
|
|
1297
|
-
await user
|
|
1298
|
-
expect(await screen.findByText('Cloud MCP Configuration')).toBeInTheDocument();
|
|
1317
|
+
await openCloudMcpSettings(user);
|
|
1299
1318
|
expect(screen.queryByText('ChatGPT Connector')).not.toBeInTheDocument();
|
|
1300
1319
|
expect(screen.queryByText(/^Connectors$/)).not.toBeInTheDocument();
|
|
1301
1320
|
}, 10000);
|
|
@@ -1309,8 +1328,7 @@ describe('TaskSettings Component', () => {
|
|
|
1309
1328
|
cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
|
|
1310
1329
|
}
|
|
1311
1330
|
});
|
|
1312
|
-
await user
|
|
1313
|
-
expect(await screen.findByText('Cloud MCP Configuration')).toBeInTheDocument();
|
|
1331
|
+
await openCloudMcpSettings(user);
|
|
1314
1332
|
expect(screen.getAllByRole('button', { name: /copy/i }).length).toBeGreaterThan(0);
|
|
1315
1333
|
}, 10000);
|
|
1316
1334
|
it('4.23g1 Cloud MCP copy warns before a token is generated', async () => {
|
|
@@ -1323,8 +1341,7 @@ describe('TaskSettings Component', () => {
|
|
|
1323
1341
|
cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
|
|
1324
1342
|
}
|
|
1325
1343
|
});
|
|
1326
|
-
await user
|
|
1327
|
-
await screen.findByText('Cloud MCP Configuration');
|
|
1344
|
+
await openCloudMcpSettings(user);
|
|
1328
1345
|
await user.click(screen.getByTitle('Copy full configuration file'));
|
|
1329
1346
|
expect(screen.getByText('Generate a token before copying the Cloud MCP configuration.')).toBeInTheDocument();
|
|
1330
1347
|
}, 10000);
|
|
@@ -1338,8 +1355,7 @@ describe('TaskSettings Component', () => {
|
|
|
1338
1355
|
cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
|
|
1339
1356
|
}
|
|
1340
1357
|
});
|
|
1341
|
-
await user
|
|
1342
|
-
await screen.findByText('Cloud MCP Configuration');
|
|
1358
|
+
await openCloudMcpSettings(user);
|
|
1343
1359
|
expect(screen.queryByText('Automation Identity (Optional)')).not.toBeInTheDocument();
|
|
1344
1360
|
expect(screen.queryByRole('combobox', { name: 'Use existing AI profile' })).not.toBeInTheDocument();
|
|
1345
1361
|
}, 10000);
|
|
@@ -1353,8 +1369,7 @@ describe('TaskSettings Component', () => {
|
|
|
1353
1369
|
cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
|
|
1354
1370
|
}
|
|
1355
1371
|
});
|
|
1356
|
-
await user
|
|
1357
|
-
await screen.findByText('Cloud MCP Configuration');
|
|
1372
|
+
await openCloudMcpSettings(user);
|
|
1358
1373
|
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
|
|
1359
1374
|
expect(screen.queryByText(/X-Taskforce-AI-Name/i)).not.toBeInTheDocument();
|
|
1360
1375
|
expect(screen.queryByText(/X-Taskforce-AI-Profile-Token/i)).not.toBeInTheDocument();
|
|
@@ -1394,7 +1409,7 @@ describe('TaskSettings Component', () => {
|
|
|
1394
1409
|
fetchCloudAuthApi
|
|
1395
1410
|
}
|
|
1396
1411
|
});
|
|
1397
|
-
await user
|
|
1412
|
+
await openCloudMcpSettings(user);
|
|
1398
1413
|
expect(await screen.findByText('Current Token')).toBeInTheDocument();
|
|
1399
1414
|
expect(screen.getByText('Old Token')).toBeInTheDocument();
|
|
1400
1415
|
const tokenLabels = screen.getAllByText(/^(Current Token|Old Token)$/);
|
|
@@ -1414,8 +1429,7 @@ describe('TaskSettings Component', () => {
|
|
|
1414
1429
|
cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
|
|
1415
1430
|
}
|
|
1416
1431
|
});
|
|
1417
|
-
await user
|
|
1418
|
-
await screen.findByText('Cloud MCP Configuration');
|
|
1432
|
+
await openCloudMcpSettings(user);
|
|
1419
1433
|
const codeNodes = Array.from(document.querySelectorAll('code'));
|
|
1420
1434
|
const snippetNode = codeNodes.find((node) => node.textContent?.includes('"mcpServers"'));
|
|
1421
1435
|
expect(snippetNode).toBeTruthy();
|
|
@@ -1435,11 +1449,96 @@ describe('TaskSettings Component', () => {
|
|
|
1435
1449
|
});
|
|
1436
1450
|
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
1437
1451
|
expect(screen.getByText('Local MCP')).toBeInTheDocument();
|
|
1438
|
-
expect(screen.
|
|
1452
|
+
expect(screen.getByRole('combobox', { name: 'MCP client' })).toHaveValue('general');
|
|
1453
|
+
expect(screen.getByText('General MCP Configuration Instructions')).toBeInTheDocument();
|
|
1439
1454
|
expect(screen.queryByText('MCP Path Mapping (Host Root)')).not.toBeInTheDocument();
|
|
1440
1455
|
await user.click(screen.getByRole('button', { name: 'Advanced' }));
|
|
1441
1456
|
expect(screen.getByText('MCP Path Mapping (Host Root)')).toBeInTheDocument();
|
|
1442
1457
|
expect(screen.queryByText('Cloud MCP Configuration')).not.toBeInTheDocument();
|
|
1443
1458
|
expect(screen.queryByRole('button', { name: 'Cloud MCP' })).not.toBeInTheDocument();
|
|
1444
1459
|
});
|
|
1460
|
+
it('4.24a Local MCP selector offers General first and keeps the rest alphabetical', async () => {
|
|
1461
|
+
const user = userEvent.setup();
|
|
1462
|
+
renderComponent({
|
|
1463
|
+
settingsModel: {
|
|
1464
|
+
runtimeMode: 'local',
|
|
1465
|
+
isAuthenticated: false
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
1469
|
+
const options = screen.getAllByRole('option').map((option) => option.textContent);
|
|
1470
|
+
expect(options).toEqual([
|
|
1471
|
+
'General',
|
|
1472
|
+
'Antigravity',
|
|
1473
|
+
'Claude Code CLI',
|
|
1474
|
+
'Cline',
|
|
1475
|
+
'Codex',
|
|
1476
|
+
'Cursor',
|
|
1477
|
+
'Gemini CLI',
|
|
1478
|
+
'OpenClaw',
|
|
1479
|
+
'VS Code / Copilot',
|
|
1480
|
+
'Windsurf'
|
|
1481
|
+
]);
|
|
1482
|
+
});
|
|
1483
|
+
it('4.24b Local Codex shows config.toml MCP output without token headers', async () => {
|
|
1484
|
+
const user = userEvent.setup();
|
|
1485
|
+
renderComponent({
|
|
1486
|
+
settingsModel: {
|
|
1487
|
+
runtimeMode: 'local',
|
|
1488
|
+
isAuthenticated: false,
|
|
1489
|
+
projectRoot: '/Users/rossburke/projects/taskforce',
|
|
1490
|
+
projectName: 'taskforce',
|
|
1491
|
+
tenantId: 'tenant-alpha',
|
|
1492
|
+
workspaceId: 'workspace-alpha',
|
|
1493
|
+
mcpScriptPath: '/usr/local/bin/taskforce'
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
1497
|
+
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
|
|
1498
|
+
expect(await screen.findByText(/Codex Configuration Instructions/i)).toBeInTheDocument();
|
|
1499
|
+
expect((await screen.findAllByText(/\.codex\/config\.toml/i)).length).toBeGreaterThan(0);
|
|
1500
|
+
expect(screen.getByText(/\[mcp_servers\./)).toBeInTheDocument();
|
|
1501
|
+
expect(screen.getByText(/command = "taskforce"/i)).toBeInTheDocument();
|
|
1502
|
+
expect(screen.queryByText(/Authorization/i)).not.toBeInTheDocument();
|
|
1503
|
+
});
|
|
1504
|
+
it('4.24c Local VS Code uses stdio config under servers', async () => {
|
|
1505
|
+
const user = userEvent.setup();
|
|
1506
|
+
renderComponent({
|
|
1507
|
+
settingsModel: {
|
|
1508
|
+
runtimeMode: 'local',
|
|
1509
|
+
isAuthenticated: false,
|
|
1510
|
+
projectRoot: '/Users/rossburke/projects/taskforce',
|
|
1511
|
+
projectName: 'taskforce',
|
|
1512
|
+
tenantId: 'tenant-alpha',
|
|
1513
|
+
workspaceId: 'workspace-alpha',
|
|
1514
|
+
mcpScriptPath: '/usr/local/bin/taskforce'
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
1518
|
+
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'vscode');
|
|
1519
|
+
expect(await screen.findByText(/VS Code Configuration Instructions/i)).toBeInTheDocument();
|
|
1520
|
+
expect(screen.getByText(/"servers"/)).toBeInTheDocument();
|
|
1521
|
+
expect(screen.getByText(/"type"/)).toBeInTheDocument();
|
|
1522
|
+
expect(screen.getByText(/"stdio"/)).toBeInTheDocument();
|
|
1523
|
+
});
|
|
1524
|
+
it('4.24d Local Claude Code shows a stdio add command', 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' }), 'claude-code');
|
|
1539
|
+
expect(await screen.findByText(/Claude Code Configuration Instructions/i)).toBeInTheDocument();
|
|
1540
|
+
expect(screen.getByText(/claude mcp add taskforce-/i)).toBeInTheDocument();
|
|
1541
|
+
expect(screen.getByText(/--env TASKFORCE_TENANT_ID=tenant-alpha/i)).toBeInTheDocument();
|
|
1542
|
+
expect(screen.getByText(/-- taskforce mcp/i)).toBeInTheDocument();
|
|
1543
|
+
});
|
|
1445
1544
|
});
|
|
@@ -23,8 +23,9 @@ interface TaskActionHeaderProps {
|
|
|
23
23
|
handleToggleCancel: (task: TaskItem) => void;
|
|
24
24
|
handleSetStatus?: (task: TaskItem, status: TaskStatus) => void;
|
|
25
25
|
handleArchiveTask: (task: TaskItem) => void;
|
|
26
|
+
handleUnarchiveTask?: (taskId: string) => void;
|
|
26
27
|
handleRestoreDeletedTask?: (deletedRecordId: string) => void;
|
|
27
28
|
handlePermanentlyDeleteDeletedTask?: (deletedRecordId: string) => void;
|
|
28
29
|
}
|
|
29
|
-
export declare function TaskActionHeader({ editingTaskId, loading, autoSaveState, title, handleSubmit, resetForm, handleCopyId, copiedId, tasks, currentTask: currentTaskProp, currentTaskWorkstream, currentTaskInitiative, workstreamInput, onWorkstreamInputChange, onSetWorkstreamForCurrentTask, handleToggleInProgress, handleToggleReview, handleToggleComplete, handleToggleCancel, handleSetStatus, handleArchiveTask, handleRestoreDeletedTask, handlePermanentlyDeleteDeletedTask }: TaskActionHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
export declare function TaskActionHeader({ editingTaskId, loading, autoSaveState, title, handleSubmit, resetForm, handleCopyId, copiedId, tasks, currentTask: currentTaskProp, currentTaskWorkstream, currentTaskInitiative, workstreamInput, onWorkstreamInputChange, onSetWorkstreamForCurrentTask, handleToggleInProgress, handleToggleReview, handleToggleComplete, handleToggleCancel, handleSetStatus, handleArchiveTask, handleUnarchiveTask, handleRestoreDeletedTask, handlePermanentlyDeleteDeletedTask }: TaskActionHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
30
31
|
export {};
|
|
@@ -7,7 +7,7 @@ import { TaskStatusActions } from './TaskStatusActions';
|
|
|
7
7
|
import { getTaskReferenceDisplayState } from '../../utils/taskReferences';
|
|
8
8
|
import { TaskReferenceBadge } from './TaskReferenceBadge';
|
|
9
9
|
import { formatInitiativeReference, getWorkstreamReferenceLabel } from '../../utils/planningReferences';
|
|
10
|
-
export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle', title, handleSubmit, resetForm, handleCopyId, copiedId, tasks, currentTask: currentTaskProp, currentTaskWorkstream, currentTaskInitiative, workstreamInput = '', onWorkstreamInputChange, onSetWorkstreamForCurrentTask, handleToggleInProgress, handleToggleReview, handleToggleComplete, handleToggleCancel, handleSetStatus, handleArchiveTask, handleRestoreDeletedTask, handlePermanentlyDeleteDeletedTask }) {
|
|
10
|
+
export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle', title, handleSubmit, resetForm, handleCopyId, copiedId, tasks, currentTask: currentTaskProp, currentTaskWorkstream, currentTaskInitiative, workstreamInput = '', onWorkstreamInputChange, onSetWorkstreamForCurrentTask, handleToggleInProgress, handleToggleReview, handleToggleComplete, handleToggleCancel, handleSetStatus, handleArchiveTask, handleUnarchiveTask, handleRestoreDeletedTask, handlePermanentlyDeleteDeletedTask }) {
|
|
11
11
|
const [showWorkstreamEditor, setShowWorkstreamEditor] = React.useState(false);
|
|
12
12
|
const currentTask = currentTaskProp ?? (editingTaskId ? tasks.find(t => t.id === editingTaskId) || null : null);
|
|
13
13
|
const isArchivedReadOnly = Boolean(currentTask?.isArchived);
|
|
@@ -48,7 +48,7 @@ export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle
|
|
|
48
48
|
} }), _jsx("button", { type: "button", className: "tf-control-icon tf-control-icon-compact", onClick: handleWorkstreamConfirm, disabled: !workstreamInput.trim() || loading, title: "Attach workstream", "aria-label": "Attach workstream", children: _jsx(Check, { size: 14 }) }), _jsx("button", { type: "button", className: "tf-control-icon tf-control-icon-compact", onClick: () => {
|
|
49
49
|
onWorkstreamInputChange?.('');
|
|
50
50
|
setShowWorkstreamEditor(false);
|
|
51
|
-
}, disabled: loading, title: "Cancel workstream attach", "aria-label": "Cancel workstream attach", children: _jsx(X, { size: 14 }) })] })) : null] }), _jsx("div", { className: styles.taskSaveStatusCenter, children: _jsx("div", { className: `${styles.taskSaveStatus} ${autoSaveState === 'error' ? styles.taskSaveStatusError : ''}`, "aria-live": "polite", children: autoSaveState === 'saving' ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { size: 14, className: styles.spinner }), _jsx("span", { children: "Saving\u2026" })] })) : autoSaveState === 'saved' ? (_jsx("span", { children: "Saved" })) : autoSaveState === 'error' ? (_jsx("span", { children: "Save failed" })) : null }) }), _jsx("div", { className: styles.editActionsGroup, children: isDeletedReadOnly && currentTask?.deletedRecordId ? (_jsxs(_Fragment, { children: [handleRestoreDeletedTask && (_jsx("button", { type: "button", className: styles.actionBtn, onClick: () => handleRestoreDeletedTask(currentTask.deletedRecordId || ''), disabled: loading, title: "Restore deleted task", children: _jsx(RotateCcw, { size: 16 }) })), handlePermanentlyDeleteDeletedTask && (_jsx("button", { type: "button", className: `${styles.actionBtn} ${styles.deleteBtn}`, onClick: () => handlePermanentlyDeleteDeletedTask(currentTask.deletedRecordId || ''), disabled: loading, title: "Permanently delete deleted task", children: _jsx(Trash2, { size: 16 }) }))] })) : (_jsx(TaskStatusActions, { task: currentTask, disabled: loading || isArchivedReadOnly, onSetStatus: (task, status) => {
|
|
51
|
+
}, disabled: loading, title: "Cancel workstream attach", "aria-label": "Cancel workstream attach", children: _jsx(X, { size: 14 }) })] })) : null] }), _jsx("div", { className: styles.taskSaveStatusCenter, children: _jsx("div", { className: `${styles.taskSaveStatus} ${autoSaveState === 'error' ? styles.taskSaveStatusError : ''}`, "aria-live": "polite", children: autoSaveState === 'saving' ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { size: 14, className: styles.spinner }), _jsx("span", { children: "Saving\u2026" })] })) : autoSaveState === 'saved' ? (_jsx("span", { children: "Saved" })) : autoSaveState === 'error' ? (_jsx("span", { children: "Save failed" })) : null }) }), _jsx("div", { className: styles.editActionsGroup, children: isDeletedReadOnly && currentTask?.deletedRecordId ? (_jsxs(_Fragment, { children: [handleRestoreDeletedTask && (_jsx("button", { type: "button", className: styles.actionBtn, onClick: () => handleRestoreDeletedTask(currentTask.deletedRecordId || ''), disabled: loading, title: "Restore deleted task", children: _jsx(RotateCcw, { size: 16 }) })), handlePermanentlyDeleteDeletedTask && (_jsx("button", { type: "button", className: `${styles.actionBtn} ${styles.deleteBtn}`, onClick: () => handlePermanentlyDeleteDeletedTask(currentTask.deletedRecordId || ''), disabled: loading, title: "Permanently delete deleted task", children: _jsx(Trash2, { size: 16 }) }))] })) : (_jsx(TaskStatusActions, { task: currentTask, disabled: loading, statusDisabled: loading || isArchivedReadOnly, archiveActionMode: isArchivedReadOnly ? 'unarchive' : 'auto', onSetStatus: (task, status) => {
|
|
52
52
|
if (handleSetStatus) {
|
|
53
53
|
handleSetStatus(task, status);
|
|
54
54
|
return;
|
|
@@ -68,5 +68,5 @@ export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle
|
|
|
68
68
|
if (status === 'cancelled') {
|
|
69
69
|
handleToggleCancel(task);
|
|
70
70
|
}
|
|
71
|
-
}, onArchiveTask: handleArchiveTask })) })] })) : (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: resetForm, disabled: loading, className: styles.secondaryHeaderBtn, title: t('actionHeader.clearFormTitle'), children: [_jsx(RotateCcw, { size: 16 }), t('actionHeader.clear')] }), _jsxs("button", { type: "button", onClick: (e) => handleSubmit(e), disabled: loading || !title.trim(), className: styles.primaryUpdateBtn, title: t('actionHeader.addTaskTitle'), children: [loading ? _jsx(Loader2, { size: 16, className: styles.spinner }) : _jsx(Plus, { size: 16 }), t('actionHeader.addTask')] })] })) }));
|
|
71
|
+
}, onArchiveTask: handleArchiveTask, onUnarchiveTask: (task) => handleUnarchiveTask?.(task.id) })) })] })) : (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: resetForm, disabled: loading, className: styles.secondaryHeaderBtn, title: t('actionHeader.clearFormTitle'), children: [_jsx(RotateCcw, { size: 16 }), t('actionHeader.clear')] }), _jsxs("button", { type: "button", onClick: (e) => handleSubmit(e), disabled: loading || !title.trim(), className: styles.primaryUpdateBtn, title: t('actionHeader.addTaskTitle'), children: [loading ? _jsx(Loader2, { size: 16, className: styles.spinner }) : _jsx(Plus, { size: 16 }), t('actionHeader.addTask')] })] })) }));
|
|
72
72
|
}
|
|
@@ -70,4 +70,12 @@ describe('TaskActionHeader', () => {
|
|
|
70
70
|
await user.click(screen.getByRole('button', { name: /cancel workstream attach/i }));
|
|
71
71
|
expect(onWorkstreamInputChange).toHaveBeenCalledWith('');
|
|
72
72
|
});
|
|
73
|
+
it('shows an unarchive action for archived tasks and wires it to the task id', async () => {
|
|
74
|
+
const user = userEvent.setup();
|
|
75
|
+
const handleUnarchiveTask = vi.fn();
|
|
76
|
+
render(_jsx(TaskActionHeader, { ...baseProps, editingTaskId: "task-1", title: "Polish task modal", currentTask: { ...baseTask, status: 'done', isArchived: true }, handleUnarchiveTask: handleUnarchiveTask }));
|
|
77
|
+
expect(screen.queryByRole('button', { name: /^Archive task$/i })).not.toBeInTheDocument();
|
|
78
|
+
await user.click(screen.getByRole('button', { name: /unarchive task/i }));
|
|
79
|
+
expect(handleUnarchiveTask).toHaveBeenCalledWith('task-1');
|
|
80
|
+
});
|
|
73
81
|
});
|
|
@@ -54,7 +54,22 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
|
|
|
54
54
|
const latestActivity = React.useMemo(() => getLatestTaskActivity(task), [task]);
|
|
55
55
|
const resolvedReadOnlyMode = readOnlyMode || (task.isDeleted ? 'deleted' : (isArchived ? 'archived' : null));
|
|
56
56
|
const isReadOnly = resolvedReadOnlyMode !== null;
|
|
57
|
-
const
|
|
57
|
+
const formatActivityFieldValue = React.useCallback((field, value) => {
|
|
58
|
+
const normalizedValue = String(value || '').trim();
|
|
59
|
+
if (!normalizedValue)
|
|
60
|
+
return undefined;
|
|
61
|
+
if (field === 'assignee') {
|
|
62
|
+
return getAssigneeLabel(normalizedValue, assigneeOptions);
|
|
63
|
+
}
|
|
64
|
+
if (field === 'workstreamId' && normalizedValue === taskWorkstream?.id) {
|
|
65
|
+
return workstreamReferenceLabel || taskWorkstream.id;
|
|
66
|
+
}
|
|
67
|
+
if (field === 'initiativeId' && normalizedValue === taskInitiative?.id) {
|
|
68
|
+
return initiativeReferenceLabel || taskInitiative.id;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}, [assigneeOptions, initiativeReferenceLabel, taskInitiative, taskWorkstream, workstreamReferenceLabel]);
|
|
72
|
+
const latestActivityText = React.useMemo(() => summarizeTaskActivity(latestActivity, formatActivityFieldValue), [formatActivityFieldValue, latestActivity]);
|
|
58
73
|
const activityCount = Array.isArray(task.activity) && task.activity.length > 0
|
|
59
74
|
? task.activity.length
|
|
60
75
|
: (task.comments?.length || 0);
|
|
@@ -108,6 +108,37 @@ describe('TaskCard parent/child progress', () => {
|
|
|
108
108
|
expect(screen.getByText(/Status: task -> review/)).toBeInTheDocument();
|
|
109
109
|
expect(screen.getByTitle('2 activity items')).toBeInTheDocument();
|
|
110
110
|
});
|
|
111
|
+
it('uses the current workstream reference in latest activity summaries', () => {
|
|
112
|
+
const task = baseTask({
|
|
113
|
+
id: 'task-relationship-summary',
|
|
114
|
+
title: 'Relationship summary task',
|
|
115
|
+
workstreamId: 'workstream-1',
|
|
116
|
+
activity: [
|
|
117
|
+
{
|
|
118
|
+
id: 'event:event-relationship-1',
|
|
119
|
+
type: 'event',
|
|
120
|
+
timestamp: '2026-03-22T10:00:00.000Z',
|
|
121
|
+
event: {
|
|
122
|
+
id: 'event-relationship-1',
|
|
123
|
+
taskId: 'task-relationship-summary',
|
|
124
|
+
workspaceId: 'default',
|
|
125
|
+
action: 'task-relationship-changed',
|
|
126
|
+
actor: 'user',
|
|
127
|
+
actorType: 'human',
|
|
128
|
+
createdAt: '2026-03-22T10:00:00.000Z',
|
|
129
|
+
details: {
|
|
130
|
+
changes: {
|
|
131
|
+
workstreamId: { from: null, to: 'workstream-1' }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
});
|
|
138
|
+
render(_jsx(TaskCard, { task: task, taskWorkstream: baseWorkstream({ id: 'workstream-1', referenceNumber: 12 }), onClick: () => { }, onCopyId: () => { } }));
|
|
139
|
+
expect(screen.getByText(/Workstream: none -> WS-12/)).toBeInTheDocument();
|
|
140
|
+
expect(screen.queryByText(/workstream-1/i)).not.toBeInTheDocument();
|
|
141
|
+
});
|
|
111
142
|
it('renders named AI assignee labels from assignee options', () => {
|
|
112
143
|
const task = baseTask({
|
|
113
144
|
id: 'task-ai-assignee',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { TaskItem, CategoryConfig, Comment, AttachmentItem, TaskAssignee, ChecklistItem } from '../../types';
|
|
2
|
+
import type { TaskItem, CategoryConfig, Comment, AttachmentItem, TaskAssignee, ChecklistItem, InitiativeItem } from '../../types';
|
|
3
3
|
import type { TaxonomyDefinition } from '../../types';
|
|
4
4
|
import { type TaskAssigneeOption } from '../../utils/assignees';
|
|
5
5
|
import type { WorkstreamItem } from '../../types';
|
|
@@ -58,6 +58,7 @@ interface TaskFormProps {
|
|
|
58
58
|
assigneeOptions: TaskAssigneeOption[];
|
|
59
59
|
taxonomies: TaxonomyDefinition[];
|
|
60
60
|
workstreams?: WorkstreamItem[];
|
|
61
|
+
initiatives?: InitiativeItem[];
|
|
61
62
|
copiedId: string | null;
|
|
62
63
|
onTitleChange: (value: string) => void;
|
|
63
64
|
onDescriptionChange: (value: string) => void;
|
|
@@ -19,7 +19,7 @@ import { summarizeTaskChange, summarizeTaskEvent } from '../../utils/taskActivit
|
|
|
19
19
|
import { ensureAssigneeOptionPresent, getAssigneeKind, getAssigneeLabel, } from '../../utils/assignees';
|
|
20
20
|
import { getTaskReferenceLabel } from '../../utils/taskReferences';
|
|
21
21
|
import { getVisibleTaskFormTaxonomies } from '../../utils/taxonomyFormDefaults.js';
|
|
22
|
-
import { getWorkstreamReferenceLabel } from '../../utils/planningReferences';
|
|
22
|
+
import { getInitiativeReferenceLabel, getWorkstreamReferenceLabel } from '../../utils/planningReferences';
|
|
23
23
|
function SortableChecklistRow({ id, item, onToggle, onRemove }) {
|
|
24
24
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id });
|
|
25
25
|
const style = {
|
|
@@ -36,7 +36,7 @@ function hexToRgba(hex, opacity) {
|
|
|
36
36
|
return `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}, ${opacity})`;
|
|
37
37
|
}
|
|
38
38
|
export function TaskForm(props) {
|
|
39
|
-
const { editingTaskId, error, title, description, category, type, priority, complexity, manualComplexityEnabled = false, assignee, scheduledDate, dueDate, workstreamInput = '', checklistItems, comments, newCommentText, contextFiles, currentWorkspaceId, apiBaseUrl, descriptionFocused, showMarkdownHelp, showChecklist = false, checklistEnabled = true, showComments, formTaxonomies, onTaxonomyChange, onOpenSettings, categories, types, priorities, taxonomyDisplayLabels, assigneeOptions, taxonomies, workstreams = [], copiedId, onTitleChange, onDescriptionChange, onCategoryChange, onTypeChange, onPriorityChange, onComplexityChange, onAssigneeChange, onScheduledDateChange, onDueDateChange, onWorkstreamInputChange = () => { }, onChecklistItemsChange, onNewCommentTextChange, onDescriptionFocusedChange, onShowMarkdownHelpChange, onShowChecklistChange = () => { }, onShowCommentsChange, onSubmit, onAddComment, onAddContextFile, onRemoveContextFile, onUpdateContextCaption, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onOpenTaskById, currentTask, commentsEndRef, } = props;
|
|
39
|
+
const { editingTaskId, error, title, description, category, type, priority, complexity, manualComplexityEnabled = false, assignee, scheduledDate, dueDate, workstreamInput = '', checklistItems, comments, newCommentText, contextFiles, currentWorkspaceId, apiBaseUrl, descriptionFocused, showMarkdownHelp, showChecklist = false, checklistEnabled = true, showComments, formTaxonomies, onTaxonomyChange, onOpenSettings, categories, types, priorities, taxonomyDisplayLabels, assigneeOptions, taxonomies, workstreams = [], initiatives = [], copiedId, onTitleChange, onDescriptionChange, onCategoryChange, onTypeChange, onPriorityChange, onComplexityChange, onAssigneeChange, onScheduledDateChange, onDueDateChange, onWorkstreamInputChange = () => { }, onChecklistItemsChange, onNewCommentTextChange, onDescriptionFocusedChange, onShowMarkdownHelpChange, onShowChecklistChange = () => { }, onShowCommentsChange, onSubmit, onAddComment, onAddContextFile, onRemoveContextFile, onUpdateContextCaption, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onOpenTaskById, currentTask, commentsEndRef, } = props;
|
|
40
40
|
const currentTaskReferenceLabel = getTaskReferenceLabel(currentTask);
|
|
41
41
|
const visibleTaxonomies = React.useMemo(() => getVisibleTaskFormTaxonomies(taxonomies, formTaxonomies), [taxonomies, formTaxonomies]);
|
|
42
42
|
const categoryOptions = React.useMemo(() => {
|
|
@@ -215,6 +215,14 @@ export function TaskForm(props) {
|
|
|
215
215
|
].filter(Boolean);
|
|
216
216
|
return new Map(entries.map((actor) => [String(actor.id), actor]));
|
|
217
217
|
}, [currentTask?.assigneeActor, currentTask?.createdByActor]);
|
|
218
|
+
const workstreamLabelById = React.useMemo(() => new Map(workstreams.map((workstream) => [
|
|
219
|
+
workstream.id,
|
|
220
|
+
getWorkstreamReferenceLabel(workstream) || workstream.id,
|
|
221
|
+
])), [workstreams]);
|
|
222
|
+
const initiativeLabelById = React.useMemo(() => new Map(initiatives.map((initiative) => [
|
|
223
|
+
initiative.id,
|
|
224
|
+
getInitiativeReferenceLabel(initiative) || initiative.id,
|
|
225
|
+
])), [initiatives]);
|
|
218
226
|
const taskActivityItems = React.useMemo(() => {
|
|
219
227
|
if (Array.isArray(currentTask?.activity) && currentTask.activity.length > 0) {
|
|
220
228
|
return currentTask.activity;
|
|
@@ -227,9 +235,19 @@ export function TaskForm(props) {
|
|
|
227
235
|
}));
|
|
228
236
|
}, [comments, currentTask?.activity]);
|
|
229
237
|
const formatActivityFieldValue = (field, value) => {
|
|
230
|
-
|
|
238
|
+
const normalizedValue = String(value || '').trim();
|
|
239
|
+
if (!normalizedValue)
|
|
231
240
|
return undefined;
|
|
232
|
-
|
|
241
|
+
if (field === 'assignee') {
|
|
242
|
+
return getAssigneeLabel(normalizedValue, resolvedAssigneeOptions);
|
|
243
|
+
}
|
|
244
|
+
if (field === 'workstreamId') {
|
|
245
|
+
return workstreamLabelById.get(normalizedValue);
|
|
246
|
+
}
|
|
247
|
+
if (field === 'initiativeId') {
|
|
248
|
+
return initiativeLabelById.get(normalizedValue);
|
|
249
|
+
}
|
|
250
|
+
return undefined;
|
|
233
251
|
};
|
|
234
252
|
const resolveActorLabel = React.useCallback((actorId, actorType, actorLabel) => {
|
|
235
253
|
if (actorLabel && actorLabel.trim())
|
|
@@ -59,6 +59,16 @@ describe('TaskForm Component', () => {
|
|
|
59
59
|
createdAt: '2026-04-03T00:00:00.000Z',
|
|
60
60
|
},
|
|
61
61
|
],
|
|
62
|
+
initiatives: [
|
|
63
|
+
{
|
|
64
|
+
id: 'initiative-1',
|
|
65
|
+
referenceNumber: 3,
|
|
66
|
+
referenceLabel: 'IN-3',
|
|
67
|
+
title: 'Execution model',
|
|
68
|
+
description: null,
|
|
69
|
+
createdAt: '2026-04-03T00:00:00.000Z',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
62
72
|
copiedId: null,
|
|
63
73
|
// Callbacks
|
|
64
74
|
onTitleChange: vi.fn(),
|
|
@@ -400,6 +410,42 @@ describe('TaskForm Component', () => {
|
|
|
400
410
|
expect(screen.getByText('ASSIGNED TO: Unassigned -> Planner Bot')).toBeInTheDocument();
|
|
401
411
|
expect(screen.queryByText(/ai-profile-1/i)).not.toBeInTheDocument();
|
|
402
412
|
});
|
|
413
|
+
it('renders planning relationship audit activity with reference labels instead of raw ids', () => {
|
|
414
|
+
renderForm({
|
|
415
|
+
editingTaskId: '123',
|
|
416
|
+
showComments: true,
|
|
417
|
+
currentTask: {
|
|
418
|
+
id: '123',
|
|
419
|
+
title: 'Existing Task',
|
|
420
|
+
activity: [
|
|
421
|
+
{
|
|
422
|
+
id: 'event:task-event-relationship',
|
|
423
|
+
type: 'event',
|
|
424
|
+
timestamp: '2026-03-23T12:00:00.000Z',
|
|
425
|
+
event: {
|
|
426
|
+
id: 'task-event-relationship',
|
|
427
|
+
taskId: '123',
|
|
428
|
+
workspaceId: 'workspace-1',
|
|
429
|
+
action: 'task-relationship-changed',
|
|
430
|
+
actor: 'member-1',
|
|
431
|
+
actorType: 'human',
|
|
432
|
+
createdAt: '2026-03-23T12:00:00.000Z',
|
|
433
|
+
details: {
|
|
434
|
+
changes: {
|
|
435
|
+
workstreamId: {
|
|
436
|
+
from: null,
|
|
437
|
+
to: 'workstream-1',
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
expect(screen.getByText('Workstream: none -> WS-12')).toBeInTheDocument();
|
|
447
|
+
expect(screen.queryByText(/workstream-1/i)).not.toBeInTheDocument();
|
|
448
|
+
});
|
|
403
449
|
it('3.13 Update title', async () => {
|
|
404
450
|
const user = userEvent.setup();
|
|
405
451
|
renderForm({ editingTaskId: '123', title: 'Old Title' });
|