@taskforcehq/taskforce 0.3.327 → 0.3.328

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.
Files changed (187) hide show
  1. package/dist/Taskforce.module.css +24 -53
  2. package/dist/components/activity/EntityActivityTimeline.js +15 -5
  3. package/dist/components/context/AttachmentPreviewHost.js +1 -1
  4. package/dist/components/context/ContextAttachmentManager.d.ts +3 -0
  5. package/dist/components/context/ContextAttachmentManager.js +116 -20
  6. package/dist/components/features/planning/PlanningEntitySummary.d.ts +3 -1
  7. package/dist/components/features/planning/PlanningEntitySummary.js +3 -2
  8. package/dist/components/features/planning/PlanningLinkPicker.d.ts +3 -1
  9. package/dist/components/features/planning/PlanningLinkPicker.js +5 -3
  10. package/dist/components/features/planning/PlanningModule.js +26 -9
  11. package/dist/components/features/planning/PlanningVisualIdentity.d.ts +14 -0
  12. package/dist/components/features/planning/PlanningVisualIdentity.js +106 -0
  13. package/dist/components/features/planning/planningWorkspaceModel.d.ts +11 -0
  14. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  15. package/dist/components/task/TaskActionHeader.js +2 -2
  16. package/dist/components/task/TaskCard.js +37 -4
  17. package/dist/components/task/TaskContextUpload.d.ts +3 -0
  18. package/dist/components/task/TaskContextUpload.js +2 -2
  19. package/dist/components/task/TaskForm.d.ts +4 -0
  20. package/dist/components/task/TaskForm.js +32 -7
  21. package/dist/components/task/TaskImageReviews.js +16 -16
  22. package/dist/components/task/TaskStatusActions.js +11 -2
  23. package/dist/components/task/TaskWorkstreamPicker.js +2 -1
  24. package/dist/components/ui/DocumentTypeIcon.d.ts +7 -0
  25. package/dist/components/ui/DocumentTypeIcon.js +8 -0
  26. package/dist/components/ui/TaxonomyDropdown.d.ts +9 -1
  27. package/dist/components/ui/TaxonomyDropdown.js +30 -3
  28. package/dist/components/views/StandaloneLayout.js +62 -9
  29. package/dist/components/views/WidgetView.js +8 -5
  30. package/dist/components/views/panels/PlanningDrawer.d.ts +7 -1
  31. package/dist/components/views/panels/PlanningDrawer.js +48 -44
  32. package/dist/config/envSchema.js +1 -0
  33. package/dist/config/localServiceCloudTarget.d.ts +10 -0
  34. package/dist/config/localServiceCloudTarget.js +76 -0
  35. package/dist/core/AiProfileService.d.ts +1 -0
  36. package/dist/core/AiProfileService.js +2 -1
  37. package/dist/core/AttachmentLinkService.js +7 -4
  38. package/dist/core/DeletedTaskLifecycleService.d.ts +4 -0
  39. package/dist/core/DeletedTaskLifecycleService.js +21 -0
  40. package/dist/core/McpTokenService.d.ts +4 -0
  41. package/dist/core/McpTokenService.js +52 -0
  42. package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +5 -0
  43. package/dist/core/TaskAttachmentLinksDurableMutationService.js +20 -11
  44. package/dist/core/Taskforce.d.ts +63 -1
  45. package/dist/core/Taskforce.js +558 -73
  46. package/dist/core/types.d.ts +2 -1
  47. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +11 -0
  48. package/dist/documentReviews/DocumentReviewCommentStore.js +35 -2
  49. package/dist/documentReviews/service.d.ts +4 -0
  50. package/dist/documentReviews/service.js +141 -46
  51. package/dist/hooks/tasks/useTaskFormActions.d.ts +3 -0
  52. package/dist/hooks/tasks/useTaskFormActions.js +33 -7
  53. package/dist/hooks/useTaskforce.d.ts +59 -50
  54. package/dist/hooks/useTaskforce.js +103 -1
  55. package/dist/mcp/canonicalAssetHelpers.d.ts +13 -0
  56. package/dist/mcp/canonicalAssetHelpers.js +39 -5
  57. package/dist/mcp/collaborationRegistrar.js +15 -13
  58. package/dist/mcp/documentAssetRegistrar.js +16 -9
  59. package/dist/mcp/localServiceProxy.js +4 -1
  60. package/dist/mcp/runtime.d.ts +16 -0
  61. package/dist/mcp/runtime.js +566 -171
  62. package/dist/mcp/taskAttachmentCommandAdapter.d.ts +2 -0
  63. package/dist/mcp/taskAttachmentCommandAdapter.js +14 -8
  64. package/dist/mcp/taskPlanningRegistrar.js +42 -34
  65. package/dist/mcp/toolRegistry.js +2 -2
  66. package/dist/migrations/taskSchemaMigrations.js +35 -0
  67. package/dist/runtime/appGateDefinitions.d.ts +2 -2
  68. package/dist/runtime/appGateDefinitions.js +2 -2
  69. package/dist/server/appAccess.d.ts +12 -0
  70. package/dist/server/appAccess.js +21 -0
  71. package/dist/server/authorizedWorkspaceAccess.d.ts +12 -0
  72. package/dist/server/authorizedWorkspaceAccess.js +40 -0
  73. package/dist/server/cloudCollectionRead.d.ts +25 -0
  74. package/dist/server/cloudCollectionRead.js +120 -0
  75. package/dist/server/cloudCollectionReadPool.d.ts +52 -0
  76. package/dist/server/cloudCollectionReadPool.js +140 -0
  77. package/dist/server/cloudCollectionReadWorker.d.ts +1 -0
  78. package/dist/server/cloudCollectionReadWorker.js +36 -0
  79. package/dist/server/index.d.ts +3 -11
  80. package/dist/server/index.js +53 -40
  81. package/dist/server/localServiceLease.d.ts +3 -0
  82. package/dist/server/localServiceLease.js +9 -1
  83. package/dist/server/routes/documents.d.ts +5 -2
  84. package/dist/server/routes/documents.js +160 -13
  85. package/dist/server/routes/durableTaskHttpRouters.d.ts +2 -0
  86. package/dist/server/routes/durableTaskHttpRouters.js +2 -2
  87. package/dist/server/routes/localService.js +2 -0
  88. package/dist/server/routes/primitives.d.ts +2 -0
  89. package/dist/server/routes/shared.d.ts +24 -0
  90. package/dist/server/routes/shared.js +114 -18
  91. package/dist/server/routes/sync.js +22 -0
  92. package/dist/server/routes/syncV3FeedRoutes.js +4 -2
  93. package/dist/server/routes/syncV3LocalCaptureRoutes.js +20 -2
  94. package/dist/server/routes/syncV3OperationRoutes.js +17 -2
  95. package/dist/server/routes/tasks.js +199 -89
  96. package/dist/server/routes.js +35 -7
  97. package/dist/service/localServiceCli.js +36 -25
  98. package/dist/service/localServiceClientLifecycle.js +67 -6
  99. package/dist/shared/planningIdentity.d.ts +20 -0
  100. package/dist/shared/planningIdentity.js +79 -0
  101. package/dist/storage/postgresAdapter.js +4 -0
  102. package/dist/storage/postgresRequestDiagnostics.d.ts +8 -0
  103. package/dist/storage/postgresRequestDiagnostics.js +24 -0
  104. package/dist/storage/workspaceAssetStore.d.ts +32 -0
  105. package/dist/storage/workspaceAssetStore.js +70 -0
  106. package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +16 -0
  107. package/dist/sync/coordinator/localSyncExecutionPermitStore.js +70 -22
  108. package/dist/sync/coordinator/workspaceSyncTransportGateway.js +4 -2
  109. package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +5 -0
  110. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.d.ts +1 -1
  111. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +15 -0
  112. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +26 -3
  113. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
  114. package/dist/sync/syncApplyHandlers.d.ts +3 -1
  115. package/dist/sync/syncApplyHandlers.js +29 -0
  116. package/dist/sync/syncService.js +9 -0
  117. package/dist/sync/taskSyncPayload.d.ts +1 -0
  118. package/dist/sync/taskSyncPayload.js +6 -0
  119. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +1 -0
  120. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +21 -3
  121. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +17 -4
  122. package/dist/sync/v3/localInitiativeOutboxService.js +4 -2
  123. package/dist/sync/v3/localOutboxDispatcher.js +5 -1
  124. package/dist/sync/v3/localTaskAttachmentLinksOutboxHandler.js +12 -2
  125. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +1 -0
  126. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +15 -3
  127. package/dist/sync/v3/localWorkstreamOutboxService.js +4 -2
  128. package/dist/sync/v3/syncDurabilityStore.d.ts +8 -0
  129. package/dist/sync/v3/syncDurabilityStore.js +171 -33
  130. package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +1 -0
  131. package/dist/sync/v3/taskAttachmentLinksMutationService.js +18 -2
  132. package/dist/sync/v3/taskMetadataCoexistence.js +4 -0
  133. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +1 -9
  134. package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +1 -0
  135. package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +7 -1
  136. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +15 -3
  137. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +4 -0
  138. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +29 -6
  139. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +1 -0
  140. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +29 -7
  141. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +62 -7
  142. package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +1 -0
  143. package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.d.ts +2 -0
  144. package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.js +10 -0
  145. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.d.ts +1 -1
  146. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.js +1 -1
  147. package/dist/sync/workspacePullFeed.d.ts +6 -1
  148. package/dist/sync/workspacePullFeed.js +172 -161
  149. package/dist/sync/workspaceSyncModel.d.ts +11 -0
  150. package/dist/sync/workspaceSyncModel.js +27 -2
  151. package/dist/types.d.ts +8 -0
  152. package/dist/ui/assets/{AiIdentityRosterCard-DB1BLO_j.js → AiIdentityRosterCard-MZs7lVED.js} +1 -1
  153. package/dist/ui/assets/{AiProfilesModule-DPxQeF9N.js → AiProfilesModule-C3i0LOxe.js} +1 -1
  154. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-DHNYaNg6.js → AnnotatedAttachmentWorkspace-DVZk5Tln.js} +1 -1
  155. package/dist/ui/assets/AssetTraySortControl-Kf1pelSy.js +1 -0
  156. package/dist/ui/assets/ContextAttachmentManager-B8boOs6Q.js +3 -0
  157. package/dist/ui/assets/{DocumentWorkspace-BC7sy5TG.js → DocumentWorkspace-DXH4XUsB.js} +4 -4
  158. package/dist/ui/assets/EntityActivityTimeline-BtSTq0ON.js +1 -0
  159. package/dist/ui/assets/PlanningModule-BPVyEa-f.js +1 -0
  160. package/dist/ui/assets/PlanningModule-CoIR9hbV.css +1 -0
  161. package/dist/ui/assets/{PlansPage-BVH_NfgT.js → PlansPage-ChqgxALc.js} +1 -1
  162. package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +1 -0
  163. package/dist/ui/assets/TaskContextUpload-qmBeUw3u.css +1 -0
  164. package/dist/ui/assets/{TaskSettings-xVsxSa0g.js → TaskSettings-g4ECKNOz.js} +1 -1
  165. package/dist/ui/assets/{TaskforceAgentsModule-CkAxW19K.js → TaskforceAgentsModule-BazdUwxZ.js} +1 -1
  166. package/dist/ui/assets/WorkflowManagerModule-DWFspC-o.js +1 -0
  167. package/dist/ui/assets/index-BUplSsv_.js +7 -0
  168. package/dist/ui/assets/index-gz2EXuoK.css +1 -0
  169. package/dist/ui/assets/{vendor-icons-B8ZNsH1g.js → vendor-icons-BkFLXavV.js} +1 -1
  170. package/dist/ui/index.html +3 -3
  171. package/dist/utils/constants.d.ts +2 -2
  172. package/dist/utils/constants.js +1 -1
  173. package/dist/utils/contextAttachmentUpload.d.ts +2 -0
  174. package/dist/utils/contextAttachmentUpload.js +7 -0
  175. package/dist/utils/contextFiles.d.ts +1 -1
  176. package/dist/utils/contextFiles.js +4 -4
  177. package/package.json +6 -1
  178. package/dist/ui/assets/AssetTraySortControl-DphbyHVc.js +0 -1
  179. package/dist/ui/assets/ContextAttachmentManager-CMxSzHqc.js +0 -3
  180. package/dist/ui/assets/EntityActivityTimeline-pJ2ZjZ-k.js +0 -1
  181. package/dist/ui/assets/PlanningModule-CeTqXMMz.css +0 -1
  182. package/dist/ui/assets/PlanningModule-t5GbvVR9.js +0 -1
  183. package/dist/ui/assets/TaskContextUpload-Ds4LnDzV.css +0 -1
  184. package/dist/ui/assets/TaskContextUpload-bl4fOOUz.js +0 -1
  185. package/dist/ui/assets/WorkflowManagerModule-BKqIGVnF.js +0 -1
  186. package/dist/ui/assets/index-BzJlVnNS.css +0 -1
  187. package/dist/ui/assets/index-Cx7vZJHL.js +0 -7
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { AlertTriangle, Archive, ArrowUpDown, ChevronLeft, Eye, EyeOff, Filter, Link2, Loader2, Pencil, Plus, RotateCcw, Search, Unlink, X, } from 'lucide-react';
4
4
  import { TASK_ASSIGNEE_UNASSIGNED_COLOR, TASK_ASSIGNEE_UNASSIGNED_ICON, } from '../../../utils/assignees';
