@zeniai/client-epic-state 5.1.41-beta0ND → 5.1.41-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.
Files changed (121) hide show
  1. package/lib/commonStateTypes/selectorTypes/pickAndMergeForecastCalculatedSection.d.ts +1 -1
  2. package/lib/entity/account/accountSelector.d.ts +6 -6
  3. package/lib/entity/account/pickAndMergeForecastAccountReport.d.ts +18 -18
  4. package/lib/entity/accountGroup/accountGroupSelector.d.ts +44 -44
  5. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  6. package/lib/entity/snackbar/snackbarTypes.js +1 -1
  7. package/lib/entity/task/taskPayload.d.ts +2 -1
  8. package/lib/entity/task/taskPayload.js +8 -1
  9. package/lib/entity/task/taskReducer.d.ts +8 -1
  10. package/lib/entity/task/taskReducer.js +29 -4
  11. package/lib/entity/task/taskState.d.ts +1 -0
  12. package/lib/entity/tenant/epic/saveExternalConnectionEpic.js +7 -15
  13. package/lib/entity/toastNotification/epic/pushToastNotificationEpic.d.ts +3 -3
  14. package/lib/epic.d.ts +5 -3
  15. package/lib/epic.js +5 -3
  16. package/lib/esm/entity/snackbar/snackbarTypes.js +1 -1
  17. package/lib/esm/entity/task/taskPayload.js +8 -1
  18. package/lib/esm/entity/task/taskReducer.js +28 -3
  19. package/lib/esm/entity/tenant/epic/saveExternalConnectionEpic.js +7 -15
  20. package/lib/esm/epic.js +5 -3
  21. package/lib/esm/index.js +711 -0
  22. package/lib/esm/view/companyTaskManagerView/epics/fetchCompanyTaskManagerViewEpic.js +3 -0
  23. package/lib/esm/view/companyTaskManagerView/epics/fetchTaskManagerMetricsEpic.js +7 -1
  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 +32 -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 +240 -12
  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/index.d.ts +995 -0
  36. package/lib/index.js +2612 -0
  37. package/lib/view/addressView/epic/fetchAddressEpic.d.ts +1 -1
  38. package/lib/view/addressView/epic/updateAddressEpic.d.ts +1 -1
  39. package/lib/view/aiAccountantView/epics/cancelAiAccountantOnboardingEpic.d.ts +3 -3
  40. package/lib/view/aiAccountantView/epics/fetchAiAccountantCustomersEpic.d.ts +2 -2
  41. package/lib/view/aiAccountantView/epics/fetchAiAccountantJobsEpic.d.ts +3 -3
  42. package/lib/view/aiAccountantView/epics/triggerAiAccountantJobEpic.d.ts +3 -3
  43. package/lib/view/aiCfoView/epics/deleteChatSessionEpic.d.ts +5 -5
  44. package/lib/view/auditReportView/auditReportViewSelector.d.ts +1 -1
  45. package/lib/view/billPayCard/billPayCardSelector.d.ts +3 -3
  46. package/lib/view/cardBalance/cardBalanceEpic.d.ts +2 -2
  47. package/lib/view/cardBalance/cardBalanceSelector.d.ts +8 -8
  48. package/lib/view/cardBalance/statementCloseDayEpic.d.ts +3 -3
  49. package/lib/view/cardPayment/epic/addCardPaymentSourceEpic.d.ts +4 -4
  50. package/lib/view/cardPayment/epic/confirmCardSetupIntentEpic.d.ts +4 -4
  51. package/lib/view/cardPayment/epic/createCardSetupIntentEpic.d.ts +5 -5
  52. package/lib/view/cardPayment/epic/fetchPaymentSourcesEpic.d.ts +5 -5
  53. package/lib/view/cashBalance/cashBalanceEpic.d.ts +2 -2
  54. package/lib/view/cashBalance/cashBalanceSelector.d.ts +8 -8
  55. package/lib/view/cashInCashOut/cashInCashOutEpic.d.ts +5 -5
  56. package/lib/view/cashInCashOut/cashInCashOutSelector.d.ts +5 -5
  57. package/lib/view/cashPosition/cashPositionEpic.d.ts +3 -3
  58. package/lib/view/cashPosition/cashPositionSelector.d.ts +5 -5
  59. package/lib/view/companyConfigView/companyConfigSelector.d.ts +2 -2
  60. package/lib/view/companyTaskManagerView/epics/fetchCompanyTaskManagerViewEpic.js +3 -0
  61. package/lib/view/companyTaskManagerView/epics/fetchTaskManagerMetricsEpic.js +7 -1
  62. package/lib/view/companyView/epic/companyPassport/saveCompanyPassportDetailsEpic.d.ts +1 -1
  63. package/lib/view/companyView/types/cockpitTypes.d.ts +3 -0
  64. package/lib/view/createTransferEntry/createTransferEntrySelector.d.ts +1 -1
  65. package/lib/view/creditAgentView/epics/saveCreditAgentMacroEpic.d.ts +3 -3
  66. package/lib/view/creditAgentView/epics/scheduleTenantCreditScoreCronEpic.d.ts +3 -3
  67. package/lib/view/creditAgentView/epics/updateCardProfileEpic.d.ts +3 -3
  68. package/lib/view/expenseAutomationView/epics/accountRecon/reparseStatementEpic.d.ts +4 -4
  69. package/lib/view/expenseAutomationView/epics/accountRecon/updateStatementInfoEpic.d.ts +3 -3
  70. package/lib/view/expenseAutomationView/epics/accountRecon/uploadAccountStatementEpic.d.ts +1 -1
  71. package/lib/view/expenseAutomationView/epics/missingReceipts/bulkUploadReceiptsEpic.d.ts +1 -1
  72. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  73. package/lib/view/forecastList/forecastListSelector.d.ts +4 -4
  74. package/lib/view/insightsCard/insightsCardEpic.d.ts +1 -1
  75. package/lib/view/people/changeZeniPersonRolesEpic.d.ts +5 -5
  76. package/lib/view/people/deletePersonEpic.d.ts +4 -4
  77. package/lib/view/people/fetchPeopleEpic.d.ts +2 -2
  78. package/lib/view/people/invitePeopleEpic.d.ts +7 -7
  79. package/lib/view/people/inviteZeniPeopleEpic.d.ts +7 -7
  80. package/lib/view/people/resendInviteEpic.d.ts +3 -3
  81. package/lib/view/reimbursementCard/reimbursementCardSelector.d.ts +3 -3
  82. package/lib/view/reportUIOptions/updateReportUIOptionCOABalancesRangeEpic.d.ts +11 -11
  83. package/lib/view/reportUIOptions/updateReportUIOptionCompareModeEpic.d.ts +8 -8
  84. package/lib/view/reportUIOptions/updateReportUIOptionThisPeriodEpic.d.ts +8 -8
  85. package/lib/view/settingsView/epic/fetchBankAccountsListEpic.d.ts +3 -3
  86. package/lib/view/settingsView/epic/saveNotificationSettingsEpic.d.ts +3 -3
  87. package/lib/view/spendManagement/billPay/magicLinkView/magicLinkViewSelector.d.ts +2 -2
  88. package/lib/view/spendManagement/commonSetup/epic/setup/parseUploadedKybDocumentEpic.d.ts +4 -4
  89. package/lib/view/spendManagement/helpers.d.ts +1 -1
  90. package/lib/view/spendManagement/reimbursement/editRemiView/epics/fetchRecommendationsAndUpdateMerchantRecommendationsEpic.d.ts +3 -3
  91. package/lib/view/tagView/epics/createTagEpic.d.ts +3 -3
  92. package/lib/view/tagView/epics/deleteTagEpic.d.ts +7 -7
  93. package/lib/view/taskManager/taskDetailView/epics/createSubTaskEpic.d.ts +9 -0
  94. package/lib/view/taskManager/taskDetailView/epics/createSubTaskEpic.js +100 -0
  95. package/lib/view/taskManager/taskDetailView/epics/fetchSubTasksEpic.d.ts +8 -0
  96. package/lib/view/taskManager/taskDetailView/epics/fetchSubTasksEpic.js +57 -0
  97. package/lib/view/taskManager/taskDetailView/epics/initializeTaskToLocalStoreEpic.js +1 -0
  98. package/lib/view/taskManager/taskDetailView/epics/saveTaskDetailEpic.d.ts +2 -2
  99. package/lib/view/taskManager/taskDetailView/epics/saveTaskDetailEpic.js +69 -8
  100. package/lib/view/taskManager/taskDetailView/taskDetail.d.ts +15 -0
  101. package/lib/view/taskManager/taskDetailView/taskDetailReducer.d.ts +14 -2
  102. package/lib/view/taskManager/taskDetailView/taskDetailReducer.js +57 -3
  103. package/lib/view/taskManager/taskDetailView/taskDetailSelector.d.ts +2 -1
  104. package/lib/view/taskManager/taskDetailView/taskDetailSelector.js +30 -7
  105. package/lib/view/taskManager/taskListView/epics/fetchTaskListEpic.js +3 -1
  106. package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
  107. package/lib/view/taskManager/taskListView/taskList.js +1 -0
  108. package/lib/view/taskManager/taskListView/taskListPayload.d.ts +4 -0
  109. package/lib/view/taskManager/taskListView/taskListReducer.d.ts +6 -2
  110. package/lib/view/taskManager/taskListView/taskListReducer.js +241 -13
  111. package/lib/view/taskManager/taskListView/taskListSelector.js +1 -0
  112. package/lib/view/taskManager/taskListView/taskListViewHelpers.d.ts +11 -0
  113. package/lib/view/taskManager/taskListView/taskListViewHelpers.js +60 -1
  114. package/lib/view/tasksCard/tasksCardSelector.d.ts +2 -2
  115. package/lib/view/topEx/topExEpic.d.ts +2 -2
  116. package/lib/view/topEx/topExSelector.d.ts +7 -7
  117. package/lib/view/transactionDetail/epics/uploadMissingAttachmentSuccessEpic.d.ts +5 -5
  118. package/lib/view/transactionDetail/transactionDetailLocalDataHelper.d.ts +1 -1
  119. package/lib/view/userRoleConfigView/userRoleConfigSelector.d.ts +2 -2
  120. package/lib/view/zeniOAuthView/epics/approveOAuthConsentEpic.d.ts +3 -3
  121. package/package.json +1 -1
