@zeniai/client-epic-state 5.1.45-betaDI2 → 5.1.45-betaDI3
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/task/taskPayload.js +8 -1
- package/lib/esm/entity/task/taskPayload.js +8 -1
- package/lib/esm/view/taskManager/taskDetailView/epics/initializeTaskToLocalStoreEpic.js +0 -1
- package/lib/esm/view/taskManager/taskDetailView/taskDetailSelector.js +3 -0
- package/lib/esm/view/taskManager/taskListView/taskListReducer.js +76 -37
- package/lib/view/taskManager/taskDetailView/epics/initializeTaskToLocalStoreEpic.js +0 -1
- package/lib/view/taskManager/taskDetailView/taskDetail.d.ts +0 -1
- package/lib/view/taskManager/taskDetailView/taskDetailSelector.d.ts +1 -0
- package/lib/view/taskManager/taskDetailView/taskDetailSelector.js +3 -0
- package/lib/view/taskManager/taskListView/taskListReducer.js +76 -37
- package/package.json +1 -1
|
@@ -53,7 +53,14 @@ const mapTaskPayloadToTask = (payload, previousTask) => ({
|
|
|
53
53
|
depth: payload.depth !== undefined
|
|
54
54
|
? (payload.depth ?? undefined)
|
|
55
55
|
: previousTask?.depth,
|
|
56
|
-
|
|
56
|
+
// `parent_task_id` may be absent on partial updates (e.g. pusher-driven
|
|
57
|
+
// task refreshes). Falling back to `undefined` there would make a real
|
|
58
|
+
// subtask look like a top-level row — breaking the depth-cap gate,
|
|
59
|
+
// `getGroupedTaskIds`'s nested-subtask filter, and the sibling
|
|
60
|
+
// `updateTasks` guard. Explicit `null` from the BE still clears it.
|
|
61
|
+
parentTaskId: payload.parent_task_id !== undefined
|
|
62
|
+
? (payload.parent_task_id ?? undefined)
|
|
63
|
+
: previousTask?.parentTaskId,
|
|
57
64
|
recurringSourceTaskId: payload.recurring_source_task_id != null
|
|
58
65
|
? payload.recurring_source_task_id
|
|
59
66
|
: undefined,
|
|
@@ -50,7 +50,14 @@ export const mapTaskPayloadToTask = (payload, previousTask) => ({
|
|
|
50
50
|
depth: payload.depth !== undefined
|
|
51
51
|
? (payload.depth ?? undefined)
|
|
52
52
|
: previousTask?.depth,
|
|
53
|
-
|
|
53
|
+
// `parent_task_id` may be absent on partial updates (e.g. pusher-driven
|
|
54
|
+
// task refreshes). Falling back to `undefined` there would make a real
|
|
55
|
+
// subtask look like a top-level row — breaking the depth-cap gate,
|
|
56
|
+
// `getGroupedTaskIds`'s nested-subtask filter, and the sibling
|
|
57
|
+
// `updateTasks` guard. Explicit `null` from the BE still clears it.
|
|
58
|
+
parentTaskId: payload.parent_task_id !== undefined
|
|
59
|
+
? (payload.parent_task_id ?? undefined)
|
|
60
|
+
: previousTask?.parentTaskId,
|
|
54
61
|
recurringSourceTaskId: payload.recurring_source_task_id != null
|
|
55
62
|
? payload.recurring_source_task_id
|
|
56
63
|
: undefined,
|
|
@@ -29,7 +29,6 @@ export const initializeTaskToLocalStoreEpic = (actions$, state$) => actions$.pip
|
|
|
29
29
|
timeSpent: task.timeSpent,
|
|
30
30
|
isPrivate: task.isPrivate,
|
|
31
31
|
taskGroupId: task.taskGroupIds[0],
|
|
32
|
-
depth: task.depth,
|
|
33
32
|
};
|
|
34
33
|
const actions = [
|
|
35
34
|
saveTaskUpdatesToLocalStore({ taskDetailLocalData, taskId }),
|
|
@@ -24,6 +24,7 @@ export const getTaskDetail = (state, taskId) => {
|
|
|
24
24
|
let createdByUser = undefined;
|
|
25
25
|
let isArchived = undefined;
|
|
26
26
|
let isDeleted = undefined;
|
|
27
|
+
let depth = undefined;
|
|
27
28
|
let recurringSourceTaskId = undefined;
|
|
28
29
|
let snoozedUntil = undefined;
|
|
29
30
|
let taskGroupId = undefined;
|
|
@@ -37,6 +38,7 @@ export const getTaskDetail = (state, taskId) => {
|
|
|
37
38
|
const fileIdsInEntity = taskEntity?.fileIds ?? [];
|
|
38
39
|
if (taskEntity != null) {
|
|
39
40
|
createdByUser = getUserAndUserRole(userState, userRoleState, addressState, taskEntity.createdBy);
|
|
41
|
+
depth = taskEntity.depth;
|
|
40
42
|
isArchived = taskEntity.isArchived;
|
|
41
43
|
isDeleted = taskEntity.isDeleted;
|
|
42
44
|
recurringSourceTaskId = taskEntity.recurringSourceTaskId;
|
|
@@ -100,6 +102,7 @@ export const getTaskDetail = (state, taskId) => {
|
|
|
100
102
|
error: fetchStatus.error,
|
|
101
103
|
version: 0,
|
|
102
104
|
createdByUser,
|
|
105
|
+
depth,
|
|
103
106
|
isArchived,
|
|
104
107
|
isDeleted,
|
|
105
108
|
recurringSourceTaskId,
|
|
@@ -105,21 +105,34 @@ const taskList = createSlice({
|
|
|
105
105
|
const { data, archived, completed, deleted, snoozed } = action.payload;
|
|
106
106
|
draft.fetchState = 'Completed';
|
|
107
107
|
draft.error = undefined;
|
|
108
|
+
// Build the nested-subtask set from the UNION of every bucket in
|
|
109
|
+
// this payload — not per-bucket. A resolved subtask can arrive in
|
|
110
|
+
// `completed` while its parent (which claims it via `subtasks`)
|
|
111
|
+
// is still in `data`; a per-bucket scan would miss the cross-tab
|
|
112
|
+
// claim and surface the subtask as a phantom top-level row in
|
|
113
|
+
// the Completed tab.
|
|
114
|
+
const nestedSubtaskIds = collectNestedSubtaskIds([
|
|
115
|
+
data,
|
|
116
|
+
completed,
|
|
117
|
+
archived,
|
|
118
|
+
deleted,
|
|
119
|
+
snoozed,
|
|
120
|
+
]);
|
|
108
121
|
// Group live tasks
|
|
109
|
-
const liveGrouped = getGroupedTaskIds(data);
|
|
122
|
+
const liveGrouped = getGroupedTaskIds(data, nestedSubtaskIds);
|
|
110
123
|
draft.byTab.live = toTabData(liveGrouped);
|
|
111
124
|
// Group other tabs if provided
|
|
112
125
|
if (completed != null) {
|
|
113
|
-
draft.byTab.completed = toTabData(getGroupedTaskIds(completed));
|
|
126
|
+
draft.byTab.completed = toTabData(getGroupedTaskIds(completed, nestedSubtaskIds));
|
|
114
127
|
}
|
|
115
128
|
if (archived != null) {
|
|
116
|
-
draft.byTab.archived = toTabData(getGroupedTaskIds(archived));
|
|
129
|
+
draft.byTab.archived = toTabData(getGroupedTaskIds(archived, nestedSubtaskIds));
|
|
117
130
|
}
|
|
118
131
|
if (deleted != null) {
|
|
119
|
-
draft.byTab.deleted = toTabData(getGroupedTaskIds(deleted));
|
|
132
|
+
draft.byTab.deleted = toTabData(getGroupedTaskIds(deleted, nestedSubtaskIds));
|
|
120
133
|
}
|
|
121
134
|
if (snoozed != null) {
|
|
122
|
-
draft.byTab.snoozed = toTabData(getGroupedTaskIds(snoozed));
|
|
135
|
+
draft.byTab.snoozed = toTabData(getGroupedTaskIds(snoozed, nestedSubtaskIds));
|
|
123
136
|
}
|
|
124
137
|
// Copy active tab data to top-level fields
|
|
125
138
|
const activeTabData = draft.byTab[draft.currentTab];
|
|
@@ -418,10 +431,17 @@ const taskList = createSlice({
|
|
|
418
431
|
// fetch. Mirrors backend split in task_handler.py: only
|
|
419
432
|
// `resolved` counts as completed; `done` stays Active.
|
|
420
433
|
//
|
|
421
|
-
//
|
|
422
|
-
// the
|
|
423
|
-
//
|
|
424
|
-
|
|
434
|
+
// "Top-level" here means: currently rendered as a standalone
|
|
435
|
+
// row in the live or completed tab (i.e. present in
|
|
436
|
+
// `byTab.<tab>.taskIds`). We use presence in byTab — NOT a
|
|
437
|
+
// `parent_task_id == null` check — so orphan subtasks (parent
|
|
438
|
+
// hidden from the customer's payload, so we render them flat)
|
|
439
|
+
// ALSO migrate between tabs on status flip. This matches the
|
|
440
|
+
// criterion `getGroupedTaskIds` uses when it builds byTab from
|
|
441
|
+
// the initial fetch — a task is nested iff a visible parent's
|
|
442
|
+
// `subtasks` array claims it, otherwise it's top-level here.
|
|
443
|
+
const isTopLevelTask = draft.byTab.live.taskIds.includes(taskId) ||
|
|
444
|
+
draft.byTab.completed.taskIds.includes(taskId);
|
|
425
445
|
const wasCompleted = prevStatusCode === 'resolved';
|
|
426
446
|
const isCompleted = newStatusCode === 'resolved';
|
|
427
447
|
if (isTopLevelTask && wasCompleted !== isCompleted) {
|
|
@@ -518,19 +538,22 @@ const taskList = createSlice({
|
|
|
518
538
|
},
|
|
519
539
|
extraReducers: (builder) => {
|
|
520
540
|
// When the entity store receives updated tasks (e.g. from task detail
|
|
521
|
-
// save, snooze/archive/unarchive, etc.), re-bucket
|
|
522
|
-
// the live and completed tabs based on the new status. Mirrors
|
|
523
|
-
// backend split in task_handler.py: only `resolved`
|
|
524
|
-
// completed.
|
|
525
|
-
//
|
|
541
|
+
// save, snooze/archive/unarchive, etc.), re-bucket top-level tasks
|
|
542
|
+
// between the live and completed tabs based on the new status. Mirrors
|
|
543
|
+
// the backend split in task_handler.py: only `resolved` counts as
|
|
544
|
+
// completed.
|
|
545
|
+
//
|
|
546
|
+
// "Top-level" here means: currently rendered as a standalone row in
|
|
547
|
+
// the live or completed tab (i.e. present in
|
|
548
|
+
// `byTab.<tab>.taskIds`). Tasks that are nested under a parent's
|
|
549
|
+
// expanded subtask row live outside byTab.taskIds and are silently
|
|
550
|
+
// skipped by the "in-bucket" check below. This uses the same
|
|
551
|
+
// criterion as `getGroupedTaskIds` — a task is nested iff a visible
|
|
552
|
+
// parent's `subtasks` array claims it, otherwise it's top-level —
|
|
553
|
+
// so orphan subtasks (parent hidden from the customer's payload)
|
|
554
|
+
// migrate correctly on status flip via pusher / detail-save.
|
|
526
555
|
builder.addCase(updateTasks, (draft, action) => {
|
|
527
556
|
action.payload.forEach((taskPayload) => {
|
|
528
|
-
// Only re-bucket parents; subtasks (parent_task_id is a non-empty
|
|
529
|
-
// string) are nested under their parent row and don't move tabs.
|
|
530
|
-
if (taskPayload.parent_task_id != null &&
|
|
531
|
-
taskPayload.parent_task_id !== '') {
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
557
|
const taskId = taskPayload.task_id;
|
|
535
558
|
// `TaskPayload.status` is required per the type — every producer
|
|
536
559
|
// in this repo sends it, and `mapTaskPayloadToTask` dereferences
|
|
@@ -810,7 +833,39 @@ const removeTaskIds = (draft, taskIds) => {
|
|
|
810
833
|
removeIdsFromTabData(draft, taskIds);
|
|
811
834
|
removeIdsFromTabData(draft.byTab[draft.currentTab], taskIds);
|
|
812
835
|
};
|
|
813
|
-
|
|
836
|
+
// Collect the union of subtask ids claimed by any parent across every
|
|
837
|
+
// bucket in the payload. See the callsite in `updateTaskList` for why
|
|
838
|
+
// this must be cross-bucket rather than per-bucket.
|
|
839
|
+
const collectNestedSubtaskIds = (buckets) => {
|
|
840
|
+
const nestedSubtaskIds = new Set();
|
|
841
|
+
buckets.forEach((bucket) => {
|
|
842
|
+
if (bucket == null) {
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
bucket.forEach((task) => {
|
|
846
|
+
(task.subtasks ?? []).forEach((subtaskId) => nestedSubtaskIds.add(subtaskId));
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
return nestedSubtaskIds;
|
|
850
|
+
};
|
|
851
|
+
// A task is a "nested subtask" (hidden from the flat top-level list +
|
|
852
|
+
// dropped from the tab count) iff a parent in the same payload claims
|
|
853
|
+
// it via its `subtasks: [...]` array. Match this criterion exactly to
|
|
854
|
+
// WC's `allSubtaskIds` union in TaskManagementPage.tsx — otherwise
|
|
855
|
+
// the tab count and the visible top-level row count disagree
|
|
856
|
+
// whenever the BE emits `parent_task_id` on a subtask but omits it
|
|
857
|
+
// from the parent's `subtasks` array (a real inconsistency this UI
|
|
858
|
+
// sees today).
|
|
859
|
+
//
|
|
860
|
+
// Using `parent_task_id` here would sometimes drop a task from the
|
|
861
|
+
// count that WC still renders as a flat top-level row — that's the
|
|
862
|
+
// bug we're fixing.
|
|
863
|
+
//
|
|
864
|
+
// `nestedSubtaskIds` is passed in from the caller and covers subtasks
|
|
865
|
+
// claimed by parents in ANY bucket (live/completed/archived/deleted/
|
|
866
|
+
// snoozed), so a subtask resolved into `completed` while its parent
|
|
867
|
+
// stays in `data` is still correctly recognized as nested.
|
|
868
|
+
const getGroupedTaskIds = (tasks, nestedSubtaskIds) => {
|
|
814
869
|
const taskIds = [];
|
|
815
870
|
const taskIdsByGroupIds = {};
|
|
816
871
|
const taskIdsByAssignees = {};
|
|
@@ -824,22 +879,6 @@ const getGroupedTaskIds = (tasks) => {
|
|
|
824
879
|
...initialTaskIdsByDueDate,
|
|
825
880
|
};
|
|
826
881
|
const taskIdsByTags = {};
|
|
827
|
-
// A task is a "nested subtask" (hidden from the flat top-level list +
|
|
828
|
-
// dropped from the tab count) iff a parent in the same payload claims
|
|
829
|
-
// it via its `subtasks: [...]` array. Match this criterion exactly to
|
|
830
|
-
// WC's `allSubtaskIds` union in TaskManagementPage.tsx — otherwise
|
|
831
|
-
// the tab count and the visible top-level row count disagree
|
|
832
|
-
// whenever the BE emits `parent_task_id` on a subtask but omits it
|
|
833
|
-
// from the parent's `subtasks` array (a real inconsistency this UI
|
|
834
|
-
// sees today).
|
|
835
|
-
//
|
|
836
|
-
// Using `parent_task_id` here would sometimes drop a task from the
|
|
837
|
-
// count that WC still renders as a flat top-level row — that's the
|
|
838
|
-
// bug we're fixing.
|
|
839
|
-
const nestedSubtaskIds = new Set();
|
|
840
|
-
tasks.forEach((task) => {
|
|
841
|
-
(task.subtasks ?? []).forEach((subtaskId) => nestedSubtaskIds.add(subtaskId));
|
|
842
|
-
});
|
|
843
882
|
tasks.forEach((task) => {
|
|
844
883
|
const isNestedSubtask = nestedSubtaskIds.has(task.task_id);
|
|
845
884
|
// Subtasks stay in every groupBy array so the UI can render them
|
|
@@ -35,7 +35,6 @@ const initializeTaskToLocalStoreEpic = (actions$, state$) => actions$.pipe((0, o
|
|
|
35
35
|
timeSpent: task.timeSpent,
|
|
36
36
|
isPrivate: task.isPrivate,
|
|
37
37
|
taskGroupId: task.taskGroupIds[0],
|
|
38
|
-
depth: task.depth,
|
|
39
38
|
};
|
|
40
39
|
const actions = [
|
|
41
40
|
(0, taskDetailReducer_1.saveTaskUpdatesToLocalStore)({ taskDetailLocalData, taskId }),
|
|
@@ -20,6 +20,7 @@ export interface TaskDetailSelectorView extends SelectorView {
|
|
|
20
20
|
updateFileStatusById: Record<ID, FetchStateAndError | undefined>;
|
|
21
21
|
userList: UserListSelectorView;
|
|
22
22
|
createdByUser?: UserAndRole;
|
|
23
|
+
depth?: number;
|
|
23
24
|
isArchived?: boolean;
|
|
24
25
|
isDeleted?: boolean;
|
|
25
26
|
recurringSourceTaskId?: ID;
|
|
@@ -30,6 +30,7 @@ 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;
|
|
@@ -43,6 +44,7 @@ const getTaskDetail = (state, taskId) => {
|
|
|
43
44
|
const fileIdsInEntity = taskEntity?.fileIds ?? [];
|
|
44
45
|
if (taskEntity != null) {
|
|
45
46
|
createdByUser = (0, userAndRole_1.getUserAndUserRole)(userState, userRoleState, addressState, taskEntity.createdBy);
|
|
47
|
+
depth = taskEntity.depth;
|
|
46
48
|
isArchived = taskEntity.isArchived;
|
|
47
49
|
isDeleted = taskEntity.isDeleted;
|
|
48
50
|
recurringSourceTaskId = taskEntity.recurringSourceTaskId;
|
|
@@ -106,6 +108,7 @@ const getTaskDetail = (state, taskId) => {
|
|
|
106
108
|
error: fetchStatus.error,
|
|
107
109
|
version: 0,
|
|
108
110
|
createdByUser,
|
|
111
|
+
depth,
|
|
109
112
|
isArchived,
|
|
110
113
|
isDeleted,
|
|
111
114
|
recurringSourceTaskId,
|
|
@@ -109,21 +109,34 @@ const taskList = (0, toolkit_1.createSlice)({
|
|
|
109
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
|
|
116
129
|
if (completed != null) {
|
|
117
|
-
draft.byTab.completed = toTabData(getGroupedTaskIds(completed));
|
|
130
|
+
draft.byTab.completed = toTabData(getGroupedTaskIds(completed, nestedSubtaskIds));
|
|
118
131
|
}
|
|
119
132
|
if (archived != null) {
|
|
120
|
-
draft.byTab.archived = toTabData(getGroupedTaskIds(archived));
|
|
133
|
+
draft.byTab.archived = toTabData(getGroupedTaskIds(archived, nestedSubtaskIds));
|
|
121
134
|
}
|
|
122
135
|
if (deleted != null) {
|
|
123
|
-
draft.byTab.deleted = toTabData(getGroupedTaskIds(deleted));
|
|
136
|
+
draft.byTab.deleted = toTabData(getGroupedTaskIds(deleted, nestedSubtaskIds));
|
|
124
137
|
}
|
|
125
138
|
if (snoozed != null) {
|
|
126
|
-
draft.byTab.snoozed = toTabData(getGroupedTaskIds(snoozed));
|
|
139
|
+
draft.byTab.snoozed = toTabData(getGroupedTaskIds(snoozed, nestedSubtaskIds));
|
|
127
140
|
}
|
|
128
141
|
// Copy active tab data to top-level fields
|
|
129
142
|
const activeTabData = draft.byTab[draft.currentTab];
|
|
@@ -422,10 +435,17 @@ const taskList = (0, toolkit_1.createSlice)({
|
|
|
422
435
|
// fetch. Mirrors backend split in task_handler.py: only
|
|
423
436
|
// `resolved` counts as completed; `done` stays Active.
|
|
424
437
|
//
|
|
425
|
-
//
|
|
426
|
-
// the
|
|
427
|
-
//
|
|
428
|
-
|
|
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);
|
|
429
449
|
const wasCompleted = prevStatusCode === 'resolved';
|
|
430
450
|
const isCompleted = newStatusCode === 'resolved';
|
|
431
451
|
if (isTopLevelTask && wasCompleted !== isCompleted) {
|
|
@@ -522,19 +542,22 @@ const taskList = (0, toolkit_1.createSlice)({
|
|
|
522
542
|
},
|
|
523
543
|
extraReducers: (builder) => {
|
|
524
544
|
// When the entity store receives updated tasks (e.g. from task detail
|
|
525
|
-
// save, snooze/archive/unarchive, etc.), re-bucket
|
|
526
|
-
// the live and completed tabs based on the new status. Mirrors
|
|
527
|
-
// backend split in task_handler.py: only `resolved`
|
|
528
|
-
// completed.
|
|
529
|
-
//
|
|
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.
|
|
530
559
|
builder.addCase(taskReducer_1.updateTasks, (draft, action) => {
|
|
531
560
|
action.payload.forEach((taskPayload) => {
|
|
532
|
-
// Only re-bucket parents; subtasks (parent_task_id is a non-empty
|
|
533
|
-
// string) are nested under their parent row and don't move tabs.
|
|
534
|
-
if (taskPayload.parent_task_id != null &&
|
|
535
|
-
taskPayload.parent_task_id !== '') {
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
561
|
const taskId = taskPayload.task_id;
|
|
539
562
|
// `TaskPayload.status` is required per the type — every producer
|
|
540
563
|
// in this repo sends it, and `mapTaskPayloadToTask` dereferences
|
|
@@ -814,7 +837,39 @@ const removeTaskIds = (draft, taskIds) => {
|
|
|
814
837
|
removeIdsFromTabData(draft, taskIds);
|
|
815
838
|
removeIdsFromTabData(draft.byTab[draft.currentTab], taskIds);
|
|
816
839
|
};
|
|
817
|
-
|
|
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) => {
|
|
818
873
|
const taskIds = [];
|
|
819
874
|
const taskIdsByGroupIds = {};
|
|
820
875
|
const taskIdsByAssignees = {};
|
|
@@ -828,22 +883,6 @@ const getGroupedTaskIds = (tasks) => {
|
|
|
828
883
|
...initialTaskIdsByDueDate,
|
|
829
884
|
};
|
|
830
885
|
const taskIdsByTags = {};
|
|
831
|
-
// A task is a "nested subtask" (hidden from the flat top-level list +
|
|
832
|
-
// dropped from the tab count) iff a parent in the same payload claims
|
|
833
|
-
// it via its `subtasks: [...]` array. Match this criterion exactly to
|
|
834
|
-
// WC's `allSubtaskIds` union in TaskManagementPage.tsx — otherwise
|
|
835
|
-
// the tab count and the visible top-level row count disagree
|
|
836
|
-
// whenever the BE emits `parent_task_id` on a subtask but omits it
|
|
837
|
-
// from the parent's `subtasks` array (a real inconsistency this UI
|
|
838
|
-
// sees today).
|
|
839
|
-
//
|
|
840
|
-
// Using `parent_task_id` here would sometimes drop a task from the
|
|
841
|
-
// count that WC still renders as a flat top-level row — that's the
|
|
842
|
-
// bug we're fixing.
|
|
843
|
-
const nestedSubtaskIds = new Set();
|
|
844
|
-
tasks.forEach((task) => {
|
|
845
|
-
(task.subtasks ?? []).forEach((subtaskId) => nestedSubtaskIds.add(subtaskId));
|
|
846
|
-
});
|
|
847
886
|
tasks.forEach((task) => {
|
|
848
887
|
const isNestedSubtask = nestedSubtaskIds.has(task.task_id);
|
|
849
888
|
// Subtasks stay in every groupBy array so the UI can render them
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeniai/client-epic-state",
|
|
3
|
-
"version": "5.1.45-
|
|
3
|
+
"version": "5.1.45-betaDI3",
|
|
4
4
|
"description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/esm/index.js",
|