@@ -10,6 +10,7 @@ import { TaxonomyDropdown } from '../../ui/TaxonomyDropdown';
10
10
  import { PlanningOwnerDropdown, normalizePlanningOwnerOptions, } from './PlanningOwnerDropdown';
11
11
  import { buildPlanningNavigatorResults, PLANNING_NAVIGATOR_SORT_OPTIONS, } from './planningNavigator';
12
12
  import { PlanningEntitySummary, PlanningReferenceBadge, } from './PlanningEntitySummary';
13
+ import { PlanningVisualIdentity } from './PlanningVisualIdentity';
13
14
  import { PlanningTaskCard } from './PlanningTaskCard';
14
15
  import { PlanningTaskArrangeControl } from './PlanningTaskArrangeControl';
15
16
  import planningDropdownStyles from './PlanningDropdownControl.module.css';
@@ -29,6 +30,8 @@ function PlanningWorkspaceEditor({ entityType, mode, initialTitle = '', initialD
29
30
  const [title, setTitle] = React.useState(initialTitle);
30
31
  const [description, setDescription] = React.useState(initialDescription);
31
32
  const [ownerId, setOwnerId] = React.useState('');
33
+ const [icon, setIcon] = React.useState(null);
34
+ const [color, setColor] = React.useState(null);
32
35
  const [titleError, setTitleError] = React.useState(null);
33
36
  const [saveError, setSaveError] = React.useState(null);
34
37
  const [isSaving, setIsSaving] = React.useState(false);
@@ -57,6 +60,7 @@ function PlanningWorkspaceEditor({ entityType, mode, initialTitle = '', initialD
57
60
  ...(entityType === 'workstream' && mode === 'create'
58
61
  ? { initiativeId: fixedInitiativeId || null }
59
62
  : {}),
63
+ ...(mode === 'create' && (icon || color) ? { icon, color } : {}),
60
64
  });
61
65
  }
62
66
  catch (error) {
@@ -72,7 +76,12 @@ function PlanningWorkspaceEditor({ entityType, mode, initialTitle = '', initialD
72
76
  return;
73
77
  event.preventDefault();
74
78
  onCancel();
75
- }, noValidate: true, children: [_jsxs("div", { className: styles.workspaceEditorField, children: [_jsxs("label", { htmlFor: titleInputId, children: [label, " title"] }), _jsx("input", { ref: titleInputRef, id: titleInputId, value: title, disabled: isSaving, required: true, "aria-invalid": titleError ? 'true' : undefined, "aria-describedby": titleError ? titleErrorId : undefined, onChange: (event) => {
79
+ }, noValidate: true, children: [mode === 'create' ? (_jsxs("div", { className: styles.workspaceEditorIdentity, children: [_jsx(PlanningVisualIdentity, { entityType: entityType, entityId: "create-preview", icon: icon, color: color || 'blue', size: "detail", onChange: (patch) => {
80
+ if (Object.prototype.hasOwnProperty.call(patch, 'icon'))
81
+ setIcon(patch.icon ?? null);
82
+ if (Object.prototype.hasOwnProperty.call(patch, 'color'))
83
+ setColor(patch.color ?? null);
84
+ } }), _jsx("span", { children: "Choose an icon and color" })] })) : null, _jsxs("div", { className: styles.workspaceEditorField, children: [_jsxs("label", { htmlFor: titleInputId, children: [label, " title"] }), _jsx("input", { ref: titleInputRef, id: titleInputId, value: title, disabled: isSaving, required: true, "aria-invalid": titleError ? 'true' : undefined, "aria-describedby": titleError ? titleErrorId : undefined, onChange: (event) => {
76
85
  setTitle(event.target.value);
77
86
  if (titleError)
78
87
  setTitleError(null);
@@ -80,24 +89,24 @@ function PlanningWorkspaceEditor({ entityType, mode, initialTitle = '', initialD
80
89
  ? 'Describe the broader outcome this initiative is meant to achieve.'
81
90
  : 'Describe the lane of work this workstream will coordinate.' })] }), mode === 'create' && ownerOptions ? (_jsxs("div", { className: styles.workspaceEditorField, children: [_jsxs("span", { children: [label, " owner"] }), _jsx(PlanningOwnerDropdown, { value: ownerId, options: ownerOptions, onChange: setOwnerId, disabled: isSaving, ariaLabel: `${label} owner`, className: `${planningDropdownStyles.control} ${styles.selectedEntityOwnerDropdown}` })] })) : null, saveError ? _jsx("p", { className: "tf-text-error", role: "alert", children: saveError }) : null, showActions ? _jsxs("div", { className: styles.workspaceEditorActions, children: [_jsx("button", { type: "button", className: "tf-button-secondary tf-button-compact", onClick: onCancel, disabled: isSaving, children: "Cancel" }), _jsxs("button", { type: "submit", className: "tf-button-primary tf-button-compact", disabled: isSaving, children: [isSaving ? _jsx(Loader2, { size: 14, className: styles.spinner, "aria-hidden": "true" }) : null, mode === 'create' ? `Create ${label}` : 'Save'] })] }) : null] }));
82
91
  }
83
- function InitiativeButton({ initiative, ownerOption, ownerOptions, workstreamCount, selected, onSelect, onChangeOwner, }) {
92
+ function InitiativeButton({ initiative, ownerOption, ownerOptions, workstreamCount, selected, onSelect, onChangeOwner, onChangeIdentity, }) {
84
93
  return (_jsxs("div", { className: `${styles.entityCard} ${initiative.isArchived ? styles.entityCardArchived : ''} ${selected ? styles.entityCardSelected : ''}`.trim(), children: [_jsx(PlanningReferenceBadge, { label: formatInitiativeReference(initiative), entityType: "initiative", entityName: initiative.title, interactive: false }), _jsx("div", { className: styles.entityCardBody, children: _jsx("div", { role: "button", tabIndex: 0, className: styles.entityCardContentButton, onClick: onSelect, onKeyDown: (event) => {
85
94
  if (event.key !== 'Enter' && event.key !== ' ')
86
95
  return;
87
96
  event.preventDefault();
88
97
  onSelect();
89
- }, "aria-pressed": selected, children: _jsx(PlanningEntitySummary, { item: initiative, entityType: "initiative", workstreamCount: workstreamCount, ownerOption: ownerOption, ownerOptions: ownerOptions, onChangeOwner: onChangeOwner }) }) })] }));
98
+ }, "aria-pressed": selected, children: _jsx(PlanningEntitySummary, { item: initiative, entityType: "initiative", workstreamCount: workstreamCount, ownerOption: ownerOption, ownerOptions: ownerOptions, onChangeOwner: onChangeOwner, onChangeIdentity: onChangeIdentity }) }) })] }));
90
99
  }
