@zeniai/client-epic-state 5.1.45 → 5.1.46

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 (85) hide show
  1. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  2. package/lib/entity/snackbar/snackbarTypes.js +3 -0
  3. package/lib/entity/task/taskPayload.d.ts +3 -1
  4. package/lib/entity/task/taskPayload.js +24 -2
  5. package/lib/entity/task/taskReducer.d.ts +8 -1
  6. package/lib/entity/task/taskReducer.js +29 -4
  7. package/lib/entity/task/taskState.d.ts +2 -0
  8. package/lib/entity/tenant/epic/saveExternalConnectionEpic.d.ts +2 -1
  9. package/lib/entity/tenant/epic/saveExternalConnectionEpic.js +17 -0
  10. package/lib/epic.d.ts +6 -3
  11. package/lib/epic.js +6 -3
  12. package/lib/esm/entity/snackbar/snackbarTypes.js +3 -0
  13. package/lib/esm/entity/task/taskPayload.js +24 -2
  14. package/lib/esm/entity/task/taskReducer.js +28 -3
  15. package/lib/esm/entity/tenant/epic/saveExternalConnectionEpic.js +17 -0
  16. package/lib/esm/epic.js +6 -3
  17. package/lib/esm/index.js +8 -8
  18. package/lib/esm/view/aiCfoView/aiCfoViewReducer.js +31 -1
  19. package/lib/esm/view/aiCfoView/epics/fetchSkillsEpic.js +33 -8
  20. package/lib/esm/view/aiCfoView/epics/submitFeedbackEpic.js +39 -0
  21. package/lib/esm/view/companyTaskManagerView/epics/fetchCompanyTaskManagerViewEpic.js +3 -0
  22. package/lib/esm/view/companyTaskManagerView/epics/fetchTaskManagerMetricsEpic.js +7 -1
  23. package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +7 -0
  24. package/lib/esm/view/taskManager/taskDetailView/epics/createSubTaskEpic.js +96 -0
  25. package/lib/esm/view/taskManager/taskDetailView/epics/fetchSubTasksEpic.js +53 -0
  26. package/lib/esm/view/taskManager/taskDetailView/epics/initializeTaskToLocalStoreEpic.js +1 -0
  27. package/lib/esm/view/taskManager/taskDetailView/epics/saveTaskDetailEpic.js +70 -9
  28. package/lib/esm/view/taskManager/taskDetailView/taskDetailReducer.js +56 -2
  29. package/lib/esm/view/taskManager/taskDetailView/taskDetailSelector.js +35 -9
  30. package/lib/esm/view/taskManager/taskListView/epics/fetchTaskListEpic.js +3 -1
  31. package/lib/esm/view/taskManager/taskListView/taskList.js +1 -0
  32. package/lib/esm/view/taskManager/taskListView/taskListReducer.js +310 -18
  33. package/lib/esm/view/taskManager/taskListView/taskListSelector.js +1 -0
  34. package/lib/esm/view/taskManager/taskListView/taskListViewHelpers.js +58 -0
  35. package/lib/esm/view/transactionDetail/epics/saveTransactionHelper.js +22 -0
  36. package/lib/esm/view/transactionDetail/epics/updateTransactionDetailEpic.js +75 -3
  37. package/lib/esm/view/transactionDetail/journalEntryLinesViewModel.js +58 -0
  38. package/lib/esm/view/transactionDetail/transactionDetailLocalDataHelper.js +10 -8
  39. package/lib/esm/view/transactionDetail/transactionDetailReducer.js +66 -1
  40. package/lib/index.d.ts +12 -11
  41. package/lib/index.js +37 -28
  42. package/lib/view/aiCfoView/aiCfoViewPayload.d.ts +10 -0
  43. package/lib/view/aiCfoView/aiCfoViewReducer.d.ts +7 -2
  44. package/lib/view/aiCfoView/aiCfoViewReducer.js +32 -2
  45. package/lib/view/aiCfoView/aiCfoViewState.d.ts +5 -1
  46. package/lib/view/aiCfoView/epics/fetchSkillsEpic.js +33 -8
  47. package/lib/view/aiCfoView/epics/submitFeedbackEpic.d.ts +13 -0
  48. package/lib/view/aiCfoView/epics/submitFeedbackEpic.js +43 -0
  49. package/lib/view/companyTaskManagerView/epics/fetchCompanyTaskManagerViewEpic.js +3 -0
  50. package/lib/view/companyTaskManagerView/epics/fetchTaskManagerMetricsEpic.js +7 -1
  51. package/lib/view/companyView/types/cockpitTypes.d.ts +3 -0
  52. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  53. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +7 -0
  54. package/lib/view/taskManager/taskDetailView/epics/createSubTaskEpic.d.ts +9 -0
  55. package/lib/view/taskManager/taskDetailView/epics/createSubTaskEpic.js +100 -0
  56. package/lib/view/taskManager/taskDetailView/epics/fetchSubTasksEpic.d.ts +8 -0
  57. package/lib/view/taskManager/taskDetailView/epics/fetchSubTasksEpic.js +57 -0
  58. package/lib/view/taskManager/taskDetailView/epics/initializeTaskToLocalStoreEpic.js +1 -0
  59. package/lib/view/taskManager/taskDetailView/epics/saveTaskDetailEpic.d.ts +2 -2
  60. package/lib/view/taskManager/taskDetailView/epics/saveTaskDetailEpic.js +69 -8
  61. package/lib/view/taskManager/taskDetailView/taskDetail.d.ts +15 -0
  62. package/lib/view/taskManager/taskDetailView/taskDetailReducer.d.ts +14 -2
  63. package/lib/view/taskManager/taskDetailView/taskDetailReducer.js +57 -3
  64. package/lib/view/taskManager/taskDetailView/taskDetailSelector.d.ts +3 -1
  65. package/lib/view/taskManager/taskDetailView/taskDetailSelector.js +33 -7
  66. package/lib/view/taskManager/taskListView/epics/fetchTaskListEpic.js +3 -1
  67. package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
  68. package/lib/view/taskManager/taskListView/taskList.js +1 -0
  69. package/lib/view/taskManager/taskListView/taskListPayload.d.ts +4 -0
  70. package/lib/view/taskManager/taskListView/taskListReducer.d.ts +6 -2
  71. package/lib/view/taskManager/taskListView/taskListReducer.js +311 -19
  72. package/lib/view/taskManager/taskListView/taskListSelector.js +1 -0
  73. package/lib/view/taskManager/taskListView/taskListViewHelpers.d.ts +11 -0
  74. package/lib/view/taskManager/taskListView/taskListViewHelpers.js +60 -1
  75. package/lib/view/transactionDetail/epics/saveTransactionHelper.js +22 -0
  76. package/lib/view/transactionDetail/epics/updateTransactionDetailEpic.d.ts +5 -1
  77. package/lib/view/transactionDetail/epics/updateTransactionDetailEpic.js +77 -4
  78. package/lib/view/transactionDetail/journalEntryLinesViewModel.d.ts +27 -0
  79. package/lib/view/transactionDetail/journalEntryLinesViewModel.js +61 -1
  80. package/lib/view/transactionDetail/transactionDetailLocalDataHelper.js +10 -8
  81. package/lib/view/transactionDetail/transactionDetailReducer.d.ts +11 -2
  82. package/lib/view/transactionDetail/transactionDetailReducer.js +67 -2
  83. package/lib/view/transactionDetail/transactionDetailState.d.ts +8 -1
  84. package/lib/view/transactionDetail/transactionDetailTypes.d.ts +2 -0
  85. package/package.json +1 -1
