@taskforcehq/taskforce 0.3.303 → 0.3.305

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +102 -5
  3. package/dist/TaskforceCore.js +635 -238
  4. package/dist/TaskforceCore.test.js +2243 -495
  5. package/dist/annotatedAttachments/service.d.ts +1 -1
  6. package/dist/annotatedAttachments/types.d.ts +1 -1
  7. package/dist/compat/workspaceSyncCompat.js +6 -0
  8. package/dist/components/features/AgentsModule.js +1 -1
  9. package/dist/components/features/AgentsModule.test.js +1 -1
  10. package/dist/components/features/DocumentIndex.d.ts +1 -1
  11. package/dist/components/features/DocumentIndex.js +1 -1
  12. package/dist/components/features/DocumentViewer.d.ts +1 -1
  13. package/dist/components/features/DocumentViewer.js +32 -9
  14. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  15. package/dist/components/features/DocumentWorkspace.js +12 -155
  16. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  17. package/dist/components/features/TaskSettings.js +87 -41
  18. package/dist/components/features/TaskSettings.test.js +180 -40
  19. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  20. package/dist/components/features/documentWorkspaceModel.js +57 -0
  21. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  22. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  23. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  24. package/dist/components/task/TaskActionHeader.js +3 -3
  25. package/dist/components/task/TaskActionHeader.test.js +8 -0
  26. package/dist/components/task/TaskCard.js +16 -1
  27. package/dist/components/task/TaskCard.test.js +31 -0
  28. package/dist/components/task/TaskForm.d.ts +9 -8
  29. package/dist/components/task/TaskForm.js +374 -219
  30. package/dist/components/task/TaskForm.test.js +482 -361
  31. package/dist/components/task/TaskKanban.d.ts +1 -1
  32. package/dist/components/task/TaskKanban.js +33 -161
  33. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  34. package/dist/components/task/TaskStatusActions.js +8 -3
  35. package/dist/components/task/TaskStatusActions.test.js +9 -0
  36. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  37. package/dist/components/task/taskKanbanDropRules.js +187 -0
  38. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  39. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  40. package/dist/components/ui/Modal.d.ts +1 -1
  41. package/dist/components/ui/Modal.js +1 -0
  42. package/dist/components/views/AppShellHeader.js +5 -1
  43. package/dist/components/views/AppShellHeader.test.js +4 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  45. package/dist/components/views/PlanComparisonPage.js +9 -10
  46. package/dist/components/views/PlanComparisonPage.test.js +55 -41
  47. package/dist/components/views/PlansPage.d.ts +5 -1
  48. package/dist/components/views/PlansPage.js +150 -81
  49. package/dist/components/views/PlansPage.test.js +367 -41
  50. package/dist/components/views/StandaloneLayout.js +135 -54
  51. package/dist/components/views/WidgetView.js +11 -2
  52. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  53. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  54. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  55. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  56. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  57. package/dist/config/envSchema.js +1 -1
  58. package/dist/core/AiProfileService.d.ts +6 -1
  59. package/dist/core/AiProfileService.js +161 -16
  60. package/dist/core/AiProfileService.test.js +3 -1
  61. package/dist/core/AiProfiles.test.js +200 -0
  62. package/dist/core/AuthTokenService.test.d.ts +1 -0
  63. package/dist/core/AuthTokenService.test.js +78 -0
  64. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  65. package/dist/core/EntitlementsPolicy.test.js +75 -13
  66. package/dist/core/PlanEntitlementService.d.ts +9 -1
  67. package/dist/core/PlanEntitlementService.js +144 -19
  68. package/dist/core/PlanVersionPolicy.test.js +19 -26
  69. package/dist/core/SignupMonetizationSettings.test.js +75 -15
  70. package/dist/core/SystemAdmin.test.js +212 -56
  71. package/dist/core/TaskActivityService.d.ts +4 -1
  72. package/dist/core/TaskActivityService.js +45 -0
  73. package/dist/core/TaskAuditTimeline.test.js +52 -1
  74. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  75. package/dist/core/Taskforce.d.ts +18 -11
  76. package/dist/core/Taskforce.js +93 -12
  77. package/dist/core/WorkspacePlanMode.test.js +73 -8
  78. package/dist/core/shared.d.ts +2 -0
  79. package/dist/core/shared.js +11 -0
  80. package/dist/documentReviews/service.d.ts +1 -1
  81. package/dist/documentReviews/types.d.ts +1 -1
  82. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  83. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  84. package/dist/hooks/sync/auth.d.ts +37 -0
  85. package/dist/hooks/sync/auth.js +63 -0
  86. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  87. package/dist/hooks/sync/bootstrap.js +52 -0
  88. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  89. package/dist/hooks/sync/orchestratorShared.js +209 -0
  90. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  91. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  92. package/dist/hooks/sync/realtime.d.ts +25 -0
  93. package/dist/hooks/sync/realtime.js +60 -0
  94. package/dist/hooks/sync/recovery.d.ts +69 -0
  95. package/dist/hooks/sync/recovery.js +118 -0
  96. package/dist/hooks/sync/transfers.d.ts +123 -0
  97. package/dist/hooks/sync/transfers.js +435 -0
  98. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  99. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  100. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  101. package/dist/hooks/ui/useResourceExport.js +5 -2
  102. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  103. package/dist/hooks/useRealtimeSync.js +2 -1
  104. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  105. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  106. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  107. package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
  108. package/dist/hooks/useSyncOrchestrator.js +388 -879
  109. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
  110. package/dist/hooks/useTaskMutations.d.ts +2 -1
  111. package/dist/hooks/useTaskMutations.js +57 -7
  112. package/dist/hooks/useTaskMutations.test.js +176 -0
  113. package/dist/hooks/useTaskforce.d.ts +13 -4
  114. package/dist/hooks/useTaskforce.js +302 -817
  115. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  116. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  117. package/dist/hooks/useWorkspaceSyncController.js +33 -11
  118. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  119. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  120. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  121. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  122. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  123. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  124. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  125. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  126. package/dist/localization/locales/en-US.js +2 -2
  127. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  128. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  129. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  130. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  131. package/dist/mcp/clientIntegrations.d.ts +12 -1
  132. package/dist/mcp/clientIntegrations.js +161 -6
  133. package/dist/mcp/clientIntegrations.test.js +76 -4
  134. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  135. package/dist/mcp/collaborationRegistrar.js +71 -0
  136. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  137. package/dist/mcp/documentAssetRegistrar.js +346 -0
  138. package/dist/mcp/documentHelpers.d.ts +100 -0
  139. package/dist/mcp/documentHelpers.js +161 -0
  140. package/dist/mcp/runtime.js +2455 -3925
  141. package/dist/mcp/runtime.test.js +240 -8
  142. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  143. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  144. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  145. package/dist/mcp/taskAttachmentTypes.js +1 -0
  146. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  147. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  148. package/dist/mcp/toolCatalog.d.ts +35 -0
  149. package/dist/mcp/toolCatalog.js +3 -0
  150. package/dist/plugins/vite.js +1 -1
  151. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  152. package/dist/runtime/appGateDefinitions.js +1 -1
  153. package/dist/server/index.d.ts +1 -0
  154. package/dist/server/index.js +46 -3
  155. package/dist/server/index.test.js +12 -1
  156. package/dist/server/routes/admin.d.ts +1 -5
  157. package/dist/server/routes/admin.js +151 -80
  158. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  159. package/dist/server/routes/annotatedAttachments.js +1 -1
  160. package/dist/server/routes/auth.d.ts +1 -4
  161. package/dist/server/routes/auth.js +59 -64
  162. package/dist/server/routes/authSupport.d.ts +30 -0
  163. package/dist/server/routes/authSupport.js +81 -0
  164. package/dist/server/routes/billing.d.ts +1 -1
  165. package/dist/server/routes/billing.js +122 -255
  166. package/dist/server/routes/billing.test.js +257 -86
  167. package/dist/server/routes/documentReviews.d.ts +1 -1
  168. package/dist/server/routes/documentReviews.js +1 -1
  169. package/dist/server/routes/documents.d.ts +4 -2
  170. package/dist/server/routes/documents.js +73 -7
  171. package/dist/server/routes/primitives.d.ts +11 -0
  172. package/dist/server/routes/primitives.js +73 -0
  173. package/dist/server/routes/resources.d.ts +1 -1
  174. package/dist/server/routes/resources.js +1 -1
  175. package/dist/server/routes/shared.d.ts +31 -1
  176. package/dist/server/routes/shared.js +26 -0
  177. package/dist/server/routes/sync.d.ts +1 -1
  178. package/dist/server/routes/sync.integration.test.js +20 -0
  179. package/dist/server/routes/sync.js +111 -238
  180. package/dist/server/routes/tasks.d.ts +1 -1
  181. package/dist/server/routes/tasks.js +50 -8
  182. package/dist/server/routes/workspaces.d.ts +1 -1
  183. package/dist/server/routes/workspaces.js +1 -1
  184. package/dist/server/routes.d.ts +2 -9
  185. package/dist/server/routes.js +84 -181
  186. package/dist/server/routes.test.js +912 -32
  187. package/dist/shared/taskActor.d.ts +10 -0
  188. package/dist/shared/taskActor.js +1 -0
  189. package/dist/sync/collaborationSyncPayload.js +4 -0
  190. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  191. package/dist/sync/collaborationSyncState.d.ts +3 -1
  192. package/dist/sync/collaborationSyncState.js +19 -0
  193. package/dist/sync/contentSyncState.js +28 -17
  194. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  195. package/dist/sync/syncApplyHandlers.js +370 -8
  196. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  197. package/dist/sync/syncApplyHandlers.test.js +379 -0
  198. package/dist/sync/syncService.d.ts +9 -1
  199. package/dist/sync/syncService.js +44 -2
  200. package/dist/sync/syncService.test.js +62 -0
  201. package/dist/sync/workspacePullFeed.d.ts +1 -0
  202. package/dist/sync/workspacePullFeed.js +66 -59
  203. package/dist/sync/workspacePullFeed.test.js +72 -4
  204. package/dist/sync/workspaceSyncModel.d.ts +25 -1
  205. package/dist/sync/workspaceSyncModel.js +77 -3
  206. package/dist/sync/workspaceSyncModel.test.js +153 -0
  207. package/dist/sync/workspaceSyncState.d.ts +2 -0
  208. package/dist/sync/workspaceSyncState.js +1 -0
  209. package/dist/types.d.ts +2 -10
  210. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
  211. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  212. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  213. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  214. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  215. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  216. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  217. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  218. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
  219. package/dist/ui/assets/index-C2-OXZV7.css +1 -0
  220. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  221. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
  222. package/dist/ui/index.html +4 -4
  223. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  224. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  225. package/dist/utils/taskActivity.js +4 -0
  226. package/dist/utils/taskActivity.test.js +25 -1
  227. package/package.json +10 -1
  228. package/scripts/check-cloud-mcp.mjs +83 -0
  229. package/scripts/playwright-auth.sh +9 -1
  230. package/scripts/run-billing-performance-smoke.sh +24 -0
  231. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  232. package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
  233. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  234. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  235. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  236. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  237. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import type { TaskItem, CategoryConfig, TaxonomyDefinition, PriorityDefinition, TaskAssignee, WorkstreamItem, InitiativeItem } from '../../types';