91
- function WorkstreamButton({ workstream, ownerOption, ownerOptions, selected, onSelect, onChangeOwner, referenceAction, feedback, }) {
100
+ function WorkstreamButton({ workstream, ownerOption, ownerOptions, selected, onSelect, onChangeOwner, onChangeIdentity, referenceAction, feedback, }) {
92
101
  const referenceLabel = formatWorkstreamReference(workstream);
93
102
  return (_jsxs("div", { className: `${styles.entityCard} ${workstream.isArchived ? styles.entityCardArchived : ''} ${selected ? styles.entityCardSelected : ''}`.trim(), children: [_jsxs("div", { className: styles.entityCardTopRow, children: [referenceAction, _jsx(PlanningReferenceBadge, { label: referenceLabel || 'Reference pending', entityType: "workstream", entityName: workstream.title, interactive: false, pending: !referenceLabel })] }), feedback, _jsx("div", { className: styles.entityCardBody, children: _jsx("div", { role: "button", tabIndex: 0, className: styles.entityCardContentButton, onClick: onSelect, onKeyDown: (event) => {
94
103
  if (event.key !== 'Enter' && event.key !== ' ')
95
104
  return;
96
105
  event.preventDefault();
97
106
  onSelect();
98
- }, "aria-pressed": selected, children: _jsx(PlanningEntitySummary, { item: workstream, entityType: "workstream", ownerOption: ownerOption, ownerOptions: ownerOptions, onChangeOwner: onChangeOwner }) }) })] }));
107
+ }, "aria-pressed": selected, children: _jsx(PlanningEntitySummary, { item: workstream, entityType: "workstream", ownerOption: ownerOption, ownerOptions: ownerOptions, onChangeOwner: onChangeOwner, onChangeIdentity: onChangeIdentity }) }) })] }));
99
108
  }
