@taskforcehq/taskforce 0.3.301 → 0.3.302
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 +26 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +13 -24
- package/dist/TaskforceCore.test.js +167 -152
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.js +9 -8
- package/dist/components/views/StandaloneLayout.js +466 -204
- package/dist/components/views/panels/PlanningDrawer.js +18 -1
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +43 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +22 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +49 -0
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +241 -78
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +102 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useTaskData.js +3 -2
- package/dist/hooks/useTaskData.test.js +66 -0
- package/dist/hooks/useTaskforce.d.ts +5 -0
- package/dist/hooks/useTaskforce.js +63 -15
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +41 -19
- package/dist/mcp/runtime.test.js +49 -2
- package/dist/migrations/taskSchemaMigrations.js +53 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +15 -1
- package/dist/server/index.test.js +14 -1
- package/dist/server/routes/admin.js +25 -22
- package/dist/server/routes/auth.d.ts +5 -0
- package/dist/server/routes/auth.js +350 -16
- package/dist/server/routes/billing.js +11 -1
- package/dist/server/routes/billing.test.js +32 -2
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +11 -3
- package/dist/server/routes/sync.integration.test.js +130 -0
- package/dist/server/routes/sync.js +226 -1924
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +26 -19
- package/dist/server/routes.test.js +352 -6
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +88 -0
- package/dist/sync/contentSyncState.js +743 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +28 -0
- package/dist/sync/workspacePullFeed.js +301 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +166 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-JG5jc3he.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-iys3zfca.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-DJvLJI3A.js} +1 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +1 -0
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-BguHeQaV.js} +1 -1
- package/dist/ui/assets/index-MXeWoebC.css +1 -0
- package/dist/ui/assets/index-YJ4k5QNa.js +6 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-C8EED3Oh.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/billingStatusCache.d.ts +21 -0
- package/dist/utils/billingStatusCache.js +70 -0
- package/dist/utils/taskActivity.js +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +2 -1
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
- package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
- package/dist/ui/assets/index-xBWLPOEb.js +0 -6
|
@@ -172,8 +172,6 @@ export function TaskSettings(props) {
|
|
|
172
172
|
const [mcpTokenNotice, setMcpTokenNotice] = useState(null);
|
|
173
173
|
const [generatedTokenValue, setGeneratedTokenValue] = useState(null);
|
|
174
174
|
const [selectedMcpClient, setSelectedMcpClient] = useState('cursor');
|
|
175
|
-
const [automationAiName, setAutomationAiName] = useState('');
|
|
176
|
-
const [automationAiProfileToken, setAutomationAiProfileToken] = useState('');
|
|
177
175
|
const [mcpConnectionToken, setMcpConnectionToken] = useState('');
|
|
178
176
|
const [mcpConnectionBusy, setMcpConnectionBusy] = useState(false);
|
|
179
177
|
const [mcpConnectionResult, setMcpConnectionResult] = useState(null);
|
|
@@ -227,77 +225,16 @@ export function TaskSettings(props) {
|
|
|
227
225
|
}, [mcpTokens]);
|
|
228
226
|
const cloudMcpServerId = useMemo(() => buildCloudMcpServerId(currentWorkspaceName || null, projectName || null), [currentWorkspaceName, projectName]);
|
|
229
227
|
const cloudMcpTokenValue = generatedTokenValue || '<<GENERATE_TOKEN>>';
|
|
230
|
-
|
|
231
|
-
if (automationAiProfileToken) {
|
|
232
|
-
const byToken = aiProfiles.find((profile) => String(profile.profileToken || '').trim() === automationAiProfileToken.trim());
|
|
233
|
-
if (byToken)
|
|
234
|
-
return byToken.id;
|
|
235
|
-
}
|
|
236
|
-
if (automationAiName) {
|
|
237
|
-
const byName = aiProfiles.find((profile) => profile.name === automationAiName.trim());
|
|
238
|
-
if (byName)
|
|
239
|
-
return byName.id;
|
|
240
|
-
}
|
|
241
|
-
return '';
|
|
242
|
-
}, [aiProfiles, automationAiName, automationAiProfileToken]);
|
|
243
|
-
const selectedAutomationProfile = useMemo(() => aiProfiles.find((profile) => profile.id === selectedAutomationProfileId) || null, [aiProfiles, selectedAutomationProfileId]);
|
|
244
|
-
const inferredAutomationProfileMetadata = useMemo(() => {
|
|
245
|
-
switch (selectedMcpClient) {
|
|
246
|
-
case 'codex':
|
|
247
|
-
return { description: 'OpenAI Codex coding agent connected through MCP.', role: 'Coding Agent', provider: 'OpenAI', model: 'Codex', surfaceType: 'coding_tool' };
|
|
248
|
-
case 'chatgpt-desktop':
|
|
249
|
-
return { description: 'ChatGPT desktop app connected through the Taskforce MCP connector.', role: 'AI Assistant', provider: 'OpenAI', model: 'ChatGPT', surfaceType: 'chat_app' };
|
|
250
|
-
case 'claude-code':
|
|
251
|
-
return { description: 'Claude Code coding agent connected through MCP.', role: 'Coding Agent', provider: 'Anthropic', model: 'Claude Code', surfaceType: 'coding_tool' };
|
|
252
|
-
case 'claude-desktop':
|
|
253
|
-
return { description: 'Claude Desktop connected through the Taskforce MCP connector.', role: 'AI Assistant', provider: 'Anthropic', model: 'Claude', surfaceType: 'chat_app' };
|
|
254
|
-
case 'gemini-cli':
|
|
255
|
-
return { description: 'Gemini CLI coding agent connected through MCP.', role: 'Coding Agent', provider: 'Google', model: 'Gemini', surfaceType: 'coding_tool' };
|
|
256
|
-
case 'vscode':
|
|
257
|
-
return { description: 'VS Code MCP-connected coding agent.', role: 'Coding Agent', provider: null, model: null, surfaceType: 'ide' };
|
|
258
|
-
case 'cline':
|
|
259
|
-
case 'windsurf':
|
|
260
|
-
case 'antigravity':
|
|
261
|
-
case 'openclaw':
|
|
262
|
-
return {
|
|
263
|
-
description: `${MCP_CLIENT_OPTIONS.find((option) => option.id === selectedMcpClient)?.label || 'MCP'} coding agent connected through MCP.`,
|
|
264
|
-
role: 'Coding Agent',
|
|
265
|
-
provider: null,
|
|
266
|
-
model: null,
|
|
267
|
-
surfaceType: 'coding_tool'
|
|
268
|
-
};
|
|
269
|
-
default:
|
|
270
|
-
return { description: null, role: null, provider: null, model: null, surfaceType: null };
|
|
271
|
-
}
|
|
272
|
-
}, [selectedMcpClient]);
|
|
228
|
+
// Launch decision: keep generated Cloud MCP config identity-neutral for shared connections.
|
|
273
229
|
const cloudClientIntegration = useMemo(() => buildCloudMcpClientIntegration({
|
|
274
230
|
clientId: selectedMcpClient,
|
|
275
231
|
endpoint: cloudMcpEndpoint,
|
|
276
232
|
tokenValue: cloudMcpTokenValue,
|
|
277
233
|
serverId: cloudMcpServerId,
|
|
278
|
-
aiProfileName: automationAiName,
|
|
279
|
-
aiProfileToken: automationAiProfileToken,
|
|
280
|
-
aiProfileDescription: selectedAutomationProfile?.description ?? inferredAutomationProfileMetadata.description,
|
|
281
|
-
aiProfileRole: selectedAutomationProfile?.role ?? inferredAutomationProfileMetadata.role,
|
|
282
|
-
aiProfileProvider: selectedAutomationProfile?.provider ?? inferredAutomationProfileMetadata.provider,
|
|
283
|
-
aiProfileModel: selectedAutomationProfile?.model ?? inferredAutomationProfileMetadata.model,
|
|
284
|
-
aiProfileSurfaceType: selectedAutomationProfile?.surfaceType ?? inferredAutomationProfileMetadata.surfaceType,
|
|
285
234
|
}), [
|
|
286
|
-
automationAiName,
|
|
287
|
-
automationAiProfileToken,
|
|
288
235
|
cloudMcpEndpoint,
|
|
289
236
|
cloudMcpServerId,
|
|
290
237
|
cloudMcpTokenValue,
|
|
291
|
-
inferredAutomationProfileMetadata.description,
|
|
292
|
-
inferredAutomationProfileMetadata.model,
|
|
293
|
-
inferredAutomationProfileMetadata.provider,
|
|
294
|
-
inferredAutomationProfileMetadata.role,
|
|
295
|
-
inferredAutomationProfileMetadata.surfaceType,
|
|
296
|
-
selectedAutomationProfile?.description,
|
|
297
|
-
selectedAutomationProfile?.model,
|
|
298
|
-
selectedAutomationProfile?.provider,
|
|
299
|
-
selectedAutomationProfile?.role,
|
|
300
|
-
selectedAutomationProfile?.surfaceType,
|
|
301
238
|
selectedMcpClient
|
|
302
239
|
]);
|
|
303
240
|
const cloudMcpServerConfig = cloudClientIntegration.kind === 'json'
|
|
@@ -903,22 +840,14 @@ export function TaskSettings(props) {
|
|
|
903
840
|
? new Date(selectedConnectorGrant.lastUsedAt).toLocaleString()
|
|
904
841
|
: 'not yet'] })] })] })) : ('No active connector authorization for this workspace yet.') }), _jsx("div", { className: styles.settingDescription, style: { marginTop: '12px' }, children: "Use the server URL above in your client, then finish the OAuth authorization flow from the Integrations tab." }), _jsx("div", { style: { display: 'flex', justifyContent: 'flex-end', width: '100%', marginTop: '12px' }, children: _jsx("button", { className: styles.copyBtn, onClick: () => setActiveTab('integrations'), children: "Open Integrations" }) })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom} ${styles.marginTop12}`, style: { minHeight: '260px' }, children: [_jsxs("div", { className: styles.codeHeader, children: [_jsx("span", { className: styles.codeLabel, children: "Cloud MCP Configuration" }), _jsxs("div", { className: styles.codeHeaderActions, children: [_jsx("select", { className: styles.settingInput, value: selectedMcpClient, onChange: (e) => setSelectedMcpClient(e.target.value), "aria-label": "MCP client", style: { width: 'auto', minWidth: '170px', padding: '6px 12px', fontSize: '12px', borderRadius: '8px' }, children: MCP_CLIENT_OPTIONS.map((option) => (_jsx("option", { value: option.id, disabled: option.disabled, children: option.label }, option.id))) }), _jsx("button", { className: `${styles.copyBtn} ${copiedConfig === 'full' ? styles.copyBtnActive : ''}`, onClick: handleCopyCloudMcpConfig, title: "Copy full configuration file", children: copiedConfig === 'full' ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) })] })] }), _jsx("pre", { className: styles.codeBlock, children: _jsx("code", { children: cloudMcpServerConfig
|
|
905
842
|
? renderMcpConfigBlock(cloudMcpServerId, cloudMcpServerConfig, cloudClientIntegration.rootKey || 'mcpServers', cloudClientIntegration.wrapperKeys || [])
|
|
906
|
-
: renderTextConfigBlock(cloudClientIntegration.renderedText || '') }) })] }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: cloudClientIntegration.instructionLabel }) }), _jsx("div", { style: { padding: '16px 20px' }, children: _jsx("div", { className: styles.settingDescription, children: cloudClientIntegration.instructionText }) })] })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.settingLabelGroup, children: [_jsx("h3", { className: styles.settingTitle, children: "Generate Token" }), _jsx("span", { className: styles.settingDescription, children: "Tokens are shown once. Default scopes allow task read/write. Add workspace read if you want config and skills metadata in the client." })] }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 2fr) minmax(220px, 1fr)', gap: '12px', alignItems: 'end' }, children: [_jsxs("div", { className: styles.settingsItem, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Token Name" }) }), _jsx("input", { type: "text", className: styles.settingInput, placeholder: "Cursor on MacBook", value: newTokenName, onChange: (e) => setNewTokenName(e.target.value) })] }), _jsxs("div", { className: styles.settingsItem, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Expiry (Optional)" }) }), _jsx("input", { type: "date", className: `${styles.settingInput} ${styles.taskFormDateInput}`, value: newTokenExpiresAt, onChange: (e) => setNewTokenExpiresAt(e.target.value) })] })] }), _jsxs("div", { className: `${styles.settingsItem} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Scopes" }) }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '96px repeat(3, minmax(110px, 1fr))', gap: '10px 16px', alignItems: 'center' }, children: [_jsx("span", {}), _jsx("span", { className: styles.settingDescription, children: "Tasks" }), _jsx("span", { className: styles.settingDescription, children: "Workspace" }), _jsx("span", { className: styles.settingDescription, style: { opacity: 0.55 }, children: "Attachments" }), _jsx("span", { className: styles.settingDescription, children: "Read" }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('tasks:read'), onChange: () => toggleTokenScope('tasks:read') }) }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('workspace:read'), onChange: () => toggleTokenScope('workspace:read') }) }), _jsx("label", { className: styles.checkboxLabel, style: { opacity: 0.55 }, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) }), _jsx("span", { className: styles.settingDescription, children: "Write" }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('tasks:write'), onChange: () => toggleTokenScope('tasks:write') }) }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) }), _jsx("label", { className: styles.checkboxLabel, style: { opacity: 0.55 }, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) })] })] })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
if (!profile)
|
|
915
|
-
return;
|
|
916
|
-
setAutomationAiName(profile.name);
|
|
917
|
-
setAutomationAiProfileToken(String(profile.profileToken || ''));
|
|
918
|
-
}, children: [_jsx("option", { value: "", children: "Custom\u2026" }), aiProfiles.map((profile) => (_jsxs("option", { value: profile.id, children: [profile.name, " (@", profile.username, ")"] }, profile.id)))] })] })), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: '12px', alignItems: 'end' }, children: [_jsxs("div", { className: styles.settingsItem, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "AI Name Header" }) }), _jsx("input", { type: "text", className: styles.settingInput, placeholder: "Automation Codex", value: automationAiName, onChange: (e) => setAutomationAiName(e.target.value) })] }), _jsxs("div", { className: styles.settingsItem, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "AI Profile Token Header" }) }), _jsx("input", { type: "text", className: styles.settingInput, placeholder: "tfp_...", value: automationAiProfileToken, onChange: (e) => setAutomationAiProfileToken(e.target.value) })] })] })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsx("div", { className: styles.settingHelper, children: "Generated tokens are only shown once. Copy the token before leaving this page." }), (() => {
|
|
919
|
-
const displayedGeneratedToken = copiedConfig === 'token'
|
|
920
|
-
? 'Copied to clipboard'
|
|
921
|
-
: (generatedTokenValue || '');
|
|
843
|
+
: renderTextConfigBlock(cloudClientIntegration.renderedText || '') }) })] }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: cloudClientIntegration.instructionLabel }) }), _jsx("div", { style: { padding: '16px 20px' }, children: _jsx("div", { className: styles.settingDescription, children: cloudClientIntegration.instructionText }) })] })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.settingLabelGroup, children: [_jsx("h3", { className: styles.settingTitle, children: "Generate Token" }), _jsx("span", { className: styles.settingDescription, children: "Tokens are shown once. Default scopes allow task read/write. Add workspace read if you want config and skills metadata in the client." })] }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 2fr) minmax(220px, 1fr)', gap: '12px', alignItems: 'end' }, children: [_jsxs("div", { className: styles.settingsItem, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Token Name" }) }), _jsx("input", { type: "text", className: styles.settingInput, placeholder: "Cursor on MacBook", value: newTokenName, onChange: (e) => setNewTokenName(e.target.value) })] }), _jsxs("div", { className: styles.settingsItem, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Expiry (Optional)" }) }), _jsx("input", { type: "date", className: `${styles.settingInput} ${styles.taskFormDateInput}`, value: newTokenExpiresAt, onChange: (e) => setNewTokenExpiresAt(e.target.value) })] })] }), _jsxs("div", { className: `${styles.settingsItem} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Scopes" }) }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '96px repeat(3, minmax(110px, 1fr))', gap: '10px 16px', alignItems: 'center' }, children: [_jsx("span", {}), _jsx("span", { className: styles.settingDescription, children: "Tasks" }), _jsx("span", { className: styles.settingDescription, children: "Workspace" }), _jsx("span", { className: styles.settingDescription, style: { opacity: 0.55 }, children: "Attachments" }), _jsx("span", { className: styles.settingDescription, children: "Read" }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('tasks:read'), onChange: () => toggleTokenScope('tasks:read') }) }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('workspace:read'), onChange: () => toggleTokenScope('workspace:read') }) }), _jsx("label", { className: styles.checkboxLabel, style: { opacity: 0.55 }, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) }), _jsx("span", { className: styles.settingDescription, children: "Write" }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('tasks:write'), onChange: () => toggleTokenScope('tasks:write') }) }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) }), _jsx("label", { className: styles.checkboxLabel, style: { opacity: 0.55 }, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) })] })] })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsx("div", { className: styles.settingHelper, children: "Generated tokens are only shown once. Copy the token before leaving this page." }), (() => {
|
|
844
|
+
const tokenBarError = mcpTokenNotice?.type === 'error'
|
|
845
|
+
? mcpTokenNotice.message
|
|
846
|
+
: null;
|
|
847
|
+
const displayedGeneratedToken = tokenBarError
|
|
848
|
+
|| (copiedConfig === 'token'
|
|
849
|
+
? 'Copied to clipboard'
|
|
850
|
+
: (generatedTokenValue || ''));
|
|
922
851
|
return (_jsx("div", { style: { display: 'flex', gap: '8px', alignItems: 'stretch' }, children: _jsx("div", { className: styles.codeBlockWrapper, style: { flex: 1, cursor: generatedTokenValue ? 'pointer' : 'default' }, onClick: () => generatedTokenValue && copyToClipboard(generatedTokenValue, 'token'), role: generatedTokenValue ? 'button' : undefined, tabIndex: generatedTokenValue ? 0 : -1, onKeyDown: (e) => {
|
|
923
852
|
if (!generatedTokenValue)
|
|
924
853
|
return;
|
|
@@ -926,8 +855,11 @@ export function TaskSettings(props) {
|
|
|
926
855
|
e.preventDefault();
|
|
927
856
|
copyToClipboard(generatedTokenValue, 'token');
|
|
928
857
|
}
|
|
929
|
-
}, title: generatedTokenValue ? (copiedConfig === 'token' ? 'Token copied' : 'Click to copy token') : undefined, children: _jsx("pre", { className: styles.codeBlock, style: {
|
|
930
|
-
|
|
858
|
+
}, title: generatedTokenValue ? (copiedConfig === 'token' ? 'Token copied' : 'Click to copy token') : undefined, children: _jsx("pre", { className: styles.codeBlock, style: {
|
|
859
|
+
textAlign: 'center',
|
|
860
|
+
color: tokenBarError ? 'var(--status-danger-strong, #f87171)' : undefined,
|
|
861
|
+
}, children: _jsx("code", { children: displayedGeneratedToken }) }) }) }));
|
|
862
|
+
})()] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom} ${styles.marginTop12}`, style: { minHeight: '260px' }, children: [_jsxs("div", { className: styles.codeHeader, children: [_jsx("span", { className: styles.codeLabel, children: "Cloud MCP Configuration" }), _jsxs("div", { className: styles.codeHeaderActions, children: [_jsx("select", { className: styles.settingInput, value: selectedMcpClient, onChange: (e) => setSelectedMcpClient(e.target.value), "aria-label": "MCP client", style: { width: 'auto', minWidth: '170px', padding: '6px 12px', fontSize: '12px', borderRadius: '8px' }, children: MCP_CLIENT_OPTIONS.map((option) => (_jsx("option", { value: option.id, disabled: option.disabled, children: option.label }, option.id))) }), _jsxs("button", { className: styles.copyBtn, onClick: () => void handleCreateMcpToken(), disabled: mcpActionBusyId === 'create', title: "Generate Token", "aria-label": "Generate Token", children: [mcpActionBusyId === 'create' ? _jsx(Loader2, { size: 14, className: styles.spinIcon }) : _jsx(Zap, { size: 14 }), _jsx("span", { children: "GENERATE TOKEN" })] }), _jsx("button", { className: `${styles.copyBtn} ${copiedConfig === 'full' ? styles.copyBtnActive : ''}`, onClick: handleCopyCloudMcpConfig, title: "Copy full configuration file", children: copiedConfig === 'full' ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) })] })] }), _jsx("pre", { className: styles.codeBlock, children: _jsx("code", { children: cloudMcpServerConfig
|
|
931
863
|
? renderMcpConfigBlock(cloudMcpServerId, cloudMcpServerConfig, cloudClientIntegration.rootKey || 'mcpServers', cloudClientIntegration.wrapperKeys || [])
|
|
932
864
|
: renderTextConfigBlock(cloudClientIntegration.renderedText || '') }) })] }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: cloudClientIntegration.instructionLabel }) }), _jsx("div", { style: { padding: '16px 20px' }, children: _jsx("div", { className: styles.settingDescription, children: cloudClientIntegration.instructionText }) })] })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => setShowMcpConnectionTest((prev) => !prev), onKeyDown: (e) => {
|
|
933
865
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
@@ -5,6 +5,8 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
5
5
|
import { TaskSettings } from './TaskSettings';
|
|
6
6
|
import { TASKFORCE_THEME_OPTIONS } from '../../utils/theme';
|
|
7
7
|
describe('TaskSettings Component', () => {
|
|
8
|
+
// Launch decision: Cloud MCP settings must stay identity-neutral for shared connections.
|
|
9
|
+
// Do not reintroduce per-connection AI identity controls here unless explicitly requested.
|
|
8
10
|
// Default mock props
|
|
9
11
|
const defaultSettingsModel = {
|
|
10
12
|
currentTheme: 'dark',
|
|
@@ -1128,76 +1130,6 @@ describe('TaskSettings Component', () => {
|
|
|
1128
1130
|
expect(screen.getByText(/http_headers = \{ Authorization = "Bearer/i)).toBeInTheDocument();
|
|
1129
1131
|
expect(screen.getAllByText(/https:\/\/mcp\.taskforcehq\.ai\/mcp/).length).toBeGreaterThan(0);
|
|
1130
1132
|
}, 10000);
|
|
1131
|
-
it('4.23e3a Codex MCP output includes optional automation identity headers', async () => {
|
|
1132
|
-
const fetchCloudAuthApi = vi.fn(async () => ({
|
|
1133
|
-
ok: true,
|
|
1134
|
-
json: async () => ({ items: [] })
|
|
1135
|
-
}));
|
|
1136
|
-
const user = userEvent.setup();
|
|
1137
|
-
renderComponent({
|
|
1138
|
-
settingsModel: {
|
|
1139
|
-
runtimeMode: 'local',
|
|
1140
|
-
isAuthenticated: true,
|
|
1141
|
-
currentWorkspaceName: 'Workspace Alpha',
|
|
1142
|
-
cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
|
|
1143
|
-
fetchCloudAuthApi
|
|
1144
|
-
}
|
|
1145
|
-
});
|
|
1146
|
-
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
1147
|
-
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
|
|
1148
|
-
await user.type(screen.getByPlaceholderText('Automation Codex'), 'Automation Codex');
|
|
1149
|
-
await user.type(screen.getByPlaceholderText('tfp_...'), 'tfp_automation_codex');
|
|
1150
|
-
expect(await screen.findByText(/X-Taskforce-AI-Name = "Automation Codex"/i)).toBeInTheDocument();
|
|
1151
|
-
expect(screen.getByText(/X-Taskforce-AI-Profile-Token = "tfp_automation_codex"/i)).toBeInTheDocument();
|
|
1152
|
-
}, 10000);
|
|
1153
|
-
it('4.23e3aa MCP tab can prefill automation identity from an existing AI profile', async () => {
|
|
1154
|
-
const fetchCloudAuthApi = vi.fn(async () => ({
|
|
1155
|
-
ok: true,
|
|
1156
|
-
json: async () => ({ items: [] })
|
|
1157
|
-
}));
|
|
1158
|
-
const fetchMock = vi.fn(async (input) => {
|
|
1159
|
-
const url = String(input);
|
|
1160
|
-
if (url.includes('/api/taskforce/workspace/assignee-options')) {
|
|
1161
|
-
return {
|
|
1162
|
-
ok: true,
|
|
1163
|
-
json: async () => ({
|
|
1164
|
-
assignees: [
|
|
1165
|
-
{
|
|
1166
|
-
value: 'ai-profile-123',
|
|
1167
|
-
label: 'Automation Codex',
|
|
1168
|
-
profileToken: 'tfp_automation_codex',
|
|
1169
|
-
username: 'automation-codex',
|
|
1170
|
-
icon: 'Bot',
|
|
1171
|
-
color: '#6B7280',
|
|
1172
|
-
kind: 'agent',
|
|
1173
|
-
}
|
|
1174
|
-
]
|
|
1175
|
-
})
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
throw new Error(`Unexpected fetch call: ${url}`);
|
|
1179
|
-
});
|
|
1180
|
-
global.fetch = fetchMock;
|
|
1181
|
-
const user = userEvent.setup();
|
|
1182
|
-
renderComponent({
|
|
1183
|
-
settingsModel: {
|
|
1184
|
-
runtimeMode: 'local',
|
|
1185
|
-
isAuthenticated: true,
|
|
1186
|
-
workspaceId: 'workspace-alpha',
|
|
1187
|
-
currentWorkspaceName: 'Workspace Alpha',
|
|
1188
|
-
cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
|
|
1189
|
-
fetchCloudAuthApi
|
|
1190
|
-
}
|
|
1191
|
-
});
|
|
1192
|
-
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
1193
|
-
await user.selectOptions(await screen.findByRole('combobox', { name: 'Use existing AI profile' }), 'ai-profile-123');
|
|
1194
|
-
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
|
|
1195
|
-
expect(await screen.findByDisplayValue('Automation Codex')).toBeInTheDocument();
|
|
1196
|
-
expect(screen.getByDisplayValue('tfp_automation_codex')).toBeInTheDocument();
|
|
1197
|
-
expect(screen.getByText(/X-Taskforce-AI-Name = "Automation Codex"/i)).toBeInTheDocument();
|
|
1198
|
-
expect(screen.getByText(/X-Taskforce-AI-Profile-Token = "tfp_automation_codex"/i)).toBeInTheDocument();
|
|
1199
|
-
global.fetch = originalFetch;
|
|
1200
|
-
}, 10000);
|
|
1201
1133
|
it('4.23e3b Claude Code shows a remote MCP add command', async () => {
|
|
1202
1134
|
const fetchCloudAuthApi = vi.fn(async () => ({
|
|
1203
1135
|
ok: true,
|
|
@@ -1396,6 +1328,37 @@ describe('TaskSettings Component', () => {
|
|
|
1396
1328
|
await user.click(screen.getByTitle('Copy full configuration file'));
|
|
1397
1329
|
expect(screen.getByText('Generate a token before copying the Cloud MCP configuration.')).toBeInTheDocument();
|
|
1398
1330
|
}, 10000);
|
|
1331
|
+
it('4.23g2 Cloud MCP settings do not surface automation identity controls', async () => {
|
|
1332
|
+
const user = userEvent.setup();
|
|
1333
|
+
renderComponent({
|
|
1334
|
+
settingsModel: {
|
|
1335
|
+
runtimeMode: 'local',
|
|
1336
|
+
isAuthenticated: true,
|
|
1337
|
+
currentWorkspaceName: 'Workspace Alpha',
|
|
1338
|
+
cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
1342
|
+
await screen.findByText('Cloud MCP Configuration');
|
|
1343
|
+
expect(screen.queryByText('Automation Identity (Optional)')).not.toBeInTheDocument();
|
|
1344
|
+
expect(screen.queryByRole('combobox', { name: 'Use existing AI profile' })).not.toBeInTheDocument();
|
|
1345
|
+
}, 10000);
|
|
1346
|
+
it('4.23g3 Cloud MCP config does not include AI identity headers by default', async () => {
|
|
1347
|
+
const user = userEvent.setup();
|
|
1348
|
+
renderComponent({
|
|
1349
|
+
settingsModel: {
|
|
1350
|
+
runtimeMode: 'local',
|
|
1351
|
+
isAuthenticated: true,
|
|
1352
|
+
currentWorkspaceName: 'Workspace Alpha',
|
|
1353
|
+
cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
await user.click(screen.getByRole('button', { name: /MCP/i }));
|
|
1357
|
+
await screen.findByText('Cloud MCP Configuration');
|
|
1358
|
+
await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
|
|
1359
|
+
expect(screen.queryByText(/X-Taskforce-AI-Name/i)).not.toBeInTheDocument();
|
|
1360
|
+
expect(screen.queryByText(/X-Taskforce-AI-Profile-Token/i)).not.toBeInTheDocument();
|
|
1361
|
+
}, 10000);
|
|
1399
1362
|
it('4.23h Active tokens sort first and revoked tokens do not show revoke or regenerate actions', async () => {
|
|
1400
1363
|
const user = userEvent.setup();
|
|
1401
1364
|
const fetchCloudAuthApi = vi.fn(async () => ({
|
|
@@ -65,6 +65,12 @@ function cloneCustomTaxonomyDefinition(taxonomy) {
|
|
|
65
65
|
: []
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
+
function handleCardKeyDown(event, onSelect) {
|
|
69
|
+
if (event.key !== 'Enter' && event.key !== ' ')
|
|
70
|
+
return;
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
onSelect();
|
|
73
|
+
}
|
|
68
74
|
function SystemTaxonomyLibrarySurface(props) {
|
|
69
75
|
const { onAnalyzeSystemTaxonomyPack, onApplySystemTaxonomyPack } = props;
|
|
70
76
|
const fallbackPacks = useMemo(() => listBuiltInTaxonomyLibraryPacks(), []);
|
|
@@ -158,7 +164,7 @@ function SystemTaxonomyLibrarySurface(props) {
|
|
|
158
164
|
}
|
|
159
165
|
return (_jsxs("div", { className: libraryStyles.libraryContainer, children: [_jsxs("section", { className: libraryStyles.introCard, children: [_jsxs("span", { className: libraryStyles.eyebrow, children: [_jsx(BookMarked, { size: 14 }), "System Taxonomy Library"] }), _jsx("h3", { className: libraryStyles.introTitle, children: "Browse starter packs before you replace anything." }), _jsx("p", { className: libraryStyles.introCopy, children: "Library packs give you coordinated categories, task types, and priority scales. Apply replaces only the sections you choose." }), libraryLoading ? _jsx("p", { className: libraryStyles.prototypeNotice, children: "Loading available packs\u2026" }) : null] }), _jsxs("div", { className: libraryStyles.libraryGrid, children: [_jsx("div", { className: libraryStyles.packList, children: packs.map((pack) => {
|
|
160
166
|
const isActive = pack.id === selectedPackId;
|
|
161
|
-
return (_jsxs("article", { className: `${libraryStyles.packCard} ${isActive ? libraryStyles.packCardActive : ''}`, children: [_jsx("div", { className: libraryStyles.packHeader, children: _jsxs("div", { className: libraryStyles.packHeaderText, children: [_jsx("div", { className: libraryStyles.packTitleRow, children: _jsx("h4", { className: libraryStyles.packTitle, children: pack.label }) }), _jsx("p", { className: libraryStyles.packDescription, children: pack.description })] }) }), _jsxs("div", { className: libraryStyles.packCounts, children: [_jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(Layers, { size: 13 }), pack.categories.length, " categories"] }), _jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(PackageOpen, { size: 13 }), pack.types.length, " types"] }), _jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(Check, { size: 13 }), pack.priorities.length, " priority levels"] })] }),
|
|
167
|
+
return (_jsxs("article", { className: `${libraryStyles.packCard} ${libraryStyles.packCardInteractive} ${isActive ? libraryStyles.packCardActive : ''}`, role: "button", tabIndex: 0, "aria-pressed": isActive, onClick: () => handleSelectPack(pack.id), onKeyDown: (event) => handleCardKeyDown(event, () => handleSelectPack(pack.id)), children: [_jsx("div", { className: libraryStyles.packHeader, children: _jsxs("div", { className: libraryStyles.packHeaderText, children: [_jsx("div", { className: libraryStyles.packTitleRow, children: _jsx("h4", { className: libraryStyles.packTitle, children: pack.label }) }), _jsx("p", { className: libraryStyles.packDescription, children: pack.description })] }) }), _jsxs("div", { className: libraryStyles.packCounts, children: [_jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(Layers, { size: 13 }), pack.categories.length, " categories"] }), _jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(PackageOpen, { size: 13 }), pack.types.length, " types"] }), _jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(Check, { size: 13 }), pack.priorities.length, " priority levels"] })] }), _jsx("div", { className: libraryStyles.packActions, children: _jsx("button", { type: "button", className: styles.submitBtn, onClick: () => handleSelectPack(pack.id), children: "Apply" }) })] }, pack.id));
|
|
162
168
|
}) }), _jsxs("aside", { className: libraryStyles.previewPanel, children: [_jsxs("div", { className: libraryStyles.previewHeader, children: [_jsx("h3", { className: libraryStyles.previewTitle, children: selectedPack.label }), _jsx("p", { className: libraryStyles.previewSubtext, children: "Read-only preview. Applying this pack will replace only the sections you choose." })] }), _jsx(PreviewListSection, { title: "Categories", items: selectedPack.categories.map((category) => ({
|
|
163
169
|
key: category.value,
|
|
164
170
|
label: category.label,
|
|
@@ -283,7 +289,7 @@ function CustomTaxonomyLibrarySurface(props) {
|
|
|
283
289
|
const isActive = pack.id === selectedPackId;
|
|
284
290
|
const summary = summarizeCustomPack(pack);
|
|
285
291
|
const taxonomyNames = pack.taxonomies.slice(0, 3).map((taxonomy) => taxonomy.label);
|
|
286
|
-
return (_jsxs("article", { className: `${libraryStyles.packCard} ${isActive ? libraryStyles.packCardActive : ''}`, children: [_jsx("div", { className: libraryStyles.packHeader, children: _jsxs("div", { className: libraryStyles.packHeaderText, children: [_jsxs("div", { className: libraryStyles.packTitleRow, children: [_jsx("h4", { className: libraryStyles.packTitle, children: pack.label }), pack.domainTag ? _jsx("span", { className: libraryStyles.packBadge, children: pack.domainTag }) : null] }), _jsx("p", { className: libraryStyles.packDescription, children: pack.description }), pack.bestFor ? (_jsxs("p", { className: libraryStyles.packBestFor, children: [_jsx("strong", { children: "Best for:" }), " ", pack.bestFor] })) : null] }) }), _jsxs("div", { className: libraryStyles.packCounts, children: [_jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(Shapes, { size: 13 }), pack.taxonomies.length, " taxonomies"] }), summary.multiSelectCount > 0 ? (_jsx("span", { className: libraryStyles.packCountChip, children: "Multi-select" })) : null, summary.numericCount > 0 ? (_jsx("span", { className: libraryStyles.packCountChip, children: "Numeric" })) : null, summary.requiredCount > 0 ? (_jsx("span", { className: libraryStyles.packCountChip, children: "Required" })) : null] }), _jsxs("div", { className: libraryStyles.packTaxonomyPreview, children: [taxonomyNames.map((name) => (_jsx("span", { className: libraryStyles.packTaxonomyChip, children: name }, name))), pack.taxonomies.length > taxonomyNames.length ? (_jsxs("span", { className: libraryStyles.packTaxonomyMore, children: ["+", pack.taxonomies.length - taxonomyNames.length, " more"] })) : null] }), _jsx("div", { className: libraryStyles.packActions, children: _jsx("
|
|
292
|
+
return (_jsxs("article", { className: `${libraryStyles.packCard} ${libraryStyles.packCardInteractive} ${isActive ? libraryStyles.packCardActive : ''}`, role: "button", tabIndex: 0, "aria-pressed": isActive, onClick: () => setSelectedPackId(pack.id), onKeyDown: (event) => handleCardKeyDown(event, () => setSelectedPackId(pack.id)), children: [_jsx("div", { className: libraryStyles.packHeader, children: _jsxs("div", { className: libraryStyles.packHeaderText, children: [_jsxs("div", { className: libraryStyles.packTitleRow, children: [_jsx("h4", { className: libraryStyles.packTitle, children: pack.label }), pack.domainTag ? _jsx("span", { className: libraryStyles.packBadge, children: pack.domainTag }) : null] }), _jsx("p", { className: libraryStyles.packDescription, children: pack.description }), pack.bestFor ? (_jsxs("p", { className: libraryStyles.packBestFor, children: [_jsx("strong", { children: "Best for:" }), " ", pack.bestFor] })) : null] }) }), _jsxs("div", { className: libraryStyles.packCounts, children: [_jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(Shapes, { size: 13 }), pack.taxonomies.length, " taxonomies"] }), summary.multiSelectCount > 0 ? (_jsx("span", { className: libraryStyles.packCountChip, children: "Multi-select" })) : null, summary.numericCount > 0 ? (_jsx("span", { className: libraryStyles.packCountChip, children: "Numeric" })) : null, summary.requiredCount > 0 ? (_jsx("span", { className: libraryStyles.packCountChip, children: "Required" })) : null] }), _jsxs("div", { className: libraryStyles.packTaxonomyPreview, children: [taxonomyNames.map((name) => (_jsx("span", { className: libraryStyles.packTaxonomyChip, children: name }, name))), pack.taxonomies.length > taxonomyNames.length ? (_jsxs("span", { className: libraryStyles.packTaxonomyMore, children: ["+", pack.taxonomies.length - taxonomyNames.length, " more"] })) : null] }), _jsx("div", { className: libraryStyles.packActions, children: _jsx("span", { className: libraryStyles.packActionHint, children: isActive ? 'Previewing' : 'Click to Preview' }) })] }, pack.id));
|
|
287
293
|
}) }), _jsxs("aside", { className: libraryStyles.previewPanel, children: [_jsxs("div", { className: libraryStyles.previewHeader, children: [_jsx("h3", { className: libraryStyles.previewTitle, children: selectedPack.label }), selectedPack.bestFor ? (_jsxs("p", { className: libraryStyles.previewBestFor, children: [_jsx("strong", { children: "Best for:" }), " ", selectedPack.bestFor] })) : null, _jsx("p", { className: libraryStyles.previewSubtext, children: "Review each taxonomy before installing it into this workspace. Installed taxonomies become normal workspace-owned custom taxonomies." })] }), _jsxs("div", { className: libraryStyles.previewSummaryRow, children: [_jsxs("span", { className: libraryStyles.packCountChip, children: [selectedPack.taxonomies.length, " taxonomies"] }), (selectedPackSummary?.formEnabledCount || 0) > 0 ? (_jsxs("span", { className: libraryStyles.packCountChip, children: [selectedPackSummary.formEnabledCount, " show in forms"] })) : null, (selectedPackSummary?.numericCount || 0) > 0 ? (_jsxs("span", { className: libraryStyles.packCountChip, children: [selectedPackSummary.numericCount, " numeric"] })) : null, (selectedPackSummary?.requiredCount || 0) > 0 ? (_jsxs("span", { className: libraryStyles.packCountChip, children: [selectedPackSummary.requiredCount, " required"] })) : null] }), _jsxs("section", { className: libraryStyles.applyPanel, children: [_jsx("h4", { className: libraryStyles.applyTitle, children: "Choose Taxonomies to Install" }), _jsx("p", { className: libraryStyles.applyCopy, children: "Select only the taxonomies you want to add. This first pass installs new custom taxonomies only and asks you to skip conflicts rather than replace them." }), _jsx("div", { className: libraryStyles.checkboxList, children: selectedPack.taxonomies.map((taxonomy) => (_jsxs("label", { className: libraryStyles.checkboxLabel, children: [_jsx("input", { type: "checkbox", checked: selectedTaxonomyIds.includes(taxonomy.id), onChange: () => handleToggleTaxonomySelection(taxonomy.id) }), _jsxs("span", { children: [taxonomy.label, taxonomies.some((existingTaxonomy) => existingTaxonomy.id === taxonomy.id) ? ' (Already installed)' : ''] })] }, taxonomy.id))) }), _jsxs("div", { className: libraryStyles.analysisPanel, children: [_jsxs("p", { className: libraryStyles.analysisCopy, children: [selectedTaxonomyIds.length, " ", selectedTaxonomyIds.length === 1 ? 'taxonomy is' : 'taxonomies are', " currently selected from this pack."] }), installableTaxonomies.length > 0 ? (_jsxs("p", { className: libraryStyles.analysisCopy, children: [installableTaxonomies.length, " selected ", installableTaxonomies.length === 1 ? 'taxonomy is' : 'taxonomies are', " ready to install right now."] })) : (_jsx("p", { className: libraryStyles.analysisCopy, children: "No selected taxonomies are currently installable." })), conflictIds.length > 0 ? (_jsxs("p", { className: libraryStyles.analysisWarning, children: [conflictIds.length, " selected ", conflictIds.length === 1 ? 'taxonomy conflicts' : 'taxonomies conflict', " with existing workspace ids: ", conflictIds.join(', '), "."] })) : null] }), _jsxs("div", { className: libraryStyles.packActions, children: [conflictIds.length > 0 ? (_jsx("button", { type: "button", className: styles.cancelBtn, onClick: handleSelectInstallableOnly, children: "Select Installable Only" })) : null, _jsx("button", { type: "button", className: styles.submitBtn, onClick: handleApply, disabled: selectedTaxonomyIds.length === 0 || installableTaxonomies.length === 0, children: "Install Selected Taxonomies" })] }), libraryNotice ? (_jsx("p", { className: `${libraryStyles.prototypeResult} ${libraryNotice.type === 'error' ? libraryStyles.prototypeError : ''}`, children: libraryNotice.message })) : null] }), selectedPack.taxonomies.map((taxonomy) => (_jsxs("section", { className: libraryStyles.customTaxonomyCard, children: [_jsx("div", { className: libraryStyles.customTaxonomyHeader, children: _jsxs("div", { children: [_jsx("h4", { className: libraryStyles.previewSectionTitle, children: taxonomy.label }), taxonomy.description ? _jsx("p", { className: libraryStyles.customTaxonomyDescription, children: taxonomy.description }) : null] }) }), conflictIds.includes(taxonomy.id) ? (_jsx("p", { className: libraryStyles.analysisWarning, children: "Conflicts with an existing taxonomy id in this workspace." })) : null, _jsx("div", { className: libraryStyles.customTaxonomyBadges, children: taxonomyTraitBadges(taxonomy).map((badge) => (_jsx("span", { className: libraryStyles.customTaxonomyBadge, children: badge }, badge))) }), _jsx(PreviewListSection, { title: taxonomy.widgetType === 'level' ? 'Levels' : 'Options', items: taxonomy.options.map((option) => ({
|
|
288
294
|
key: `${taxonomy.id}:${option.value}`,
|
|
289
295
|
label: option.label,
|
|
@@ -70,7 +70,7 @@ describe('TaxonomyLibraryManager', () => {
|
|
|
70
70
|
});
|
|
71
71
|
expect(screen.getAllByText('Client Delivery')).toHaveLength(2);
|
|
72
72
|
expect(screen.getAllByText('2 taxonomies').length).toBeGreaterThan(0);
|
|
73
|
-
await user.click(screen.getByRole('button', { name: /
|
|
73
|
+
await user.click(screen.getByRole('button', { name: /client delivery/i }));
|
|
74
74
|
expect(screen.getAllByText('Approval Stage').length).toBeGreaterThan(0);
|
|
75
75
|
expect(screen.getByText('Track approvals')).toBeInTheDocument();
|
|
76
76
|
expect(screen.getAllByText('Required').length).toBeGreaterThan(0);
|
|
@@ -4,7 +4,7 @@ import { Check, Loader2, RotateCcw, Plus, Trash2, X } from 'lucide-react';
|
|
|
4
4
|
import styles from '../../Taskforce.module.css';
|
|
5
5
|
import { t } from '../../localization';
|
|
6
6
|
import { TaskStatusActions } from './TaskStatusActions';
|
|
7
|
-
import {
|
|
7
|
+
import { getTaskReferenceDisplayState } from '../../utils/taskReferences';
|
|
8
8
|
import { TaskReferenceBadge } from './TaskReferenceBadge';
|
|
9
9
|
import { formatInitiativeReference, getWorkstreamReferenceLabel } from '../../utils/planningReferences';
|
|
10
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 }) {
|
|
@@ -12,7 +12,10 @@ export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle
|
|
|
12
12
|
const currentTask = currentTaskProp ?? (editingTaskId ? tasks.find(t => t.id === editingTaskId) || null : null);
|
|
13
13
|
const isArchivedReadOnly = Boolean(currentTask?.isArchived);
|
|
14
14
|
const isDeletedReadOnly = Boolean(currentTask?.isDeleted);
|
|
15
|
-
const
|
|
15
|
+
const currentTaskReference = getTaskReferenceDisplayState(currentTask);
|
|
16
|
+
const currentTaskReferenceLabel = currentTaskReference.label || (currentTaskReference.isProvisional ? 'Pending' : '');
|
|
17
|
+
const currentTaskReferenceIsProvisional = currentTaskReference.isProvisional;
|
|
18
|
+
const canCopyCurrentTaskReference = Boolean(currentTaskReference.label);
|
|
16
19
|
const currentWorkstreamReferenceLabel = currentTaskWorkstream
|
|
17
20
|
? (getWorkstreamReferenceLabel(currentTaskWorkstream) || currentTaskWorkstream.id)
|
|
18
21
|
: '';
|
|
@@ -32,7 +35,7 @@ export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle
|
|
|
32
35
|
return (_jsx("div", { className: styles.stickyActionHeader, children: editingTaskId ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.taskHierarchyHeader, children: [_jsxs("div", { className: styles.taskHierarchyBadgeRow, children: [currentTaskInitiative ? (_jsxs(_Fragment, { children: [_jsx(TaskReferenceBadge, { copied: copiedId === currentInitiativeReferenceLabel, onClick: (e) => handleCopyId(e, currentInitiativeReferenceLabel), disabled: loading, title: "Copy initiative reference", label: currentInitiativeReferenceLabel }), _jsx("span", { className: styles.taskHierarchyDivider, children: "/" })] })) : null, currentTaskWorkstream ? (_jsxs(_Fragment, { children: [_jsx(TaskReferenceBadge, { copied: copiedId === currentWorkstreamReferenceLabel, onClick: (e) => handleCopyId(e, currentWorkstreamReferenceLabel), disabled: loading, title: "Copy workstream reference", label: currentWorkstreamReferenceLabel }), _jsx("span", { className: styles.taskHierarchyDivider, children: "/" })] })) : (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: `${styles.taskIdBadge} ${styles.taskHierarchyAddBtn}`, onClick: () => {
|
|
33
36
|
onWorkstreamInputChange?.('');
|
|
34
37
|
setShowWorkstreamEditor((value) => !value);
|
|
35
|
-
}, disabled: loading || isArchivedReadOnly || isDeletedReadOnly, title: "Attach to workstream", "aria-label": "Attach to workstream", children: _jsx(Plus, { size: 13 }) }), _jsx("span", { className: styles.taskHierarchyDivider, children: "/" })] })), _jsx(TaskReferenceBadge, { copied: copiedId ===
|
|
38
|
+
}, disabled: loading || isArchivedReadOnly || isDeletedReadOnly, title: "Attach to workstream", "aria-label": "Attach to workstream", children: _jsx(Plus, { size: 13 }) }), _jsx("span", { className: styles.taskHierarchyDivider, children: "/" })] })), currentTaskReferenceLabel ? (_jsx(TaskReferenceBadge, { copied: canCopyCurrentTaskReference && copiedId === currentTaskReference.label, onClick: (e) => handleCopyId(e, currentTaskReference.label), disabled: loading || !canCopyCurrentTaskReference, title: canCopyCurrentTaskReference ? t('actionHeader.copyTaskIdTitle') : 'Task reference pending sync', label: currentTaskReferenceLabel })) : null, currentTaskReferenceIsProvisional ? (_jsx("span", { className: styles.taskHierarchyMeta, title: "Temporary local reference until cloud sync assigns the final task number.", children: "Pending sync" })) : null] }), showWorkstreamEditor && !currentTaskWorkstream ? (_jsxs("div", { className: styles.taskHierarchyEditor, children: [_jsx("input", { type: "text", className: `${styles.input} ${styles.taskHierarchyInput}`, value: workstreamInput, onChange: (event) => onWorkstreamInputChange?.(event.target.value), placeholder: "WS-123", "aria-label": "Attach workstream reference", onKeyDown: (event) => {
|
|
36
39
|
if (event.key === 'Enter') {
|
|
37
40
|
event.preventDefault();
|
|
38
41
|
handleWorkstreamConfirm();
|
|
@@ -13,14 +13,17 @@ import { dispatchOpenMarkdownDocument } from '../../utils/contextFiles';
|
|
|
13
13
|
import { dispatchOpenAnnotatedAttachment } from '../../utils/annotatedAttachments';
|
|
14
14
|
import { getDocumentDownloadName } from '../../utils/documentNames';
|
|
15
15
|
import { TaskStatusActions } from './TaskStatusActions';
|
|
16
|
-
import {
|
|
16
|
+
import { getTaskReferenceDisplayState } from '../../utils/taskReferences';
|
|
17
17
|
import { getAssigneeKind, getAssigneeLabel } from '../../utils/assignees';
|
|
18
18
|
import { getLatestTaskActivity, summarizeTaskActivity } from '../../utils/taskActivity';
|
|
19
19
|
import { DEFAULT_TYPE_VALUE } from '../../shared/taxonomyDefaults.js';
|
|
20
20
|
import { getVisibleTaxonomiesForTasks } from '../../utils/taxonomyLifecycle.js';
|
|
21
21
|
import { getInitiativeReferenceLabel, getWorkstreamReferenceLabel } from '../../utils/planningReferences';
|
|
22
22
|
const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [], types = [], priorities = [], categories = [], assigneeOptions = [], taskWorkstream = null, taskInitiative = null, isOverlay = false, isArchived = false, readOnlyMode = null, isRecentlyChanged = false, onClick, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onDelete, deleteActionTitle, onOpenTaskById, compressed = false, showStatusLabel = true }) => {
|
|
23
|
-
const
|
|
23
|
+
const taskReference = getTaskReferenceDisplayState(task);
|
|
24
|
+
const taskReferenceLabel = taskReference.label || (taskReference.isProvisional ? 'Pending' : '');
|
|
25
|
+
const isProvisionalReference = taskReference.isProvisional;
|
|
26
|
+
const canCopyTaskReference = Boolean(taskReference.label);
|
|
24
27
|
const workstreamReferenceLabel = getWorkstreamReferenceLabel(taskWorkstream) || taskWorkstream?.id || '';
|
|
25
28
|
const initiativeReferenceLabel = getInitiativeReferenceLabel(taskInitiative) || taskInitiative?.id || '';
|
|
26
29
|
const priorityConfig = priorities.find(p => String(p.value) === String(task.priority));
|
|
@@ -160,7 +163,7 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
|
|
|
160
163
|
event.stopPropagation();
|
|
161
164
|
onCopyId?.(event, label);
|
|
162
165
|
};
|
|
163
|
-
return (_jsxs("div", { className: cardClasses, onClick: () => onClick?.(task), style: cardStyle, "data-task-card": "true", children: [_jsxs("div", { className: cardStyles.taskHeader, children: [_jsxs("div", { className: cardStyles.taskReferenceCluster, children: [taskInitiative ? (_jsxs(_Fragment, { children: [_jsx("span", { className: cardStyles.taskReferenceText, children: initiativeReferenceLabel }), _jsx("span", { className: cardStyles.taskReferenceDivider, children: "/" })] })) : null, taskWorkstream ? (_jsxs(_Fragment, { children: [_jsx("span", { className: cardStyles.taskReferenceText, children: workstreamReferenceLabel }), _jsx("span", { className: cardStyles.taskReferenceDivider, children: "/" })] })) : null, _jsx(TaskReferenceBadge, { copied: copiedId ===
|
|
166
|
+
return (_jsxs("div", { className: cardClasses, onClick: () => onClick?.(task), style: cardStyle, "data-task-card": "true", children: [_jsxs("div", { className: cardStyles.taskHeader, children: [_jsxs("div", { className: cardStyles.taskReferenceCluster, children: [taskInitiative ? (_jsxs(_Fragment, { children: [_jsx("span", { className: cardStyles.taskReferenceText, children: initiativeReferenceLabel }), _jsx("span", { className: cardStyles.taskReferenceDivider, children: "/" })] })) : null, taskWorkstream ? (_jsxs(_Fragment, { children: [_jsx("span", { className: cardStyles.taskReferenceText, children: workstreamReferenceLabel }), _jsx("span", { className: cardStyles.taskReferenceDivider, children: "/" })] })) : null, taskReferenceLabel ? (_jsx(TaskReferenceBadge, { copied: canCopyTaskReference && copiedId === taskReference.label, onClick: (e) => handleReferenceCopy(e, taskReference.label), disabled: !canCopyTaskReference, title: canCopyTaskReference ? 'Copy task reference' : 'Task reference pending sync', className: isArchived ? cardStyles.archiveBadge : '', label: renderHighlighted(taskReferenceLabel, searchQuery) })) : null, isProvisionalReference ? (_jsx("span", { className: styles.taskHierarchyMeta, title: "Temporary local reference until cloud sync assigns the final task number.", children: "Pending sync" })) : null] }), _jsx("div", { className: cardStyles.taskActions, onClick: (e) => e.stopPropagation(), children: !isReadOnly ? (_jsx(TaskStatusActions, { task: task, compressed: compressed, shortLabels: true, showLabel: showStatusLabel, onSetStatus: (nextTask, status) => {
|
|
164
167
|
if (onSetStatus) {
|
|
165
168
|
onSetStatus(nextTask, status);
|
|
166
169
|
return;
|
|
@@ -349,7 +349,7 @@ export function TaskForm(props) {
|
|
|
349
349
|
: entry));
|
|
350
350
|
}, onRemove: () => onChecklistItemsChange(checklistItems
|
|
351
351
|
.filter((_, entryIndex) => entryIndex !== index)
|
|
352
|
-
.map((entry, entryIndex) => ({ ...entry, order: entryIndex }))) }, item.id || `checklist-${index}`))) }) })) : (_jsx("div", { className: `${formStyles.settingsHint} tf-text-helper`, children: "No checklist items yet." }))] }))] }))] }), _jsxs("div", { children: [editingTaskId && (_jsxs("div", { className: styles.specialistSection, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => onShowCommentsChange(!showComments), title: showComments ? t('taskForm.hideComments') : t('taskForm.showComments'), children: [_jsxs("label", { className: styles.label, children: [t('taskForm.commentsSectionTitle'), " ", activityCount > 0 && `(${activityCount})`] }), showComments ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 })] }), showComments && (_jsxs("div", { className: formStyles.commentPanel, children: [_jsxs("div", { className: formStyles.commentThread, children: [activityCount === 0 && (_jsx("div", { className: formStyles.emptyComments, children: t('taskForm.noComments') })), taskActivityItems.map((item) => {
|
|
352
|
+
.map((entry, entryIndex) => ({ ...entry, order: entryIndex }))) }, item.id || `checklist-${index}`))) }) })) : (_jsx("div", { className: `${formStyles.settingsHint} tf-text-helper`, children: "No checklist items yet." }))] }))] }))] }), _jsxs("div", { children: [_jsx(TaskContextUpload, { taskId: editingTaskId, taskReferenceLabel: currentTaskReferenceLabel, workspaceId: currentWorkspaceId, apiBaseUrl: apiBaseUrl, contextFiles: contextFiles, onAddContextFile: onAddContextFile, onRemoveContextFile: onRemoveContextFile, onUpdateContextCaption: onUpdateContextCaption }), editingTaskId && (_jsxs("div", { className: styles.specialistSection, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => onShowCommentsChange(!showComments), title: showComments ? t('taskForm.hideComments') : t('taskForm.showComments'), children: [_jsxs("label", { className: styles.label, children: [t('taskForm.commentsSectionTitle'), " ", activityCount > 0 && `(${activityCount})`] }), showComments ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 })] }), showComments && (_jsxs("div", { className: formStyles.commentPanel, children: [_jsxs("div", { className: formStyles.commentThread, children: [activityCount === 0 && (_jsx("div", { className: formStyles.emptyComments, children: t('taskForm.noComments') })), taskActivityItems.map((item) => {
|
|
353
353
|
if (item.type === 'comment') {
|
|
354
354
|
const c = item.comment;
|
|
355
355
|
const authorRef = String(c.author || '').trim();
|
|
@@ -416,5 +416,5 @@ export function TaskForm(props) {
|
|
|
416
416
|
e.preventDefault();
|
|
417
417
|
onAddComment();
|
|
418
418
|
}
|
|
419
|
-
}, rows: 1 }), _jsx("button", { type: "button", className: formStyles.sendCommentBtn, onClick: onAddComment, disabled: !newCommentText.trim(), children: _jsx(Send, { size: 16 }) })] })] }))] })), _jsx(
|
|
419
|
+
}, rows: 1 }), _jsx("button", { type: "button", className: formStyles.sendCommentBtn, onClick: onAddComment, disabled: !newCommentText.trim(), children: _jsx(Send, { size: 16 }) })] })] }))] })), _jsx("div", { className: `${formStyles.taskFormLifecycle} ${formStyles.taskFormMetaDivider}`, children: _jsx("div", { className: formStyles.taskFormMetaGrid, children: currentTask && (_jsxs(_Fragment, { children: [_jsxs("div", { className: formStyles.taskFormDateRow, children: [_jsx("span", { className: formStyles.taskFormDateLabel, children: t('taskForm.createdLabel') }), _jsxs("span", { className: formStyles.taskFormMetaStack, children: [_jsx("span", { className: formStyles.taskFormDateValue, children: createdLabel || t('taskForm.emptyValue') }), _jsx("span", { className: formStyles.taskFormMetaActor, children: createdByLabel ? `by ${createdByLabel}` : t('taskForm.emptyValue') })] })] }), _jsxs("div", { className: formStyles.taskFormDateRow, children: [_jsx("span", { className: formStyles.taskFormDateLabel, children: t('taskForm.updatedLabel') }), _jsxs("span", { className: formStyles.taskFormMetaStack, children: [_jsx("span", { className: formStyles.taskFormDateValue, children: updatedLabel || t('taskForm.emptyValue') }), _jsx("span", { className: formStyles.taskFormMetaActor, children: updatedByLabel ? `by ${updatedByLabel}` : t('taskForm.emptyValue') })] })] }), _jsxs("div", { className: formStyles.taskFormDateRow, children: [_jsx("span", { className: formStyles.taskFormDateLabel, children: t('taskForm.completedLabel') }), _jsxs("span", { className: formStyles.taskFormMetaStack, children: [_jsx("span", { className: formStyles.taskFormDateValue, children: completedLabel || t('taskForm.emptyValue') }), _jsx("span", { className: formStyles.taskFormMetaActor, children: completedByLabel ? `by ${completedByLabel}` : t('taskForm.emptyValue') })] })] })] })) }) })] }), _jsxs("div", { className: formStyles.keyboardHint, children: ["Press ", _jsx("kbd", { children: navigator.platform.includes('Mac') ? '⌥' : 'Alt' }), " to toggle"] })] })] }));
|
|
420
420
|
}
|
|
@@ -397,7 +397,7 @@ describe('TaskForm Component', () => {
|
|
|
397
397
|
],
|
|
398
398
|
},
|
|
399
399
|
});
|
|
400
|
-
expect(screen.getByText('
|
|
400
|
+
expect(screen.getByText('ASSIGNED TO: Unassigned -> Planner Bot')).toBeInTheDocument();
|
|
401
401
|
expect(screen.queryByText(/ai-profile-1/i)).not.toBeInTheDocument();
|
|
402
402
|
});
|
|
403
403
|
it('3.13 Update title', async () => {
|
|
@@ -601,11 +601,50 @@ describe('TaskForm Component', () => {
|
|
|
601
601
|
]
|
|
602
602
|
},
|
|
603
603
|
});
|
|
604
|
-
expect(screen.getByText('
|
|
604
|
+
expect(screen.getByText('DESCRIPTION: added')).toBeInTheDocument();
|
|
605
605
|
expect(screen.queryByText(/Line one/)).not.toBeInTheDocument();
|
|
606
606
|
expect(screen.queryByText(/Line two/)).not.toBeInTheDocument();
|
|
607
607
|
expect(screen.queryByText(/Line three/)).not.toBeInTheDocument();
|
|
608
608
|
});
|
|
609
|
+
it('summarizes title audit events without rendering before and after title text', () => {
|
|
610
|
+
renderForm({
|
|
611
|
+
editingTaskId: '123',
|
|
612
|
+
showComments: true,
|
|
613
|
+
currentTask: {
|
|
614
|
+
id: 'task-activity-3',
|
|
615
|
+
title: 'Renamed task',
|
|
616
|
+
status: 'task',
|
|
617
|
+
createdAt: '2026-03-22T09:00:00.000Z',
|
|
618
|
+
activity: [
|
|
619
|
+
{
|
|
620
|
+
id: 'event:3',
|
|
621
|
+
type: 'event',
|
|
622
|
+
timestamp: '2026-03-22T10:00:00.000Z',
|
|
623
|
+
event: {
|
|
624
|
+
id: 'event-3',
|
|
625
|
+
taskId: 'task-activity-3',
|
|
626
|
+
workspaceId: 'default',
|
|
627
|
+
action: 'task-updated',
|
|
628
|
+
actor: 'user',
|
|
629
|
+
actorType: 'human',
|
|
630
|
+
createdAt: '2026-03-22T10:00:00.000Z',
|
|
631
|
+
details: {
|
|
632
|
+
changes: {
|
|
633
|
+
title: {
|
|
634
|
+
from: 'Old title',
|
|
635
|
+
to: 'New title'
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
]
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
expect(screen.getByText('TITLE: updated')).toBeInTheDocument();
|
|
645
|
+
expect(screen.queryByText('Old title')).not.toBeInTheDocument();
|
|
646
|
+
expect(screen.queryByText('New title')).not.toBeInTheDocument();
|
|
647
|
+
});
|
|
609
648
|
it('falls back to the current task actor label for AI comments when only the id is present', () => {
|
|
610
649
|
renderForm({
|
|
611
650
|
editingTaskId: '123',
|
|
@@ -2,5 +2,5 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Check, Copy } from 'lucide-react';
|
|
3
3
|
import styles from '../../Taskforce.module.css';
|
|
4
4
|
export function ReferenceBadgeButton({ label, title, ariaLabel, copied = false, disabled = false, className = '', onClick, children, }) {
|
|
5
|
-
return (_jsxs("button", { type: "button", className: `${styles.taskIdBadge} ${copied ? styles.copiedId : ''} ${className}`.trim(), onClick: onClick, disabled: disabled, title: title, "aria-label": ariaLabel, children: [copied ? _jsx(Check, { size: 13 }) : _jsx(Copy, { size: 13 }), _jsx("span", { children: children ?? label })] }));
|
|
5
|
+
return (_jsxs("button", { type: "button", className: `${styles.taskIdBadge} ${copied ? styles.copiedId : ''} ${className}`.trim(), onClick: onClick, disabled: disabled, title: title, "aria-label": ariaLabel, children: [copied ? _jsx(Check, { size: 13 }) : _jsx(Copy, { size: 13 }), _jsx("span", { className: styles.taskIdBadgeLabel, children: children ?? label })] }));
|
|
6
6
|
}
|
|
@@ -6,6 +6,7 @@ import styles from './PlansPage.module.css';
|
|
|
6
6
|
import { PlanComparisonPage } from './PlanComparisonPage';
|
|
7
7
|
import { AppShellHeader } from './AppShellHeader';
|
|
8
8
|
import { resolveTaskforceCloudEnvironmentFromBaseUrl } from '../../config/cloudEnvironment';
|
|
9
|
+
import { invalidateBillingStatusCache, loadBillingStatusCached } from '../../utils/billingStatusCache';
|
|
9
10
|
const TRANSIENT_CHECKOUT_QUERY_CLEAR_DELAY_MS = 1600;
|
|
10
11
|
const ACTIVE_ENTITLEMENT_STATES = new Set(['active', 'trialing', 'grace']);
|
|
11
12
|
const GATE_MESSAGES = {
|
|
@@ -116,19 +117,15 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
116
117
|
}, TRANSIENT_CHECKOUT_QUERY_CLEAR_DELAY_MS);
|
|
117
118
|
}, [clearTransientQueryState]);
|
|
118
119
|
const loadBillingStatus = useCallback(async () => {
|
|
120
|
+
const billingStatusUrl = buildApiUrl('/api/taskforce/billing/status');
|
|
119
121
|
if (!isAuthenticated) {
|
|
120
122
|
setBillingStatus(null);
|
|
123
|
+
invalidateBillingStatusCache(billingStatusUrl);
|
|
121
124
|
return;
|
|
122
125
|
}
|
|
123
126
|
try {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
credentials: 'include'
|
|
127
|
-
});
|
|
128
|
-
if (!res.ok)
|
|
129
|
-
return;
|
|
130
|
-
const payload = await res.json().catch(() => ({}));
|
|
131
|
-
setBillingStatus(payload || null);
|
|
127
|
+
const payload = await loadBillingStatusCached(billingStatusUrl);
|
|
128
|
+
setBillingStatus(payload);
|
|
132
129
|
}
|
|
133
130
|
catch {
|
|
134
131
|
// best effort only
|
|
@@ -156,16 +153,19 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
156
153
|
const checkoutState = String(payload?.checkoutState || '').trim().toLowerCase();
|
|
157
154
|
if (checkoutState === 'free') {
|
|
158
155
|
setFeedback({ type: 'success', message: 'Plan selected. Your access is ready.' });
|
|
156
|
+
invalidateBillingStatusCache(buildApiUrl('/api/taskforce/billing/status'));
|
|
159
157
|
await loadBillingStatus();
|
|
160
158
|
return;
|
|
161
159
|
}
|
|
162
160
|
if (checkoutState === 'updated') {
|
|
163
161
|
setFeedback({ type: 'success', message: String(payload?.message || 'Your subscription was updated successfully.') });
|
|
162
|
+
invalidateBillingStatusCache(buildApiUrl('/api/taskforce/billing/status'));
|
|
164
163
|
await loadBillingStatus();
|
|
165
164
|
return;
|
|
166
165
|
}
|
|
167
166
|
if (checkoutState === 'already_current_plan') {
|
|
168
167
|
setFeedback({ type: 'success', message: String(payload?.message || 'You are already on this plan.') });
|
|
168
|
+
invalidateBillingStatusCache(buildApiUrl('/api/taskforce/billing/status'));
|
|
169
169
|
await loadBillingStatus();
|
|
170
170
|
return;
|
|
171
171
|
}
|
|
@@ -216,6 +216,7 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
216
216
|
setFeedback({ type: 'error', message: String(payload?.error || 'Unable to confirm checkout completion.') });
|
|
217
217
|
return false;
|
|
218
218
|
}
|
|
219
|
+
invalidateBillingStatusCache(buildApiUrl('/api/taskforce/billing/status'));
|
|
219
220
|
await loadBillingStatus();
|
|
220
221
|
return true;
|
|
221
222
|
}
|