@taskforcehq/taskforce 0.3.303 → 0.3.304

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/Taskforce.module.css +102 -5
  2. package/dist/TaskforceCore.js +514 -212
  3. package/dist/TaskforceCore.test.js +1172 -133
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  21. package/dist/components/task/TaskStatusActions.js +8 -3
  22. package/dist/components/task/TaskStatusActions.test.js +9 -0
  23. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  24. package/dist/components/task/taskKanbanDropRules.js +187 -0
  25. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  26. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  27. package/dist/components/ui/Modal.d.ts +1 -1
  28. package/dist/components/ui/Modal.js +1 -0
  29. package/dist/components/views/AppShellHeader.js +5 -1
  30. package/dist/components/views/AppShellHeader.test.js +4 -0
  31. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  32. package/dist/components/views/PlanComparisonPage.js +4 -2
  33. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  34. package/dist/components/views/PlansPage.d.ts +5 -1
  35. package/dist/components/views/PlansPage.js +39 -12
  36. package/dist/components/views/PlansPage.test.js +59 -2
  37. package/dist/components/views/StandaloneLayout.js +113 -38
  38. package/dist/components/views/WidgetView.js +11 -2
  39. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  40. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  41. package/dist/core/PlanEntitlementService.d.ts +3 -1
  42. package/dist/core/PlanEntitlementService.js +61 -11
  43. package/dist/core/SignupMonetizationSettings.test.js +37 -2
  44. package/dist/core/TaskActivityService.d.ts +4 -1
  45. package/dist/core/TaskActivityService.js +45 -0
  46. package/dist/core/TaskAuditTimeline.test.js +52 -1
  47. package/dist/core/Taskforce.d.ts +15 -11
  48. package/dist/core/Taskforce.js +43 -2
  49. package/dist/core/WorkspacePlanMode.test.js +65 -0
  50. package/dist/documentReviews/service.d.ts +1 -1
  51. package/dist/documentReviews/types.d.ts +1 -1
  52. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  53. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  54. package/dist/hooks/sync/auth.d.ts +37 -0
  55. package/dist/hooks/sync/auth.js +61 -0
  56. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  57. package/dist/hooks/sync/bootstrap.js +52 -0
  58. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  59. package/dist/hooks/sync/orchestratorShared.js +209 -0
  60. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  61. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  62. package/dist/hooks/sync/realtime.d.ts +25 -0
  63. package/dist/hooks/sync/realtime.js +60 -0
  64. package/dist/hooks/sync/recovery.d.ts +69 -0
  65. package/dist/hooks/sync/recovery.js +118 -0
  66. package/dist/hooks/sync/transfers.d.ts +122 -0
  67. package/dist/hooks/sync/transfers.js +431 -0
  68. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  69. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  70. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  71. package/dist/hooks/ui/useResourceExport.js +5 -2
  72. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  73. package/dist/hooks/useRealtimeSync.js +2 -1
  74. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  75. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  76. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  77. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  78. package/dist/hooks/useSyncOrchestrator.js +314 -873
  79. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
  80. package/dist/hooks/useTaskMutations.js +45 -6
  81. package/dist/hooks/useTaskMutations.test.js +147 -0
  82. package/dist/hooks/useTaskforce.d.ts +8 -2
  83. package/dist/hooks/useTaskforce.js +285 -812
  84. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  85. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  86. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  87. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  88. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  89. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  90. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  91. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  92. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  93. package/dist/localization/locales/en-US.js +2 -2
  94. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  95. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  96. package/dist/mcp/clientIntegrations.d.ts +12 -1
  97. package/dist/mcp/clientIntegrations.js +161 -6
  98. package/dist/mcp/clientIntegrations.test.js +76 -4
  99. package/dist/mcp/documentHelpers.d.ts +100 -0
  100. package/dist/mcp/documentHelpers.js +161 -0
  101. package/dist/mcp/runtime.js +174 -476
  102. package/dist/mcp/runtime.test.js +81 -8
  103. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  104. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  105. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  106. package/dist/mcp/taskAttachmentTypes.js +1 -0
  107. package/dist/plugins/vite.js +1 -1
  108. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  109. package/dist/runtime/appGateDefinitions.js +1 -1
  110. package/dist/server/index.d.ts +1 -0
  111. package/dist/server/index.js +46 -3
  112. package/dist/server/index.test.js +12 -1
  113. package/dist/server/routes/admin.js +13 -19
  114. package/dist/server/routes/auth.js +37 -2
  115. package/dist/server/routes/billing.js +18 -67
  116. package/dist/server/routes/billing.test.js +43 -0
  117. package/dist/server/routes/shared.d.ts +30 -0
  118. package/dist/server/routes/shared.js +26 -0
  119. package/dist/server/routes/sync.integration.test.js +20 -0
  120. package/dist/server/routes/sync.js +110 -237
  121. package/dist/server/routes/tasks.js +49 -7
  122. package/dist/server/routes.js +8 -19
  123. package/dist/server/routes.test.js +243 -5
  124. package/dist/shared/taskActor.d.ts +10 -0
  125. package/dist/shared/taskActor.js +1 -0
  126. package/dist/sync/collaborationSyncPayload.js +4 -0
  127. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  128. package/dist/sync/collaborationSyncState.d.ts +3 -1
  129. package/dist/sync/collaborationSyncState.js +19 -0
  130. package/dist/sync/contentSyncState.js +12 -10
  131. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  132. package/dist/sync/syncApplyHandlers.js +370 -8
  133. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  134. package/dist/sync/syncApplyHandlers.test.js +377 -0
  135. package/dist/sync/syncService.d.ts +8 -1
  136. package/dist/sync/syncService.js +36 -2
  137. package/dist/sync/syncService.test.js +48 -0
  138. package/dist/sync/workspacePullFeed.d.ts +1 -0
  139. package/dist/sync/workspacePullFeed.js +66 -59
  140. package/dist/sync/workspacePullFeed.test.js +72 -4
  141. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  142. package/dist/sync/workspaceSyncModel.js +65 -0
  143. package/dist/sync/workspaceSyncModel.test.js +119 -0
  144. package/dist/types.d.ts +2 -10
  145. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  146. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  147. package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  148. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-BjR6iBf9.js} +1 -1
  149. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  150. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  151. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  152. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-DnFqdUME.js} +1 -1
  153. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  154. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  155. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
  156. package/dist/ui/index.html +4 -4
  157. package/dist/utils/taskActivity.js +4 -0
  158. package/dist/utils/taskActivity.test.js +25 -1
  159. package/package.json +3 -1
  160. package/scripts/playwright-auth.sh +4 -0
  161. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  162. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  163. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  164. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  165. 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::';