3
3
  import type { TaskAssigneeOption } from '../../utils/assignees';
4
4
  import { type TaskSortKey } from '../../utils/taxonomySorting.js';
5
+ import { type ScheduleDayKey } from './taskKanbanDropRules';
5
6
  export interface KanbanColumnDef {
6
7
  value: string | number;
7
8
  label: string;
@@ -11,7 +12,6 @@ export interface KanbanColumnDef {
11
12
  isSelected?: boolean;
12
13
  isWeekend?: boolean;
13
14
  }
14
- type ScheduleDayKey = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';
15
15
  interface TaskKanbanProps {
16
16
  tasks: TaskItem[];
17
17
  allTasks?: TaskItem[];
@@ -7,6 +7,7 @@ import { createPortal } from 'react-dom';
7
7
  import styles from '../../Taskforce.module.css';
8
8
  import { TaskCard } from './TaskCard';
9
9
  import { compareTasksBySortKey } from '../../utils/taxonomySorting.js';
10
+ import { buildScheduleDropResult, buildStatusTransitionUpdates, getGroupedTaskStatus, } from './taskKanbanDropRules';
10
11
  import * as Icons from 'lucide-react';
11
12
  import { IconMap, resolveColor } from '../../utils/constants';
12
13
  const EXPIRED_CARD_ID_PREFIX = 'expired::';
@@ -52,18 +53,14 @@ export function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery
52
53
  // Sensors
53
54
  const sensors = useSensors(useSensor(PointerSensor, {
54
55
  activationConstraint: {
55
- distance: 5, // Prevent accidental drags
56
+ // Give normal clicks a bit more tolerance so slight trackpad drift still opens the task.
57
+ distance: 10,
56
58
  },
57
59
  }), useSensor(KeyboardSensor, {
58
60
  coordinateGetter: sortableKeyboardCoordinates,
59
61
  }));
60
62
  // Helper to determine status string from task flags
61
- const getTaskStatus = (task) => {
62
- const status = task.status || 'task';
63
- if (status === 'done' || status === 'cancelled')
64
- return 'completed';
65
- return status;
66
- };
63
+ const getTaskStatus = getGroupedTaskStatus;
67
64
  const todayDateOnly = useMemo(() => {
68
65
  const now = new Date();
69
66
  const year = now.getFullYear();
@@ -481,171 +478,46 @@ export function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery
481
478
  if (column) {
482
479
  if (groupBy === 'schedule') {
483
480
  const targetColumn = String(column.value);
484
- const scheduleKeys = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
485
- const isScheduleReorderColumn = targetColumn === 'backlog' || targetColumn === 'expired' || scheduleKeys.includes(targetColumn);
486
- const buildInsertIndex = (targetTasks) => {
487
- let insertAt = targetTasks.length;
488
- const candidateOverTaskId = overId && isTaskCardId(overId) ? getTaskIdFromCardId(overId) : null;
489
- if (!candidateOverTaskId || !taskIdSet.has(candidateOverTaskId))
490
- return insertAt;
491
- const overTaskId = candidateOverTaskId;
492
- const overIndex = targetTasks.findIndex(t => t.id === overTaskId);
493
- if (overIndex < 0)
494
- return insertAt;
495
- const overRect = over?.rect;
496
- const translatedActiveRect = active.rect.current.translated;
497
- const activeRect = translatedActiveRect || active.rect.current.initial;
498
- const isBelowOverItem = Boolean(overRect
499
- && activeRect
500
- && (activeRect.top + activeRect.height / 2) > (overRect.top + overRect.height / 2));
501
- return overIndex + (isBelowOverItem ? 1 : 0);
502
- };
503
- const reindexSourceColumnAfterMove = (sourceColumnId, targetColumnId, pendingUpdates) => {
504
- if (!sourceColumnId || sourceColumnId === targetColumnId)
505
- return;
506
- const sourceTasks = (groupedTasks[String(sourceColumnId)] || []).filter(t => t.id !== activeTaskId);
507
- sourceTasks.forEach((task, index) => {
508
- pendingUpdates.set(task.id, {
509
- ...(pendingUpdates.get(task.id) || {}),
510
- orderInDay: index
511
- });
512
- });
513
- };
514
- if (targetColumn === 'expired') {
515
- // Expired is virtual and source-only: allow reordering within Expired,
516
- // but do not allow drops from other columns into Expired.
517
- if (resolvedSourceContainerId !== 'expired')
518
- return;
519
- const pendingUpdates = new Map();
520
- const targetTasks = (groupedTasks.expired || []).filter(t => t.id !== activeTaskId);
521
- const insertAt = buildInsertIndex(targetTasks);
522
- const reorderedTarget = [...targetTasks];
523
- reorderedTarget.splice(insertAt, 0, activeTask);
524
- reorderedTarget.forEach((task, index) => {
525
- pendingUpdates.set(task.id, {
526
- ...(pendingUpdates.get(task.id) || {}),
527
- orderInDay: index
528
- });
529
- });
530
- pendingUpdates.forEach((updates, taskId) => onUpdateTask(taskId, updates));
481
+ const candidateOverTaskId = overId && isTaskCardId(overId) ? getTaskIdFromCardId(overId) : null;
482
+ const overRect = over?.rect;
483
+ const translatedActiveRect = active.rect.current.translated;
484
+ const activeRect = translatedActiveRect || active.rect.current.initial;
485
+ const isBelowOverItem = Boolean(overRect
486
+ && activeRect
487
+ && (activeRect.top + activeRect.height / 2) > (overRect.top + overRect.height / 2));
488
+ const targetTasks = (groupedTasks[targetColumn] || []).filter((task) => task.id !== activeTaskId);
489
+ const sourceTasks = (groupedTasks[String(resolvedSourceContainerId)] || []).filter((task) => task.id !== activeTaskId);
490
+ const scheduleDropResult = buildScheduleDropResult({
491
+ activeTask,
492
+ sourceColumnId: resolvedSourceContainerId,
493
+ targetColumnId: targetColumn,
494
+ targetTasks,
495
+ sourceTasks,
496
+ overTaskId: candidateOverTaskId,
497
+ isBelowOverItem,
498
+ scheduleDates: scheduleContext.dates,
499
+ parseToIsoWeekKey: scheduleContext.parseToIsoWeekKey,
500
+ todayDate: todayDateOnly,
501
+ });
502
+ if (!scheduleDropResult)
531
503
  return;
532
- }
533
- if (targetColumn === 'backlog') {
534
- const pendingUpdates = new Map();
535
- const targetTasks = (groupedTasks.backlog || []).filter(t => t.id !== activeTaskId);
536
- const insertAt = buildInsertIndex(targetTasks);
537
- const reorderedTarget = [...targetTasks];
538
- reorderedTarget.splice(insertAt, 0, activeTask);
539
- reorderedTarget.forEach((task, index) => {
540
- if (task.id === activeTaskId) {
541
- if (activeTask.scheduledDate) {
542
- pendingUpdates.set(task.id, {
543
- ...(pendingUpdates.get(task.id) || {}),
544
- scheduledDate: null,
545
- scheduledWeekKey: null,
546
- orderInDay: index
547
- });
548
- }
549
- else {
550
- pendingUpdates.set(task.id, {
551
- ...(pendingUpdates.get(task.id) || {}),
552
- orderInDay: index
553
- });
554
- }
555
- }
556
- else {
557
- pendingUpdates.set(task.id, {
558
- ...(pendingUpdates.get(task.id) || {}),
559
- orderInDay: index
560
- });
561
- }
562
- });
563
- reindexSourceColumnAfterMove(resolvedSourceContainerId, targetColumn, pendingUpdates);
564
- pendingUpdates.forEach((updates, taskId) => onUpdateTask(taskId, updates));
504
+ if (scheduleDropResult.kind === 'blocked') {
505
+ window.alert(scheduleDropResult.reason);
565
506
  return;
566
507
  }
567
- if (!isScheduleReorderColumn || !scheduleKeys.includes(targetColumn))
568
- return;
569
- const scheduledDate = scheduleContext.dates[targetColumn];
570
- if (!scheduledDate)
571
- return;
572
- const isTerminal = activeTask.status === 'done' || activeTask.status === 'cancelled';
573
- if (!isTerminal && scheduledDate < todayDateOnly) {
574
- window.alert('Only completed work can be scheduled in the past.');
575
- return;
508
+ scheduleDropResult.updates.forEach(({ taskId, updates }) => onUpdateTask(taskId, updates));
509
+ if (scheduleDropResult.selectedDate) {
510
+ onScheduleDaySelected?.(scheduleDropResult.selectedDate);
576
511
  }
577
- const scheduledWeekKey = scheduleContext.parseToIsoWeekKey(scheduledDate);
578
- const pendingUpdates = new Map();
579
- const targetTasks = (groupedTasks[targetColumn] || []).filter(t => t.id !== activeTaskId);
580
- const insertAt = buildInsertIndex(targetTasks);
581
- const reorderedTarget = [...targetTasks];
582
- reorderedTarget.splice(insertAt, 0, activeTask);
583
- reorderedTarget.forEach((task, index) => {
584
- if (task.id === activeTaskId) {
585
- pendingUpdates.set(task.id, {
586
- ...(pendingUpdates.get(task.id) || {}),
587
- scheduledDate,
588
- scheduledWeekKey,
589
- orderInDay: index
590
- });
591
- }
592
- else {
593
- pendingUpdates.set(task.id, {
594
- ...(pendingUpdates.get(task.id) || {}),
595
- orderInDay: index
596
- });
597
- }
598
- });
599
- reindexSourceColumnAfterMove(resolvedSourceContainerId, targetColumn, pendingUpdates);
600
- pendingUpdates.forEach((updates, taskId) => onUpdateTask(taskId, updates));
601
- onScheduleDaySelected?.(scheduledDate);
602
512
  return;
603
513
  }
604
514
  // Determine the value to update based on the grouping
605
515
  let targetValue = column.value;
606
516
  // STATUS HANDLING
607
517
  if (groupBy === 'status') {
608
- const currentStatus = getTaskStatus(activeTask);
609
518
  const targetStatus = String(targetValue);
610
- if (currentStatus !== targetStatus) {
611
- const updates = {
612
- status: targetStatus
613
- };
614
- if (targetStatus === 'task') {
615
- updates.inProgress = false;
616
- updates.readyForReview = false;
617
- updates.completed = false;
618
- updates.cancelled = false;
619
- }
620
- else if (targetStatus === 'on-hold') {
621
- updates.inProgress = false;
622
- updates.readyForReview = false;
623
- updates.completed = false;
624
- updates.cancelled = false;
625
- }
626
- else if (targetStatus === 'in-progress') {
627
- updates.inProgress = true;
628
- updates.readyForReview = false;
629
- updates.completed = false;
630
- updates.cancelled = false;
631
- }
632
- else if (targetStatus === 'review') {
633
- updates.inProgress = false;
634
- updates.readyForReview = true;
635
- updates.completed = false;
636
- updates.cancelled = false;
637
- }
638
- else if (targetStatus === 'done' || targetStatus === 'completed') {
639
- updates.status = 'done';
640
- updates.inProgress = false;
641
- updates.readyForReview = false;
642
- updates.completed = true;
643
- updates.cancelled = false;
644
- updates.completedAt = new Date().toISOString();
645
- }
646
- else if (targetStatus === 'cancelled') {
647
- updates.status = 'cancelled';
648
- }
519
+ const updates = buildStatusTransitionUpdates(activeTask, targetStatus, new Date().toISOString());
520
+ if (updates) {
649
521
  onUpdateTask(activeTask.id, updates);
650
522
  }
651
523
  return;
@@ -1,12 +1,17 @@
1
1
  import type { TaskItem, TaskStatus } from '../../types';
2
+ type ArchiveActionMode = 'auto' | 'archive' | 'unarchive' | 'none';
2
3
  interface TaskStatusActionsProps {
3
4
  task?: TaskItem | null;
4
5
  disabled?: boolean;
6
+ actionDisabled?: boolean;
7
+ statusDisabled?: boolean;
5
8
  compressed?: boolean;
6
9
  shortLabels?: boolean;
7
10
  showLabel?: boolean;
8
11
  onSetStatus?: (task: TaskItem, status: TaskStatus) => void;
9
12
  onArchiveTask?: (task: TaskItem) => void;
13
+ onUnarchiveTask?: (task: TaskItem) => void;
14
+ archiveActionMode?: ArchiveActionMode;
10
15
  }
11
- export declare function TaskStatusActions({ task, disabled, compressed, shortLabels, showLabel, onSetStatus, onArchiveTask }: TaskStatusActionsProps): import("react/jsx-runtime").JSX.Element;
16
+ export declare function TaskStatusActions({ task, disabled, actionDisabled, statusDisabled, compressed, shortLabels, showLabel, onSetStatus, onArchiveTask, onUnarchiveTask, archiveActionMode }: TaskStatusActionsProps): import("react/jsx-runtime").JSX.Element;
12
17
  export {};
@@ -1,16 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Archive } from 'lucide-react';
2
+ import { Archive, RotateCcw } from 'lucide-react';
3
3
  import styles from '../../Taskforce.module.css';
4
4
  import { STATUS_DEFINITIONS } from '../../utils/constants';
5
5
  import { TaxonomyDropdown } from '../ui/TaxonomyDropdown';
6
- export function TaskStatusActions({ task, disabled = false, compressed = false, shortLabels = false, showLabel = true, onSetStatus, onArchiveTask }) {
6
+ export function TaskStatusActions({ task, disabled = false, actionDisabled, statusDisabled, compressed = false, shortLabels = false, showLabel = true, onSetStatus, onArchiveTask, onUnarchiveTask, archiveActionMode = 'auto' }) {
7
7
  const currentStatus = task?.status || 'task';
8
8
  const canArchive = !!task && (currentStatus === 'done' || currentStatus === 'cancelled');
9
+ const resolvedArchiveActionMode = archiveActionMode === 'auto'
10
+ ? (canArchive ? 'archive' : 'none')
11
+ : archiveActionMode;
12
+ const resolvedActionDisabled = actionDisabled ?? disabled;
13
+ const resolvedStatusDisabled = statusDisabled ?? disabled;
9
14
  const statusOptions = STATUS_DEFINITIONS.map((option) => ({
10
15
  ...option,
11
16
  label: (compressed || shortLabels) ? (option.shortLabel || option.label) : option.label
12
17
  }));
13
- return (_jsxs("div", { className: `${styles.statusActionsGroup} ${compressed ? styles.statusActionsGroupCompact : ''}`, children: [canArchive && (_jsx("button", { type: "button", className: `${styles.actionBtn} ${styles.archiveTaskBtn}`, onClick: () => task && !disabled && onArchiveTask?.(task), disabled: disabled, title: "Archive Now", "aria-label": "Archive task", children: _jsx(Archive, { size: compressed ? 12 : 16 }) })), _jsx("div", { className: `${styles.statusSelectWrap} ${styles.statusTaxonomyDropdownWrap}`, children: _jsx(TaxonomyDropdown, { value: currentStatus, options: statusOptions, disabled: disabled || !task, ariaLabel: "Status", hideSelectedLabel: !showLabel, hideChevron: !showLabel, className: `${styles.statusTaxonomyDropdown} ${compressed ? styles.statusTaxonomyDropdownCompact : ''} ${!showLabel ? styles.statusTaxonomyDropdownIconOnly : ''}`, panelClassName: !showLabel ? styles.statusTaxonomyDropdownIconOnlyPanel : '', portalPanel: !showLabel, panelMinWidth: !showLabel ? 144 : undefined, onChange: (value) => {
18
+ return (_jsxs("div", { className: `${styles.statusActionsGroup} ${compressed ? styles.statusActionsGroupCompact : ''}`, children: [resolvedArchiveActionMode === 'archive' && (_jsx("button", { type: "button", className: `${styles.actionBtn} ${styles.archiveTaskBtn}`, onClick: () => task && !resolvedActionDisabled && onArchiveTask?.(task), disabled: resolvedActionDisabled, title: "Archive Now", "aria-label": "Archive task", children: _jsx(Archive, { size: compressed ? 12 : 16 }) })), resolvedArchiveActionMode === 'unarchive' && (_jsx("button", { type: "button", className: `${styles.actionBtn} ${styles.archiveTaskBtn}`, onClick: () => task && !resolvedActionDisabled && onUnarchiveTask?.(task), disabled: resolvedActionDisabled, title: "Unarchive task", "aria-label": "Unarchive task", children: _jsx(RotateCcw, { size: compressed ? 12 : 16 }) })), _jsx("div", { className: `${styles.statusSelectWrap} ${styles.statusTaxonomyDropdownWrap}`, children: _jsx(TaxonomyDropdown, { value: currentStatus, options: statusOptions, disabled: resolvedStatusDisabled || !task, ariaLabel: "Status", hideSelectedLabel: !showLabel, hideChevron: !showLabel, className: `${styles.statusTaxonomyDropdown} ${compressed ? styles.statusTaxonomyDropdownCompact : ''} ${!showLabel ? styles.statusTaxonomyDropdownIconOnly : ''}`, panelClassName: !showLabel ? styles.statusTaxonomyDropdownIconOnlyPanel : '', portalPanel: !showLabel, panelMinWidth: !showLabel ? 144 : undefined, onChange: (value) => {
14
19
  if (!task)
15
20
  return;
16
21
  onSetStatus?.(task, value);
@@ -25,6 +25,15 @@ describe('TaskStatusActions', () => {
25
25
  rerender(_jsx(TaskStatusActions, { task: makeTask({ status: 'done' }) }));
26
26
  expect(screen.getByRole('button', { name: 'Archive task' })).toBeInTheDocument();
27
27
  });
28
+ it('can render an unarchive action for archived read-only views', async () => {
29
+ const user = userEvent.setup();
30
+ const task = makeTask({ status: 'done' });
31
+ const onUnarchiveTask = vi.fn();
32
+ render(_jsx(TaskStatusActions, { task: task, archiveActionMode: "unarchive", onUnarchiveTask: onUnarchiveTask }));
33
+ await user.click(screen.getByRole('button', { name: 'Unarchive task' }));
34
+ expect(screen.queryByRole('button', { name: 'Archive task' })).not.toBeInTheDocument();
35
+ expect(onUnarchiveTask).toHaveBeenCalledWith(task);
36
+ });
28
37
  it('uses the compact short label when compressed', () => {
29
38
  render(_jsx(TaskStatusActions, { task: makeTask({ status: 'in-progress' }), compressed: true }));
30
39
  expect(screen.getByRole('combobox', { name: 'Status' })).toHaveTextContent('Working');
@@ -0,0 +1,58 @@
1
+ import type { TaskItem } from '../../types';
2
+ export type ScheduleDayKey = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';
3
+ export type TaskUpdatePair = {
4
+ taskId: string;
5
+ updates: Partial<TaskItem>;
6
+ };
7
+ export type ScheduleDropBlockedResult = {
8
+ kind: 'blocked';
9
+ reason: string;
10
+ };
11
+ export type ScheduleDropUpdatesResult = {
12
+ kind: 'updates';
13
+ updates: TaskUpdatePair[];
14
+ selectedDate?: string;
15
+ };
16
+ export type ScheduleDropResult = ScheduleDropBlockedResult | ScheduleDropUpdatesResult;
17
+ export declare const getGroupedTaskStatus: (task: TaskItem) => string;
18
+ export declare const buildStatusTransitionUpdates: (task: TaskItem, targetStatus: string, nowIso: string) => Partial<TaskItem> | null;
19
+ export declare const buildInsertIndex: ({ targetTasks, overTaskId, isBelowOverItem, }: {
20
+ targetTasks: TaskItem[];
21
+ overTaskId: string | null;
22
+ isBelowOverItem: boolean;
23
+ }) => number;
24
+ export declare const reindexTasks: (tasks: TaskItem[], baseUpdates?: Map<string, Partial<TaskItem>>) => Map<string, Partial<TaskItem>>;
25
+ export declare const buildBacklogDropUpdates: ({ activeTask, sourceColumnId, targetTasks, sourceTasks, insertAt, }: {
26
+ activeTask: TaskItem;
27
+ sourceColumnId: string | null;
28
+ targetTasks: TaskItem[];
29
+ sourceTasks: TaskItem[];
30
+ insertAt: number;
31
+ }) => TaskUpdatePair[];
32
+ export declare const buildExpiredReorderUpdates: ({ activeTask, targetTasks, insertAt, }: {
33
+ activeTask: TaskItem;
34
+ targetTasks: TaskItem[];
35
+ insertAt: number;
36
+ }) => TaskUpdatePair[];
37
+ export declare const buildScheduledDayDropUpdates: ({ activeTask, sourceColumnId, targetColumnId, targetTasks, sourceTasks, insertAt, scheduledDate, scheduledWeekKey, }: {
38
+ activeTask: TaskItem;
39
+ sourceColumnId: string | null;
40
+ targetColumnId: string;
41
+ targetTasks: TaskItem[];
42
+ sourceTasks: TaskItem[];
43
+ insertAt: number;
44
+ scheduledDate: string;
45
+ scheduledWeekKey: string | null;
46
+ }) => TaskUpdatePair[];
47
+ export declare const buildScheduleDropResult: ({ activeTask, sourceColumnId, targetColumnId, targetTasks, sourceTasks, overTaskId, isBelowOverItem, scheduleDates, parseToIsoWeekKey, todayDate, }: {
48
+ activeTask: TaskItem;
49
+ sourceColumnId: string | null;
50
+ targetColumnId: string;
51
+ targetTasks: TaskItem[];
52
+ sourceTasks: TaskItem[];
53
+ overTaskId: string | null;
54
+ isBelowOverItem: boolean;
55
+ scheduleDates: Record<ScheduleDayKey, string>;
56
+ parseToIsoWeekKey: (dateOnly: string) => string | null;
57
+ todayDate: string;
58
+ }) => ScheduleDropResult | null;
@@ -0,0 +1,187 @@
1
+ export const getGroupedTaskStatus = (task) => {
2
+ const status = task.status || 'task';
3
+ if (status === 'done' || status === 'cancelled')
4
+ return 'completed';
5
+ return status;
6
+ };
7
+ export const buildStatusTransitionUpdates = (task, targetStatus, nowIso) => {
8
+ const currentStatus = getGroupedTaskStatus(task);
9
+ if (currentStatus === targetStatus)
10
+ return null;
11
+ const updates = {
12
+ status: targetStatus
13
+ };
14
+ if (targetStatus === 'task' || targetStatus === 'on-hold') {
15
+ updates.inProgress = false;
16
+ updates.readyForReview = false;
17
+ updates.completed = false;
18
+ updates.cancelled = false;
19
+ updates.completedAt = null;
20
+ return updates;
21
+ }
22
+ if (targetStatus === 'in-progress') {
23
+ updates.inProgress = true;
24
+ updates.readyForReview = false;
25
+ updates.completed = false;
26
+ updates.cancelled = false;
27
+ updates.completedAt = null;
28
+ return updates;
29
+ }
30
+ if (targetStatus === 'review') {
31
+ updates.inProgress = false;
32
+ updates.readyForReview = true;
33
+ updates.completed = false;
34
+ updates.cancelled = false;
35
+ updates.completedAt = null;
36
+ return updates;
37
+ }
38
+ if (targetStatus === 'done' || targetStatus === 'completed') {
39
+ updates.status = 'done';
40
+ updates.inProgress = false;
41
+ updates.readyForReview = false;
42
+ updates.completed = true;
43
+ updates.cancelled = false;
44
+ updates.completedAt = nowIso;
45
+ return updates;
46
+ }
47
+ if (targetStatus === 'cancelled') {
48
+ updates.status = 'cancelled';
49
+ return updates;
50
+ }
51
+ return updates;
52
+ };
53
+ export const buildInsertIndex = ({ targetTasks, overTaskId, isBelowOverItem, }) => {
54
+ if (!overTaskId)
55
+ return targetTasks.length;
56
+ const overIndex = targetTasks.findIndex((task) => task.id === overTaskId);
57
+ if (overIndex < 0)
58
+ return targetTasks.length;
59
+ return overIndex + (isBelowOverItem ? 1 : 0);
60
+ };
61
+ export const reindexTasks = (tasks, baseUpdates = new Map()) => {
62
+ const updates = new Map(baseUpdates);
63
+ tasks.forEach((task, index) => {
64
+ updates.set(task.id, {
65
+ ...(updates.get(task.id) || {}),
66
+ orderInDay: index,
67
+ });
68
+ });
69
+ return updates;
70
+ };
71
+ const toUpdatePairs = (updates) => (Array.from(updates.entries()).map(([taskId, taskUpdates]) => ({ taskId, updates: taskUpdates })));
72
+ const reindexSourceColumnAfterMove = ({ sourceColumnId, targetColumnId, sourceTasks, updates, }) => {
73
+ if (!sourceColumnId || sourceColumnId === targetColumnId)
74
+ return updates;
75
+ return reindexTasks(sourceTasks, updates);
76
+ };
77
+ export const buildBacklogDropUpdates = ({ activeTask, sourceColumnId, targetTasks, sourceTasks, insertAt, }) => {
78
+ const reorderedTarget = [...targetTasks];
79
+ reorderedTarget.splice(insertAt, 0, activeTask);
80
+ let updates = new Map();
81
+ reorderedTarget.forEach((task, index) => {
82
+ if (task.id === activeTask.id) {
83
+ updates.set(task.id, {
84
+ ...(updates.get(task.id) || {}),
85
+ ...(activeTask.scheduledDate ? { scheduledDate: null, scheduledWeekKey: null } : {}),
86
+ orderInDay: index,
87
+ });
88
+ return;
89
+ }
90
+ updates.set(task.id, {
91
+ ...(updates.get(task.id) || {}),
92
+ orderInDay: index,
93
+ });
94
+ });
95
+ updates = reindexSourceColumnAfterMove({
96
+ sourceColumnId,
97
+ targetColumnId: 'backlog',
98
+ sourceTasks,
99
+ updates,
100
+ });
101
+ return toUpdatePairs(updates);
102
+ };
103
+ export const buildExpiredReorderUpdates = ({ activeTask, targetTasks, insertAt, }) => {
104
+ const reorderedTarget = [...targetTasks];
105
+ reorderedTarget.splice(insertAt, 0, activeTask);
106
+ return toUpdatePairs(reindexTasks(reorderedTarget));
107
+ };
108
+ export const buildScheduledDayDropUpdates = ({ activeTask, sourceColumnId, targetColumnId, targetTasks, sourceTasks, insertAt, scheduledDate, scheduledWeekKey, }) => {
109
+ const reorderedTarget = [...targetTasks];
110
+ reorderedTarget.splice(insertAt, 0, activeTask);
111
+ let updates = new Map();
112
+ reorderedTarget.forEach((task, index) => {
113
+ if (task.id === activeTask.id) {
114
+ updates.set(task.id, {
115
+ ...(updates.get(task.id) || {}),
116
+ scheduledDate,
117
+ scheduledWeekKey,
118
+ orderInDay: index,
119
+ });
120
+ return;
121
+ }
122
+ updates.set(task.id, {
123
+ ...(updates.get(task.id) || {}),
124
+ orderInDay: index,
125
+ });
126
+ });
127
+ updates = reindexSourceColumnAfterMove({
128
+ sourceColumnId,
129
+ targetColumnId,
130
+ sourceTasks,
131
+ updates,
132
+ });
133
+ return toUpdatePairs(updates);
134
+ };
135
+ export const buildScheduleDropResult = ({ activeTask, sourceColumnId, targetColumnId, targetTasks, sourceTasks, overTaskId, isBelowOverItem, scheduleDates, parseToIsoWeekKey, todayDate, }) => {
136
+ const scheduleKeys = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
137
+ const isScheduleReorderColumn = targetColumnId === 'backlog' || targetColumnId === 'expired' || scheduleKeys.includes(targetColumnId);
138
+ if (!isScheduleReorderColumn)
139
+ return null;
140
+ const insertAt = buildInsertIndex({ targetTasks, overTaskId, isBelowOverItem });
141
+ if (targetColumnId === 'expired') {
142
+ if (sourceColumnId !== 'expired')
143
+ return null;
144
+ return {
145
+ kind: 'updates',
146
+ updates: buildExpiredReorderUpdates({ activeTask, targetTasks, insertAt }),
147
+ };
148
+ }
149
+ if (targetColumnId === 'backlog') {
150
+ return {
151
+ kind: 'updates',
152
+ updates: buildBacklogDropUpdates({
153
+ activeTask,
154
+ sourceColumnId,
155
+ targetTasks,
156
+ sourceTasks,
157
+ insertAt,
158
+ }),
159
+ };
160
+ }
161
+ if (!scheduleKeys.includes(targetColumnId))
162
+ return null;
163
+ const scheduledDate = scheduleDates[targetColumnId];
164
+ if (!scheduledDate)
165
+ return null;
166
+ const isTerminal = activeTask.status === 'done' || activeTask.status === 'cancelled';
167
+ if (!isTerminal && scheduledDate < todayDate) {
168
+ return {
169
+ kind: 'blocked',
170
+ reason: 'Only completed work can be scheduled in the past.',
171
+ };
172
+ }
173
+ return {
174
+ kind: 'updates',
175
+ updates: buildScheduledDayDropUpdates({
176
+ activeTask,
177
+ sourceColumnId,
178
+ targetColumnId,
179
+ targetTasks,
180
+ sourceTasks,
181
+ insertAt,
182
+ scheduledDate,
183
+ scheduledWeekKey: parseToIsoWeekKey(scheduledDate),
184
+ }),
185
+ selectedDate: scheduledDate,
186
+ };
187
+ };
@@ -0,0 +1 @@
1
+ export {};