100
- function SelectedEntityContext({ item, entityType, ownerOptions, onChangeOwner, workspaceId, currentActorId, taskReferences, hydrationState, onRetryHydration, onAddContextFile, onRemoveContextFile, onUpdateContextCaption, onUpdate, onArchive, onUnarchive, referenceAction, relationshipActions, relationshipContent, }) {
109
+ function SelectedEntityContext({ item, entityType, ownerOptions, onChangeOwner, onChangeIdentity, workspaceId, currentActorId, taskReferences, hydrationState, onRetryHydration, onAddContextFile, onRemoveContextFile, onUpdateContextCaption, onUpdate, onArchive, onUnarchive, referenceAction, relationshipActions, relationshipContent, }) {
101
110
  const label = entityType === 'initiative' ? 'Initiative' : 'Workstream';
102
111
  const referenceLabel = entityType === 'initiative'
103
112
  ? formatInitiativeReference(item)
@@ -181,7 +190,9 @@ function SelectedEntityContext({ item, entityType, ownerOptions, onChangeOwner,
181
190
  setArchiveBusy(false);
182
191
  }
183
192
  };
184
- return (_jsxs("section", { className: `${styles.selectedEntityContext} ${item.isArchived ? styles.selectedEntityContextArchived : ''}`.trim(), "aria-label": `Selected ${label.toLowerCase()}`, "aria-busy": hydrationState?.status === 'loading', children: [_jsxs("div", { className: styles.selectedEntityReferenceRow, children: [_jsxs("div", { className: styles.selectedEntityReferenceIdentity, children: [referenceAction, _jsx(PlanningReferenceBadge, { label: referenceLabel || 'Reference pending', entityType: entityType, entityName: item.title, interactive: false, pending: !referenceLabel })] }), _jsxs("div", { className: styles.selectedEntityReferenceActions, children: [_jsx("span", { children: label }), relationshipActions, !isEditing && (onArchive || onUnarchive) ? (_jsx("button", { type: "button", className: "tf-control-icon", disabled: archiveBusy
193
+ return (_jsxs("section", { className: `${styles.selectedEntityContext} ${item.isArchived ? styles.selectedEntityContextArchived : ''}`.trim(), "aria-label": `Selected ${label.toLowerCase()}`, "aria-busy": hydrationState?.status === 'loading', children: [_jsxs("div", { className: styles.selectedEntityReferenceRow, children: [_jsxs("div", { className: styles.selectedEntityReferenceIdentity, children: [_jsx(PlanningVisualIdentity, { entityType: entityType, entityId: item.id, icon: item.icon, color: item.color, size: "detail", disabled: Boolean(item.isArchived), onChange: onChangeIdentity
194
+ ? (patch) => onChangeIdentity(entityType, item.id, patch)
195
+ : undefined }), referenceAction, _jsx(PlanningReferenceBadge, { label: referenceLabel || 'Reference pending', entityType: entityType, entityName: item.title, interactive: false, pending: !referenceLabel })] }), _jsxs("div", { className: styles.selectedEntityReferenceActions, children: [_jsx("span", { children: label }), relationshipActions, !isEditing && (onArchive || onUnarchive) ? (_jsx("button", { type: "button", className: "tf-control-icon", disabled: archiveBusy
185
196
  || archiveState.mode === 'blocked'
186
197
  || archiveState.mode === 'checking'
187
198
  || (archiveState.mode === 'archived' ? !onUnarchive : !onArchive), onClick: () => void handleArchiveAction(), "aria-label": archiveActionLabel, title: archiveActionLabel, "aria-expanded": archiveState.mode === 'ready'
@@ -500,12 +511,16 @@ export function PlanningModule({ model }) {
500
511
  return (_jsxs("main", { className: styles.module, "aria-label": "Planning workspace", "aria-busy": model.loadState.isRefreshing, children: [model.loadState.isRefreshing ? (_jsx("span", { className: styles.visuallyHidden, role: "status", "aria-live": "polite", children: "Refreshing planning" })) : null, (model.loadState.status === 'partial' || model.loadState.status === 'error') && (_jsxs("div", { className: styles.inlineError, role: "alert", children: [_jsx(AlertTriangle, { size: 16 }), _jsx("span", { children: model.loadState.error || 'Some planning information could not be loaded.' }), _jsx("button", { type: "button", className: "tf-button-secondary tf-button-compact", onClick: model.retry, children: "Retry" })] })), _jsxs("div", { className: styles.columns, "data-active-pane": activePane, children: [_jsxs("section", { className: `${styles.column} ${styles.columnPortfolio} ${activePane === 'portfolio' ? styles.columnActive : ''}`.trim(), "aria-labelledby": "planning-portfolio-heading", children: [_jsxs("div", { className: styles.columnHeader, children: [_jsx("h2", { ref: portfolioHeadingRef, id: "planning-portfolio-heading", className: `tf-heading-section ${styles.paneHeading}`, tabIndex: -1, children: "Portfolio" }), _jsxs("div", { className: styles.headerActions, children: [_jsx("span", { children: visibleInitiatives.length + visibleStandaloneWorkstreams.length }), _jsx("button", { type: "button", className: `tf-control-icon ${showArchivedPlanning ? styles.controlActive : ''}`.trim(), onClick: () => setShowArchivedPlanning((current) => !current), disabled: !hasArchivedPlanning, "aria-label": showArchivedPlanning ? 'Hide archived planning items' : 'Show archived planning items', "aria-pressed": showArchivedPlanning, title: hasArchivedPlanning ? (showArchivedPlanning ? 'Hide archived planning items' : 'Show archived planning items') : 'No archived planning items', children: showArchivedPlanning ? _jsx(EyeOff, { size: 15, "aria-hidden": "true" }) : _jsx(Eye, { size: 15, "aria-hidden": "true" }) })] })] }), _jsxs("div", { className: styles.portfolioControls, children: [_jsxs("div", { className: styles.searchRow, children: [_jsxs("div", { className: styles.searchShell, children: [_jsx(Search, { size: 14, "aria-hidden": "true" }), _jsx("input", { type: "search", value: searchQuery, onChange: (event) => setSearchQuery(event.target.value), placeholder: "Search initiatives and workstreams", "aria-label": "Search planning" }), searchQuery ? (_jsx("button", { type: "button", className: styles.clearSearch, onClick: () => setSearchQuery(''), "aria-label": "Clear planning search", title: "Clear search", children: _jsx(X, { size: 13, "aria-hidden": "true" }) })) : null] }), _jsx("button", { type: "button", className: `tf-control-icon ${filtersExpanded || ownerFilter !== 'all' || sort !== 'default' ? styles.controlActive : ''}`.trim(), "aria-label": "Filter and sort planning", title: "Filter and sort planning", "aria-expanded": filtersExpanded, "aria-controls": "planning-workspace-navigator-options", onClick: () => setFiltersExpanded((current) => !current), children: _jsx(Filter, { size: 15, "aria-hidden": "true" }) })] }), filtersExpanded ? (_jsxs("div", { id: "planning-workspace-navigator-options", className: styles.filterPanel, children: [_jsxs("div", { className: styles.filterField, children: [_jsx("span", { className: styles.filterLabel, children: "Owner" }), _jsx(PlanningOwnerDropdown, { value: ownerFilter, options: model.assigneeOptions, leadingOptions: ownerFilterLeadingOptions, includeUnassigned: false, onChange: setOwnerFilter, disabled: false, ariaLabel: "Filter planning by owner", className: styles.filterDropdown })] }), _jsxs("div", { className: styles.filterField, children: [_jsx("span", { className: styles.filterLabel, children: "Sort" }), _jsx(TaxonomyDropdown, { value: sort, options: PLANNING_NAVIGATOR_SORT_OPTIONS, onChange: (value) => setSort(String(value)), ariaLabel: "Sort planning", className: styles.filterDropdown, portalPanel: true, panelAlign: "start", triggerContent: (_jsxs("span", { className: styles.sortTrigger, children: [_jsx(ArrowUpDown, { size: 14, "aria-hidden": "true" }), _jsx("span", { children: PLANNING_NAVIGATOR_SORT_OPTIONS.find((option) => option.value === sort)?.label })] })), renderOptionContent: (option) => option.label })] }), _jsx("button", { type: "button", className: "tf-control-icon", onClick: clearNavigator, disabled: !hasAdjustedNavigator, "aria-label": "Reset planning filters and sort", title: "Reset planning filters and sort", children: _jsx(RotateCcw, { size: 14, "aria-hidden": "true" }) })] })) : null] }), _jsxs("div", { className: styles.scrollRegion, children: [_jsxs("div", { className: styles.sectionHeadingRow, children: [_jsx("h3", { className: `tf-heading-section ${styles.sectionLabel}`, children: "Initiatives" }), model.createEntity ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: () => setCreateState({ entityType: 'initiative' }), "aria-label": "Create initiative", title: "Create initiative", children: _jsx(Plus, { size: 15, "aria-hidden": "true" }) })) : null] }), createState?.entityType === 'initiative' ? (_jsx(PlanningWorkspaceEditor, { entityType: "initiative", mode: "create", ownerOptions: model.assigneeOptions, onCancel: () => setCreateState(null), onSave: createPlanningEntity })) : null, navigatorResults.initiatives.map(({ initiative, workstreams: resultWorkstreams }) => {
501
512
  return (_jsx(InitiativeButton, { initiative: initiative, workstreamCount: resultWorkstreams.length, ownerOption: initiative.ownerId ? assigneeOptionByValue.get(initiative.ownerId) : undefined, ownerOptions: model.assigneeOptions, selected: selectedInitiativeId === initiative.id, onSelect: () => selectInitiative(initiative.id), onChangeOwner: model.changeOwner
502
513
  ? (ownerId) => model.changeOwner?.('initiative', initiative.id, ownerId === 'unassigned' ? '' : ownerId)
514
+ : undefined, onChangeIdentity: model.changeIdentity
515
+ ? (patch) => model.changeIdentity?.('initiative', initiative.id, patch)
503
516
  : undefined }, initiative.id));
504
517
  }), _jsxs("div", { className: styles.sectionHeadingRow, children: [_jsx("h3", { className: `tf-heading-section ${styles.sectionLabel}`, children: "Standalone workstreams" }), model.createEntity ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: () => setCreateState({ entityType: 'workstream', initiativeId: null }), "aria-label": "Create standalone workstream", title: "Create standalone workstream", children: _jsx(Plus, { size: 15, "aria-hidden": "true" }) })) : null] }), createState?.entityType === 'workstream' && !createState.initiativeId ? (_jsx(PlanningWorkspaceEditor, { entityType: "workstream", mode: "create", ownerOptions: model.assigneeOptions, fixedInitiativeId: null, onCancel: () => setCreateState(null), onSave: createPlanningEntity })) : null, visibleStandaloneWorkstreams.map((workstream) => (_jsx(WorkstreamButton, { workstream: workstream, ownerOption: workstream.ownerId ? assigneeOptionByValue.get(workstream.ownerId) : undefined, ownerOptions: model.assigneeOptions, selected: selectedWorkstreamId === workstream.id, onSelect: () => selectWorkstream(workstream), onChangeOwner: model.changeOwner
505
518
  ? (ownerId) => model.changeOwner?.('workstream', workstream.id, ownerId === 'unassigned' ? '' : ownerId)
519
+ : undefined, onChangeIdentity: model.changeIdentity
520
+ ? (patch) => model.changeIdentity?.('workstream', workstream.id, patch)
506
521
  : undefined }, workstream.id))), visibleInitiatives.length === 0 && visibleStandaloneWorkstreams.length === 0 && (_jsx("p", { className: `tf-text-helper ${styles.empty}`, children: navigatorResults.hasCriteria ? 'No planning matches.' : 'No planning items yet.' }))] })] }), _jsxs("section", { className: `${styles.column} ${styles.columnWorkstreams} ${activePane === 'workstreams' ? styles.columnActive : ''}`.trim(), "aria-labelledby": "planning-workstreams-heading", children: [_jsxs("div", { className: styles.columnHeader, children: [_jsxs("div", { className: styles.columnHeadingGroup, children: [_jsx("button", { type: "button", className: `tf-control-icon ${styles.responsiveBack}`.trim(), onClick: () => navigateToPane('portfolio'), "aria-label": "Back to Portfolio", title: "Back to Portfolio", children: _jsx(ChevronLeft, { size: 15, "aria-hidden": "true" }) }), _jsxs("h2", { ref: workstreamsHeadingRef, id: "planning-workstreams-heading", className: `tf-heading-section ${styles.paneHeading}`, tabIndex: -1, children: ["Workstreams (", workstreams.length, ")"] })] }), _jsx("div", { className: styles.headerActions, children: selectedInitiative && model.createEntity ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: () => setCreateState({ entityType: 'workstream', initiativeId: selectedInitiative.id }), "aria-label": `Create workstream in ${selectedInitiative.title}`, title: "Create workstream", children: _jsx(Plus, { size: 15, "aria-hidden": "true" }) })) : null })] }), _jsxs("div", { className: styles.scrollRegion, children: [selectedInitiative ? (_jsxs(_Fragment, { children: [_jsx(SelectedEntityContext, { item: selectedInitiative, entityType: "initiative", ownerOptions: model.assigneeOptions, onChangeOwner: model.changeOwner
507
522
  ? (ownerId) => model.changeOwner?.('initiative', selectedInitiative.id, ownerId)
508
- : undefined, workspaceId: model.currentWorkspaceId, currentActorId: model.currentOwnerId, taskReferences: model.taskReferences, hydrationState: selectedWorkstream ? undefined : selectedEntityHydrationState, onRetryHydration: model.hydrateEntity
523
+ : undefined, onChangeIdentity: model.changeIdentity, workspaceId: model.currentWorkspaceId, currentActorId: model.currentOwnerId, taskReferences: model.taskReferences, hydrationState: selectedWorkstream ? undefined : selectedEntityHydrationState, onRetryHydration: model.hydrateEntity
509
524
  ? () => { void model.hydrateEntity?.('initiative', selectedInitiative.id); }
510
525
  : undefined, onAddContextFile: model.addContextFile, onRemoveContextFile: model.removeContextFile, onUpdateContextCaption: model.updateContextCaption, onUpdate: model.updateEntity
511
526
  ? (input) => model.updateEntity('initiative', selectedInitiative.id, input)
@@ -515,6 +530,8 @@ export function PlanningModule({ model }) {
515
530
  ? () => model.unarchiveEntity('initiative', selectedInitiative.id)
516
531
  : undefined }), _jsx(PlanningLinkPicker, { options: availableWorkstreamLinks, entityLabel: "workstream", onSelect: (option) => attachRelationship('workstream', option), disabled: !model.attachWorkstreamToInitiative || isChangingRelationship })] })) : null, createState?.entityType === 'workstream' && createState.initiativeId === selectedInitiative?.id ? (_jsx(PlanningWorkspaceEditor, { entityType: "workstream", mode: "create", ownerOptions: model.assigneeOptions, fixedInitiativeId: selectedInitiative.id, onCancel: () => setCreateState(null), onSave: createPlanningEntity })) : null, workstreams.map((workstream) => (_jsx(WorkstreamButton, { workstream: workstream, ownerOption: workstream.ownerId ? assigneeOptionByValue.get(workstream.ownerId) : undefined, ownerOptions: model.assigneeOptions, selected: selectedWorkstreamId === workstream.id, onSelect: () => selectWorkstream(workstream), onChangeOwner: model.changeOwner
517
532
  ? (ownerId) => model.changeOwner?.('workstream', workstream.id, ownerId === 'unassigned' ? '' : ownerId)
533
+ : undefined, onChangeIdentity: model.changeIdentity
534
+ ? (patch) => model.changeIdentity?.('workstream', workstream.id, patch)
518
535
  : undefined, referenceAction: selectedInitiative ? (_jsx(HierarchyActionBadge, { disabled: !model.assignWorkstreamToInitiative || isChangingRelationship, onClick: () => setPendingUnlink({
519
536
  entityType: 'workstream',
520
537
  id: workstream.id,
@@ -0,0 +1,14 @@
1
+ import { type PlanningIdentityColor, type PlanningIdentityEntityType, type PlanningIdentityIcon } from '../../../shared/planningIdentity';
2
+ export type PlanningIdentityPatch = {
3
+ icon?: PlanningIdentityIcon | null;
4
+ color?: PlanningIdentityColor | null;
5
+ };
6
+ export declare function PlanningVisualIdentity({ entityType, entityId, icon, color, disabled, size, onChange, }: {
7
+ entityType: PlanningIdentityEntityType;
8
+ entityId: string;
9
+ icon?: PlanningIdentityIcon | null;
10
+ color?: PlanningIdentityColor | null;
11
+ disabled?: boolean;
12
+ size?: 'card' | 'inline' | 'detail';
13
+ onChange?: (patch: PlanningIdentityPatch) => void | Promise<void>;
14
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,106 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { BookOpen, Briefcase, Bug, Camera, Code, Compass, Flag, FlaskConical, Folder, Globe, Heart, Landmark, Layers, Lightbulb, Map, Music, Palette, Rocket, Route, Shield, Sparkles, Target, Users, Wrench, } from 'lucide-react';
5
+ import { PLANNING_IDENTITY_COLORS, PLANNING_IDENTITY_ICONS, planningIdentityColorToken, resolvePlanningIdentity, } from '../../../shared/planningIdentity';
6
+ import styles from './PlanningVisualIdentity.module.css';
7
+ const ICONS = {
8
+ Target, Route, Flag, Rocket, Briefcase, Folder, Layers, Map, Compass, Landmark,
9
+ BookOpen, Lightbulb, Wrench, Shield, Bug, Palette, Camera, Music, Heart, Globe,
10
+ Users, Sparkles, FlaskConical, Code,
11
+ };
12
+ export function PlanningVisualIdentity({ entityType, entityId, icon, color, disabled = false, size = 'card', onChange, }) {
13
+ const [open, setOpen] = React.useState(false);
14
+ const [pending, setPending] = React.useState(false);
15
+ const [optimisticPatch, setOptimisticPatch] = React.useState(null);
16
+ const [portalTheme, setPortalTheme] = React.useState();
17
+ const [popoverPosition, setPopoverPosition] = React.useState({ top: 0, left: 0 });
18
+ const rootRef = React.useRef(null);
19
+ const triggerRef = React.useRef(null);
20
+ const popoverRef = React.useRef(null);
21
+ const optimisticIcon = optimisticPatch && Object.prototype.hasOwnProperty.call(optimisticPatch, 'icon')
22
+ ? optimisticPatch.icon
23
+ : icon;
24
+ const optimisticColor = optimisticPatch && Object.prototype.hasOwnProperty.call(optimisticPatch, 'color')
25
+ ? optimisticPatch.color
26
+ : color;
27
+ const resolved = resolvePlanningIdentity({
28
+ entityType,
29
+ entityId,
30
+ icon: optimisticIcon,
31
+ color: optimisticColor,
32
+ });
33
+ const Icon = ICONS[resolved.icon];
34
+ const editable = !disabled && Boolean(onChange);
35
+ React.useEffect(() => {
36
+ if (!open)
37
+ return undefined;
38
+ const close = (event) => {
39
+ const target = event.target;
40
+ if (!rootRef.current?.contains(target) && !popoverRef.current?.contains(target))
41
+ setOpen(false);
42
+ };
43
+ document.addEventListener('pointerdown', close);
44
+ return () => document.removeEventListener('pointerdown', close);
45
+ }, [open]);
46
+ React.useLayoutEffect(() => {
47
+ if (!open)
48
+ return undefined;
49
+ const positionPopover = () => {
50
+ const triggerRect = triggerRef.current?.getBoundingClientRect();
51
+ if (!triggerRect)
52
+ return;
53
+ setPortalTheme(triggerRef.current?.closest('[data-theme]')?.dataset.theme);
54
+ const popoverRect = popoverRef.current?.getBoundingClientRect();
55
+ const width = popoverRect?.width || 248;
56
+ const height = popoverRect?.height || 320;
57
+ const gap = 8;
58
+ const viewportPadding = 8;
59
+ const left = Math.max(viewportPadding, Math.min(triggerRect.left, window.innerWidth - width - viewportPadding));
60
+ const preferredTop = triggerRect.bottom + gap;
61
+ const top = preferredTop + height <= window.innerHeight - viewportPadding
62
+ ? preferredTop
63
+ : Math.max(viewportPadding, triggerRect.top - height - gap);
64
+ setPopoverPosition({ top, left });
65
+ };
66
+ positionPopover();
67
+ window.addEventListener('resize', positionPopover);
68
+ window.addEventListener('scroll', positionPopover, true);
69
+ return () => {
70
+ window.removeEventListener('resize', positionPopover);
71
+ window.removeEventListener('scroll', positionPopover, true);
72
+ };
73
+ }, [open]);
74
+ React.useEffect(() => {
75
+ if (!optimisticPatch)
76
+ return;
77
+ const iconSettled = !Object.prototype.hasOwnProperty.call(optimisticPatch, 'icon')
78
+ || (optimisticPatch.icon ?? null) === (icon ?? null);
79
+ const colorSettled = !Object.prototype.hasOwnProperty.call(optimisticPatch, 'color')
80
+ || (optimisticPatch.color ?? null) === (color ?? null);
81
+ if (iconSettled && colorSettled)
82
+ setOptimisticPatch(null);
83
+ }, [color, icon, optimisticPatch]);
84
+ const commit = async (patch) => {
85
+ if (!onChange || pending)
86
+ return;
87
+ setOptimisticPatch((current) => ({ ...current, ...patch }));
88
+ setPending(true);
89
+ try {
90
+ await onChange(patch);
91
+ }
92
+ catch {
93
+ setOptimisticPatch(null);
94
+ }
95
+ finally {
96
+ setPending(false);
97
+ }
98
+ };
99
+ const marker = (_jsx("span", { className: `${styles.marker} ${styles[size]} ${disabled ? styles.disabled : ''}`.trim(), style: { color: planningIdentityColorToken(resolved.color) }, "aria-hidden": "true", children: _jsx(Icon, { size: size === 'detail' ? 26 : size === 'inline' ? 18 : 24, strokeWidth: 1.9 }) }));
100
+ if (!editable)
101
+ return marker;
102
+ return (_jsxs("div", { ref: rootRef, className: styles.root, onClick: (event) => event.stopPropagation(), children: [_jsx("button", { ref: triggerRef, type: "button", className: styles.trigger, onClick: () => setOpen((current) => !current), onKeyDown: (event) => event.stopPropagation(), "aria-label": `Change ${entityType} icon and color`, "aria-expanded": open, disabled: pending, children: marker }), open ? createPortal((_jsxs("div", { ref: popoverRef, className: styles.popover, role: "dialog", "aria-label": `${entityType} icon and color`, "aria-busy": pending, "data-theme": portalTheme, style: popoverPosition, onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: [_jsx("div", { className: styles.colorGrid, "aria-label": "Colors", children: PLANNING_IDENTITY_COLORS.map((option) => (_jsx("button", { type: "button", className: `${styles.colorOption} ${resolved.color === option ? styles.selected : ''}`.trim(), style: { backgroundColor: planningIdentityColorToken(option) }, onClick: () => void commit({ color: option }), "aria-label": `${option} color`, "aria-pressed": resolved.color === option, disabled: pending }, option))) }), _jsx("div", { className: styles.divider }), _jsx("div", { className: styles.iconGrid, "aria-label": "Icons", children: PLANNING_IDENTITY_ICONS.map((option) => {
103
+ const OptionIcon = ICONS[option];
104
+ return (_jsx("button", { type: "button", className: `${styles.iconOption} ${resolved.icon === option ? styles.selected : ''}`.trim(), onClick: () => void commit({ icon: option }), "aria-label": option, "aria-pressed": resolved.icon === option, disabled: pending, children: _jsx(OptionIcon, { size: 18, strokeWidth: 1.8 }) }, option));
105
+ }) }), _jsx("div", { className: styles.divider }), _jsx("button", { type: "button", className: styles.reset, onClick: () => void commit({ icon: null, color: null }), disabled: !resolved.customized || pending, children: "Reset to default" })] })), document.body) : null] }));
106
+ }
@@ -3,6 +3,7 @@ import type { TaskAssigneeOption } from '../../../utils/assignees';
3
3
  import type { TaskReferenceContract } from '../../../utils/taskReferences';
4
4
  import type { TaskforceTheme } from '../../../utils/theme';
5
5
  import type { PlanningTaskArrangeMode } from './planningTaskArrangement';
6
+ import type { PlanningIdentityColor, PlanningIdentityIcon } from '../../../shared/planningIdentity';
6
7
  export type PlanningStatusCounts = Record<TaskStatus, number>;
7
8
  export declare function shouldUsePlanningBootstrapTaskSummaries(input: {
8
9
  taskScope: 'open' | 'archived' | 'deleted';
@@ -34,6 +35,8 @@ export type PlanningNodeSummary = {
34
35
  description?: string;
35
36
  ownerId?: string | null;
36
37
  ownerLabel?: string | null;
38
+ icon?: PlanningIdentityIcon | null;
39
+ color?: PlanningIdentityColor | null;
37
40
  progressPercent: number;
38
41
  taskCount: number;
39
42
  completedTaskCount: number;
@@ -67,12 +70,16 @@ export type PlanningEntitySaveInput = {
67
70
  description: string | null;
68
71
  ownerId?: string | null;
69
72
  initiativeId?: string | null;
73
+ icon?: PlanningIdentityIcon | null;
74
+ color?: PlanningIdentityColor | null;
70
75
  };
71
76
  export type PlanningLinkOption = {
72
77
  id: string;
73
78
  referenceLabel: string;
74
79
  title: string;
75
80
  meta?: string;
81
+ icon?: PlanningIdentityIcon | null;
82
+ color?: PlanningIdentityColor | null;
76
83
  };
77
84
  export type PlanningLinkOptions = {
78
85
  initiatives: PlanningLinkOption[];
@@ -107,6 +114,10 @@ export type PlanningWorkspaceModel = {
107
114
  archiveEntity?: (entityType: 'initiative' | 'workstream', entityId: string) => boolean | Promise<boolean>;
108
115
  unarchiveEntity?: (entityType: 'initiative' | 'workstream', entityId: string) => boolean | Promise<boolean>;
109
116
  changeOwner?: (entityType: 'initiative' | 'workstream', entityId: string, ownerId: string) => void | Promise<void>;
117
+ changeIdentity?: (entityType: 'initiative' | 'workstream', entityId: string, patch: {
118
+ icon?: PlanningIdentityIcon | null;
119
+ color?: PlanningIdentityColor | null;
120
+ }) => void | Promise<void>;
110
121
  createEntity?: (entityType: 'initiative' | 'workstream', input: PlanningEntitySaveInput) => Promise<{
111
122
  id: string;
112
123
  }>;
@@ -4,6 +4,7 @@ import type { TaskAutoSaveState } from '../../hooks/tasks/useTaskFormActions';
4
4
  interface TaskActionHeaderProps {
5
5
  editingTaskId: string | null;
6
6
  loading: boolean;
7
+ submitDisabled?: boolean;
7
8
  autoSaveState?: TaskAutoSaveState;
8
9
  title: string;
9
10
  status?: TaskStatus;
@@ -30,5 +31,5 @@ interface TaskActionHeaderProps {
30
31
  handlePermanentlyDeleteDeletedTask?: (deletedRecordId: string) => void;
31
32
  onAskAgent?: (task: TaskItem) => void;
32
33
  }
33
- export declare function TaskActionHeader({ editingTaskId, loading, autoSaveState, title, status, onStatusChange, showCreateSubmit, handleSubmit, handleCopyId, copiedId, tasks, currentTask: currentTaskProp, currentTaskWorkstream, currentTaskInitiative, workstreams, onWorkstreamInputChange, onSetWorkstreamForCurrentTask, handleToggleInProgress, handleToggleReview, handleToggleComplete, handleToggleCancel, handleSetStatus, handleArchiveTask, handleUnarchiveTask, handleRestoreDeletedTask, handlePermanentlyDeleteDeletedTask, onAskAgent }: TaskActionHeaderProps): import("react/jsx-runtime").JSX.Element;
34
+ export declare function TaskActionHeader({ editingTaskId, loading, submitDisabled, autoSaveState, title, status, onStatusChange, showCreateSubmit, handleSubmit, handleCopyId, copiedId, tasks, currentTask: currentTaskProp, currentTaskWorkstream, currentTaskInitiative, workstreams, onWorkstreamInputChange, onSetWorkstreamForCurrentTask, handleToggleInProgress, handleToggleReview, handleToggleComplete, handleToggleCancel, handleSetStatus, handleArchiveTask, handleUnarchiveTask, handleRestoreDeletedTask, handlePermanentlyDeleteDeletedTask, onAskAgent }: TaskActionHeaderProps): import("react/jsx-runtime").JSX.Element;
34
35
  export {};
@@ -9,7 +9,7 @@ import { TaskReferenceBadge } from './TaskReferenceBadge';
9
9
  import { HierarchyActionBadge } from '../ui/HierarchyActionBadge';
10
10
  import { formatInitiativeReference, getWorkstreamReferenceLabel } from '../../utils/planningReferences';
11
11
  import { TaskWorkstreamPicker } from './TaskWorkstreamPicker';
12
- export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle', title, status = 'task', onStatusChange, showCreateSubmit = true, handleSubmit, handleCopyId, copiedId, tasks, currentTask: currentTaskProp, currentTaskWorkstream, currentTaskInitiative, workstreams = [], onWorkstreamInputChange, onSetWorkstreamForCurrentTask, handleToggleInProgress, handleToggleReview, handleToggleComplete, handleToggleCancel, handleSetStatus, handleArchiveTask, handleUnarchiveTask, handleRestoreDeletedTask, handlePermanentlyDeleteDeletedTask, onAskAgent }) {
12
+ export function TaskActionHeader({ editingTaskId, loading, submitDisabled = false, autoSaveState = 'idle', title, status = 'task', onStatusChange, showCreateSubmit = true, handleSubmit, handleCopyId, copiedId, tasks, currentTask: currentTaskProp, currentTaskWorkstream, currentTaskInitiative, workstreams = [], onWorkstreamInputChange, onSetWorkstreamForCurrentTask, handleToggleInProgress, handleToggleReview, handleToggleComplete, handleToggleCancel, handleSetStatus, handleArchiveTask, handleUnarchiveTask, handleRestoreDeletedTask, handlePermanentlyDeleteDeletedTask, onAskAgent }) {
13
13
  const currentTask = currentTaskProp ?? (editingTaskId ? tasks.find(t => t.id === editingTaskId) || null : null);
14
14
  const isArchivedReadOnly = Boolean(currentTask?.isArchived);
15
15
  const isDeletedReadOnly = Boolean(currentTask?.isDeleted);
@@ -67,5 +67,5 @@ export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle
67
67
  if (status === 'cancelled') {
68
68
  handleToggleCancel(task);
69
69
  }
70
- }, onArchiveTask: handleArchiveTask, onUnarchiveTask: (task) => handleUnarchiveTask?.(task.id) }))] })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.taskSaveStatusCenter }), _jsxs("div", { className: styles.editActionsGroup, children: [_jsx(TaskStatusActions, { status: status, disabled: loading, archiveActionMode: "none", onStatusChange: onStatusChange }), showCreateSubmit ? (_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')] })) : null] })] }))] }));
70
+ }, onArchiveTask: handleArchiveTask, onUnarchiveTask: (task) => handleUnarchiveTask?.(task.id) }))] })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.taskSaveStatusCenter }), _jsxs("div", { className: styles.editActionsGroup, children: [_jsx(TaskStatusActions, { status: status, disabled: loading, archiveActionMode: "none", onStatusChange: onStatusChange }), showCreateSubmit ? (_jsxs("button", { type: "button", onClick: (e) => handleSubmit(e), disabled: loading || submitDisabled || !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')] })) : null] })] }))] }));
71
71
  }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import * as Icons from 'lucide-react';
4
- const { MessageSquare, Gauge, ClipboardList, HelpCircle, File } = Icons;
4
+ const { MessageSquare, Gauge, ClipboardList, HelpCircle } = Icons;
5
5
  import styles from '../../Taskforce.module.css';
6
6
  import cardStyles from './TaskCard.module.css';
7
7
  import { Markdown } from '../ui/Markdown';
@@ -22,6 +22,7 @@ import { getInitiativeReferenceLabel, getWorkstreamReferenceLabel } from '../../
22
22
  import { TaxonomyDropdown } from '../ui/TaxonomyDropdown';
23
23
  import { AssigneeAvatar, AssigneeIdentity } from '../ui/AssigneeIdentity';
24
24
  import { TaskWorkstreamPicker } from './TaskWorkstreamPicker';
25
+ import { DocumentTypeIcon } from '../ui/DocumentTypeIcon';
25
26
  const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [], types = [], priorities = [], categories = [], assigneeOptions = [], workstreams = [], taskWorkstream = null, taskInitiative = null, workspaceId = null, isOverlay = false, isArchived = false, readOnlyMode = null, isRecentlyChanged = false, selectable = false, selected = false, onSelectionChange, onClick, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onDelete, deleteActionTitle, taskReferences, onAssigneeChange, onWorkstreamChange, compressed = false, showStatusLabel = false }) => {
26
27
  const taskReference = getTaskReferenceDisplayState(task);
27
28
  const taskReferenceLabel = taskReference.label || (taskReference.isProvisional ? 'Pending' : '');
@@ -228,6 +229,24 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
228
229
  workspaceId,
229
230
  });
230
231
  };
232
+ const cardCoverAttachment = (() => {
233
+ if (compressed || isOverlay || !task.cardCoverAssetId)
234
+ return null;
235
+ return (task.attachments || []).find((attachment) => (typeof attachment !== 'string'
236
+ && String(attachment.assetId || '').trim() === task.cardCoverAssetId
237
+ && attachment.linkRole === 'image'
238
+ && isImageAttachment(attachment))) || null;
239
+ })();
240
+ const cardCoverIdentity = cardCoverAttachment
241
+ ? `${cardCoverAttachment.assetId || ''}:${cardCoverAttachment.path}`
242
+ : '';
243
+ const cardAttachments = (task.attachments || []).filter((attachment) => (!cardCoverAttachment
244
+ || typeof attachment === 'string'
245
+ || attachment.assetId !== cardCoverAttachment.assetId));
246
+ const [cardCoverLoadFailed, setCardCoverLoadFailed] = React.useState(false);
247
+ React.useEffect(() => {
248
+ setCardCoverLoadFailed(false);
249
+ }, [cardCoverIdentity]);
231
250
  return (_jsxs("div", { className: cardClasses, onClick: () => onClick?.(task), style: cardStyle, "data-task-card": "true", children: [_jsxs("div", { className: cardStyles.taskHeader, children: [selectable && (_jsx("label", { className: cardStyles.selectionControl, onClick: (event) => event.stopPropagation(), onPointerDown: (event) => event.stopPropagation(), children: _jsx("input", { type: "checkbox", checked: selected, onChange: (event) => onSelectionChange?.(event.target.checked), "aria-label": `Select ${taskReferenceLabel || task.title} for restore` }) })), _jsxs("div", { className: cardStyles.taskReferenceCluster, children: [taskInitiative ? (_jsxs(_Fragment, { children: [_jsx(TaskReferenceBadge, { copied: copiedId === initiativeReferenceLabel, onClick: (event) => handleReferenceCopy(event, initiativeReferenceLabel), disabled: !initiativeReferenceLabel, title: taskInitiative.title, ariaLabel: `Copy initiative reference ${initiativeReferenceLabel}: ${taskInitiative.title}`, className: isArchived ? cardStyles.archiveBadge : '', label: initiativeReferenceLabel }), _jsx("span", { className: cardStyles.taskReferenceDivider, children: "/" })] })) : null, taskWorkstream ? (_jsxs(_Fragment, { children: [_jsx(TaskReferenceBadge, { copied: copiedId === workstreamReferenceLabel, onClick: (event) => handleReferenceCopy(event, workstreamReferenceLabel), disabled: !workstreamReferenceLabel, title: taskWorkstream.title, ariaLabel: `Copy workstream reference ${workstreamReferenceLabel}: ${taskWorkstream.title}`, className: isArchived ? cardStyles.archiveBadge : '', label: workstreamReferenceLabel }), _jsx("span", { className: cardStyles.taskReferenceDivider, children: "/" })] })) : null, canChangeWorkstream && !taskWorkstream ? (_jsx("div", { className: cardStyles.taskReferenceSegment, onClick: (event) => event.stopPropagation(), onPointerDown: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: _jsx(TaskWorkstreamPicker, { workstreams: workstreams, disabled: workstreamChangeBusy, onSelect: (workstream) => {
232
251
  void handleWorkstreamChange(workstream.id);
233
252
  } }) })) : null, taskReferenceLabel ? (_jsx("div", { className: cardStyles.taskReferenceSegment, onClick: (event) => event.stopPropagation(), onPointerDown: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: _jsx(TaskReferenceBadge, { copied: canCopyTaskReference && copiedId === taskReference.label, onClick: (e) => handleReferenceCopy(e, taskReference.label), disabled: !canCopyTaskReference, title: isProvisionalReference
@@ -256,7 +275,20 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
256
275
  }
257
276
  }, onArchiveTask: onArchiveTask })) : ((onUnarchive || onDelete) ? (_jsxs(_Fragment, { children: [onUnarchive && (_jsx("button", { type: "button", className: styles.actionBtn, onClick: () => onUnarchive(task.id), title: resolvedReadOnlyMode === 'deleted'
258
277
  ? `Restore to ${task.isArchived === true ? 'Archived' : 'Active'}`
259
- : 'Restore archived task', children: _jsx(Icons.RotateCcw, { size: 16 }) })), onDelete && (_jsx("button", { type: "button", className: `${styles.actionBtn} ${styles.deleteBtn}`, onClick: () => onDelete(task.id), title: deleteActionTitle || (resolvedReadOnlyMode === 'deleted' ? 'Delete Permanently' : 'Move to Trash'), children: _jsx(Icons.Trash2, { size: 16 }) }))] })) : null) })] }), _jsxs("div", { className: cardStyles.taskContent, children: [/* Meta Badges moved to bottom */ isReadOnly && (_jsx("div", { className: `${cardStyles.taskMeta} ${cardStyles.taskMetaCompact}` })), _jsxs("div", { children: [_jsxs("div", { className: cardStyles.taskTitle, style: compressed ? { fontSize: '13px', lineHeight: '1.4' } : {}, children: [resolvedReadOnlyMode === 'archived' && '✓ ', resolvedReadOnlyMode === 'deleted' && 'Deleted: ', renderHighlighted(task.title, searchQuery)] }), deletedExpiryLabel && (_jsx("div", { className: cardStyles.deletedExpiry, title: "Trash items are permanently deleted 90 days after deletion.", children: deletedExpiryLabel })), task.description && !isReadOnly && !compressed && (_jsx("div", { className: cardStyles.taskDescription, children: _jsx(Markdown, { variant: "compact", taskReferences: taskReferences, children: task.description }) }))] }), task.attachments && task.attachments.length > 0 && !isReadOnly && !compressed && (_jsxs("div", { className: cardStyles.attachmentThumbnails, children: [_jsx("div", { className: cardStyles.attachmentImageRow, children: (task.attachments || []).map((attachment, idx) => {
278
+ : 'Restore archived task', children: _jsx(Icons.RotateCcw, { size: 16 }) })), onDelete && (_jsx("button", { type: "button", className: `${styles.actionBtn} ${styles.deleteBtn}`, onClick: () => onDelete(task.id), title: deleteActionTitle || (resolvedReadOnlyMode === 'deleted' ? 'Delete Permanently' : 'Move to Trash'), children: _jsx(Icons.Trash2, { size: 16 }) }))] })) : null) })] }), cardCoverAttachment && !cardCoverLoadFailed && (_jsx("button", { type: "button", className: cardStyles.taskCover, "aria-label": `Preview ${getContextName(cardCoverAttachment)}`, onPointerDown: (event) => event.stopPropagation(), onClick: (event) => {
279
+ event.stopPropagation();
280
+ if (handleAttachmentActivation(cardCoverAttachment))
281
+ return;
282
+ if (dispatchOpenAnnotatedAttachment(cardCoverAttachment, {
283
+ taskId: task.id,
284
+ taskReferenceLabel,
285
+ }))
286
+ return;
287
+ window.open(cardCoverAttachment.path, '_blank');
288
+ }, children: _jsx("img", { src: cardCoverAttachment.path, alt: cardCoverAttachment.caption
289
+ || cardCoverAttachment.displayName
290
+ || cardCoverAttachment.originalFilename
291
+ || task.title, loading: "lazy", decoding: "async", onError: () => setCardCoverLoadFailed(true) }) })), _jsxs("div", { className: cardStyles.taskContent, children: [/* Meta Badges moved to bottom */ isReadOnly && (_jsx("div", { className: `${cardStyles.taskMeta} ${cardStyles.taskMetaCompact}` })), _jsxs("div", { children: [_jsxs("div", { className: cardStyles.taskTitle, style: compressed ? { fontSize: '13px', lineHeight: '1.4' } : {}, children: [resolvedReadOnlyMode === 'archived' && '✓ ', resolvedReadOnlyMode === 'deleted' && 'Deleted: ', renderHighlighted(task.title, searchQuery)] }), deletedExpiryLabel && (_jsx("div", { className: cardStyles.deletedExpiry, title: "Trash items are permanently deleted 90 days after deletion.", children: deletedExpiryLabel })), task.description && !isReadOnly && !compressed && (_jsx("div", { className: cardStyles.taskDescription, children: _jsx(Markdown, { variant: "compact", taskReferences: taskReferences, children: task.description }) }))] }), cardAttachments.length > 0 && !isReadOnly && !compressed && (_jsxs("div", { className: cardStyles.attachmentThumbnails, children: [_jsx("div", { className: cardStyles.attachmentImageRow, children: cardAttachments.map((attachment, idx) => {
260
292
  const path = typeof attachment === 'string' ? attachment : attachment.path;
261
293
  if (!isImageAttachment(attachment))
262
294
  return null;
@@ -268,13 +300,14 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
268
300
  return;
269
301
  window.open(path, '_blank');
270
302
  }, children: _jsx("img", { src: path, alt: `Attachment ${idx}` }) }, idx));
271
- }) }), _jsx("div", { className: cardStyles.attachmentDocumentList, children: (task.attachments || []).map((attachment, idx) => {
303
+ }) }), _jsx("div", { className: cardStyles.attachmentDocumentList, children: cardAttachments.map((attachment, idx) => {
272
304
  const path = typeof attachment === 'string' ? attachment : attachment.path;
273
305
  const fsPath = typeof attachment === 'string' ? undefined : attachment.fsPath;
274
306
  const name = getContextName(attachment);
275
307
  if (isImageAttachment(attachment))
276
308
  return null;
277
309
  const fileType = getContextType(attachment);
310
+ const fileAccentColor = getFileTypeAccentColor(fileType);
278
311
  return (_jsxs("button", { type: "button", className: cardStyles.contextDocThumb, onPointerDown: (event) => event.stopPropagation(), onClick: (e) => {
279
312
  e.stopPropagation();
280
313
  if (handleAttachmentActivation(attachment))
@@ -285,7 +318,7 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
285
318
  if (dispatchOpenMarkdownDocument(markdownTarget, fsPath))
286
319
  return;
287
320
  window.open(path, '_blank');
288
- }, title: name, style: { '--context-doc-accent': getFileTypeAccentColor(fileType) }, children: [_jsxs("span", { className: cardStyles.contextDocMain, children: [_jsx("span", { className: cardStyles.contextDocIcon, "aria-hidden": "true", children: _jsx(File, { size: 13 }) }), _jsx("span", { className: cardStyles.contextDocText, children: _jsx("span", { className: cardStyles.contextDocName, children: name }) })] }), _jsx("span", { className: cardStyles.contextDocType, children: fileType })] }, idx));
321
+ }, title: name, "aria-label": `${name} ${fileType}`, style: { '--context-doc-accent': fileAccentColor }, children: [_jsx(DocumentTypeIcon, { label: fileType, accentColor: fileAccentColor, compact: true }), _jsx("span", { className: cardStyles.contextDocMain, children: _jsx("span", { className: cardStyles.contextDocName, children: name }) })] }, idx));
289
322
  }) })] })), task.status === 'cancelled' && task.canceledReason && !compressed && (_jsxs("div", { className: cardStyles.taskCancellationReason, children: [_jsx("strong", { children: "Cancellation Reason:" }), " ", renderHighlighted(task.canceledReason, searchQuery)] })), (latestActivityText || commentCount > 0) && !isReadOnly && !compressed && (_jsxs("div", { className: cardStyles.taskLatestComment, "data-task-activity-summary": "true", children: [_jsxs("div", { className: cardStyles.taskLatestCommentHeader, children: [_jsx("strong", { children: latestActivityText ? 'Latest Activity:' : 'Activity:' }), commentCount > 0 && (_jsxs("span", { className: `${styles.metaBadge} ${cardStyles.taskCommentCount}`, title: `${commentCount} ${commentCount === 1 ? 'comment' : 'comments'}`, "aria-label": `${commentCount} ${commentCount === 1 ? 'comment' : 'comments'}`, children: [_jsx(MessageSquare, { size: 12, "aria-hidden": "true" }), _jsx("span", { children: commentCount })] }))] }), latestActivityText && (_jsx("div", { className: cardStyles.taskLatestCommentText, title: latestActivityText, children: renderHighlighted(latestActivityText, searchQuery) }))] })), !isReadOnly && visibleTaxonomies.length > 0 && !compressed && (_jsx("div", { className: `${cardStyles.taskSpecialists} ${cardStyles.taxonomiesList}`, children: visibleTaxonomies.map(tax => {
290
323
  const val = task.taxonomies?.[tax.id];
291
324
  if (!val)
@@ -9,6 +9,9 @@ interface TaskContextUploadProps {
9
9
  onAddContextFile: (file: AttachmentItem) => void;
10
10
  onRemoveContextFile: (index: number) => void;
11
11
  onUpdateContextCaption: (index: number, caption: string) => void;
12
+ cardCoverAssetId?: string | null;
13
+ onSetCardCover?: (assetId: string | null) => void | Promise<void>;
14
+ cardCoverDisabled?: boolean;
12
15
  }
13
16
  export declare function TaskContextUpload(props: TaskContextUploadProps): import("react/jsx-runtime").JSX.Element;
14
17
  export {};
@@ -2,6 +2,6 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { ContextAttachmentManager } from '../context/ContextAttachmentManager';
3
3
  import { TaskImageReviews } from './TaskImageReviews';
4
4
  export function TaskContextUpload(props) {
5
- const { taskId, taskReferenceLabel, workspaceId, apiBaseUrl, showImageReviews = false, contextFiles, onAddContextFile, onRemoveContextFile, onUpdateContextCaption, } = props;
6
- return (_jsx(ContextAttachmentManager, { ownerType: "task", ownerId: taskId, ownerReferenceLabel: taskReferenceLabel, workspaceId: workspaceId, apiBaseUrl: apiBaseUrl, attachments: contextFiles, onAddAttachment: onAddContextFile, onRemoveAttachment: onRemoveContextFile, onUpdateAttachmentCaption: onUpdateContextCaption, supplementalContent: showImageReviews && taskId && workspaceId ? (_jsx(TaskImageReviews, { taskId: taskId, taskReferenceLabel: taskReferenceLabel, workspaceId: workspaceId, apiBaseUrl: apiBaseUrl })) : null }));
5
+ const { taskId, taskReferenceLabel, workspaceId, apiBaseUrl, showImageReviews = false, contextFiles, onAddContextFile, onRemoveContextFile, onUpdateContextCaption, cardCoverAssetId, onSetCardCover, cardCoverDisabled, } = props;
6
+ return (_jsx(ContextAttachmentManager, { ownerType: "task", ownerId: taskId, ownerReferenceLabel: taskReferenceLabel, workspaceId: workspaceId, apiBaseUrl: apiBaseUrl, attachments: contextFiles, onAddAttachment: onAddContextFile, onRemoveAttachment: onRemoveContextFile, onUpdateAttachmentCaption: onUpdateContextCaption, cardCoverAssetId: cardCoverAssetId, onSetCardCover: onSetCardCover, cardCoverDisabled: cardCoverDisabled, supplementalContent: showImageReviews && taskId && workspaceId ? (_jsx(TaskImageReviews, { taskId: taskId, taskReferenceLabel: taskReferenceLabel, workspaceId: workspaceId, apiBaseUrl: apiBaseUrl })) : null }));
7
7
  }
@@ -22,6 +22,7 @@ interface TaskFormProps {
22
22
  newCommentText: string;
23
23
  contextFiles: Array<string | AttachmentItem>;
24
24
  currentWorkspaceId?: string | null;
25
+ descriptionImageDraftId?: string | null;
25
26
  currentActorId?: string | null;
26
27
  apiBaseUrl?: string;
27
28
  runtimeMode?: 'local' | 'cloud';
@@ -73,9 +74,12 @@ interface TaskFormProps {
73
74
  onOpenSettings?: (section: "categories" | "types") => void;
74
75
  onSubmit: (e: React.FormEvent) => void;
75
76
  onAddComment: () => void;
77
+ onDescriptionImageUploadStart?: (upload: Promise<void>) => void;
76
78
  onAddContextFile: (file: AttachmentItem) => void;
77
79
  onRemoveContextFile: (index: number) => void;
78
80
  onUpdateContextCaption: (index: number, caption: string) => void;
81
+ onSetCardCover?: (assetId: string | null) => void | Promise<void>;
82
+ cardCoverDisabled?: boolean;
79
83
  onOpenContextImage?: (attachment: AttachmentItem) => void;
80
84
  onCopyId: (e: React.MouseEvent, id: string) => void;
81
85
  onToggleInProgress?: (task: TaskItem) => void;
@@ -50,7 +50,7 @@ function SortableChecklistRow({ id, item, onToggle, onRemove, }) {
50
50
  : "Mark checklist item complete", children: item.isCompleted ? _jsx(Check, { size: 14, strokeWidth: 2.1 }) : null }), _jsx("span", { className: `${formStyles.checklistItemText} ${item.isCompleted ? formStyles.checklistItemTextCompleted : ""}`.trim(), children: item.title }), _jsx("button", { type: "button", className: formStyles.checklistRemoveBtn, onClick: onRemove, title: "Remove checklist item", "aria-label": "Remove checklist item", children: _jsx(X, { size: 15, strokeWidth: 1.9 }) })] }));
51
51
  }
52
52
  export function TaskForm(props) {
53
- const { editingTaskId, initialActivityEntryId, title, description, category, type, priority, complexity, manualComplexityEnabled = false, assignee, scheduledDate, dueDate, checklistItems, comments, newCommentText, contextFiles, currentWorkspaceId, currentActorId, apiBaseUrl, runtimeMode = 'local', showImageReviews = false, descriptionFocused, showMarkdownHelp, checklistEnabled = true, formTaxonomies, onTaxonomyChange, onOpenSettings, categories, types, priorities, taxonomyDisplayLabels, assigneeOptions, taxonomies, workstreams = [], initiatives = [], copiedId, onTitleChange, onTitleDraftChange, onDescriptionChange, onDescriptionDraftChange, onCategoryChange, onTypeChange, onPriorityChange, onComplexityChange, onAssigneeChange, onScheduledDateChange, onDueDateChange, onChecklistItemsChange, onNewCommentTextChange, onDescriptionFocusedChange, onShowMarkdownHelpChange, onSubmit, onAddComment, onAddContextFile, onRemoveContextFile, onUpdateContextCaption, onOpenContextImage, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, taskReferences, currentTask, commentsEndRef, } = props;
53
+ const { editingTaskId, initialActivityEntryId, title, description, category, type, priority, complexity, manualComplexityEnabled = false, assignee, scheduledDate, dueDate, checklistItems, comments, newCommentText, contextFiles, currentWorkspaceId, descriptionImageDraftId, currentActorId, apiBaseUrl, runtimeMode = 'local', showImageReviews = false, descriptionFocused, showMarkdownHelp, checklistEnabled = true, formTaxonomies, onTaxonomyChange, onOpenSettings, categories, types, priorities, taxonomyDisplayLabels, assigneeOptions, taxonomies, workstreams = [], initiatives = [], copiedId, onTitleChange, onTitleDraftChange, onDescriptionChange, onDescriptionDraftChange, onCategoryChange, onTypeChange, onPriorityChange, onComplexityChange, onAssigneeChange, onScheduledDateChange, onDueDateChange, onChecklistItemsChange, onNewCommentTextChange, onDescriptionFocusedChange, onShowMarkdownHelpChange, onSubmit, onAddComment, onDescriptionImageUploadStart, onAddContextFile, onRemoveContextFile, onUpdateContextCaption, onSetCardCover, cardCoverDisabled, onOpenContextImage, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, taskReferences, currentTask, commentsEndRef, } = props;
54
54
  const emitTitleDraftChange = onTitleDraftChange || onTitleChange;
55
55
  const emitDescriptionDraftChange = onDescriptionDraftChange || onDescriptionChange;
56
56
  const currentTaskReferenceLabel = getTaskReferenceLabel(currentTask);
@@ -66,6 +66,7 @@ export function TaskForm(props) {
66
66
  descriptionImageGenerationRef.current += 1;
67
67
  }
68
68
  const [descriptionImageUploading, setDescriptionImageUploading] = React.useState(false);
69
+ const descriptionImageUploadRef = React.useRef(null);
69
70
  const [descriptionImageError, setDescriptionImageError] = React.useState(null);
70
71
  React.useLayoutEffect(() => {
71
72
  const taskChanged = textDraftOwnerRef.current !== editingTaskId;
@@ -80,6 +81,7 @@ export function TaskForm(props) {
80
81
  textDraftOwnerRef.current = editingTaskId;
81
82
  }, [description, editingTaskId, title]);
82
83
  React.useEffect(() => {
84
+ descriptionImageUploadRef.current = null;
83
85
  setDescriptionImageUploading(false);
84
86
  setDescriptionImageError(null);
85
87
  return () => {
@@ -260,14 +262,11 @@ export function TaskForm(props) {
260
262
  window.requestAnimationFrame(() => checklistAddTriggerRef.current?.focus());
261
263
  }
262
264
  };
263
- const addDescriptionImages = async (files, selectionStart, selectionEnd) => {
264
- if (files.length === 0 || descriptionImageUploading)
265
- return;
265
+ const performDescriptionImageUpload = async (files, selectionStart, selectionEnd) => {
266
266
  const uploadGeneration = descriptionImageGenerationRef.current;
267
267
  const uploadOwnerId = editingTaskId;
268
268
  const uploadIsCurrent = () => descriptionImageGenerationRef.current === uploadGeneration &&
269
269
  descriptionImageOwnerRef.current === descriptionImageOwnerKey;
270
- setDescriptionImageUploading(true);
271
270
  setDescriptionImageError(null);
272
271
  const uploadedReferences = [];
273
272
  let failedCount = 0;
@@ -283,6 +282,12 @@ export function TaskForm(props) {
283
282
  ownerType: "task",
284
283
  ownerId: uploadOwnerId,
285
284
  workspaceId: currentWorkspaceId,
285
+ ...(!uploadOwnerId && descriptionImageDraftId
286
+ ? {
287
+ draftUploadId: descriptionImageDraftId,
288
+ uploadPurpose: "task-description-draft",
289
+ }
290
+ : {}),
286
291
  });
287
292
  if (!uploadIsCurrent())
288
293
  return;
@@ -338,7 +343,27 @@ export function TaskForm(props) {
338
343
  : "The images could not be added.");
339
344
  setDescriptionImageError(failureMessage);
340
345
  }
341
- setDescriptionImageUploading(false);
346
+ };
347
+ const addDescriptionImages = (files, selectionStart, selectionEnd) => {
348
+ if (files.length === 0 || descriptionImageUploadRef.current) {
349
+ return descriptionImageUploadRef.current || Promise.resolve();
350
+ }
351
+ setDescriptionImageUploading(true);
352
+ const upload = performDescriptionImageUpload(files, selectionStart, selectionEnd);
353
+ descriptionImageUploadRef.current = upload;
354
+ onDescriptionImageUploadStart?.(upload);
355
+ void upload.then(() => {
356
+ if (descriptionImageUploadRef.current !== upload)
357
+ return;
358
+ descriptionImageUploadRef.current = null;
359
+ setDescriptionImageUploading(false);
360
+ }, () => {
361
+ if (descriptionImageUploadRef.current !== upload)
362
+ return;
363
+ descriptionImageUploadRef.current = null;
364
+ setDescriptionImageUploading(false);
365
+ });
366
+ return upload;
342
367
  };
343
368
  const getDescriptionClipboardImages = (dataTransfer) => Array.from(dataTransfer.items)
344
369
  .filter((item) => item.kind === "file" &&
@@ -855,7 +880,7 @@ export function TaskForm(props) {
855
880
  .map((entry, entryIndex) => ({
856
881
  ...entry,
857
882
  order: entryIndex,
858
- }))) }, item.id || `checklist-${index}`))) }) }))] }))] }))] }), _jsxs("div", { children: [_jsx(React.Suspense, { fallback: null, children: _jsx(DeferredTaskContextUpload, { taskId: editingTaskId, taskReferenceLabel: currentTaskReferenceLabel, workspaceId: currentWorkspaceId, apiBaseUrl: apiBaseUrl, showImageReviews: showImageReviews && Boolean(currentTask), contextFiles: contextFiles, onAddContextFile: onAddContextFile, onRemoveContextFile: onRemoveContextFile, onUpdateContextCaption: onUpdateContextCaption }) }), _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
883
+ }))) }, item.id || `checklist-${index}`))) }) }))] }))] }))] }), _jsxs("div", { children: [_jsx(React.Suspense, { fallback: null, children: _jsx(DeferredTaskContextUpload, { taskId: editingTaskId, taskReferenceLabel: currentTaskReferenceLabel, workspaceId: currentWorkspaceId, apiBaseUrl: apiBaseUrl, showImageReviews: showImageReviews && Boolean(currentTask), contextFiles: contextFiles, onAddContextFile: onAddContextFile, onRemoveContextFile: onRemoveContextFile, onUpdateContextCaption: onUpdateContextCaption, cardCoverAssetId: currentTask?.cardCoverAssetId, onSetCardCover: onSetCardCover, cardCoverDisabled: cardCoverDisabled }) }), _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
859
884
  ? `by ${createdByLabel}`
860
885
  : 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
861
886
  ? `by ${updatedByLabel}`