@@ -58,12 +59,7 @@ export function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery
58
59
  coordinateGetter: sortableKeyboardCoordinates,
59
60
  }));
60
61
  // 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
- };
62
+ const getTaskStatus = getGroupedTaskStatus;
67
63
  const todayDateOnly = useMemo(() => {
68
64
  const now = new Date();
69
65
  const year = now.getFullYear();
@@ -481,171 +477,46 @@ export function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery
481
477
  if (column) {
482
478
  if (groupBy === 'schedule') {
483
479
  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));
480
+ const candidateOverTaskId = overId && isTaskCardId(overId) ? getTaskIdFromCardId(overId) : null;
481
+ const overRect = over?.rect;
482
+ const translatedActiveRect = active.rect.current.translated;
483
+ const activeRect = translatedActiveRect || active.rect.current.initial;
484
+ const isBelowOverItem = Boolean(overRect
485
+ && activeRect
486
+ && (activeRect.top + activeRect.height / 2) > (overRect.top + overRect.height / 2));
487
+ const targetTasks = (groupedTasks[targetColumn] || []).filter((task) => task.id !== activeTaskId);
488
+ const sourceTasks = (groupedTasks[String(resolvedSourceContainerId)] || []).filter((task) => task.id !== activeTaskId);
489
+ const scheduleDropResult = buildScheduleDropResult({
490
+ activeTask,
491
+ sourceColumnId: resolvedSourceContainerId,
492
+ targetColumnId: targetColumn,
493
+ targetTasks,
494
+ sourceTasks,
495
+ overTaskId: candidateOverTaskId,
496
+ isBelowOverItem,
497
+ scheduleDates: scheduleContext.dates,
498
+ parseToIsoWeekKey: scheduleContext.parseToIsoWeekKey,
499
+ todayDate: todayDateOnly,
500
+ });
501
+ if (!scheduleDropResult)
531
502
  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));
503
+ if (scheduleDropResult.kind === 'blocked') {
504
+ window.alert(scheduleDropResult.reason);
565
505
  return;
566
506
  }
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;
507
+ scheduleDropResult.updates.forEach(({ taskId, updates }) => onUpdateTask(taskId, updates));
508
+ if (scheduleDropResult.selectedDate) {
509
+ onScheduleDaySelected?.(scheduleDropResult.selectedDate);
576
510
  }
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
511
  return;
603
512
  }
604
513
  // Determine the value to update based on the grouping
605
514
  let targetValue = column.value;
606
515
  // STATUS HANDLING
607
516
  if (groupBy === 'status') {
608
- const currentStatus = getTaskStatus(activeTask);
609
517
  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
- }
518
+ const updates = buildStatusTransitionUpdates(activeTask, targetStatus, new Date().toISOString());
519
+ if (updates) {
649
520
  onUpdateTask(activeTask.id, updates);
650
521
  }
651
522
  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 {};