@@ -30,40 +30,58 @@ const getTaskDetail = (state, taskId) => {
30
30
  let createdByUser = undefined;
31
31
  let isArchived = undefined;
32
32
  let isDeleted = undefined;
33
+ let depth = undefined;
33
34
  let recurringSourceTaskId = undefined;
34
35
  let snoozedUntil = undefined;
35
36
  let taskGroupId = undefined;
37
+ let subtasks = [];
36
38
  if (taskId != null && sourceTaskDetail != null) {
37
39
  const taskEntity = (0, taskSelector_1.getTaskById)(taskState, taskId);
40
+ subtasks =
41
+ taskEntity != null
42
+ ? (0, taskSelector_1.getTasksByIds)(taskState, taskEntity.subTasksIds)
43
+ : [];
38
44
  const fileIdsInEntity = taskEntity?.fileIds ?? [];
39
45
  if (taskEntity != null) {
40
46
  createdByUser = (0, userAndRole_1.getUserAndUserRole)(userState, userRoleState, addressState, taskEntity.createdBy);
47
+ depth = taskEntity.depth;
41
48
  isArchived = taskEntity.isArchived;
42
49
  isDeleted = taskEntity.isDeleted;
43
50
  recurringSourceTaskId = taskEntity.recurringSourceTaskId;
44
51
  snoozedUntil = taskEntity.snoozedUntil;
45
52
  taskGroupId = taskEntity.taskGroupIds[0];
46
53
  }
54
+ // Treat "no entry" as Completed so it doesn't block the combined
55
+ // fetch state. The Not-Started default would pin the detail page
56
+ // below Completed forever before the first fetchSubTasks dispatches.
57
+ const completedSubTaskStatus = {
58
+ fetchState: 'Completed',
59
+ error: undefined,
60
+ };
61
+ const subTaskStatus = taskDetailState.subTaskListFetchStatusByParentId[taskId] ??
62
+ completedSubTaskStatus;
47
63
  if (fileIdsInEntity.length > 0) {
48
- fetchStatus = (0, reduceFetchState_1.reduceFetchState)([
64
+ fetchStatus = (0, reduceFetchState_1.reduceAnyFetchState)([
49
65
  fileViewState.fetchFilesStatus,
50
66
  sourceTaskDetail.fetchTaskStatus,
51
67
  userList,
52
68
  classListState,
69
+ subTaskStatus,
53
70
  ]);
54
71
  }
55
72
  else {
56
- fetchStatus = (0, reduceFetchState_1.reduceFetchState)([
73
+ fetchStatus = (0, reduceFetchState_1.reduceAnyFetchState)([
57
74
  sourceTaskDetail.fetchTaskStatus,
58
75
  userList,
59
76
  classListState,
77
+ subTaskStatus,
60
78
  ]);
61
79
  }
62
80
  showTaskDetailFormFooter = showFormFooter(sourceTaskDetail.taskDetailLocalData, taskEntity);
63
81
  taskHistory = taskDetailState.taskHistoryById[taskId]?.historicEvents ?? [];
64
82
  }
65
83
  else if (taskId == null) {
66
- fetchStatus = (0, reduceFetchState_1.reduceFetchState)([userList, classListState]);
84
+ fetchStatus = (0, reduceFetchState_1.reduceAnyFetchState)([userList, classListState]);
67
85
  }
68
86
  const { classIds } = classListState;
69
87
  const allAccountingClasses = (0, classSelector_1.getClassesByIds)(classState, {
@@ -90,12 +108,14 @@ const getTaskDetail = (state, taskId) => {
90
108
  error: fetchStatus.error,
91
109
  version: 0,
92
110
  createdByUser,
111
+ depth,
93
112
  isArchived,
94
113
  isDeleted,
95
114
  recurringSourceTaskId,
96
115
  showTaskDetailFormFooter,
97
116
  snoozedUntil,
98
117
  taskGroupId,
118
+ subtasks,
99
119
  };
100
120
  };
101
121
  exports.getTaskDetail = getTaskDetail;
@@ -144,13 +164,16 @@ exports.allTaskPriority = [
144
164
  },
145
165
  ];
146
166
  const showFormFooter = (taskDetailLocalData, taskDetailInStore) => {
147
- if (taskDetailLocalData.name !== taskDetailInStore?.name ||
167
+ if (taskDetailInStore == null) {
168
+ return false;
169
+ }
170
+ if (taskDetailLocalData.name !== taskDetailInStore.name ||
148
171
  taskDetailLocalData.description !== taskDetailInStore.description ||
149
172
  taskDetailLocalData.priority !== taskDetailInStore.priority.code ||
150
173
  taskDetailLocalData.status !== taskDetailInStore.status.code ||
151
174
  !(0, isEqual_1.default)(taskDetailInStore.tagIds, taskDetailLocalData.tagIds) ||
152
- !(0, isEqual_1.default)(taskDetailLocalData.assignee, taskDetailInStore?.assignees ?? []) ||
153
- !(0, isEqual_1.default)(taskDetailLocalData.groupAssignees, taskDetailInStore?.groupAssignees ?? []) ||
175
+ !(0, isEqual_1.default)(taskDetailLocalData.assignee, taskDetailInStore.assignees ?? []) ||
176
+ !(0, isEqual_1.default)(taskDetailLocalData.groupAssignees, taskDetailInStore.groupAssignees ?? []) ||
154
177
  !(0, isEqual_1.default)(taskDetailLocalData.fileIds, taskDetailInStore.fileIds ?? []) ||
155
178
  !(0, isEqual_1.default)(taskDetailLocalData.dueDate, taskDetailInStore.dueDate) ||
156
179
  !(0, isEqual_1.default)(taskDetailLocalData.type, taskDetailInStore.type) ||
@@ -158,7 +181,10 @@ const showFormFooter = (taskDetailLocalData, taskDetailInStore) => {
158
181
  !(0, isEqual_1.default)(taskDetailLocalData.recurringFrequency, taskDetailInStore.recurringFrequency) ||
159
182
  !(0, isEqual_1.default)(taskDetailLocalData.recurringDaysOfWeek, taskDetailInStore.recurringDaysOfWeek) ||
160
183
  !(0, isEqual_1.default)(taskDetailLocalData.recurringStartDate, taskDetailInStore.recurringStartDate) ||
161
- !(0, isEqual_1.default)(taskDetailLocalData.timeSpent, taskDetailInStore.timeSpent)) {
184
+ !(0, isEqual_1.default)(taskDetailLocalData.timeSpent, taskDetailInStore.timeSpent) ||
185
+ !(0, isEqual_1.default)(taskDetailLocalData.taskGroupId, taskDetailInStore.taskGroupIds.length > 0
186
+ ? taskDetailInStore.taskGroupIds[0]
187
+ : undefined)) {
162
188
  return true;
163
189
  }
164
190
  return false;
@@ -12,15 +12,17 @@ const fetchTaskListEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, oper
12
12
  .getJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks?query=${encodeURIComponent(`{"task_type": "all"}`)}`)
13
13
  .pipe((0, operators_1.mergeMap)((response) => {
14
14
  if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
15
- const { tasks, deleted, archived, snoozed } = response.data;
15
+ const { tasks, deleted, archived, snoozed, completed } = response.data;
16
16
  const allTasks = [
17
17
  ...tasks,
18
+ ...(completed ?? []),
18
19
  ...(deleted ?? []),
19
20
  ...(archived ?? []),
20
21
  ...(snoozed ?? []),
21
22
  ];
22
23
  return (0, rxjs_1.of)((0, taskReducer_1.updateTasks)(allTasks), (0, taskListReducer_1.updateTaskList)({
23
24
  data: tasks,
25
+ completed: completed ?? [],
24
26
  deleted: deleted ?? [],
25
27
  archived: archived ?? [],
26
28
  snoozed: snoozed ?? [],
@@ -3,7 +3,7 @@ import { PriorityCodeType, TaskStatusCodeType } from '../../../entity/task/taskS
3
3
  import { ZeniDate } from '../../../zeniDayJS';
4
4
  import { SpendManagementUIState } from '../../spendManagement/billPay/billList/billListState';
5
5
  import { CategoryCombinationOperator, FilterCategoryType } from '../../spendManagement/spendManagementFilterHelpers';
6
- export declare const ALL_TASK_LIST_TABS: readonly ["live", "archived", "deleted", "snoozed"];
6
+ export declare const ALL_TASK_LIST_TABS: readonly ["live", "completed", "archived", "deleted", "snoozed"];
7
7
  export type TaskListTab = (typeof ALL_TASK_LIST_TABS)[number];
8
8
  export interface TaskListTabData {
9
9
  taskIds: ID[];
@@ -4,6 +4,7 @@ exports.TASK_LIST_GROUP_BY_CATEGORIES = exports.TASK_LIST_FILTER_CATEGORIES = ex
4
4
  const stringToUnion_1 = require("../../../commonStateTypes/stringToUnion");
5
5
  exports.ALL_TASK_LIST_TABS = [
6
6
  'live',
7
+ 'completed',
7
8
  'archived',
8
9
  'deleted',
9
10
  'snoozed',
@@ -3,6 +3,10 @@ import { ZeniAPIResponse } from '../../../responsePayload';
3
3
  interface FetchTaskListPayload {
4
4
  tasks: TaskPayload[];
5
5
  archived?: TaskPayload[];
6
+ /** Tasks whose status is "done" or "resolved". Returned as a separate
7
+ * bucket so the "Completed" tab can show them without duplicating
8
+ * them under Active. */
9
+ completed?: TaskPayload[];
6
10
  deleted?: TaskPayload[];
7
11
  snoozed?: TaskPayload[];
8
12
  }
@@ -12,9 +12,10 @@ export declare const fetchTaskListPage: import("@reduxjs/toolkit").ActionCreator
12
12
  }, "taskList/fetchTaskListPage", never, never>, fetchTaskList: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"taskList/fetchTaskList">, updateTaskList: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
13
13
  data: TaskPayload[];
14
14
  archived?: TaskPayload[];
15
+ completed?: TaskPayload[];
15
16
  deleted?: TaskPayload[];
16
17
  snoozed?: TaskPayload[];
17
- }, "taskList/updateTaskList">, updateTaskListTab: import("@reduxjs/toolkit").ActionCreatorWithPayload<"deleted" | "archived" | "live" | "snoozed", "taskList/updateTaskListTab">, updateTaskListFetchStatus: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
18
+ }, "taskList/updateTaskList">, updateTaskListTab: import("@reduxjs/toolkit").ActionCreatorWithPayload<"completed" | "deleted" | "archived" | "live" | "snoozed", "taskList/updateTaskListTab">, updateTaskListFetchStatus: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
18
19
  fetchState: FetchState;
19
20
  error?: ZeniAPIStatus;
20
21
  }, "taskList/updateTaskListFetchStatus">, updateTaskListSearchText: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
@@ -58,6 +59,9 @@ export declare const fetchTaskListPage: import("@reduxjs/toolkit").ActionCreator
58
59
  error: ZeniAPIStatus;
59
60
  }, "taskList/updateTasksListOnUpdateTaskFailure">, updateTaskListOnCreateTaskFromTemplateSuccess: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
60
61
  tasks: TaskPayload[];
61
- }, "taskList/updateTaskListOnCreateTaskFromTemplateSuccess">, removeTaskFromList: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "taskList/removeTaskFromList">, clearTaskList: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"taskList/clearTaskList">;
62
+ }, "taskList/updateTaskListOnCreateTaskFromTemplateSuccess">, removeTaskFromList: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "taskList/removeTaskFromList">, removeTaskFromGroupBucket: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
63
+ taskGroupId: ID;
64
+ taskId: ID;
65
+ }, "taskList/removeTaskFromGroupBucket">, clearTaskList: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"taskList/clearTaskList">;
62
66
  declare const _default: import("redux").Reducer<TaskListState>;
63
67
  export default _default;
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.clearTaskList = exports.removeTaskFromList = exports.updateTaskListOnCreateTaskFromTemplateSuccess = exports.updateTasksListOnUpdateTaskFailure = exports.updateTasksListOnUpdateTaskSuccess = exports.updateTaskFromListView = exports.updateTaskListOnTaskGroupNameUpdate = exports.updateTaskListOnNewTaskCreationSuccess = exports.dragNDropTasks = exports.updateTaskListOnTaskGroupDeletion = exports.updateTaskListLocalData = exports.initiateTaskListLocalData = exports.updateTasksListOnBulkUpdateTasksFailure = exports.updateTasksListOnBulkUpdateTasksSuccess = exports.bulkUpdateTaskList = exports.updateTaskListOnNewGroupCreationSuccess = exports.updateTaskFilters = exports.updateTaskListUIState = exports.updateTaskListSearchText = exports.updateTaskListFetchStatus = exports.updateTaskListTab = exports.updateTaskList = exports.fetchTaskList = exports.fetchTaskListPage = exports.initialState = void 0;
4
+ exports.clearTaskList = exports.removeTaskFromGroupBucket = exports.removeTaskFromList = exports.updateTaskListOnCreateTaskFromTemplateSuccess = exports.updateTasksListOnUpdateTaskFailure = exports.updateTasksListOnUpdateTaskSuccess = exports.updateTaskFromListView = exports.updateTaskListOnTaskGroupNameUpdate = exports.updateTaskListOnNewTaskCreationSuccess = exports.dragNDropTasks = exports.updateTaskListOnTaskGroupDeletion = exports.updateTaskListLocalData = exports.initiateTaskListLocalData = exports.updateTasksListOnBulkUpdateTasksFailure = exports.updateTasksListOnBulkUpdateTasksSuccess = exports.bulkUpdateTaskList = exports.updateTaskListOnNewGroupCreationSuccess = exports.updateTaskFilters = exports.updateTaskListUIState = exports.updateTaskListSearchText = exports.updateTaskListFetchStatus = exports.updateTaskListTab = exports.updateTaskList = exports.fetchTaskList = exports.fetchTaskListPage = exports.initialState = void 0;
5
5
  const toolkit_1 = require("@reduxjs/toolkit");
6
+ const taskReducer_1 = require("../../../entity/task/taskReducer");
6
7
  const taskState_1 = require("../../../entity/task/taskState");
7
8
  const zeniDayJS_1 = require("../../../zeniDayJS");
8
9
  const taskList_1 = require("./taskList");
@@ -46,6 +47,7 @@ exports.initialState = {
46
47
  currentTab: 'live',
47
48
  byTab: {
48
49
  live: { ...initialTabData },
50
+ completed: { ...initialTabData },
49
51
  archived: { ...initialTabData },
50
52
  deleted: { ...initialTabData },
51
53
  snoozed: { ...initialTabData },
@@ -67,14 +69,12 @@ exports.initialState = {
67
69
  },
68
70
  filters: {
69
71
  categoryCombinationOperator: 'AND',
70
- categories: [
71
- {
72
- field: 'status',
73
- matchingOperator: 'not_equal',
74
- values: ['resolved'],
75
- valuesCombinationOperator: 'ANY',
76
- },
77
- ],
72
+ // Default to no filters. The Completed tab now owns the "hide resolved
73
+ // tasks from the live list" responsibility via the byTab split in
74
+ // updateTaskList / updateTaskFromListView / the updateTasks
75
+ // extraReducer. Keeping the historical `status != resolved` filter
76
+ // here would empty out the Completed tab.
77
+ categories: [],
78
78
  },
79
79
  hasValidState() {
80
80
  return this.fetchState == 'Completed';
@@ -106,21 +106,37 @@ const taskList = (0, toolkit_1.createSlice)({
106
106
  draft.error = undefined;
107
107
  },
108
108
  updateTaskList(draft, action) {
109
- const { data, archived, deleted, snoozed } = action.payload;
109
+ const { data, archived, completed, deleted, snoozed } = action.payload;
110
110
  draft.fetchState = 'Completed';
111
111
  draft.error = undefined;
112
+ // Build the nested-subtask set from the UNION of every bucket in
113
+ // this payload — not per-bucket. A resolved subtask can arrive in
114
+ // `completed` while its parent (which claims it via `subtasks`)
115
+ // is still in `data`; a per-bucket scan would miss the cross-tab
116
+ // claim and surface the subtask as a phantom top-level row in
117
+ // the Completed tab.
118
+ const nestedSubtaskIds = collectNestedSubtaskIds([
119
+ data,
120
+ completed,
121
+ archived,
122
+ deleted,
123
+ snoozed,
124
+ ]);
112
125
  // Group live tasks
113
- const liveGrouped = getGroupedTaskIds(data);
126
+ const liveGrouped = getGroupedTaskIds(data, nestedSubtaskIds);
114
127
  draft.byTab.live = toTabData(liveGrouped);
115
128
  // Group other tabs if provided
129
+ if (completed != null) {
130
+ draft.byTab.completed = toTabData(getGroupedTaskIds(completed, nestedSubtaskIds));
131
+ }
116
132
  if (archived != null) {
117
- draft.byTab.archived = toTabData(getGroupedTaskIds(archived));
133
+ draft.byTab.archived = toTabData(getGroupedTaskIds(archived, nestedSubtaskIds));
118
134
  }
119
135
  if (deleted != null) {
120
- draft.byTab.deleted = toTabData(getGroupedTaskIds(deleted));
136
+ draft.byTab.deleted = toTabData(getGroupedTaskIds(deleted, nestedSubtaskIds));
121
137
  }
122
138
  if (snoozed != null) {
123
- draft.byTab.snoozed = toTabData(getGroupedTaskIds(snoozed));
139
+ draft.byTab.snoozed = toTabData(getGroupedTaskIds(snoozed, nestedSubtaskIds));
124
140
  }
125
141
  // Copy active tab data to top-level fields
126
142
  const activeTabData = draft.byTab[draft.currentTab];
@@ -358,9 +374,26 @@ const taskList = (0, toolkit_1.createSlice)({
358
374
  draft.taskIdsByAssignees[assigneeKey] = draft.taskIdsByAssignees[assigneeKey].filter((taskId) => taskId !== currentTaskState.id);
359
375
  draft.taskIdsByTags[tagsKey] = draft.taskIdsByTags[tagsKey].filter((taskId) => taskId !== currentTaskState.id);
360
376
  draft.taskIdsByDueDate[dueDateKey] = draft.taskIdsByDueDate[dueDateKey].filter((taskId) => taskId !== currentTaskState.id);
361
- draft.taskIdsByGroupsIds[taskGroupId] = draft.taskIdsByGroupsIds[taskGroupId].filter((taskId) => taskId !== currentTaskState.id);
377
+ // Remove from EVERY previous group bucket the task lived in, not
378
+ // just the destination bucket. Otherwise a detail-page save that
379
+ // moves a task from group A to group B leaves the row listed
380
+ // under both A and B until refetch.
381
+ const previousGroupIds = currentTaskState.taskGroupIds ?? [];
382
+ const allGroupIdsToClean = new Set([
383
+ ...previousGroupIds,
384
+ taskGroupId,
385
+ ]);
386
+ allGroupIdsToClean.forEach((gid) => {
387
+ if (draft.taskIdsByGroupsIds[gid] != null) {
388
+ draft.taskIdsByGroupsIds[gid] = draft.taskIdsByGroupsIds[gid].filter((id) => id !== currentTaskState.id);
389
+ }
390
+ });
362
391
  }
363
392
  doUpdateTaskList(draft, task, taskGroupId);
393
+ // Mirror top-level grouping into the active tab's byTab snapshot,
394
+ // otherwise the next updateTaskListTab call copies stale byTab data
395
+ // back over this optimistic update.
396
+ syncTopLevelToActiveTab(draft);
364
397
  },
365
398
  updateTaskListOnCreateTaskFromTemplateSuccess(draft, action) {
366
399
  const { tasks } = action.payload;
@@ -384,6 +417,7 @@ const taskList = (0, toolkit_1.createSlice)({
384
417
  const taskId = task.id;
385
418
  draft.updateTasksFetchState.fetchState = 'In-Progress';
386
419
  draft.updateTasksFetchState.error = undefined;
420
+ let didMigrateTab = false;
387
421
  if (updates.priority != null) {
388
422
  const prevPriorityCode = (0, taskState_1.toPriorityCodeType)(task.priority.code);
389
423
  draft.taskIdsByPriority[prevPriorityCode] = draft.taskIdsByPriority[prevPriorityCode].filter((id) => id != taskId);
@@ -395,6 +429,32 @@ const taskList = (0, toolkit_1.createSlice)({
395
429
  draft.taskIdsByStatus[prevStatusCode] = draft.taskIdsByStatus[prevStatusCode].filter((id) => id != taskId);
396
430
  const newStatusCode = (0, taskState_1.toTaskStatusCodeType)(updates.status.code);
397
431
  draft.taskIdsByStatus[newStatusCode].push(taskId);
432
+ // Optimistically re-bucket between the live and completed tabs
433
+ // when status transitions in/out of "resolved", so the task
434
+ // moves to the Completed tab without waiting for the next
435
+ // fetch. Mirrors backend split in task_handler.py: only
436
+ // `resolved` counts as completed; `done` stays Active.
437
+ //
438
+ // "Top-level" here means: currently rendered as a standalone
439
+ // row in the live or completed tab (i.e. present in
440
+ // `byTab.<tab>.taskIds`). We use presence in byTab — NOT a
441
+ // `parent_task_id == null` check — so orphan subtasks (parent
442
+ // hidden from the customer's payload, so we render them flat)
443
+ // ALSO migrate between tabs on status flip. This matches the
444
+ // criterion `getGroupedTaskIds` uses when it builds byTab from
445
+ // the initial fetch — a task is nested iff a visible parent's
446
+ // `subtasks` array claims it, otherwise it's top-level here.
447
+ const isTopLevelTask = draft.byTab.live.taskIds.includes(taskId) ||
448
+ draft.byTab.completed.taskIds.includes(taskId);
449
+ const wasCompleted = prevStatusCode === 'resolved';
450
+ const isCompleted = newStatusCode === 'resolved';
451
+ if (isTopLevelTask && wasCompleted !== isCompleted) {
452
+ const from = isCompleted ? 'live' : 'completed';
453
+ const to = isCompleted ? 'completed' : 'live';
454
+ const updatedTask = { ...task, ...updates };
455
+ migrateTaskBetweenTabs(draft, taskId, updatedTask, from, to);
456
+ didMigrateTab = true;
457
+ }
398
458
  }
399
459
  if (updates.assignees != null) {
400
460
  const prevGroupId = getAssigneesGroupKey(task.assignees);
@@ -434,7 +494,18 @@ const taskList = (0, toolkit_1.createSlice)({
434
494
  };
435
495
  }
436
496
  }
437
- syncTopLevelToActiveTab(draft);
497
+ if (didMigrateTab) {
498
+ // The migration moved the task out of the source byTab and into
499
+ // the destination byTab. The active tab's grouping arrays are
500
+ // now authoritative — mirror them back to the top-level so the
501
+ // UI sees the task gone from Live (or freshly arrived in
502
+ // Completed). Inverse of syncTopLevelToActiveTab, used only on
503
+ // this path.
504
+ copyTabDataToTopLevel(draft, draft.byTab[draft.currentTab]);
505
+ }
506
+ else {
507
+ syncTopLevelToActiveTab(draft);
508
+ }
438
509
  },
439
510
  updateTasksListOnUpdateTaskSuccess(draft) {
440
511
  draft.updateTasksFetchState.fetchState = 'Completed';
@@ -447,12 +518,83 @@ const taskList = (0, toolkit_1.createSlice)({
447
518
  removeTaskFromList(draft, action) {
448
519
  removeTaskIds(draft, [action.payload]);
449
520
  },
521
+ /**
522
+ * Remove a task id from a single group bucket without touching tab
523
+ * membership or other groupings. Used when the user clears a task's
524
+ * group on the Detail page so the row stops appearing under the old
525
+ * group while saveTaskDetailEpic skips the standard list re-insert.
526
+ */
527
+ removeTaskFromGroupBucket(draft, action) {
528
+ const { taskId, taskGroupId } = action.payload;
529
+ if (draft.taskIdsByGroupsIds[taskGroupId] != null) {
530
+ draft.taskIdsByGroupsIds[taskGroupId] = draft.taskIdsByGroupsIds[taskGroupId].filter((id) => id !== taskId);
531
+ }
532
+ // Mirror to the active tab's grouping so the UI re-renders.
533
+ const activeTab = draft.byTab[draft.currentTab];
534
+ if (activeTab.taskIdsByGroupsIds[taskGroupId] != null) {
535
+ activeTab.taskIdsByGroupsIds[taskGroupId] =
536
+ activeTab.taskIdsByGroupsIds[taskGroupId].filter((id) => id !== taskId);
537
+ }
538
+ },
450
539
  clearTaskList(draft) {
451
540
  Object.assign(draft, exports.initialState);
452
541
  },
453
542
  },
543
+ extraReducers: (builder) => {
544
+ // When the entity store receives updated tasks (e.g. from task detail
545
+ // save, snooze/archive/unarchive, etc.), re-bucket top-level tasks
546
+ // between the live and completed tabs based on the new status. Mirrors
547
+ // the backend split in task_handler.py: only `resolved` counts as
548
+ // completed.
549
+ //
550
+ // "Top-level" here means: currently rendered as a standalone row in
551
+ // the live or completed tab (i.e. present in
552
+ // `byTab.<tab>.taskIds`). Tasks that are nested under a parent's
553
+ // expanded subtask row live outside byTab.taskIds and are silently
554
+ // skipped by the "in-bucket" check below. This uses the same
555
+ // criterion as `getGroupedTaskIds` — a task is nested iff a visible
556
+ // parent's `subtasks` array claims it, otherwise it's top-level —
557
+ // so orphan subtasks (parent hidden from the customer's payload)
558
+ // migrate correctly on status flip via pusher / detail-save.
559
+ builder.addCase(taskReducer_1.updateTasks, (draft, action) => {
560
+ action.payload.forEach((taskPayload) => {
561
+ const taskId = taskPayload.task_id;
562
+ // `TaskPayload.status` is required per the type — every producer
563
+ // in this repo sends it, and `mapTaskPayloadToTask` dereferences
564
+ // it unguarded. The optional chain + null guard are defensive
565
+ // only, in case a downstream consumer (web-app-ui / cockpit-ui)
566
+ // ever dispatches `updateTasks` with a partial payload; in that
567
+ // case we treat missing status as "no status change" and skip
568
+ // re-bucketing so we don't migrate a Completed task back into
569
+ // the Live tab.
570
+ const statusCode = taskPayload.status?.code;
571
+ if (statusCode == null) {
572
+ return;
573
+ }
574
+ const isCompleted = statusCode === 'resolved';
575
+ const targetTab = isCompleted ? 'completed' : 'live';
576
+ const otherTab = isCompleted ? 'live' : 'completed';
577
+ // Skip if the task isn't currently in either of these buckets
578
+ // (archived/snoozed/deleted are managed by their own actions).
579
+ const inLive = draft.byTab.live.taskIds.includes(taskId);
580
+ const inCompleted = draft.byTab.completed.taskIds.includes(taskId);
581
+ if (!inLive && !inCompleted) {
582
+ return;
583
+ }
584
+ if (draft.byTab[otherTab].taskIds.includes(taskId)) {
585
+ migrateTaskPayloadBetweenTabs(draft, taskId, taskPayload, otherTab, targetTab);
586
+ }
587
+ else if (!draft.byTab[targetTab].taskIds.includes(taskId)) {
588
+ draft.byTab[targetTab].taskIds.push(taskId);
589
+ }
590
+ });
591
+ // Re-sync the top-level taskIds / grouping arrays for whichever
592
+ // tab the user is currently looking at.
593
+ copyTabDataToTopLevel(draft, draft.byTab[draft.currentTab]);
594
+ });
595
+ },
454
596
  });
455
- _a = taskList.actions, exports.fetchTaskListPage = _a.fetchTaskListPage, exports.fetchTaskList = _a.fetchTaskList, exports.updateTaskList = _a.updateTaskList, exports.updateTaskListTab = _a.updateTaskListTab, exports.updateTaskListFetchStatus = _a.updateTaskListFetchStatus, exports.updateTaskListSearchText = _a.updateTaskListSearchText, exports.updateTaskListUIState = _a.updateTaskListUIState, exports.updateTaskFilters = _a.updateTaskFilters, exports.updateTaskListOnNewGroupCreationSuccess = _a.updateTaskListOnNewGroupCreationSuccess, exports.bulkUpdateTaskList = _a.bulkUpdateTaskList, exports.updateTasksListOnBulkUpdateTasksSuccess = _a.updateTasksListOnBulkUpdateTasksSuccess, exports.updateTasksListOnBulkUpdateTasksFailure = _a.updateTasksListOnBulkUpdateTasksFailure, exports.initiateTaskListLocalData = _a.initiateTaskListLocalData, exports.updateTaskListLocalData = _a.updateTaskListLocalData, exports.updateTaskListOnTaskGroupDeletion = _a.updateTaskListOnTaskGroupDeletion, exports.dragNDropTasks = _a.dragNDropTasks, exports.updateTaskListOnNewTaskCreationSuccess = _a.updateTaskListOnNewTaskCreationSuccess, exports.updateTaskListOnTaskGroupNameUpdate = _a.updateTaskListOnTaskGroupNameUpdate, exports.updateTaskFromListView = _a.updateTaskFromListView, exports.updateTasksListOnUpdateTaskSuccess = _a.updateTasksListOnUpdateTaskSuccess, exports.updateTasksListOnUpdateTaskFailure = _a.updateTasksListOnUpdateTaskFailure, exports.updateTaskListOnCreateTaskFromTemplateSuccess = _a.updateTaskListOnCreateTaskFromTemplateSuccess, exports.removeTaskFromList = _a.removeTaskFromList, exports.clearTaskList = _a.clearTaskList;
597
+ _a = taskList.actions, exports.fetchTaskListPage = _a.fetchTaskListPage, exports.fetchTaskList = _a.fetchTaskList, exports.updateTaskList = _a.updateTaskList, exports.updateTaskListTab = _a.updateTaskListTab, exports.updateTaskListFetchStatus = _a.updateTaskListFetchStatus, exports.updateTaskListSearchText = _a.updateTaskListSearchText, exports.updateTaskListUIState = _a.updateTaskListUIState, exports.updateTaskFilters = _a.updateTaskFilters, exports.updateTaskListOnNewGroupCreationSuccess = _a.updateTaskListOnNewGroupCreationSuccess, exports.bulkUpdateTaskList = _a.bulkUpdateTaskList, exports.updateTasksListOnBulkUpdateTasksSuccess = _a.updateTasksListOnBulkUpdateTasksSuccess, exports.updateTasksListOnBulkUpdateTasksFailure = _a.updateTasksListOnBulkUpdateTasksFailure, exports.initiateTaskListLocalData = _a.initiateTaskListLocalData, exports.updateTaskListLocalData = _a.updateTaskListLocalData, exports.updateTaskListOnTaskGroupDeletion = _a.updateTaskListOnTaskGroupDeletion, exports.dragNDropTasks = _a.dragNDropTasks, exports.updateTaskListOnNewTaskCreationSuccess = _a.updateTaskListOnNewTaskCreationSuccess, exports.updateTaskListOnTaskGroupNameUpdate = _a.updateTaskListOnTaskGroupNameUpdate, exports.updateTaskFromListView = _a.updateTaskFromListView, exports.updateTasksListOnUpdateTaskSuccess = _a.updateTasksListOnUpdateTaskSuccess, exports.updateTasksListOnUpdateTaskFailure = _a.updateTasksListOnUpdateTaskFailure, exports.updateTaskListOnCreateTaskFromTemplateSuccess = _a.updateTaskListOnCreateTaskFromTemplateSuccess, exports.removeTaskFromList = _a.removeTaskFromList, exports.removeTaskFromGroupBucket = _a.removeTaskFromGroupBucket, exports.clearTaskList = _a.clearTaskList;
456
598
  exports.default = taskList.reducer;
457
599
  /**
458
600
  * helpers
@@ -559,6 +701,115 @@ const copyTabDataToTopLevel = (draft, tabData) => {
559
701
  draft.taskIdsByDueDate = tabData.taskIdsByDueDate;
560
702
  draft.taskIdsByTags = tabData.taskIdsByTags;
561
703
  };
704
+ /**
705
+ * Migrate a parent task between two `byTab` buckets (live ↔ completed)
706
+ * when its status flips into / out of `resolved`. The destination tab
707
+ * picks up the task in EVERY grouping dimension (status, priority,
708
+ * group id, assignees, dueDate, tags) using the UPDATED task as the
709
+ * source-of-truth for the new bucket keys. Otherwise the Completed tab
710
+ * shows the task in the right tab-level list but the grouped views
711
+ * (Group by Status, Group by Priority, etc.) miss it until the next
712
+ * full fetch.
713
+ */
714
+ const migrateTaskBetweenTabs = (draft, taskId, updatedTask, from, to) => {
715
+ const fromTab = draft.byTab[from];
716
+ const toTab = draft.byTab[to];
717
+ // Source tab: drop the task from every grouping dimension.
718
+ removeIdsFromTabData(fromTab, [taskId]);
719
+ // Destination tab: add the task to taskIds (deduped) plus the
720
+ // appropriate bucket inside each grouping dimension based on the
721
+ // updated task.
722
+ if (!toTab.taskIds.includes(taskId)) {
723
+ toTab.taskIds.push(taskId);
724
+ }
725
+ const priorityKey = (0, taskState_1.toPriorityCodeType)(updatedTask.priority.code);
726
+ if (!toTab.taskIdsByPriority[priorityKey].includes(taskId)) {
727
+ toTab.taskIdsByPriority[priorityKey].push(taskId);
728
+ }
729
+ const statusKey = (0, taskState_1.toTaskStatusCodeType)(updatedTask.status.code);
730
+ if (!toTab.taskIdsByStatus[statusKey].includes(taskId)) {
731
+ toTab.taskIdsByStatus[statusKey].push(taskId);
732
+ }
733
+ const dueDateKey = updatedTask.dueDate != null
734
+ ? getDueDateGroupKey(updatedTask.dueDate.toString())
735
+ : 'upcoming';
736
+ if (!toTab.taskIdsByDueDate[dueDateKey].includes(taskId)) {
737
+ toTab.taskIdsByDueDate[dueDateKey].push(taskId);
738
+ }
739
+ const assigneeKey = getAssigneesGroupKey(updatedTask.assignees);
740
+ if (toTab.taskIdsByAssignees[assigneeKey] == null) {
741
+ toTab.taskIdsByAssignees[assigneeKey] = [taskId];
742
+ }
743
+ else if (!toTab.taskIdsByAssignees[assigneeKey].includes(taskId)) {
744
+ toTab.taskIdsByAssignees[assigneeKey].push(taskId);
745
+ }
746
+ const tagIds = [...updatedTask.tagIds].sort();
747
+ const tagsKey = tagIds.join(',');
748
+ if (toTab.taskIdsByTags[tagsKey] == null) {
749
+ toTab.taskIdsByTags[tagsKey] = [taskId];
750
+ }
751
+ else if (!toTab.taskIdsByTags[tagsKey].includes(taskId)) {
752
+ toTab.taskIdsByTags[tagsKey].push(taskId);
753
+ }
754
+ (updatedTask.taskGroupIds ?? []).forEach((groupId) => {
755
+ if (toTab.taskIdsByGroupsIds[groupId] == null) {
756
+ toTab.taskIdsByGroupsIds[groupId] = [taskId];
757
+ }
758
+ else if (!toTab.taskIdsByGroupsIds[groupId].includes(taskId)) {
759
+ toTab.taskIdsByGroupsIds[groupId].push(taskId);
760
+ }
761
+ });
762
+ };
763
+ /**
764
+ * `updateTasks` extraReducer variant — same migration semantics but
765
+ * keyed off the raw `TaskPayload` instead of the rich `Task` entity.
766
+ */
767
+ const migrateTaskPayloadBetweenTabs = (draft, taskId, payload, from, to) => {
768
+ const fromTab = draft.byTab[from];
769
+ const toTab = draft.byTab[to];
770
+ removeIdsFromTabData(fromTab, [taskId]);
771
+ if (!toTab.taskIds.includes(taskId)) {
772
+ toTab.taskIds.push(taskId);
773
+ }
774
+ if (payload.priority?.code != null) {
775
+ const priorityKey = (0, taskState_1.toPriorityCodeType)(payload.priority.code);
776
+ if (!toTab.taskIdsByPriority[priorityKey].includes(taskId)) {
777
+ toTab.taskIdsByPriority[priorityKey].push(taskId);
778
+ }
779
+ }
780
+ if (payload.status?.code != null) {
781
+ const statusKey = (0, taskState_1.toTaskStatusCodeType)(payload.status.code);
782
+ if (!toTab.taskIdsByStatus[statusKey].includes(taskId)) {
783
+ toTab.taskIdsByStatus[statusKey].push(taskId);
784
+ }
785
+ }
786
+ const dueDateKey = getDueDateGroupKey(payload.due_date);
787
+ if (!toTab.taskIdsByDueDate[dueDateKey].includes(taskId)) {
788
+ toTab.taskIdsByDueDate[dueDateKey].push(taskId);
789
+ }
790
+ const assigneeKey = getAssigneesGroupKey(payload.assignees ?? []);
791
+ if (toTab.taskIdsByAssignees[assigneeKey] == null) {
792
+ toTab.taskIdsByAssignees[assigneeKey] = [taskId];
793
+ }
794
+ else if (!toTab.taskIdsByAssignees[assigneeKey].includes(taskId)) {
795
+ toTab.taskIdsByAssignees[assigneeKey].push(taskId);
796
+ }
797
+ const tagsKey = getTagsGroupKey(payload.tags ?? []);
798
+ if (toTab.taskIdsByTags[tagsKey] == null) {
799
+ toTab.taskIdsByTags[tagsKey] = [taskId];
800
+ }
801
+ else if (!toTab.taskIdsByTags[tagsKey].includes(taskId)) {
802
+ toTab.taskIdsByTags[tagsKey].push(taskId);
803
+ }
804
+ (payload.task_group_ids ?? []).forEach((groupId) => {
805
+ if (toTab.taskIdsByGroupsIds[groupId] == null) {
806
+ toTab.taskIdsByGroupsIds[groupId] = [taskId];
807
+ }
808
+ else if (!toTab.taskIdsByGroupsIds[groupId].includes(taskId)) {
809
+ toTab.taskIdsByGroupsIds[groupId].push(taskId);
810
+ }
811
+ });
812
+ };
562
813
  const removeIdsFromTabData = (tabData, taskIds) => {
563
814
  const removedSet = new Set(taskIds);
564
815
  const isRemoved = (id) => removedSet.has(id);
@@ -586,7 +837,39 @@ const removeTaskIds = (draft, taskIds) => {
586
837
  removeIdsFromTabData(draft, taskIds);
587
838
  removeIdsFromTabData(draft.byTab[draft.currentTab], taskIds);
588
839
  };
589
- const getGroupedTaskIds = (tasks) => {
840
+ // Collect the union of subtask ids claimed by any parent across every
841
+ // bucket in the payload. See the callsite in `updateTaskList` for why
842
+ // this must be cross-bucket rather than per-bucket.
843
+ const collectNestedSubtaskIds = (buckets) => {
844
+ const nestedSubtaskIds = new Set();
845
+ buckets.forEach((bucket) => {
846
+ if (bucket == null) {
847
+ return;
848
+ }
849
+ bucket.forEach((task) => {
850
+ (task.subtasks ?? []).forEach((subtaskId) => nestedSubtaskIds.add(subtaskId));
851
+ });
852
+ });
853
+ return nestedSubtaskIds;
854
+ };
855
+ // A task is a "nested subtask" (hidden from the flat top-level list +
856
+ // dropped from the tab count) iff a parent in the same payload claims
857
+ // it via its `subtasks: [...]` array. Match this criterion exactly to
858
+ // WC's `allSubtaskIds` union in TaskManagementPage.tsx — otherwise
859
+ // the tab count and the visible top-level row count disagree
860
+ // whenever the BE emits `parent_task_id` on a subtask but omits it
861
+ // from the parent's `subtasks` array (a real inconsistency this UI
862
+ // sees today).
863
+ //
864
+ // Using `parent_task_id` here would sometimes drop a task from the
865
+ // count that WC still renders as a flat top-level row — that's the
866
+ // bug we're fixing.
867
+ //
868
+ // `nestedSubtaskIds` is passed in from the caller and covers subtasks
869
+ // claimed by parents in ANY bucket (live/completed/archived/deleted/
870
+ // snoozed), so a subtask resolved into `completed` while its parent
871
+ // stays in `data` is still correctly recognized as nested.
872
+ const getGroupedTaskIds = (tasks, nestedSubtaskIds) => {
590
873
  const taskIds = [];
591
874
  const taskIdsByGroupIds = {};
592
875
  const taskIdsByAssignees = {};
@@ -601,12 +884,21 @@ const getGroupedTaskIds = (tasks) => {
601
884
  };
602
885
  const taskIdsByTags = {};
603
886
  tasks.forEach((task) => {
604
- taskIds.push(task.task_id);
887
+ const isNestedSubtask = nestedSubtaskIds.has(task.task_id);
888
+ // Subtasks stay in every groupBy array so the UI can render them
889
+ // nested under their parent (WC's TaskManagementPage builds
890
+ // `subtasksByParentId` from `group.tasks` and then filters them
891
+ // out of top-level rows via `filterOutSubtasks`). Only the flat
892
+ // top-level `taskIds` array — which drives the Active/Completed/…
893
+ // tab counts — must exclude nested subtasks.
605
894
  const priorityKey = (0, taskState_1.toPriorityCodeType)(task.priority.code);
606
895
  const statusKey = (0, taskState_1.toTaskStatusCodeType)(task.status.code);
607
896
  const assigneeKey = getAssigneesGroupKey(task.assignees);
608
897
  const dueDateKey = getDueDateGroupKey(task.due_date);
609
898
  const tagsKey = getTagsGroupKey(task.tags);
899
+ if (!isNestedSubtask) {
900
+ taskIds.push(task.task_id);
901
+ }
610
902
  taskIdsGroupedByPriority[priorityKey] = [
611
903
  ...taskIdsGroupedByPriority[priorityKey],
612
904
  task.task_id,
@@ -342,6 +342,7 @@ exports.getAllTasks = (0, toolkit_1.createSelector)((state) => state.taskListSta
342
342
  const taskGroupTemplates = (0, taskGroupTemplateSelector_1.getTaskGroupTemplateByIds)(taskGroupTemplateState, taskGroupTemplateViewState.taskGroupTemplateIds);
343
343
  const tabCounts = {
344
344
  live: taskListState.byTab.live.taskIds.length,
345
+ completed: taskListState.byTab.completed.taskIds.length,
345
346
  archived: taskListState.byTab.archived.taskIds.length,
346
347
  deleted: taskListState.byTab.deleted.taskIds.length,
347
348
  snoozed: taskListState.byTab.snoozed.taskIds.length,
@@ -1,5 +1,16 @@
1
+ import { Tag } from '../../../entity/tag/tagState';
1
2
  import { Task } from '../../../entity/task/taskState';
3
+ import { User } from '../../../entity/user/userState';
2
4
  import { ZeniDate } from '../../../zeniDayJS';
3
5
  import { TaskGroupKey } from './taskList';
6
+ export type SubtaskSortKey = 'name' | 'status' | 'assignee' | 'tag';
7
+ export type SubtaskSortOrder = 'asc' | 'desc';
4
8
  export declare const getDueDateValueFromDueDateGroupId: (groupId: string) => ZeniDate;
5
9
  export declare const getTaskUpdates: (groupByKey: TaskGroupKey, groupId: string) => Partial<Task>;
10
+ /**
11
+ * Sort a subtask list by one of the sortable columns. Pure — no state
12
+ * dependency. `statusOrder` is derived from `allTaskStatus` so any new
13
+ * status added upstream slots in by its enum position instead of
14
+ * falling into a catch-all bucket.
15
+ */
16
+ export declare const sortSubtasks: (subtasks: Task[], sortKey: SubtaskSortKey | null, sortOrder: SubtaskSortOrder, userList: User[], tagList: Tag[]) => Task[];