@@ -18,6 +18,9 @@ export const fetchCompanyTaskManagerViewEpic = (actions$, _state$, zeniAPI) => a
18
18
  ...(filters?.categories != null && filters.categories.length > 0
19
19
  ? { filter_by: toFilterByPayload(filters) }
20
20
  : {}),
21
+ task_group_scopes: filters?.taskGroupScopes != null && filters.taskGroupScopes.length > 0
22
+ ? filters.taskGroupScopes
23
+ : ['internal'],
21
24
  };
22
25
  return zeniAPI
23
26
  .getJSON(`${zeniAPI.apiEndPoints.tenantMicroServiceBaseUrl}/1.0/cockpit-panels/tasks-es?query=${encodeURIComponent(JSON.stringify(queryValue))}`)
@@ -3,7 +3,13 @@ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { isSuccessResponse } from '../../../responsePayload';
4
4
  import { fetchTaskManagerMetrics, updateTaskManagerMetrics, updateTaskManagerMetricsOnFailure, } from '../companyTaskManagerViewReducer';
5
5
  export const fetchTaskManagerMetricsEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchTaskManagerMetrics.match), switchMap(() => {
6
- const query = JSON.stringify({ is_filter_by_logged_in_user: true });
6
+ // Include subtasks so the cockpit count matches the customer dashboard
7
+ // pending count. The dashboard counts every task (subtasks included);
8
+ // cockpit's default of include_subtasks=false otherwise undercounts.
9
+ const query = JSON.stringify({
10
+ is_filter_by_logged_in_user: true,
11
+ include_subtasks: true,
12
+ });
7
13
  return zeniAPI
8
14
  .getJSON(`${zeniAPI.apiEndPoints.tenantMicroServiceBaseUrl}/1.0/cockpit-panels/tasks-es-metrics?query=${encodeURIComponent(query)}`)
9
15
  .pipe(mergeMap((response) => {
@@ -0,0 +1,96 @@
1
+ import { from, of } from 'rxjs';
2
+ import { catchError, filter, mergeMap } from 'rxjs/operators';
3
+ import { convertHHMMStrToMinutes } from '../../../../commonStateTypes/fiscalYearHelpers/formatMinutesToFromHHMM';
4
+ import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
5
+ import { appendSubTaskId, updateTasks, } from '../../../../entity/task/taskReducer';
6
+ import { getTaskById } from '../../../../entity/task/taskSelector';
7
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
8
+ import { updateTaskListOnNewTaskCreationSuccess } from '../../taskListView/taskListReducer';
9
+ import { createSubTask, createSubTaskSuccessOrFailure, } from '../taskDetailReducer';
10
+ export const createSubTaskEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(createSubTask.match), mergeMap((action) => {
11
+ const state = state$.value;
12
+ const parentTask = getTaskById(state.taskState, action.payload.parentTaskId);
13
+ // Subtasks inherit every parent group membership, not just the first,
14
+ // so the subtask surfaces in all the same buckets as its parent.
15
+ const parentTaskGroupIds = parentTask?.taskGroupIds ?? [];
16
+ const parentTaskGroupId = parentTaskGroupIds[0];
17
+ const isTaskListFetched = state.taskListState.fetchState === 'Completed';
18
+ const payload = prepareSubTaskPayload(action.payload, parentTaskGroupIds);
19
+ return zeniAPI
20
+ .postAndGetJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks`, payload)
21
+ .pipe(mergeMap((response) => {
22
+ if (isSuccessResponse(response) &&
23
+ response.data != null &&
24
+ response.data.tasks.length > 0) {
25
+ const newTaskId = response.data.tasks[0].task_id;
26
+ const actions = [
27
+ updateTasks(response.data.tasks),
28
+ appendSubTaskId({
29
+ parentTaskId: action.payload.parentTaskId,
30
+ subTaskId: newTaskId,
31
+ }),
32
+ createSubTaskSuccessOrFailure({
33
+ fetchState: 'Completed',
34
+ newTaskId,
35
+ }),
36
+ openSnackbar({
37
+ messageSection: 'create_sub_task',
38
+ messageText: 'success',
39
+ type: 'success',
40
+ }),
41
+ ];
42
+ // The task list is keyed on parent tasks (no parent_task_id)
43
+ // — subtasks belong to their parent's children list, not the
44
+ // top-level list. Pushing a subtask into the list would
45
+ // surface it as an orphan row at the top of the group.
46
+ // Only fire the list-update for actual parent tasks.
47
+ const createdTask = response.data.tasks[0];
48
+ const isSubtask = createdTask.parent_task_id != null &&
49
+ createdTask.parent_task_id !== '';
50
+ if (!isSubtask &&
51
+ isTaskListFetched === true &&
52
+ parentTaskGroupId != null) {
53
+ actions.push(updateTaskListOnNewTaskCreationSuccess({
54
+ taskGroupId: parentTaskGroupId,
55
+ task: createdTask,
56
+ }));
57
+ }
58
+ return from(actions);
59
+ }
60
+ else {
61
+ return of(createSubTaskSuccessOrFailure({
62
+ fetchState: 'Error',
63
+ error: response.status,
64
+ }), openSnackbar({
65
+ messageSection: 'create_sub_task',
66
+ messageText: 'failed',
67
+ type: 'error',
68
+ variables: [
69
+ {
70
+ variableName: '_api-error_',
71
+ variableValue: response.status.message,
72
+ },
73
+ ],
74
+ }));
75
+ }
76
+ }), catchError((error) => of(createSubTaskSuccessOrFailure({
77
+ fetchState: 'Error',
78
+ error: createZeniAPIStatus('Unexpected Error', 'Create Sub Task REST API call errored out' +
79
+ JSON.stringify(error)),
80
+ }))));
81
+ }));
82
+ const prepareSubTaskPayload = (subTask, parentTaskGroupIds) => ({
83
+ name: subTask.name,
84
+ assignees: subTask.assignee,
85
+ description: subTask.description,
86
+ priority: subTask.priority,
87
+ status: subTask.status,
88
+ tags: subTask.tagIds,
89
+ time_spent: convertHHMMStrToMinutes(subTask.timeSpent),
90
+ group_assignees: subTask.groupAssignees,
91
+ parent_task_id: subTask.parentTaskId,
92
+ // Inherit every parent group, not just the first — see callsite for
93
+ // the two-scope rationale.
94
+ task_group_ids: parentTaskGroupIds,
95
+ due_date: subTask.dueDate?.format('YYYY-MM-DD') ?? null,
96
+ });
@@ -0,0 +1,53 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, filter, mergeMap } from 'rxjs/operators';
3
+ import { setSubTaskIds, updateTasks } from '../../../../entity/task/taskReducer';
4
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
5
+ import { fetchSubTasks, updateSubTasks, updateSubTasksFetchStatus, } from '../taskDetailReducer';
6
+ export const fetchSubTasksEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchSubTasks.match),
7
+ // Use mergeMap, not switchMap: switchMap cancels in-flight requests for
8
+ // the previous parent without dispatching success or failure, leaving
9
+ // subTaskListFetchStatusByParentId stuck at In-Progress and blocking the
10
+ // detail page's combined fetchStatus via reduceAnyFetchState. Per-parent
11
+ // races on the same parentTaskId are handled last-write-wins downstream.
12
+ mergeMap((action) => {
13
+ const { parentTaskId } = action.payload;
14
+ const query = JSON.stringify({
15
+ task_type: 'subtasks',
16
+ parent_task_id: parentTaskId,
17
+ });
18
+ return zeniAPI
19
+ .getJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks?query=${encodeURIComponent(query)}`)
20
+ .pipe(mergeMap((response) => {
21
+ if (isSuccessResponse(response) && response.data != null) {
22
+ const { tasks } = response.data;
23
+ // Keep the parent's `subTasksIds` in sync with the fetched
24
+ // bucket. Without this, the parent entity record can drift
25
+ // out of sync with what the children fetch returned —
26
+ // e.g. a server-side delete won't drop from the parent
27
+ // record until the next list re-fetch.
28
+ const fetchedChildIds = tasks
29
+ .filter((t) => t.parent_task_id === parentTaskId)
30
+ .map((t) => t.task_id);
31
+ return of(updateTasks(tasks), setSubTaskIds({
32
+ parentTaskId,
33
+ subTaskIds: fetchedChildIds,
34
+ }), updateSubTasks({
35
+ parentTaskId,
36
+ }));
37
+ }
38
+ else {
39
+ return of(updateSubTasksFetchStatus({
40
+ parentTaskId,
41
+ fetchState: 'Error',
42
+ error: response.status,
43
+ }));
44
+ }
45
+ }), catchError((error) => {
46
+ return of(updateSubTasksFetchStatus({
47
+ parentTaskId,
48
+ fetchState: 'Error',
49
+ error: createZeniAPIStatus('Unexpected Error', 'Fetch Sub Tasks REST API call errored out' +
50
+ JSON.stringify(error)),
51
+ }));
52
+ }));
53
+ }));
@@ -28,6 +28,7 @@ export const initializeTaskToLocalStoreEpic = (actions$, state$) => actions$.pip
28
28
  recurringStartDate: task.recurringStartDate,
29
29
  timeSpent: task.timeSpent,
30
30
  isPrivate: task.isPrivate,
31
+ taskGroupId: task.taskGroupIds[0],
31
32
  };
32
33
  const actions = [
33
34
  saveTaskUpdatesToLocalStore({ taskDetailLocalData, taskId }),
@@ -6,23 +6,52 @@ import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
6
6
  import { updateTasks } from '../../../../entity/task/taskReducer';
7
7
  import { getTaskById } from '../../../../entity/task/taskSelector';
8
8
  import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
9
- import { updateTaskListOnNewTaskCreationSuccess } from '../../taskListView/taskListReducer';
9
+ import { removeTaskFromGroupBucket, updateTaskListOnNewTaskCreationSuccess, } from '../../taskListView/taskListReducer';
10
10
  import { saveTaskDetail, saveTaskSuccessOrFailure } from '../taskDetailReducer';
11
11
  export const saveTaskDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(saveTaskDetail.match), mergeMap((action) => {
12
12
  const { taskId, taskGroupId } = action.payload;
13
13
  const state = state$.value;
14
14
  const isTaskListFetched = state.taskListState.fetchState === 'Completed';
15
- const updatedTaskGroupId = taskId == null
16
- ? taskGroupId
17
- : getTaskById(state.taskState, taskId)?.taskGroupIds[0];
18
- const payload = prepareTaskPayload(state, taskId, updatedTaskGroupId);
15
+ // On edit, prefer the locally-edited group; fall back to the entity's
16
+ // current group. On create, use the taskGroupId from the action payload.
17
+ const localTaskGroupId = taskId != null
18
+ ? state.taskDetailState.editTaskStateById[taskId]?.taskDetailLocalData
19
+ ?.taskGroupId
20
+ : undefined;
21
+ // groupWasCleared distinguishes "user explicitly cleared the group"
22
+ // (localTaskGroupId === null → send []) from "user did not touch it"
23
+ // (undefined → fall back to entity). Without this, the `??` fallback
24
+ // would resurrect the stale entity value after a clear.
25
+ const groupWasCleared = taskId != null && localTaskGroupId === null;
26
+ const entityTaskGroupIds = taskId != null
27
+ ? (getTaskById(state.taskState, taskId)?.taskGroupIds ?? [])
28
+ : [];
29
+ // Distinguish three save shapes:
30
+ // - cleared → []
31
+ // - user picked a different group locally → [localTaskGroupId]
32
+ // - no local edit → entire entityTaskGroupIds (preserves multi-
33
+ // group membership; without this, unrelated field edits silently
34
+ // drop extra groups).
35
+ const localGroupChanged = localTaskGroupId != null;
36
+ const taskGroupIdsForPayload = groupWasCleared
37
+ ? []
38
+ : localGroupChanged
39
+ ? [localTaskGroupId]
40
+ : taskId != null
41
+ ? entityTaskGroupIds
42
+ : taskGroupId != null
43
+ ? [taskGroupId]
44
+ : [];
45
+ // Keep `updatedTaskGroupId` (single id) for downstream list-update
46
+ // dispatches that still bucket against one group at a time.
47
+ const updatedTaskGroupId = taskGroupIdsForPayload[0];
48
+ const payload = prepareTaskPayload(state, taskId, taskGroupIdsForPayload);
19
49
  const saveTaskApi = taskId != null
20
50
  ? zeniAPI.putAndGetJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks/${taskId}`, payload)
21
51
  : zeniAPI.postAndGetJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks`, payload);
22
52
  return saveTaskApi.pipe(mergeMap((response) => {
23
53
  if (isSuccessResponse(response) &&
24
54
  response.data != null &&
25
- updatedTaskGroupId != null &&
26
55
  response.data.tasks.length > 0) {
27
56
  const newTaskId = response.data.tasks[0].task_id;
28
57
  const actions = [
@@ -33,7 +62,24 @@ export const saveTaskDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe(f
33
62
  taskId,
34
63
  }),
35
64
  ];
36
- if (isTaskListFetched === true) {
65
+ // Only suppress the list-insert on the live/completed tabs
66
+ // when the saved task's status no longer matches the tab —
67
+ // otherwise doUpdateTaskList writes the row back into the
68
+ // current tab's grouping arrays even after updateTasks moved
69
+ // it to Completed. Archived/snoozed/deleted are owned by
70
+ // their dedicated actions and must keep the pre-PR optimistic
71
+ // update path so their grouping arrays don't go stale.
72
+ const savedTask = response.data.tasks[0];
73
+ const savedIsCompleted = savedTask.status?.code === 'resolved';
74
+ const activeTab = state.taskListState.currentTab;
75
+ const isLiveOrCompletedTab = activeTab === 'live' || activeTab === 'completed';
76
+ const statusMatchesActiveTab = !isLiveOrCompletedTab ||
77
+ (savedIsCompleted && activeTab === 'completed') ||
78
+ (!savedIsCompleted && activeTab === 'live');
79
+ if (isTaskListFetched === true &&
80
+ groupWasCleared === false &&
81
+ updatedTaskGroupId != null &&
82
+ statusMatchesActiveTab === true) {
37
83
  actions.push(updateTaskListOnNewTaskCreationSuccess({
38
84
  taskGroupId: updatedTaskGroupId,
39
85
  task: response.data.tasks[0],
@@ -42,6 +88,21 @@ export const saveTaskDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe(f
42
88
  : undefined,
43
89
  }));
44
90
  }
91
+ else if (isTaskListFetched === true &&
92
+ groupWasCleared === true &&
93
+ taskId != null) {
94
+ // Group was cleared — skip the list-insert action (no
95
+ // destination bucket) and explicitly drop the row from every
96
+ // previously-held group so the list re-renders without the
97
+ // stale entry under the old group until next refetch.
98
+ const previousTask = getTaskById(state.taskState, taskId);
99
+ previousTask?.taskGroupIds.forEach((previousGroupId) => {
100
+ actions.push(removeTaskFromGroupBucket({
101
+ taskId,
102
+ taskGroupId: previousGroupId,
103
+ }));
104
+ });
105
+ }
45
106
  return from(actions);
46
107
  }
47
108
  else {
@@ -67,7 +128,7 @@ export const saveTaskDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe(f
67
128
  error: createZeniAPIStatus('Unexpected Error', 'Save Task REST API call errored out' + JSON.stringify(error)),
68
129
  }))));
69
130
  }));
70
- const prepareTaskPayload = (state, taskId, taskGroupId) => {
131
+ const prepareTaskPayload = (state, taskId, taskGroupIds) => {
71
132
  const { taskDetailState, taskState } = state;
72
133
  const syncToken = taskId != null ? (getTaskById(taskState, taskId)?.syncToken ?? '') : '';
73
134
  const sourceState = taskId != null
@@ -96,7 +157,7 @@ const prepareTaskPayload = (state, taskId, taskGroupId) => {
96
157
  ? (localData.recurringDaysOfWeek ?? [])
97
158
  : [],
98
159
  sync_token: syncToken,
99
- task_group_ids: taskGroupId != null ? [taskGroupId] : [],
160
+ task_group_ids: taskGroupIds,
100
161
  time_spent: convertHHMMStrToMinutes(localData.timeSpent),
101
162
  group_assignees: localData.groupAssignees,
102
163
  ...(taskId == null ? { is_private: localData.isPrivate ?? false } : {}),
@@ -6,6 +6,11 @@ import { initialTaskDetail, } from './taskDetail';
6
6
  export const initialState = {
7
7
  newTaskState: initialTaskDetail,
8
8
  editTaskStateById: {},
9
+ subTaskCreateStatus: {
10
+ fetchState: 'Not-Started',
11
+ error: undefined,
12
+ },
13
+ subTaskListFetchStatusByParentId: {},
9
14
  taskHistoryById: {},
10
15
  };
11
16
  const taskDetailView = createSlice({
@@ -50,7 +55,7 @@ const taskDetailView = createSlice({
50
55
  updateEditTaskFetchStatus(draft, action) {
51
56
  const { taskId, fetchState, error } = action.payload;
52
57
  draft.editTaskStateById[taskId] = {
53
- ...draft.editTaskStateById[taskId],
58
+ ...(draft.editTaskStateById[taskId] ?? initialTaskDetail),
54
59
  fetchTaskStatus: {
55
60
  fetchState,
56
61
  error: fetchState === 'Error' ? error : undefined,
@@ -318,10 +323,59 @@ const taskDetailView = createSlice({
318
323
  error,
319
324
  };
320
325
  },
326
+ createSubTask: {
327
+ reducer(draft) {
328
+ draft.subTaskCreateStatus = {
329
+ fetchState: 'In-Progress',
330
+ error: undefined,
331
+ };
332
+ },
333
+ prepare(payload) {
334
+ return { payload };
335
+ },
336
+ },
337
+ createSubTaskSuccessOrFailure(draft, action) {
338
+ const { fetchState, error } = action.payload;
339
+ draft.subTaskCreateStatus = {
340
+ fetchState,
341
+ error,
342
+ };
343
+ },
344
+ resetSubTaskCreateStatus(draft) {
345
+ draft.subTaskCreateStatus = {
346
+ ...initialState.subTaskCreateStatus,
347
+ };
348
+ },
349
+ fetchSubTasks: {
350
+ reducer(draft, action) {
351
+ const { parentTaskId } = action.payload;
352
+ draft.subTaskListFetchStatusByParentId[parentTaskId] = {
353
+ fetchState: 'In-Progress',
354
+ error: undefined,
355
+ };
356
+ },
357
+ prepare(parentTaskId) {
358
+ return { payload: { parentTaskId } };
359
+ },
360
+ },
361
+ updateSubTasks(draft, action) {
362
+ const { parentTaskId } = action.payload;
363
+ draft.subTaskListFetchStatusByParentId[parentTaskId] = {
364
+ fetchState: 'Completed',
365
+ error: undefined,
366
+ };
367
+ },
368
+ updateSubTasksFetchStatus(draft, action) {
369
+ const { parentTaskId, fetchState, error } = action.payload;
370
+ draft.subTaskListFetchStatusByParentId[parentTaskId] = {
371
+ fetchState,
372
+ error,
373
+ };
374
+ },
321
375
  clearTaskDetail(draft) {
322
376
  Object.assign(draft, initialState);
323
377
  },
324
378
  },
325
379
  });
326
- export const { fetchTaskDetailPage, fetchTaskDetail, initializeTaskToLocalStore, updateEditTaskFetchStatus, saveTaskUpdatesToLocalStore, discardTaskUpdatesInLocalStore, saveTaskDetail, archiveTask, archiveTaskSuccessOrFailure, saveTaskSuccessOrFailure, deleteTask, removeTaskDetail, deleteTaskSuccessOrFailure, snoozeTask, snoozeTaskSuccessOrFailure, unsnoozeTask, fetchTaskHistory, updateTaskHistory, updateTaskHistoryFetchStatus, clearTaskDetail, updateCreatedTagToLocalStore, updateDeletedTagToLocalStore, } = taskDetailView.actions;
380
+ export const { fetchTaskDetailPage, fetchTaskDetail, initializeTaskToLocalStore, updateEditTaskFetchStatus, saveTaskUpdatesToLocalStore, discardTaskUpdatesInLocalStore, saveTaskDetail, archiveTask, archiveTaskSuccessOrFailure, saveTaskSuccessOrFailure, createSubTask, createSubTaskSuccessOrFailure, resetSubTaskCreateStatus, fetchSubTasks, updateSubTasks, updateSubTasksFetchStatus, deleteTask, removeTaskDetail, deleteTaskSuccessOrFailure, snoozeTask, snoozeTaskSuccessOrFailure, unsnoozeTask, fetchTaskHistory, updateTaskHistory, updateTaskHistoryFetchStatus, clearTaskDetail, updateCreatedTagToLocalStore, updateDeletedTagToLocalStore, } = taskDetailView.actions;
327
381
  export default taskDetailView.reducer;
@@ -1,8 +1,8 @@
1
1
  import isEqual from 'lodash/isEqual';
2
- import { reduceFetchState } from '../../../commonStateTypes/reduceFetchState';
2
+ import { reduceAnyFetchState } from '../../../commonStateTypes/reduceFetchState';
3
3
  import { getClassesByIds } from '../../../entity/class/classSelector';
4
4
  import { getFilesByFileIds } from '../../../entity/file/fileSelector';
5
- import { getTaskById } from '../../../entity/task/taskSelector';
5
+ import { getTaskById, getTasksByIds } from '../../../entity/task/taskSelector';
6
6
  import { getUserAndUserRole, } from '../../companyView/types/userAndRole';
7
7
  import { getFileDeleteStatusById, getFileUpdateNameStatusById, } from '../../fileView/fileViewSelector';
8
8
  import { getUserList, } from '../../userListView/userListViewSelector';
@@ -27,8 +27,13 @@ export const getTaskDetail = (state, taskId) => {
27
27
  let recurringSourceTaskId = undefined;
28
28
  let snoozedUntil = undefined;
29
29
  let taskGroupId = undefined;
30
+ let subtasks = [];
30
31
  if (taskId != null && sourceTaskDetail != null) {
31
32
  const taskEntity = getTaskById(taskState, taskId);
33
+ subtasks =
34
+ taskEntity != null
35
+ ? getTasksByIds(taskState, taskEntity.subTasksIds)
36
+ : [];
32
37
  const fileIdsInEntity = taskEntity?.fileIds ?? [];
33
38
  if (taskEntity != null) {
34
39
  createdByUser = getUserAndUserRole(userState, userRoleState, addressState, taskEntity.createdBy);
@@ -38,26 +43,37 @@ export const getTaskDetail = (state, taskId) => {
38
43
  snoozedUntil = taskEntity.snoozedUntil;
39
44
  taskGroupId = taskEntity.taskGroupIds[0];
40
45
  }
46
+ // Treat "no entry" as Completed so it doesn't block the combined
47
+ // fetch state. The Not-Started default would pin the detail page
48
+ // below Completed forever before the first fetchSubTasks dispatches.
49
+ const completedSubTaskStatus = {
50
+ fetchState: 'Completed',
51
+ error: undefined,
52
+ };
53
+ const subTaskStatus = taskDetailState.subTaskListFetchStatusByParentId[taskId] ??
54
+ completedSubTaskStatus;
41
55
  if (fileIdsInEntity.length > 0) {
42
- fetchStatus = reduceFetchState([
56
+ fetchStatus = reduceAnyFetchState([
43
57
  fileViewState.fetchFilesStatus,
44
58
  sourceTaskDetail.fetchTaskStatus,
45
59
  userList,
46
60
  classListState,
61
+ subTaskStatus,
47
62
  ]);
48
63
  }
49
64
  else {
50
- fetchStatus = reduceFetchState([
65
+ fetchStatus = reduceAnyFetchState([
51
66
  sourceTaskDetail.fetchTaskStatus,
52
67
  userList,
53
68
  classListState,
69
+ subTaskStatus,
54
70
  ]);
55
71
  }
56
72
  showTaskDetailFormFooter = showFormFooter(sourceTaskDetail.taskDetailLocalData, taskEntity);
57
73
  taskHistory = taskDetailState.taskHistoryById[taskId]?.historicEvents ?? [];
58
74
  }
59
75
  else if (taskId == null) {
60
- fetchStatus = reduceFetchState([userList, classListState]);
76
+ fetchStatus = reduceAnyFetchState([userList, classListState]);
61
77
  }
62
78
  const { classIds } = classListState;
63
79
  const allAccountingClasses = getClassesByIds(classState, {
@@ -90,6 +106,7 @@ export const getTaskDetail = (state, taskId) => {
90
106
  showTaskDetailFormFooter,
91
107
  snoozedUntil,
92
108
  taskGroupId,
109
+ subtasks,
93
110
  };
94
111
  };
95
112
  export const allTaskStatus = [
@@ -137,13 +154,16 @@ export const allTaskPriority = [
137
154
  },
138
155
  ];
139
156
  const showFormFooter = (taskDetailLocalData, taskDetailInStore) => {
140
- if (taskDetailLocalData.name !== taskDetailInStore?.name ||
157
+ if (taskDetailInStore == null) {
158
+ return false;
159
+ }
160
+ if (taskDetailLocalData.name !== taskDetailInStore.name ||
141
161
  taskDetailLocalData.description !== taskDetailInStore.description ||
142
162
  taskDetailLocalData.priority !== taskDetailInStore.priority.code ||
143
163
  taskDetailLocalData.status !== taskDetailInStore.status.code ||
144
164
  !isEqual(taskDetailInStore.tagIds, taskDetailLocalData.tagIds) ||
145
- !isEqual(taskDetailLocalData.assignee, taskDetailInStore?.assignees ?? []) ||
146
- !isEqual(taskDetailLocalData.groupAssignees, taskDetailInStore?.groupAssignees ?? []) ||
165
+ !isEqual(taskDetailLocalData.assignee, taskDetailInStore.assignees ?? []) ||
166
+ !isEqual(taskDetailLocalData.groupAssignees, taskDetailInStore.groupAssignees ?? []) ||
147
167
  !isEqual(taskDetailLocalData.fileIds, taskDetailInStore.fileIds ?? []) ||
148
168
  !isEqual(taskDetailLocalData.dueDate, taskDetailInStore.dueDate) ||
149
169
  !isEqual(taskDetailLocalData.type, taskDetailInStore.type) ||
@@ -151,7 +171,10 @@ const showFormFooter = (taskDetailLocalData, taskDetailInStore) => {
151
171
  !isEqual(taskDetailLocalData.recurringFrequency, taskDetailInStore.recurringFrequency) ||
152
172
  !isEqual(taskDetailLocalData.recurringDaysOfWeek, taskDetailInStore.recurringDaysOfWeek) ||
153
173
  !isEqual(taskDetailLocalData.recurringStartDate, taskDetailInStore.recurringStartDate) ||
154
- !isEqual(taskDetailLocalData.timeSpent, taskDetailInStore.timeSpent)) {
174
+ !isEqual(taskDetailLocalData.timeSpent, taskDetailInStore.timeSpent) ||
175
+ !isEqual(taskDetailLocalData.taskGroupId, taskDetailInStore.taskGroupIds.length > 0
176
+ ? taskDetailInStore.taskGroupIds[0]
177
+ : undefined)) {
155
178
  return true;
156
179
  }
157
180
  return false;
@@ -9,15 +9,17 @@ export const fetchTaskListEpic = (actions$, _state$, zeniAPI) => actions$.pipe(f
9
9
  .getJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks?query=${encodeURIComponent(`{"task_type": "all"}`)}`)
10
10
  .pipe(mergeMap((response) => {
11
11
  if (isSuccessResponse(response) && response.data != null) {
12
- const { tasks, deleted, archived, snoozed } = response.data;
12
+ const { tasks, deleted, archived, snoozed, completed } = response.data;
13
13
  const allTasks = [
14
14
  ...tasks,
15
+ ...(completed ?? []),
15
16
  ...(deleted ?? []),
16
17
  ...(archived ?? []),
17
18
  ...(snoozed ?? []),
18
19
  ];
19
20
  return of(updateTasks(allTasks), updateTaskList({
20
21
  data: tasks,
22
+ completed: completed ?? [],
21
23
  deleted: deleted ?? [],
22
24
  archived: archived ?? [],
23
25
  snoozed: snoozed ?? [],
@@ -1,6 +1,7 @@
1
1
  import { stringToUnion, stringToUnionStrict, } from '../../../commonStateTypes/stringToUnion';
2
2
  export const ALL_TASK_LIST_TABS = [
3
3
  'live',
4
+ 'completed',
4
5
  'archived',
5
6
  'deleted',
6
7
  'snoozed',