@zeniai/client-epic-state 5.1.43 → 5.1.45-betaDI1

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.
@@ -22,6 +22,7 @@ export interface TaskPayload {
22
22
  update_time: string | null;
23
23
  company_id?: string;
24
24
  company_name?: string;
25
+ depth?: number | null;
25
26
  group_assignees?: string[];
26
27
  parent_task_id?: string | null;
27
28
  recurring_days_of_week?: string[] | null;
@@ -45,6 +45,14 @@ const mapTaskPayloadToTask = (payload, previousTask) => ({
45
45
  updateTime: payload.update_time != null ? (0, zeniDayJS_1.date)(payload.update_time) : undefined,
46
46
  companyId: payload.company_id,
47
47
  companyName: payload.company_name,
48
+ // `depth` may be absent on partial updates (e.g. pusher-driven task
49
+ // refreshes that don't include it). Preserve the previously-known depth
50
+ // there — otherwise the Redux store's depth flips to undefined and any
51
+ // WC depth-cap gate ("hide Create Subtask at depth 3") re-opens
52
+ // incorrectly. Explicit `null` from the BE still clears it via `??`.
53
+ depth: payload.depth !== undefined
54
+ ? (payload.depth ?? undefined)
55
+ : previousTask?.depth,
48
56
  parentTaskId: payload.parent_task_id ?? undefined,
49
57
  recurringSourceTaskId: payload.recurring_source_task_id != null
50
58
  ? payload.recurring_source_task_id
@@ -23,6 +23,7 @@ export interface Task {
23
23
  type: TaskType;
24
24
  companyId?: ID;
25
25
  companyName?: string;
26
+ depth?: number;
26
27
  dueDate?: ZeniDate;
27
28
  parentTaskId?: ID | null;
28
29
  recurringDaysOfWeek?: DayOfWeek[];
@@ -42,6 +42,14 @@ export const mapTaskPayloadToTask = (payload, previousTask) => ({
42
42
  updateTime: payload.update_time != null ? date(payload.update_time) : undefined,
43
43
  companyId: payload.company_id,
44
44
  companyName: payload.company_name,
45
+ // `depth` may be absent on partial updates (e.g. pusher-driven task
46
+ // refreshes that don't include it). Preserve the previously-known depth
47
+ // there — otherwise the Redux store's depth flips to undefined and any
48
+ // WC depth-cap gate ("hide Create Subtask at depth 3") re-opens
49
+ // incorrectly. Explicit `null` from the BE still clears it via `??`.
50
+ depth: payload.depth !== undefined
51
+ ? (payload.depth ?? undefined)
52
+ : previousTask?.depth,
45
53
  parentTaskId: payload.parent_task_id ?? undefined,
46
54
  recurringSourceTaskId: payload.recurring_source_task_id != null
47
55
  ? payload.recurring_source_task_id
@@ -95,12 +95,12 @@ const expenseAutomationReconciliationView = createSlice({
95
95
  }
96
96
  }
97
97
  else {
98
- // reset the selected tab to "review" when visiing detail page and we are not refreshing in Background
98
+ // Visiting an account detail page selects it and opens review; background
99
+ // refresh (e.g. statement Pusher) must not change navigation selection.
99
100
  if (action.payload.refreshViewInBackground !== true) {
100
101
  draft.selectedTab = 'review';
102
+ draft.selectedAccountId = action.payload.accountId;
101
103
  }
102
- // select the account for which we are fetching
103
- draft.selectedAccountId = action.payload.accountId;
104
104
  const accountReconciliationRecords = draft.accountReconciliationRecordsBySelectedPeriod[toMonthYearPeriodId(action.payload.selectedPeriod)];
105
105
  const { transactionFetchState } = accountReconciliationRecords.reconciliationByAccountID[action.payload.accountId];
106
106
  if (transactionFetchState.fetchState !== 'Completed' ||
@@ -517,21 +517,33 @@ const expenseAutomationReconciliationView = createSlice({
517
517
  },
518
518
  parseStatementSuccess: (draft, action) => {
519
519
  const { accountId, selectedPeriod } = action.payload;
520
- draft.accountReconciliationRecordsBySelectedPeriod[toMonthYearPeriodId(selectedPeriod)].reconciliationByAccountID[accountId].statementParseStatus = {
520
+ const accountRecon = draft.accountReconciliationRecordsBySelectedPeriod[toMonthYearPeriodId(selectedPeriod)]?.reconciliationByAccountID[accountId];
521
+ if (accountRecon == null) {
522
+ return;
523
+ }
524
+ accountRecon.statementParseStatus = {
521
525
  fetchState: 'Completed',
522
526
  error: undefined,
523
527
  };
524
- draft.accountReconciliationRecordsBySelectedPeriod[toMonthYearPeriodId(selectedPeriod)].reconciliationByAccountID[accountId].statementParseInProgress = false;
525
- draft.accountReconciliationRecordsBySelectedPeriod[toMonthYearPeriodId(selectedPeriod)].reconciliationByAccountID[accountId].statementProcessingFailed = false;
528
+ accountRecon.statementParseInProgress = false;
529
+ accountRecon.statementProcessingFailed = false;
530
+ // Stop elapsed-time step simulation; keep dismissed flag so the
531
+ // app-level completion snackbar can still fire.
532
+ accountRecon.statementProcessingStartedAtMs = undefined;
533
+ accountRecon.statementProcessingTotalDurationSeconds = undefined;
526
534
  },
527
535
  parseStatementFailure: (draft, action) => {
528
536
  const { accountId, selectedPeriod } = action.payload;
529
- draft.accountReconciliationRecordsBySelectedPeriod[toMonthYearPeriodId(selectedPeriod)].reconciliationByAccountID[accountId].statementParseStatus = {
537
+ const accountRecon = draft.accountReconciliationRecordsBySelectedPeriod[toMonthYearPeriodId(selectedPeriod)]?.reconciliationByAccountID[accountId];
538
+ if (accountRecon == null) {
539
+ return;
540
+ }
541
+ accountRecon.statementParseStatus = {
530
542
  fetchState: 'Error',
531
543
  error: action.payload.error,
532
544
  };
533
- draft.accountReconciliationRecordsBySelectedPeriod[toMonthYearPeriodId(selectedPeriod)].reconciliationByAccountID[accountId].statementParseInProgress = false;
534
- draft.accountReconciliationRecordsBySelectedPeriod[toMonthYearPeriodId(selectedPeriod)].reconciliationByAccountID[accountId].statementProcessingFailed = true;
545
+ accountRecon.statementParseInProgress = false;
546
+ accountRecon.statementProcessingFailed = true;
535
547
  },
536
548
  reparseStatement: (draft, action) => {
537
549
  const { accountId, selectedPeriod } = action.payload;
@@ -29,6 +29,7 @@ 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,
32
33
  };
33
34
  const actions = [
34
35
  saveTaskUpdatesToLocalStore({ taskDetailLocalData, taskId }),
@@ -824,13 +824,40 @@ const getGroupedTaskIds = (tasks) => {
824
824
  ...initialTaskIdsByDueDate,
825
825
  };
826
826
  const taskIdsByTags = {};
827
+ // Which parents are actually present in this payload. A subtask is
828
+ // only treated as "nested" (and dropped from the count / groupings)
829
+ // if its parent is in the visible set — otherwise it renders as an
830
+ // orphan top-level row in the UI (SubtaskDetailTable has no parent to
831
+ // attach under) and MUST count. Customer callers routinely see this:
832
+ // the parent is Zeni-internal / private and filtered server-side, but
833
+ // the subtask itself is public and lands in the payload.
834
+ const visibleTaskIds = new Set(tasks.map((t) => t.task_id));
827
835
  tasks.forEach((task) => {
828
- taskIds.push(task.task_id);
836
+ // A task is a "nested subtask" only when its parent is in the
837
+ // visible payload — otherwise it's an orphan and renders as a
838
+ // top-level row in the UI (customer callers routinely see this:
839
+ // the parent is Zeni-internal / private and filtered server-side,
840
+ // but the subtask itself is public and lands in the payload).
841
+ // Datastore string properties default to '' when unset, so treat
842
+ // empty string as null. Matches the sibling `updateTasks` guard
843
+ // at L719-722.
844
+ const isNestedSubtask = task.parent_task_id != null &&
845
+ task.parent_task_id !== '' &&
846
+ visibleTaskIds.has(task.parent_task_id);
847
+ // Subtasks stay in every groupBy array so the UI can render them
848
+ // nested under their parent (WC's TaskManagementPage builds
849
+ // `subtasksByParentId` from `group.tasks` and then filters them
850
+ // out of top-level rows via `filterOutSubtasks`). Only the flat
851
+ // top-level `taskIds` array — which drives the Active/Completed/…
852
+ // tab counts — must exclude nested subtasks.
829
853
  const priorityKey = toPriorityCodeType(task.priority.code);
830
854
  const statusKey = toTaskStatusCodeType(task.status.code);
831
855
  const assigneeKey = getAssigneesGroupKey(task.assignees);
832
856
  const dueDateKey = getDueDateGroupKey(task.due_date);
833
857
  const tagsKey = getTagsGroupKey(task.tags);
858
+ if (!isNestedSubtask) {
859
+ taskIds.push(task.task_id);
860
+ }
834
861
  taskIdsGroupedByPriority[priorityKey] = [
835
862
  ...taskIdsGroupedByPriority[priorityKey],
836
863
  task.task_id,
@@ -100,12 +100,12 @@ const expenseAutomationReconciliationView = (0, toolkit_1.createSlice)({
100
100
  }
101
101
  }
102
102
  else {
103
- // reset the selected tab to "review" when visiing detail page and we are not refreshing in Background
103
+ // Visiting an account detail page selects it and opens review; background
104
+ // refresh (e.g. statement Pusher) must not change navigation selection.
104
105
  if (action.payload.refreshViewInBackground !== true) {
105
106
  draft.selectedTab = 'review';
107
+ draft.selectedAccountId = action.payload.accountId;
106
108
  }
107
- // select the account for which we are fetching
108
- draft.selectedAccountId = action.payload.accountId;
109
109
  const accountReconciliationRecords = draft.accountReconciliationRecordsBySelectedPeriod[(0, timePeriod_1.toMonthYearPeriodId)(action.payload.selectedPeriod)];
110
110
  const { transactionFetchState } = accountReconciliationRecords.reconciliationByAccountID[action.payload.accountId];
111
111
  if (transactionFetchState.fetchState !== 'Completed' ||
@@ -522,21 +522,33 @@ const expenseAutomationReconciliationView = (0, toolkit_1.createSlice)({
522
522
  },
523
523
  parseStatementSuccess: (draft, action) => {
524
524
  const { accountId, selectedPeriod } = action.payload;
525
- draft.accountReconciliationRecordsBySelectedPeriod[(0, timePeriod_1.toMonthYearPeriodId)(selectedPeriod)].reconciliationByAccountID[accountId].statementParseStatus = {
525
+ const accountRecon = draft.accountReconciliationRecordsBySelectedPeriod[(0, timePeriod_1.toMonthYearPeriodId)(selectedPeriod)]?.reconciliationByAccountID[accountId];
526
+ if (accountRecon == null) {
527
+ return;
528
+ }
529
+ accountRecon.statementParseStatus = {
526
530
  fetchState: 'Completed',
527
531
  error: undefined,
528
532
  };
529
- draft.accountReconciliationRecordsBySelectedPeriod[(0, timePeriod_1.toMonthYearPeriodId)(selectedPeriod)].reconciliationByAccountID[accountId].statementParseInProgress = false;
530
- draft.accountReconciliationRecordsBySelectedPeriod[(0, timePeriod_1.toMonthYearPeriodId)(selectedPeriod)].reconciliationByAccountID[accountId].statementProcessingFailed = false;
533
+ accountRecon.statementParseInProgress = false;
534
+ accountRecon.statementProcessingFailed = false;
535
+ // Stop elapsed-time step simulation; keep dismissed flag so the
536
+ // app-level completion snackbar can still fire.
537
+ accountRecon.statementProcessingStartedAtMs = undefined;
538
+ accountRecon.statementProcessingTotalDurationSeconds = undefined;
531
539
  },
532
540
  parseStatementFailure: (draft, action) => {
533
541
  const { accountId, selectedPeriod } = action.payload;
534
- draft.accountReconciliationRecordsBySelectedPeriod[(0, timePeriod_1.toMonthYearPeriodId)(selectedPeriod)].reconciliationByAccountID[accountId].statementParseStatus = {
542
+ const accountRecon = draft.accountReconciliationRecordsBySelectedPeriod[(0, timePeriod_1.toMonthYearPeriodId)(selectedPeriod)]?.reconciliationByAccountID[accountId];
543
+ if (accountRecon == null) {
544
+ return;
545
+ }
546
+ accountRecon.statementParseStatus = {
535
547
  fetchState: 'Error',
536
548
  error: action.payload.error,
537
549
  };
538
- draft.accountReconciliationRecordsBySelectedPeriod[(0, timePeriod_1.toMonthYearPeriodId)(selectedPeriod)].reconciliationByAccountID[accountId].statementParseInProgress = false;
539
- draft.accountReconciliationRecordsBySelectedPeriod[(0, timePeriod_1.toMonthYearPeriodId)(selectedPeriod)].reconciliationByAccountID[accountId].statementProcessingFailed = true;
550
+ accountRecon.statementParseInProgress = false;
551
+ accountRecon.statementProcessingFailed = true;
540
552
  },
541
553
  reparseStatement: (draft, action) => {
542
554
  const { accountId, selectedPeriod } = action.payload;
@@ -35,6 +35,7 @@ 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,
38
39
  };
39
40
  const actions = [
40
41
  (0, taskDetailReducer_1.saveTaskUpdatesToLocalStore)({ taskDetailLocalData, taskId }),
@@ -47,6 +47,7 @@ export interface EditTaskLocalData {
47
47
  tagIds: ID[];
48
48
  timeSpent: string;
49
49
  type: TaskType;
50
+ depth?: number;
50
51
  dueDate?: ZeniDate;
51
52
  isPrivate?: boolean;
52
53
  recurringDaysOfWeek?: DayOfWeek[];
@@ -828,13 +828,40 @@ const getGroupedTaskIds = (tasks) => {
828
828
  ...initialTaskIdsByDueDate,
829
829
  };
830
830
  const taskIdsByTags = {};
831
+ // Which parents are actually present in this payload. A subtask is
832
+ // only treated as "nested" (and dropped from the count / groupings)
833
+ // if its parent is in the visible set — otherwise it renders as an
834
+ // orphan top-level row in the UI (SubtaskDetailTable has no parent to
835
+ // attach under) and MUST count. Customer callers routinely see this:
836
+ // the parent is Zeni-internal / private and filtered server-side, but
837
+ // the subtask itself is public and lands in the payload.
838
+ const visibleTaskIds = new Set(tasks.map((t) => t.task_id));
831
839
  tasks.forEach((task) => {
832
- taskIds.push(task.task_id);
840
+ // A task is a "nested subtask" only when its parent is in the
841
+ // visible payload — otherwise it's an orphan and renders as a
842
+ // top-level row in the UI (customer callers routinely see this:
843
+ // the parent is Zeni-internal / private and filtered server-side,
844
+ // but the subtask itself is public and lands in the payload).
845
+ // Datastore string properties default to '' when unset, so treat
846
+ // empty string as null. Matches the sibling `updateTasks` guard
847
+ // at L719-722.
848
+ const isNestedSubtask = task.parent_task_id != null &&
849
+ task.parent_task_id !== '' &&
850
+ visibleTaskIds.has(task.parent_task_id);
851
+ // Subtasks stay in every groupBy array so the UI can render them
852
+ // nested under their parent (WC's TaskManagementPage builds
853
+ // `subtasksByParentId` from `group.tasks` and then filters them
854
+ // out of top-level rows via `filterOutSubtasks`). Only the flat
855
+ // top-level `taskIds` array — which drives the Active/Completed/…
856
+ // tab counts — must exclude nested subtasks.
833
857
  const priorityKey = (0, taskState_1.toPriorityCodeType)(task.priority.code);
834
858
  const statusKey = (0, taskState_1.toTaskStatusCodeType)(task.status.code);
835
859
  const assigneeKey = getAssigneesGroupKey(task.assignees);
836
860
  const dueDateKey = getDueDateGroupKey(task.due_date);
837
861
  const tagsKey = getTagsGroupKey(task.tags);
862
+ if (!isNestedSubtask) {
863
+ taskIds.push(task.task_id);
864
+ }
838
865
  taskIdsGroupedByPriority[priorityKey] = [
839
866
  ...taskIdsGroupedByPriority[priorityKey],
840
867
  task.task_id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.1.43",
3
+ "version": "5.1.45-betaDI1",
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",