@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.
- package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
- package/lib/entity/snackbar/snackbarTypes.js +3 -0
- package/lib/entity/task/taskPayload.d.ts +3 -1
- package/lib/entity/task/taskPayload.js +24 -2
- package/lib/entity/task/taskReducer.d.ts +8 -1
- package/lib/entity/task/taskReducer.js +29 -4
- package/lib/entity/task/taskState.d.ts +2 -0
- package/lib/entity/tenant/epic/saveExternalConnectionEpic.d.ts +2 -1
- package/lib/entity/tenant/epic/saveExternalConnectionEpic.js +17 -0
- package/lib/epic.d.ts +6 -3
- package/lib/epic.js +6 -3
- package/lib/esm/entity/snackbar/snackbarTypes.js +3 -0
- package/lib/esm/entity/task/taskPayload.js +24 -2
- package/lib/esm/entity/task/taskReducer.js +28 -3
- package/lib/esm/entity/tenant/epic/saveExternalConnectionEpic.js +17 -0
- package/lib/esm/epic.js +6 -3
- package/lib/esm/index.js +8 -8
- package/lib/esm/view/aiCfoView/aiCfoViewReducer.js +31 -1
- package/lib/esm/view/aiCfoView/epics/fetchSkillsEpic.js +33 -8
- package/lib/esm/view/aiCfoView/epics/submitFeedbackEpic.js +39 -0
- package/lib/esm/view/companyTaskManagerView/epics/fetchCompanyTaskManagerViewEpic.js +3 -0
- package/lib/esm/view/companyTaskManagerView/epics/fetchTaskManagerMetricsEpic.js +7 -1
- package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +7 -0
- package/lib/esm/view/taskManager/taskDetailView/epics/createSubTaskEpic.js +96 -0
- package/lib/esm/view/taskManager/taskDetailView/epics/fetchSubTasksEpic.js +53 -0
- package/lib/esm/view/taskManager/taskDetailView/epics/initializeTaskToLocalStoreEpic.js +1 -0
- package/lib/esm/view/taskManager/taskDetailView/epics/saveTaskDetailEpic.js +70 -9
- package/lib/esm/view/taskManager/taskDetailView/taskDetailReducer.js +56 -2
- package/lib/esm/view/taskManager/taskDetailView/taskDetailSelector.js +35 -9
- package/lib/esm/view/taskManager/taskListView/epics/fetchTaskListEpic.js +3 -1
- package/lib/esm/view/taskManager/taskListView/taskList.js +1 -0
- package/lib/esm/view/taskManager/taskListView/taskListReducer.js +310 -18
- package/lib/esm/view/taskManager/taskListView/taskListSelector.js +1 -0
- package/lib/esm/view/taskManager/taskListView/taskListViewHelpers.js +58 -0
- package/lib/esm/view/transactionDetail/epics/saveTransactionHelper.js +22 -0
- package/lib/esm/view/transactionDetail/epics/updateTransactionDetailEpic.js +75 -3
- package/lib/esm/view/transactionDetail/journalEntryLinesViewModel.js +58 -0
- package/lib/esm/view/transactionDetail/transactionDetailLocalDataHelper.js +10 -8
- package/lib/esm/view/transactionDetail/transactionDetailReducer.js +66 -1
- package/lib/index.d.ts +12 -11
- package/lib/index.js +37 -28
- package/lib/view/aiCfoView/aiCfoViewPayload.d.ts +10 -0
- package/lib/view/aiCfoView/aiCfoViewReducer.d.ts +7 -2
- package/lib/view/aiCfoView/aiCfoViewReducer.js +32 -2
- package/lib/view/aiCfoView/aiCfoViewState.d.ts +5 -1
- package/lib/view/aiCfoView/epics/fetchSkillsEpic.js +33 -8
- package/lib/view/aiCfoView/epics/submitFeedbackEpic.d.ts +13 -0
- package/lib/view/aiCfoView/epics/submitFeedbackEpic.js +43 -0
- package/lib/view/companyTaskManagerView/epics/fetchCompanyTaskManagerViewEpic.js +3 -0
- package/lib/view/companyTaskManagerView/epics/fetchTaskManagerMetricsEpic.js +7 -1
- package/lib/view/companyView/types/cockpitTypes.d.ts +3 -0
- package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +7 -0
- package/lib/view/taskManager/taskDetailView/epics/createSubTaskEpic.d.ts +9 -0
- package/lib/view/taskManager/taskDetailView/epics/createSubTaskEpic.js +100 -0
- package/lib/view/taskManager/taskDetailView/epics/fetchSubTasksEpic.d.ts +8 -0
- package/lib/view/taskManager/taskDetailView/epics/fetchSubTasksEpic.js +57 -0
- package/lib/view/taskManager/taskDetailView/epics/initializeTaskToLocalStoreEpic.js +1 -0
- package/lib/view/taskManager/taskDetailView/epics/saveTaskDetailEpic.d.ts +2 -2
- package/lib/view/taskManager/taskDetailView/epics/saveTaskDetailEpic.js +69 -8
- package/lib/view/taskManager/taskDetailView/taskDetail.d.ts +15 -0
- package/lib/view/taskManager/taskDetailView/taskDetailReducer.d.ts +14 -2
- package/lib/view/taskManager/taskDetailView/taskDetailReducer.js +57 -3
- package/lib/view/taskManager/taskDetailView/taskDetailSelector.d.ts +3 -1
- package/lib/view/taskManager/taskDetailView/taskDetailSelector.js +33 -7
- package/lib/view/taskManager/taskListView/epics/fetchTaskListEpic.js +3 -1
- package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
- package/lib/view/taskManager/taskListView/taskList.js +1 -0
- package/lib/view/taskManager/taskListView/taskListPayload.d.ts +4 -0
- package/lib/view/taskManager/taskListView/taskListReducer.d.ts +6 -2
- package/lib/view/taskManager/taskListView/taskListReducer.js +311 -19
- package/lib/view/taskManager/taskListView/taskListSelector.js +1 -0
- package/lib/view/taskManager/taskListView/taskListViewHelpers.d.ts +11 -0
- package/lib/view/taskManager/taskListView/taskListViewHelpers.js +60 -1
- package/lib/view/transactionDetail/epics/saveTransactionHelper.js +22 -0
- package/lib/view/transactionDetail/epics/updateTransactionDetailEpic.d.ts +5 -1
- package/lib/view/transactionDetail/epics/updateTransactionDetailEpic.js +77 -4
- package/lib/view/transactionDetail/journalEntryLinesViewModel.d.ts +27 -0
- package/lib/view/transactionDetail/journalEntryLinesViewModel.js +61 -1
- package/lib/view/transactionDetail/transactionDetailLocalDataHelper.js +10 -8
- package/lib/view/transactionDetail/transactionDetailReducer.d.ts +11 -2
- package/lib/view/transactionDetail/transactionDetailReducer.js +67 -2
- package/lib/view/transactionDetail/transactionDetailState.d.ts +8 -1
- package/lib/view/transactionDetail/transactionDetailTypes.d.ts +2 -0
- 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.
|
|
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.
|
|
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.
|
|
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 (
|
|
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
|
|
153
|
-
!(0, isEqual_1.default)(taskDetailLocalData.groupAssignees, taskDetailInStore
|
|
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[];
|
|
@@ -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">,
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[];
|