@zeniai/client-epic-state 5.2.76 → 5.2.78
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/esm/view/invoicing/createInvoice/createInvoiceFormConfig.js +10 -0
- package/lib/esm/view/invoicing/createInvoice/createInvoiceSelector.js +6 -0
- package/lib/esm/view/taskManager/taskDetailView/epics/fetchTaskDetailEpic.js +82 -41
- package/lib/esm/view/taskManager/taskDetailView/epics/fetchTaskDetailPageEpic.js +9 -1
- package/lib/esm/view/taskManager/taskDetailView/epics/initializeTaskToLocalStoreEpic.js +2 -23
- package/lib/esm/view/taskManager/taskDetailView/taskDetailLocalData.js +36 -0
- package/lib/esm/view/taskManager/taskDetailView/taskDetailReducer.js +11 -3
- package/lib/index.d.ts +1 -1
- package/lib/view/invoicing/createInvoice/createInvoiceFormConfig.js +10 -0
- package/lib/view/invoicing/createInvoice/createInvoicePayload.d.ts +4 -0
- package/lib/view/invoicing/createInvoice/createInvoiceSelector.d.ts +2 -1
- package/lib/view/invoicing/createInvoice/createInvoiceSelector.js +6 -0
- package/lib/view/invoicing/createInvoice/createInvoiceState.d.ts +11 -0
- package/lib/view/taskManager/taskDetailView/epics/fetchTaskDetailEpic.d.ts +2 -2
- package/lib/view/taskManager/taskDetailView/epics/fetchTaskDetailEpic.js +80 -39
- package/lib/view/taskManager/taskDetailView/epics/fetchTaskDetailPageEpic.js +9 -1
- package/lib/view/taskManager/taskDetailView/epics/initializeTaskToLocalStoreEpic.js +2 -23
- package/lib/view/taskManager/taskDetailView/taskDetailLocalData.d.ts +5 -0
- package/lib/view/taskManager/taskDetailView/taskDetailLocalData.js +45 -0
- package/lib/view/taskManager/taskDetailView/taskDetailReducer.js +11 -3
- package/package.json +1 -1
|
@@ -41,6 +41,16 @@ export const createInvoiceFormLocalDataToRequest = (localData) => {
|
|
|
41
41
|
quantity: toQuantity(line.quantity),
|
|
42
42
|
tax_amount: line.taxAmount.amount,
|
|
43
43
|
unit_amount: line.unitAmount.amount,
|
|
44
|
+
...(line.catalogLine != null
|
|
45
|
+
? {
|
|
46
|
+
entity_id: line.catalogLine.entityId,
|
|
47
|
+
entity_type: line.catalogLine.entityType,
|
|
48
|
+
is_taxed: line.catalogLine.isTaxed,
|
|
49
|
+
...(line.catalogLine.isTaxed
|
|
50
|
+
? {}
|
|
51
|
+
: { tax_exempt_reason: 'not_taxable' }),
|
|
52
|
+
}
|
|
53
|
+
: {}),
|
|
44
54
|
}));
|
|
45
55
|
const invoiceDate = localData.invoiceDate != null && localData.invoiceDate !== ''
|
|
46
56
|
? localData.invoiceDate
|
|
@@ -73,6 +73,12 @@ export const getCreateInvoiceFormView = (state) => {
|
|
|
73
73
|
planNames: catalogListView.plans.flatMap((plan) => (plan.subFamilies ?? [])
|
|
74
74
|
.filter((subFamily) => (subFamily.name ?? '') !== '')
|
|
75
75
|
.map((subFamily) => ({
|
|
76
|
+
// A price point with no flag is taxable, as subscription billing reads it.
|
|
77
|
+
catalogLine: {
|
|
78
|
+
entityId: subFamily.id,
|
|
79
|
+
entityType: plan.itemType ?? 'plan',
|
|
80
|
+
isTaxed: subFamily.isTaxable ?? true,
|
|
81
|
+
},
|
|
76
82
|
planName: plan.name ?? '',
|
|
77
83
|
price: planNamePrefillPrice(subFamily),
|
|
78
84
|
subFamilyName: subFamily.name ?? '',
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import recordGet from 'lodash/get';
|
|
2
|
-
import
|
|
2
|
+
import isEqual from 'lodash/isEqual';
|
|
3
|
+
import { EMPTY, concat, from, of } from 'rxjs';
|
|
3
4
|
import { catchError, filter, mergeMap } from 'rxjs/operators';
|
|
4
5
|
import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
|
|
5
6
|
import { updateTags } from '../../../../entity/tag/tagReducer';
|
|
7
|
+
import { mapTaskPayloadToTask } from '../../../../entity/task/taskPayload';
|
|
6
8
|
import { updateTasks } from '../../../../entity/task/taskReducer';
|
|
9
|
+
import { getTaskById } from '../../../../entity/task/taskSelector';
|
|
7
10
|
import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
|
|
8
11
|
import { fetchFileList } from '../../../fileView/fileViewReducer';
|
|
9
|
-
import {
|
|
12
|
+
import { buildTaskDetailLocalData, isLoadedTaskDetail, rebaseTaskDetailLocalData, } from '../taskDetailLocalData';
|
|
13
|
+
import { fetchTaskDetail, initializeTaskToLocalStore, removeTaskDetail, saveTaskUpdatesToLocalStore, updateEditTaskFetchStatus, } from '../taskDetailReducer';
|
|
10
14
|
export const fetchTaskDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchTaskDetail.match), mergeMap((action) => {
|
|
11
15
|
const { taskId, cacheOverride } = action.payload;
|
|
12
16
|
const taskActions = [];
|
|
13
17
|
const state = state$.value;
|
|
14
18
|
const taskDetail = recordGet(state.taskDetailState.editTaskStateById, taskId, undefined);
|
|
19
|
+
if (cacheOverride === true && isLoadedTaskDetail(taskDetail)) {
|
|
20
|
+
return refreshLoadedTaskDetail(taskId, state$, zeniAPI);
|
|
21
|
+
}
|
|
15
22
|
if (cacheOverride === true) {
|
|
16
23
|
taskActions.push(removeTaskDetail(taskId));
|
|
17
24
|
}
|
|
@@ -20,7 +27,7 @@ export const fetchTaskDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe(
|
|
|
20
27
|
taskDetail.fetchTaskStatus.fetchState !== 'In-Progress') {
|
|
21
28
|
taskActions.push(updateEditTaskFetchStatus({ taskId, fetchState: 'In-Progress' }));
|
|
22
29
|
const getTaskDetail$ = zeniAPI
|
|
23
|
-
.getJSON(
|
|
30
|
+
.getJSON(getTaskDetailUrl(zeniAPI, taskId))
|
|
24
31
|
.pipe(mergeMap((response) => {
|
|
25
32
|
if (isSuccessResponse(response) && response.data != null) {
|
|
26
33
|
const fetchActions = [
|
|
@@ -38,44 +45,7 @@ export const fetchTaskDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe(
|
|
|
38
45
|
}
|
|
39
46
|
return from(fetchActions);
|
|
40
47
|
}
|
|
41
|
-
|
|
42
|
-
const fetchActions = [
|
|
43
|
-
updateEditTaskFetchStatus({
|
|
44
|
-
taskId,
|
|
45
|
-
fetchState: 'Error',
|
|
46
|
-
error: response.status,
|
|
47
|
-
}),
|
|
48
|
-
];
|
|
49
|
-
if (response.status.message.includes('delete')) {
|
|
50
|
-
fetchActions.push(openSnackbar({
|
|
51
|
-
messageSection: 'fetch_task_detail',
|
|
52
|
-
messageText: 'failed',
|
|
53
|
-
type: 'error',
|
|
54
|
-
showStatusIcon: true,
|
|
55
|
-
variables: [
|
|
56
|
-
{
|
|
57
|
-
variableName: '_api-error_',
|
|
58
|
-
variableValue: 'Cannot retrieve deleted task.',
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
}));
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
fetchActions.push(openSnackbar({
|
|
65
|
-
messageSection: 'fetch_task_detail',
|
|
66
|
-
messageText: 'failed',
|
|
67
|
-
type: 'error',
|
|
68
|
-
showStatusIcon: true,
|
|
69
|
-
variables: [
|
|
70
|
-
{
|
|
71
|
-
variableName: '_api-error_',
|
|
72
|
-
variableValue: response.status.message,
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
}));
|
|
76
|
-
}
|
|
77
|
-
return from(fetchActions);
|
|
78
|
-
}
|
|
48
|
+
return from(getFetchFailureActions(taskId, response.status));
|
|
79
49
|
}), catchError((error) => {
|
|
80
50
|
console.error(`REST API call errored out ${error}`);
|
|
81
51
|
return of(updateEditTaskFetchStatus({
|
|
@@ -91,3 +61,74 @@ export const fetchTaskDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe(
|
|
|
91
61
|
return from(taskActions);
|
|
92
62
|
}
|
|
93
63
|
}));
|
|
64
|
+
const getTaskDetailUrl = (zeniAPI, taskId) => `${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks/${taskId}`;
|
|
65
|
+
const getFetchFailureActions = (taskId, status) => [
|
|
66
|
+
updateEditTaskFetchStatus({ taskId, fetchState: 'Error', error: status }),
|
|
67
|
+
openSnackbar({
|
|
68
|
+
messageSection: 'fetch_task_detail',
|
|
69
|
+
messageText: 'failed',
|
|
70
|
+
type: 'error',
|
|
71
|
+
showStatusIcon: true,
|
|
72
|
+
variables: [
|
|
73
|
+
{
|
|
74
|
+
variableName: '_api-error_',
|
|
75
|
+
variableValue: status.message.includes('delete')
|
|
76
|
+
? 'Cannot retrieve deleted task.'
|
|
77
|
+
: status.message,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
}),
|
|
81
|
+
];
|
|
82
|
+
// An unparseable token is never treated as stale, so the refresh still applies.
|
|
83
|
+
const isOlderSyncToken = (candidate, stored) => {
|
|
84
|
+
const candidateTime = Date.parse(candidate ?? '');
|
|
85
|
+
const storedTime = Date.parse(stored);
|
|
86
|
+
return (!Number.isNaN(candidateTime) &&
|
|
87
|
+
!Number.isNaN(storedTime) &&
|
|
88
|
+
candidateTime < storedTime);
|
|
89
|
+
};
|
|
90
|
+
const isSaveInProgress = (state, taskId) => state.taskDetailState.editTaskStateById[taskId]?.saveStatus.fetchState ===
|
|
91
|
+
'In-Progress';
|
|
92
|
+
// Live refresh of an open page: no reset or loading state, unsaved edits kept,
|
|
93
|
+
// skipped during the user's own save, stale responses dropped.
|
|
94
|
+
const refreshLoadedTaskDetail = (taskId, state$, zeniAPI) => {
|
|
95
|
+
if (isSaveInProgress(state$.value, taskId)) {
|
|
96
|
+
return EMPTY;
|
|
97
|
+
}
|
|
98
|
+
return zeniAPI
|
|
99
|
+
.getJSON(getTaskDetailUrl(zeniAPI, taskId))
|
|
100
|
+
.pipe(mergeMap((response) => {
|
|
101
|
+
if (!isSuccessResponse(response) || response.data == null) {
|
|
102
|
+
return from(getFetchFailureActions(taskId, response.status));
|
|
103
|
+
}
|
|
104
|
+
const state = state$.value;
|
|
105
|
+
const taskPayload = response.data.tasks.find((task) => task.task_id === taskId);
|
|
106
|
+
const storedTask = getTaskById(state.taskState, taskId);
|
|
107
|
+
const taskDetail = state.taskDetailState.editTaskStateById[taskId];
|
|
108
|
+
if (taskPayload == null ||
|
|
109
|
+
storedTask == null ||
|
|
110
|
+
taskDetail == null ||
|
|
111
|
+
isSaveInProgress(state, taskId) ||
|
|
112
|
+
isOlderSyncToken(taskPayload.sync_token, storedTask.syncToken)) {
|
|
113
|
+
return EMPTY;
|
|
114
|
+
}
|
|
115
|
+
const refreshedTask = mapTaskPayloadToTask(taskPayload, storedTask);
|
|
116
|
+
const actions = [
|
|
117
|
+
updateTasks(response.data.tasks),
|
|
118
|
+
updateTags(response.data.tasks.map((task) => task.tags).flat()),
|
|
119
|
+
saveTaskUpdatesToLocalStore({
|
|
120
|
+
taskId,
|
|
121
|
+
taskDetailLocalData: rebaseTaskDetailLocalData(taskDetail.taskDetailLocalData, buildTaskDetailLocalData(storedTask), buildTaskDetailLocalData(refreshedTask)),
|
|
122
|
+
}),
|
|
123
|
+
];
|
|
124
|
+
if (refreshedTask.fileIds.length > 0 &&
|
|
125
|
+
!isEqual(refreshedTask.fileIds, storedTask.fileIds)) {
|
|
126
|
+
actions.push(fetchFileList(refreshedTask.fileIds));
|
|
127
|
+
}
|
|
128
|
+
return from(actions);
|
|
129
|
+
}), catchError((error) => {
|
|
130
|
+
// Keep the page on screen; the next update or reload recovers.
|
|
131
|
+
console.error(`Task detail refresh errored out ${error}`);
|
|
132
|
+
return EMPTY;
|
|
133
|
+
}));
|
|
134
|
+
};
|
|
@@ -2,12 +2,20 @@ import { concat, from } from 'rxjs';
|
|
|
2
2
|
import { filter, mergeMap } from 'rxjs/operators';
|
|
3
3
|
import { fetchClassList } from '../../../classList/classListReducer';
|
|
4
4
|
import { fetchUserListByType } from '../../../userListView/userListViewReducer';
|
|
5
|
+
import { isLoadedTaskDetail } from '../taskDetailLocalData';
|
|
5
6
|
import { fetchTaskDetail, fetchTaskDetailPage, fetchTaskHistory, } from '../taskDetailReducer';
|
|
6
7
|
export const fetchTaskDetailPageEpic = (actions$, state$) => actions$.pipe(filter(fetchTaskDetailPage.match), mergeMap((action) => {
|
|
7
8
|
const { cacheOverride, taskId } = action.payload;
|
|
8
9
|
const taskDetailActions = [];
|
|
9
10
|
const state = state$.value;
|
|
10
|
-
if (cacheOverride === true
|
|
11
|
+
if (cacheOverride === true &&
|
|
12
|
+
taskId != null &&
|
|
13
|
+
isLoadedTaskDetail(state.taskDetailState.editTaskStateById[taskId])) {
|
|
14
|
+
// Only the task changes on a live refresh; the list refetches would show the skeleton.
|
|
15
|
+
taskDetailActions.push(fetchTaskDetail(taskId, true));
|
|
16
|
+
taskDetailActions.push(fetchTaskHistory(taskId, true));
|
|
17
|
+
}
|
|
18
|
+
else if (cacheOverride === true) {
|
|
11
19
|
taskDetailActions.push(fetchUserListByType('taskManagerCandidate', true));
|
|
12
20
|
taskDetailActions.push(fetchClassList());
|
|
13
21
|
if (taskId != null) {
|
|
@@ -1,35 +1,14 @@
|
|
|
1
1
|
import recordGet from 'lodash/get';
|
|
2
2
|
import { from } from 'rxjs';
|
|
3
3
|
import { filter, mergeMap } from 'rxjs/operators';
|
|
4
|
+
import { buildTaskDetailLocalData } from '../taskDetailLocalData';
|
|
4
5
|
import { initializeTaskToLocalStore, saveTaskUpdatesToLocalStore, updateEditTaskFetchStatus, } from '../taskDetailReducer';
|
|
5
6
|
export const initializeTaskToLocalStoreEpic = (actions$, state$) => actions$.pipe(filter(initializeTaskToLocalStore.match), mergeMap((action) => {
|
|
6
7
|
const { taskId } = action.payload;
|
|
7
8
|
const state = state$.value;
|
|
8
9
|
const task = recordGet(state.taskState.taskByID, taskId, undefined);
|
|
9
10
|
if (task != null) {
|
|
10
|
-
const taskDetailLocalData =
|
|
11
|
-
name: task.name,
|
|
12
|
-
description: task.description,
|
|
13
|
-
status: task.status.code,
|
|
14
|
-
assignee: task.assignees,
|
|
15
|
-
groupAssignees: task.groupAssignees,
|
|
16
|
-
dueDate: task.dueDate,
|
|
17
|
-
priority: task.priority.code,
|
|
18
|
-
tagIds: task.tagIds,
|
|
19
|
-
fileIds: task.fileIds,
|
|
20
|
-
type: task.type,
|
|
21
|
-
recurringFrequency: task.recurringFrequency,
|
|
22
|
-
recurringDaysOfWeek: task.recurringDaysOfWeek,
|
|
23
|
-
recurringEndDateLocalData: {
|
|
24
|
-
recurringDatePickerOptionsType: 'date', // by default, date would be selected
|
|
25
|
-
recurringEndDate: task.recurringEndDate,
|
|
26
|
-
},
|
|
27
|
-
savedRecurringEndDate: task.recurringEndDate,
|
|
28
|
-
recurringStartDate: task.recurringStartDate,
|
|
29
|
-
timeSpent: task.timeSpent,
|
|
30
|
-
isPrivate: task.isPrivate,
|
|
31
|
-
taskGroupId: task.taskGroupIds[0],
|
|
32
|
-
};
|
|
11
|
+
const taskDetailLocalData = buildTaskDetailLocalData(task);
|
|
33
12
|
const actions = [
|
|
34
13
|
saveTaskUpdatesToLocalStore({ taskDetailLocalData, taskId }),
|
|
35
14
|
updateEditTaskFetchStatus({ taskId, fetchState: 'Completed' }),
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import isEqual from 'lodash/isEqual';
|
|
2
|
+
export const buildTaskDetailLocalData = (task) => ({
|
|
3
|
+
name: task.name,
|
|
4
|
+
description: task.description,
|
|
5
|
+
status: task.status.code,
|
|
6
|
+
assignee: task.assignees,
|
|
7
|
+
groupAssignees: task.groupAssignees,
|
|
8
|
+
dueDate: task.dueDate,
|
|
9
|
+
priority: task.priority.code,
|
|
10
|
+
tagIds: task.tagIds,
|
|
11
|
+
fileIds: task.fileIds,
|
|
12
|
+
type: task.type,
|
|
13
|
+
recurringFrequency: task.recurringFrequency,
|
|
14
|
+
recurringDaysOfWeek: task.recurringDaysOfWeek,
|
|
15
|
+
recurringEndDateLocalData: {
|
|
16
|
+
recurringDatePickerOptionsType: 'date', // by default, date would be selected
|
|
17
|
+
recurringEndDate: task.recurringEndDate,
|
|
18
|
+
},
|
|
19
|
+
savedRecurringEndDate: task.recurringEndDate,
|
|
20
|
+
recurringStartDate: task.recurringStartDate,
|
|
21
|
+
timeSpent: task.timeSpent,
|
|
22
|
+
isPrivate: task.isPrivate,
|
|
23
|
+
taskGroupId: task.taskGroupIds[0],
|
|
24
|
+
});
|
|
25
|
+
// Untouched fields (still equal to `baseline`) take the server value; edited ones keep the user's.
|
|
26
|
+
export const rebaseTaskDetailLocalData = (local, baseline, fresh) => {
|
|
27
|
+
const rebased = { ...fresh };
|
|
28
|
+
const keys = Object.keys({ ...local, ...fresh });
|
|
29
|
+
keys.forEach((key) => {
|
|
30
|
+
if (!isEqual(local[key], baseline[key])) {
|
|
31
|
+
Object.assign(rebased, { [key]: local[key] });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return rebased;
|
|
35
|
+
};
|
|
36
|
+
export const isLoadedTaskDetail = (taskDetail) => taskDetail?.fetchTaskStatus.fetchState === 'Completed';
|
|
@@ -4,6 +4,7 @@ import { clearAll } from '../../../rootActions';
|
|
|
4
4
|
import { initialCommonHistory } from '../../spendManagement/commonHistoryView/commonHistory';
|
|
5
5
|
import { toHistoricEvent, } from '../../spendManagement/commonHistoryView/commonHistoryPayload';
|
|
6
6
|
import { initialTaskDetail, } from './taskDetail';
|
|
7
|
+
import { isLoadedTaskDetail } from './taskDetailLocalData';
|
|
7
8
|
export const initialState = {
|
|
8
9
|
newTaskState: initialTaskDetail,
|
|
9
10
|
editTaskStateById: {},
|
|
@@ -23,9 +24,11 @@ const taskDetailView = createSlice({
|
|
|
23
24
|
const { taskId } = action.payload;
|
|
24
25
|
if (taskId != null) {
|
|
25
26
|
const detail = recordGet(draft.editTaskStateById, taskId, undefined);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const isRefresh = action.payload.cacheOverride === true && isLoadedTaskDetail(detail);
|
|
28
|
+
if (!isRefresh &&
|
|
29
|
+
(action.payload.cacheOverride === true ||
|
|
30
|
+
detail == null ||
|
|
31
|
+
detail.fetchTaskStatus.fetchState !== 'In-Progress')) {
|
|
29
32
|
draft.editTaskStateById[taskId] = {
|
|
30
33
|
...initialTaskDetail,
|
|
31
34
|
};
|
|
@@ -350,6 +353,11 @@ const taskDetailView = createSlice({
|
|
|
350
353
|
fetchSubTasks: {
|
|
351
354
|
reducer(draft, action) {
|
|
352
355
|
const { parentTaskId } = action.payload;
|
|
356
|
+
// Only the first load reports In-Progress; it gates the detail page skeleton.
|
|
357
|
+
if (draft.subTaskListFetchStatusByParentId[parentTaskId]?.fetchState ===
|
|
358
|
+
'Completed') {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
353
361
|
draft.subTaskListFetchStatusByParentId[parentTaskId] = {
|
|
354
362
|
fetchState: 'In-Progress',
|
|
355
363
|
error: undefined,
|
package/lib/index.d.ts
CHANGED
|
@@ -1088,7 +1088,7 @@ export { getInvoiceDetail } from './view/invoicing/invoiceDetail/invoiceDetailSe
|
|
|
1088
1088
|
export type { InvoiceDetailView } from './view/invoicing/invoiceDetail/invoiceDetailSelector';
|
|
1089
1089
|
export { createInvoice, fetchCreateInvoiceFormPage, resetCreateInvoice, updateInvoicingCreateInvoiceFormDraft, } from './view/invoicing/createInvoice/createInvoiceReducer';
|
|
1090
1090
|
export type { CreateInvoiceLineItemRequest, CreateInvoiceRequest, } from './view/invoicing/createInvoice/createInvoicePayload';
|
|
1091
|
-
export type { InvoicingCreateInvoiceFormLineItem, InvoicingCreateInvoiceFormLocalData, } from './view/invoicing/createInvoice/createInvoiceState';
|
|
1091
|
+
export type { InvoicingCreateInvoiceCatalogLine, InvoicingCreateInvoiceFormLineItem, InvoicingCreateInvoiceFormLocalData, } from './view/invoicing/createInvoice/createInvoiceState';
|
|
1092
1092
|
export { blankInvoicingCreateInvoiceLineItem, createInvoiceFormLocalDataToRequest, initialInvoicingCreateInvoiceFormLocalData, } from './view/invoicing/createInvoice/createInvoiceFormConfig';
|
|
1093
1093
|
export { resetRecordPayment, initializeRecordPaymentDraft, submitRecordPayment, updateRecordPaymentFormDraft, } from './view/invoicing/recordPayment/recordPaymentReducer';
|
|
1094
1094
|
export { initializeIssueCreditNoteDraft, issueCreditNoteFailure, issueCreditNoteSuccess, resetIssueCreditNote, submitIssueCreditNote, updateIssueCreditNoteFormDraft, } from './view/invoicing/issueCreditNote/issueCreditNoteReducer';
|
|
@@ -45,6 +45,16 @@ const createInvoiceFormLocalDataToRequest = (localData) => {
|
|
|
45
45
|
quantity: toQuantity(line.quantity),
|
|
46
46
|
tax_amount: line.taxAmount.amount,
|
|
47
47
|
unit_amount: line.unitAmount.amount,
|
|
48
|
+
...(line.catalogLine != null
|
|
49
|
+
? {
|
|
50
|
+
entity_id: line.catalogLine.entityId,
|
|
51
|
+
entity_type: line.catalogLine.entityType,
|
|
52
|
+
is_taxed: line.catalogLine.isTaxed,
|
|
53
|
+
...(line.catalogLine.isTaxed
|
|
54
|
+
? {}
|
|
55
|
+
: { tax_exempt_reason: 'not_taxable' }),
|
|
56
|
+
}
|
|
57
|
+
: {}),
|
|
48
58
|
}));
|
|
49
59
|
const invoiceDate = localData.invoiceDate != null && localData.invoiceDate !== ''
|
|
50
60
|
? localData.invoiceDate
|
|
@@ -5,7 +5,11 @@ export interface CreateInvoiceLineItemRequest {
|
|
|
5
5
|
description: string;
|
|
6
6
|
quantity: number;
|
|
7
7
|
unit_amount: number;
|
|
8
|
+
entity_id?: string;
|
|
9
|
+
entity_type?: string;
|
|
10
|
+
is_taxed?: boolean;
|
|
8
11
|
tax_amount?: number;
|
|
12
|
+
tax_exempt_reason?: string;
|
|
9
13
|
}
|
|
10
14
|
/** Request body for creating an invoice; mirrors the Invoicing backend. */
|
|
11
15
|
export interface CreateInvoiceRequest {
|
|
@@ -4,7 +4,7 @@ import { Address } from '../../../entity/address/addressState';
|
|
|
4
4
|
import { InvoicingCatalogPricePoint } from '../../../entity/invoicing/invoicingCatalog/invoicingCatalogState';
|
|
5
5
|
import { InvoicingFieldHelpByCode, InvoicingSettingsPayload } from '../../../entity/invoicing/invoicingCommonPayload';
|
|
6
6
|
import { RootState } from '../../../rootStateTypes';
|
|
7
|
-
import { InvoicingCreateInvoiceFormLocalData } from './createInvoiceState';
|
|
7
|
+
import { InvoicingCreateInvoiceCatalogLine, InvoicingCreateInvoiceFormLocalData } from './createInvoiceState';
|
|
8
8
|
/**
|
|
9
9
|
* One catalog plan price, split into its two naming parts so consumers can
|
|
10
10
|
* compose the label themselves (the plan family name and the price point/price
|
|
@@ -18,6 +18,7 @@ import { InvoicingCreateInvoiceFormLocalData } from './createInvoiceState';
|
|
|
18
18
|
* produce a plausible-looking wrong total. Those lines start unpriced.
|
|
19
19
|
*/
|
|
20
20
|
export interface InvoicingPlanNameOption {
|
|
21
|
+
catalogLine: InvoicingCreateInvoiceCatalogLine;
|
|
21
22
|
planName: string;
|
|
22
23
|
subFamilyName: string;
|
|
23
24
|
price?: number;
|
|
@@ -76,6 +76,12 @@ const getCreateInvoiceFormView = (state) => {
|
|
|
76
76
|
planNames: catalogListView.plans.flatMap((plan) => (plan.subFamilies ?? [])
|
|
77
77
|
.filter((subFamily) => (subFamily.name ?? '') !== '')
|
|
78
78
|
.map((subFamily) => ({
|
|
79
|
+
// A price point with no flag is taxable, as subscription billing reads it.
|
|
80
|
+
catalogLine: {
|
|
81
|
+
entityId: subFamily.id,
|
|
82
|
+
entityType: plan.itemType ?? 'plan',
|
|
83
|
+
isTaxed: subFamily.isTaxable ?? true,
|
|
84
|
+
},
|
|
79
85
|
planName: plan.name ?? '',
|
|
80
86
|
price: planNamePrefillPrice(subFamily),
|
|
81
87
|
subFamilyName: subFamily.name ?? '',
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { Amount } from '../../../commonStateTypes/amount';
|
|
2
2
|
import { FetchedState, ID } from '../../../commonStateTypes/common';
|
|
3
3
|
import { CreateInvoiceRequest } from './createInvoicePayload';
|
|
4
|
+
/**
|
|
5
|
+
* The catalog price point a line was picked from, so the invoice line links to
|
|
6
|
+
* it and takes its taxability, the way subscription billing builds its lines.
|
|
7
|
+
*/
|
|
8
|
+
export interface InvoicingCreateInvoiceCatalogLine {
|
|
9
|
+
entityId: ID;
|
|
10
|
+
entityType: string;
|
|
11
|
+
isTaxed: boolean;
|
|
12
|
+
}
|
|
4
13
|
/**
|
|
5
14
|
* A single line item in the create-invoice form draft. Amounts are USD values
|
|
6
15
|
* (whole units, e.g. dollars) held via the `Amount` interface — never minor
|
|
@@ -11,6 +20,8 @@ export interface InvoicingCreateInvoiceFormLineItem {
|
|
|
11
20
|
quantity: string;
|
|
12
21
|
taxAmount: Amount;
|
|
13
22
|
unitAmount: Amount;
|
|
23
|
+
/** Unset for a typed description, which the backend taxes by default. */
|
|
24
|
+
catalogLine?: InvoicingCreateInvoiceCatalogLine;
|
|
14
25
|
}
|
|
15
26
|
/**
|
|
16
27
|
* Draft form state for the create-invoice form. Held in CES so the form flows
|
|
@@ -6,6 +6,6 @@ import { updateTasks } from '../../../../entity/task/taskReducer';
|
|
|
6
6
|
import { RootState } from '../../../../rootStateTypes';
|
|
7
7
|
import { ZeniAPI } from '../../../../zeniAPI';
|
|
8
8
|
import { fetchFileList } from '../../../fileView/fileViewReducer';
|
|
9
|
-
import { fetchTaskDetail, initializeTaskToLocalStore, removeTaskDetail, updateEditTaskFetchStatus } from '../taskDetailReducer';
|
|
10
|
-
export type ActionType = ReturnType<typeof fetchTaskDetail> | ReturnType<typeof updateTags> | ReturnType<typeof updateEditTaskFetchStatus> | ReturnType<typeof removeTaskDetail> | ReturnType<typeof updateTasks> | ReturnType<typeof initializeTaskToLocalStore> | ReturnType<typeof fetchFileList> | ReturnType<typeof openSnackbar>;
|
|
9
|
+
import { fetchTaskDetail, initializeTaskToLocalStore, removeTaskDetail, saveTaskUpdatesToLocalStore, updateEditTaskFetchStatus } from '../taskDetailReducer';
|
|
10
|
+
export type ActionType = ReturnType<typeof fetchTaskDetail> | ReturnType<typeof updateTags> | ReturnType<typeof updateEditTaskFetchStatus> | ReturnType<typeof removeTaskDetail> | ReturnType<typeof updateTasks> | ReturnType<typeof initializeTaskToLocalStore> | ReturnType<typeof saveTaskUpdatesToLocalStore> | ReturnType<typeof fetchFileList> | ReturnType<typeof openSnackbar>;
|
|
11
11
|
export declare const fetchTaskDetailEpic: (actions$: ActionsObservable<ActionType>, state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
|
|
@@ -5,19 +5,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.fetchTaskDetailEpic = void 0;
|
|
7
7
|
const get_1 = __importDefault(require("lodash/get"));
|
|
8
|
+
const isEqual_1 = __importDefault(require("lodash/isEqual"));
|
|
8
9
|
const rxjs_1 = require("rxjs");
|
|
9
10
|
const operators_1 = require("rxjs/operators");
|
|
10
11
|
const snackbarReducer_1 = require("../../../../entity/snackbar/snackbarReducer");
|
|
11
12
|
const tagReducer_1 = require("../../../../entity/tag/tagReducer");
|
|
13
|
+
const taskPayload_1 = require("../../../../entity/task/taskPayload");
|
|
12
14
|
const taskReducer_1 = require("../../../../entity/task/taskReducer");
|
|
15
|
+
const taskSelector_1 = require("../../../../entity/task/taskSelector");
|
|
13
16
|
const responsePayload_1 = require("../../../../responsePayload");
|
|
14
17
|
const fileViewReducer_1 = require("../../../fileView/fileViewReducer");
|
|
18
|
+
const taskDetailLocalData_1 = require("../taskDetailLocalData");
|
|
15
19
|
const taskDetailReducer_1 = require("../taskDetailReducer");
|
|
16
20
|
const fetchTaskDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(taskDetailReducer_1.fetchTaskDetail.match), (0, operators_1.mergeMap)((action) => {
|
|
17
21
|
const { taskId, cacheOverride } = action.payload;
|
|
18
22
|
const taskActions = [];
|
|
19
23
|
const state = state$.value;
|
|
20
24
|
const taskDetail = (0, get_1.default)(state.taskDetailState.editTaskStateById, taskId, undefined);
|
|
25
|
+
if (cacheOverride === true && (0, taskDetailLocalData_1.isLoadedTaskDetail)(taskDetail)) {
|
|
26
|
+
return refreshLoadedTaskDetail(taskId, state$, zeniAPI);
|
|
27
|
+
}
|
|
21
28
|
if (cacheOverride === true) {
|
|
22
29
|
taskActions.push((0, taskDetailReducer_1.removeTaskDetail)(taskId));
|
|
23
30
|
}
|
|
@@ -26,7 +33,7 @@ const fetchTaskDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, ope
|
|
|
26
33
|
taskDetail.fetchTaskStatus.fetchState !== 'In-Progress') {
|
|
27
34
|
taskActions.push((0, taskDetailReducer_1.updateEditTaskFetchStatus)({ taskId, fetchState: 'In-Progress' }));
|
|
28
35
|
const getTaskDetail$ = zeniAPI
|
|
29
|
-
.getJSON(
|
|
36
|
+
.getJSON(getTaskDetailUrl(zeniAPI, taskId))
|
|
30
37
|
.pipe((0, operators_1.mergeMap)((response) => {
|
|
31
38
|
if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
|
|
32
39
|
const fetchActions = [
|
|
@@ -44,44 +51,7 @@ const fetchTaskDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, ope
|
|
|
44
51
|
}
|
|
45
52
|
return (0, rxjs_1.from)(fetchActions);
|
|
46
53
|
}
|
|
47
|
-
|
|
48
|
-
const fetchActions = [
|
|
49
|
-
(0, taskDetailReducer_1.updateEditTaskFetchStatus)({
|
|
50
|
-
taskId,
|
|
51
|
-
fetchState: 'Error',
|
|
52
|
-
error: response.status,
|
|
53
|
-
}),
|
|
54
|
-
];
|
|
55
|
-
if (response.status.message.includes('delete')) {
|
|
56
|
-
fetchActions.push((0, snackbarReducer_1.openSnackbar)({
|
|
57
|
-
messageSection: 'fetch_task_detail',
|
|
58
|
-
messageText: 'failed',
|
|
59
|
-
type: 'error',
|
|
60
|
-
showStatusIcon: true,
|
|
61
|
-
variables: [
|
|
62
|
-
{
|
|
63
|
-
variableName: '_api-error_',
|
|
64
|
-
variableValue: 'Cannot retrieve deleted task.',
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
}));
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
fetchActions.push((0, snackbarReducer_1.openSnackbar)({
|
|
71
|
-
messageSection: 'fetch_task_detail',
|
|
72
|
-
messageText: 'failed',
|
|
73
|
-
type: 'error',
|
|
74
|
-
showStatusIcon: true,
|
|
75
|
-
variables: [
|
|
76
|
-
{
|
|
77
|
-
variableName: '_api-error_',
|
|
78
|
-
variableValue: response.status.message,
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
|
-
return (0, rxjs_1.from)(fetchActions);
|
|
84
|
-
}
|
|
54
|
+
return (0, rxjs_1.from)(getFetchFailureActions(taskId, response.status));
|
|
85
55
|
}), (0, operators_1.catchError)((error) => {
|
|
86
56
|
console.error(`REST API call errored out ${error}`);
|
|
87
57
|
return (0, rxjs_1.of)((0, taskDetailReducer_1.updateEditTaskFetchStatus)({
|
|
@@ -98,3 +68,74 @@ const fetchTaskDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, ope
|
|
|
98
68
|
}
|
|
99
69
|
}));
|
|
100
70
|
exports.fetchTaskDetailEpic = fetchTaskDetailEpic;
|
|
71
|
+
const getTaskDetailUrl = (zeniAPI, taskId) => `${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks/${taskId}`;
|
|
72
|
+
const getFetchFailureActions = (taskId, status) => [
|
|
73
|
+
(0, taskDetailReducer_1.updateEditTaskFetchStatus)({ taskId, fetchState: 'Error', error: status }),
|
|
74
|
+
(0, snackbarReducer_1.openSnackbar)({
|
|
75
|
+
messageSection: 'fetch_task_detail',
|
|
76
|
+
messageText: 'failed',
|
|
77
|
+
type: 'error',
|
|
78
|
+
showStatusIcon: true,
|
|
79
|
+
variables: [
|
|
80
|
+
{
|
|
81
|
+
variableName: '_api-error_',
|
|
82
|
+
variableValue: status.message.includes('delete')
|
|
83
|
+
? 'Cannot retrieve deleted task.'
|
|
84
|
+
: status.message,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
}),
|
|
88
|
+
];
|
|
89
|
+
// An unparseable token is never treated as stale, so the refresh still applies.
|
|
90
|
+
const isOlderSyncToken = (candidate, stored) => {
|
|
91
|
+
const candidateTime = Date.parse(candidate ?? '');
|
|
92
|
+
const storedTime = Date.parse(stored);
|
|
93
|
+
return (!Number.isNaN(candidateTime) &&
|
|
94
|
+
!Number.isNaN(storedTime) &&
|
|
95
|
+
candidateTime < storedTime);
|
|
96
|
+
};
|
|
97
|
+
const isSaveInProgress = (state, taskId) => state.taskDetailState.editTaskStateById[taskId]?.saveStatus.fetchState ===
|
|
98
|
+
'In-Progress';
|
|
99
|
+
// Live refresh of an open page: no reset or loading state, unsaved edits kept,
|
|
100
|
+
// skipped during the user's own save, stale responses dropped.
|
|
101
|
+
const refreshLoadedTaskDetail = (taskId, state$, zeniAPI) => {
|
|
102
|
+
if (isSaveInProgress(state$.value, taskId)) {
|
|
103
|
+
return rxjs_1.EMPTY;
|
|
104
|
+
}
|
|
105
|
+
return zeniAPI
|
|
106
|
+
.getJSON(getTaskDetailUrl(zeniAPI, taskId))
|
|
107
|
+
.pipe((0, operators_1.mergeMap)((response) => {
|
|
108
|
+
if (!(0, responsePayload_1.isSuccessResponse)(response) || response.data == null) {
|
|
109
|
+
return (0, rxjs_1.from)(getFetchFailureActions(taskId, response.status));
|
|
110
|
+
}
|
|
111
|
+
const state = state$.value;
|
|
112
|
+
const taskPayload = response.data.tasks.find((task) => task.task_id === taskId);
|
|
113
|
+
const storedTask = (0, taskSelector_1.getTaskById)(state.taskState, taskId);
|
|
114
|
+
const taskDetail = state.taskDetailState.editTaskStateById[taskId];
|
|
115
|
+
if (taskPayload == null ||
|
|
116
|
+
storedTask == null ||
|
|
117
|
+
taskDetail == null ||
|
|
118
|
+
isSaveInProgress(state, taskId) ||
|
|
119
|
+
isOlderSyncToken(taskPayload.sync_token, storedTask.syncToken)) {
|
|
120
|
+
return rxjs_1.EMPTY;
|
|
121
|
+
}
|
|
122
|
+
const refreshedTask = (0, taskPayload_1.mapTaskPayloadToTask)(taskPayload, storedTask);
|
|
123
|
+
const actions = [
|
|
124
|
+
(0, taskReducer_1.updateTasks)(response.data.tasks),
|
|
125
|
+
(0, tagReducer_1.updateTags)(response.data.tasks.map((task) => task.tags).flat()),
|
|
126
|
+
(0, taskDetailReducer_1.saveTaskUpdatesToLocalStore)({
|
|
127
|
+
taskId,
|
|
128
|
+
taskDetailLocalData: (0, taskDetailLocalData_1.rebaseTaskDetailLocalData)(taskDetail.taskDetailLocalData, (0, taskDetailLocalData_1.buildTaskDetailLocalData)(storedTask), (0, taskDetailLocalData_1.buildTaskDetailLocalData)(refreshedTask)),
|
|
129
|
+
}),
|
|
130
|
+
];
|
|
131
|
+
if (refreshedTask.fileIds.length > 0 &&
|
|
132
|
+
!(0, isEqual_1.default)(refreshedTask.fileIds, storedTask.fileIds)) {
|
|
133
|
+
actions.push((0, fileViewReducer_1.fetchFileList)(refreshedTask.fileIds));
|
|
134
|
+
}
|
|
135
|
+
return (0, rxjs_1.from)(actions);
|
|
136
|
+
}), (0, operators_1.catchError)((error) => {
|
|
137
|
+
// Keep the page on screen; the next update or reload recovers.
|
|
138
|
+
console.error(`Task detail refresh errored out ${error}`);
|
|
139
|
+
return rxjs_1.EMPTY;
|
|
140
|
+
}));
|
|
141
|
+
};
|
|
@@ -5,12 +5,20 @@ const rxjs_1 = require("rxjs");
|
|
|
5
5
|
const operators_1 = require("rxjs/operators");
|
|
6
6
|
const classListReducer_1 = require("../../../classList/classListReducer");
|
|
7
7
|
const userListViewReducer_1 = require("../../../userListView/userListViewReducer");
|
|
8
|
+
const taskDetailLocalData_1 = require("../taskDetailLocalData");
|
|
8
9
|
const taskDetailReducer_1 = require("../taskDetailReducer");
|
|
9
10
|
const fetchTaskDetailPageEpic = (actions$, state$) => actions$.pipe((0, operators_1.filter)(taskDetailReducer_1.fetchTaskDetailPage.match), (0, operators_1.mergeMap)((action) => {
|
|
10
11
|
const { cacheOverride, taskId } = action.payload;
|
|
11
12
|
const taskDetailActions = [];
|
|
12
13
|
const state = state$.value;
|
|
13
|
-
if (cacheOverride === true
|
|
14
|
+
if (cacheOverride === true &&
|
|
15
|
+
taskId != null &&
|
|
16
|
+
(0, taskDetailLocalData_1.isLoadedTaskDetail)(state.taskDetailState.editTaskStateById[taskId])) {
|
|
17
|
+
// Only the task changes on a live refresh; the list refetches would show the skeleton.
|
|
18
|
+
taskDetailActions.push((0, taskDetailReducer_1.fetchTaskDetail)(taskId, true));
|
|
19
|
+
taskDetailActions.push((0, taskDetailReducer_1.fetchTaskHistory)(taskId, true));
|
|
20
|
+
}
|
|
21
|
+
else if (cacheOverride === true) {
|
|
14
22
|
taskDetailActions.push((0, userListViewReducer_1.fetchUserListByType)('taskManagerCandidate', true));
|
|
15
23
|
taskDetailActions.push((0, classListReducer_1.fetchClassList)());
|
|
16
24
|
if (taskId != null) {
|
|
@@ -7,35 +7,14 @@ exports.initializeTaskToLocalStoreEpic = void 0;
|
|
|
7
7
|
const get_1 = __importDefault(require("lodash/get"));
|
|
8
8
|
const rxjs_1 = require("rxjs");
|
|
9
9
|
const operators_1 = require("rxjs/operators");
|
|
10
|
+
const taskDetailLocalData_1 = require("../taskDetailLocalData");
|
|
10
11
|
const taskDetailReducer_1 = require("../taskDetailReducer");
|
|
11
12
|
const initializeTaskToLocalStoreEpic = (actions$, state$) => actions$.pipe((0, operators_1.filter)(taskDetailReducer_1.initializeTaskToLocalStore.match), (0, operators_1.mergeMap)((action) => {
|
|
12
13
|
const { taskId } = action.payload;
|
|
13
14
|
const state = state$.value;
|
|
14
15
|
const task = (0, get_1.default)(state.taskState.taskByID, taskId, undefined);
|
|
15
16
|
if (task != null) {
|
|
16
|
-
const taskDetailLocalData =
|
|
17
|
-
name: task.name,
|
|
18
|
-
description: task.description,
|
|
19
|
-
status: task.status.code,
|
|
20
|
-
assignee: task.assignees,
|
|
21
|
-
groupAssignees: task.groupAssignees,
|
|
22
|
-
dueDate: task.dueDate,
|
|
23
|
-
priority: task.priority.code,
|
|
24
|
-
tagIds: task.tagIds,
|
|
25
|
-
fileIds: task.fileIds,
|
|
26
|
-
type: task.type,
|
|
27
|
-
recurringFrequency: task.recurringFrequency,
|
|
28
|
-
recurringDaysOfWeek: task.recurringDaysOfWeek,
|
|
29
|
-
recurringEndDateLocalData: {
|
|
30
|
-
recurringDatePickerOptionsType: 'date', // by default, date would be selected
|
|
31
|
-
recurringEndDate: task.recurringEndDate,
|
|
32
|
-
},
|
|
33
|
-
savedRecurringEndDate: task.recurringEndDate,
|
|
34
|
-
recurringStartDate: task.recurringStartDate,
|
|
35
|
-
timeSpent: task.timeSpent,
|
|
36
|
-
isPrivate: task.isPrivate,
|
|
37
|
-
taskGroupId: task.taskGroupIds[0],
|
|
38
|
-
};
|
|
17
|
+
const taskDetailLocalData = (0, taskDetailLocalData_1.buildTaskDetailLocalData)(task);
|
|
39
18
|
const actions = [
|
|
40
19
|
(0, taskDetailReducer_1.saveTaskUpdatesToLocalStore)({ taskDetailLocalData, taskId }),
|
|
41
20
|
(0, taskDetailReducer_1.updateEditTaskFetchStatus)({ taskId, fetchState: 'Completed' }),
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Task } from '../../../entity/task/taskState';
|
|
2
|
+
import { EditTaskDetail, EditTaskLocalData } from './taskDetail';
|
|
3
|
+
export declare const buildTaskDetailLocalData: (task: Task) => EditTaskLocalData;
|
|
4
|
+
export declare const rebaseTaskDetailLocalData: (local: EditTaskLocalData, baseline: EditTaskLocalData, fresh: EditTaskLocalData) => EditTaskLocalData;
|
|
5
|
+
export declare const isLoadedTaskDetail: (taskDetail: EditTaskDetail | undefined) => taskDetail is EditTaskDetail;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isLoadedTaskDetail = exports.rebaseTaskDetailLocalData = exports.buildTaskDetailLocalData = void 0;
|
|
7
|
+
const isEqual_1 = __importDefault(require("lodash/isEqual"));
|
|
8
|
+
const buildTaskDetailLocalData = (task) => ({
|
|
9
|
+
name: task.name,
|
|
10
|
+
description: task.description,
|
|
11
|
+
status: task.status.code,
|
|
12
|
+
assignee: task.assignees,
|
|
13
|
+
groupAssignees: task.groupAssignees,
|
|
14
|
+
dueDate: task.dueDate,
|
|
15
|
+
priority: task.priority.code,
|
|
16
|
+
tagIds: task.tagIds,
|
|
17
|
+
fileIds: task.fileIds,
|
|
18
|
+
type: task.type,
|
|
19
|
+
recurringFrequency: task.recurringFrequency,
|
|
20
|
+
recurringDaysOfWeek: task.recurringDaysOfWeek,
|
|
21
|
+
recurringEndDateLocalData: {
|
|
22
|
+
recurringDatePickerOptionsType: 'date', // by default, date would be selected
|
|
23
|
+
recurringEndDate: task.recurringEndDate,
|
|
24
|
+
},
|
|
25
|
+
savedRecurringEndDate: task.recurringEndDate,
|
|
26
|
+
recurringStartDate: task.recurringStartDate,
|
|
27
|
+
timeSpent: task.timeSpent,
|
|
28
|
+
isPrivate: task.isPrivate,
|
|
29
|
+
taskGroupId: task.taskGroupIds[0],
|
|
30
|
+
});
|
|
31
|
+
exports.buildTaskDetailLocalData = buildTaskDetailLocalData;
|
|
32
|
+
// Untouched fields (still equal to `baseline`) take the server value; edited ones keep the user's.
|
|
33
|
+
const rebaseTaskDetailLocalData = (local, baseline, fresh) => {
|
|
34
|
+
const rebased = { ...fresh };
|
|
35
|
+
const keys = Object.keys({ ...local, ...fresh });
|
|
36
|
+
keys.forEach((key) => {
|
|
37
|
+
if (!(0, isEqual_1.default)(local[key], baseline[key])) {
|
|
38
|
+
Object.assign(rebased, { [key]: local[key] });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return rebased;
|
|
42
|
+
};
|
|
43
|
+
exports.rebaseTaskDetailLocalData = rebaseTaskDetailLocalData;
|
|
44
|
+
const isLoadedTaskDetail = (taskDetail) => taskDetail?.fetchTaskStatus.fetchState === 'Completed';
|
|
45
|
+
exports.isLoadedTaskDetail = isLoadedTaskDetail;
|
|
@@ -11,6 +11,7 @@ const rootActions_1 = require("../../../rootActions");
|
|
|
11
11
|
const commonHistory_1 = require("../../spendManagement/commonHistoryView/commonHistory");
|
|
12
12
|
const commonHistoryPayload_1 = require("../../spendManagement/commonHistoryView/commonHistoryPayload");
|
|
13
13
|
const taskDetail_1 = require("./taskDetail");
|
|
14
|
+
const taskDetailLocalData_1 = require("./taskDetailLocalData");
|
|
14
15
|
exports.initialState = {
|
|
15
16
|
newTaskState: taskDetail_1.initialTaskDetail,
|
|
16
17
|
editTaskStateById: {},
|
|
@@ -30,9 +31,11 @@ const taskDetailView = (0, toolkit_1.createSlice)({
|
|
|
30
31
|
const { taskId } = action.payload;
|
|
31
32
|
if (taskId != null) {
|
|
32
33
|
const detail = (0, get_1.default)(draft.editTaskStateById, taskId, undefined);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
const isRefresh = action.payload.cacheOverride === true && (0, taskDetailLocalData_1.isLoadedTaskDetail)(detail);
|
|
35
|
+
if (!isRefresh &&
|
|
36
|
+
(action.payload.cacheOverride === true ||
|
|
37
|
+
detail == null ||
|
|
38
|
+
detail.fetchTaskStatus.fetchState !== 'In-Progress')) {
|
|
36
39
|
draft.editTaskStateById[taskId] = {
|
|
37
40
|
...taskDetail_1.initialTaskDetail,
|
|
38
41
|
};
|
|
@@ -357,6 +360,11 @@ const taskDetailView = (0, toolkit_1.createSlice)({
|
|
|
357
360
|
fetchSubTasks: {
|
|
358
361
|
reducer(draft, action) {
|
|
359
362
|
const { parentTaskId } = action.payload;
|
|
363
|
+
// Only the first load reports In-Progress; it gates the detail page skeleton.
|
|
364
|
+
if (draft.subTaskListFetchStatusByParentId[parentTaskId]?.fetchState ===
|
|
365
|
+
'Completed') {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
360
368
|
draft.subTaskListFetchStatusByParentId[parentTaskId] = {
|
|
361
369
|
fetchState: 'In-Progress',
|
|
362
370
|
error: undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeniai/client-epic-state",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.78",
|
|
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",
|