@zeniai/client-epic-state 5.3.10 → 5.3.12-beta0DI
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/entity/task/taskPayload.d.ts +1 -0
- package/lib/entity/task/taskPayload.js +1 -0
- package/lib/entity/task/taskReducer.d.ts +4 -1
- package/lib/entity/task/taskReducer.js +19 -2
- package/lib/entity/task/taskState.d.ts +3 -0
- package/lib/entity/timeEntry/timeEntryPayload.d.ts +26 -0
- package/lib/entity/timeEntry/timeEntryPayload.js +2 -0
- package/lib/entity/timeEntry/timeEntryReducer.d.ts +8 -0
- package/lib/entity/timeEntry/timeEntryReducer.js +76 -0
- package/lib/entity/timeEntry/timeEntrySelector.d.ts +10 -0
- package/lib/entity/timeEntry/timeEntrySelector.js +32 -0
- package/lib/entity/timeEntry/timeEntryState.d.ts +43 -0
- package/lib/entity/timeEntry/timeEntryState.js +13 -0
- package/lib/epic.d.ts +4 -1
- package/lib/epic.js +9 -0
- package/lib/esm/entity/task/taskPayload.js +1 -0
- package/lib/esm/entity/task/taskReducer.js +18 -1
- package/lib/esm/entity/timeEntry/timeEntryPayload.js +1 -0
- package/lib/esm/entity/timeEntry/timeEntryReducer.js +70 -0
- package/lib/esm/entity/timeEntry/timeEntrySelector.js +23 -0
- package/lib/esm/entity/timeEntry/timeEntryState.js +8 -0
- package/lib/esm/epic.js +9 -0
- package/lib/esm/index.js +4 -1
- package/lib/esm/reducer.js +6 -0
- package/lib/esm/view/taskManager/taskDetailView/taskDetailSelector.js +3 -0
- package/lib/esm/view/taskManager/taskListView/taskList.js +6 -0
- package/lib/esm/view/taskManager/taskListView/taskListFilterHelpers.js +7 -0
- package/lib/esm/view/taskManager/taskListView/taskListSelector.js +27 -11
- package/lib/esm/view/taskManager/taskListView/taskListViewHelpers.js +37 -0
- package/lib/esm/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.js +24 -0
- package/lib/esm/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.js +73 -0
- package/lib/esm/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.js +77 -0
- package/lib/esm/view/taskManager/timeEntryView/timeEntryViewPayload.js +1 -0
- package/lib/esm/view/taskManager/timeEntryView/timeEntryViewReducer.js +160 -0
- package/lib/esm/view/taskManager/timeEntryView/timeEntryViewSelector.js +41 -0
- package/lib/esm/view/taskManager/timeEntryView/timeEntryViewState.js +1 -0
- package/lib/index.d.ts +5 -1
- package/lib/index.js +31 -16
- package/lib/reducer.d.ts +2 -0
- package/lib/reducer.js +6 -0
- package/lib/rootStateTypes.d.ts +4 -0
- package/lib/view/taskManager/taskDetailView/taskDetailSelector.d.ts +4 -0
- package/lib/view/taskManager/taskDetailView/taskDetailSelector.js +3 -0
- package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
- package/lib/view/taskManager/taskListView/taskList.js +6 -0
- package/lib/view/taskManager/taskListView/taskListFilterHelpers.d.ts +4 -0
- package/lib/view/taskManager/taskListView/taskListFilterHelpers.js +9 -1
- package/lib/view/taskManager/taskListView/taskListSelector.d.ts +6 -3
- package/lib/view/taskManager/taskListView/taskListSelector.js +26 -10
- package/lib/view/taskManager/taskListView/taskListViewHelpers.d.ts +1 -1
- package/lib/view/taskManager/taskListView/taskListViewHelpers.js +37 -0
- package/lib/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.d.ts +18 -0
- package/lib/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.js +28 -0
- package/lib/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.d.ts +23 -0
- package/lib/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.js +78 -0
- package/lib/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.d.ts +23 -0
- package/lib/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.js +83 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewPayload.d.ts +27 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewPayload.js +2 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewReducer.d.ts +48 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewReducer.js +164 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewSelector.d.ts +24 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewSelector.js +46 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewState.d.ts +21 -0
- package/lib/view/taskManager/timeEntryView/timeEntryViewState.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import recordGet from 'lodash/get';
|
|
2
|
+
export function getTimeEntryById(timeEntryState, timeEntryId) {
|
|
3
|
+
return recordGet(timeEntryState.timeEntriesById, timeEntryId, undefined);
|
|
4
|
+
}
|
|
5
|
+
export function getTimeEntriesByIds(timeEntryState, timeEntryIds) {
|
|
6
|
+
return timeEntryIds
|
|
7
|
+
.map((timeEntryId) => getTimeEntryById(timeEntryState, timeEntryId))
|
|
8
|
+
.filter((entry) => entry != null);
|
|
9
|
+
}
|
|
10
|
+
export function getTaskCategoryById(timeEntryState, taskCategoryId) {
|
|
11
|
+
return recordGet(timeEntryState.taskCategoriesById, taskCategoryId, undefined);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The catalog, ordered service → vertical → name, matching the order the
|
|
15
|
+
* server sends so the dropdown reads the same on both sides.
|
|
16
|
+
*/
|
|
17
|
+
export function getTaskCategories(timeEntryState, includeInactive = false) {
|
|
18
|
+
return Object.values(timeEntryState.taskCategoriesById)
|
|
19
|
+
.filter((category) => includeInactive || category.isActive)
|
|
20
|
+
.sort((left, right) => (left.service ?? '').localeCompare(right.service ?? '') ||
|
|
21
|
+
(left.vertical ?? '').localeCompare(right.vertical ?? '') ||
|
|
22
|
+
left.name.localeCompare(right.name));
|
|
23
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { stringToUnionStrict } from '../../commonStateTypes/stringToUnion';
|
|
2
|
+
export const ALL_TIME_ENTRY_SOURCES = ['timer', 'manual', 'migration'];
|
|
3
|
+
export const ALL_BILLING_TYPES = ['billable', 'non_billable'];
|
|
4
|
+
/** Unknown values become `undefined` rather than being cast through.
|
|
5
|
+
* A server that gains a source we do not know about must not land an
|
|
6
|
+
* unmatched string in a union the UI switches on. */
|
|
7
|
+
export const toTimeEntrySource = (raw) => raw == null ? undefined : stringToUnionStrict(raw, ALL_TIME_ENTRY_SOURCES);
|
|
8
|
+
export const toBillingType = (raw) => raw == null ? undefined : stringToUnionStrict(raw, ALL_BILLING_TYPES);
|
package/lib/esm/epic.js
CHANGED
|
@@ -731,6 +731,9 @@ import { dragNDropTasksEpic, } from './view/taskManager/taskListView/epics/dragN
|
|
|
731
731
|
import { fetchTaskListEpic, } from './view/taskManager/taskListView/epics/fetchTaskListEpic';
|
|
732
732
|
import { fetchTaskListPageEpic, } from './view/taskManager/taskListView/epics/fetchTaskListPageEpic';
|
|
733
733
|
import { updateTaskFromListViewEpic, } from './view/taskManager/taskListView/epics/updateTaskFromListViewEpic';
|
|
734
|
+
import { fetchTaskCategoriesEpic, } from './view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic';
|
|
735
|
+
import { fetchTimeEntriesForRangeEpic, fetchTimeEntriesForTaskEpic, } from './view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic';
|
|
736
|
+
import { deleteTimeEntryEpic, logTimeEpic, updateTimeEntryEpic, } from './view/taskManager/timeEntryView/epic/writeTimeEntryEpic';
|
|
734
737
|
import { fetchTasksCardEpic, } from './view/tasksCard/fetchTasksCardEpic';
|
|
735
738
|
import { fetchTopExEpic, } from './view/topEx/topExEpic';
|
|
736
739
|
import { fetchTransactionActivityLogEpic, } from './view/transactionActivityLogView/fetchTransactionActivityLogEpic';
|
|
@@ -882,6 +885,7 @@ const ALL_EPICS = [
|
|
|
882
885
|
deleteTagEpic,
|
|
883
886
|
deleteTaskEpic,
|
|
884
887
|
deleteTaskGroupEpic,
|
|
888
|
+
deleteTimeEntryEpic,
|
|
885
889
|
deleteTransactionAttachmentEpic,
|
|
886
890
|
deleteUserBankAccountEpic,
|
|
887
891
|
disableAccountingProjectsEpic,
|
|
@@ -1233,6 +1237,7 @@ const ALL_EPICS = [
|
|
|
1233
1237
|
fetchSubscriptionViewEpic,
|
|
1234
1238
|
fetchSubTasksEpic,
|
|
1235
1239
|
fetchSuggestedQuestionsEpic,
|
|
1240
|
+
fetchTaskCategoriesEpic,
|
|
1236
1241
|
fetchTaskDetailEpic,
|
|
1237
1242
|
fetchTaskDetailPageEpic,
|
|
1238
1243
|
fetchTaskGroupTemplatesEpic,
|
|
@@ -1241,6 +1246,8 @@ const ALL_EPICS = [
|
|
|
1241
1246
|
fetchTaskListPageEpic,
|
|
1242
1247
|
fetchTaskManagerMetricsEpic,
|
|
1243
1248
|
fetchTasksCardEpic,
|
|
1249
|
+
fetchTimeEntriesForRangeEpic,
|
|
1250
|
+
fetchTimeEntriesForTaskEpic,
|
|
1244
1251
|
fetchTopExEpic,
|
|
1245
1252
|
fetchTransactionActivityLogEpic,
|
|
1246
1253
|
fetchTransactionDetailEpic,
|
|
@@ -1333,6 +1340,7 @@ const ALL_EPICS = [
|
|
|
1333
1340
|
issueCreditNoteEpic,
|
|
1334
1341
|
lockChargeCardEpic,
|
|
1335
1342
|
lockChargeCardsEpic,
|
|
1343
|
+
logTimeEpic,
|
|
1336
1344
|
markAsCompleteScheduleDetailEpic,
|
|
1337
1345
|
markCoachmarkDismissedEpic,
|
|
1338
1346
|
markTransactionAsNotMiscategorizedEpic,
|
|
@@ -1558,6 +1566,7 @@ const ALL_EPICS = [
|
|
|
1558
1566
|
updateStatementInfoEpic,
|
|
1559
1567
|
updateTaskFromListViewEpic,
|
|
1560
1568
|
updateTaskGroupNameEpic,
|
|
1569
|
+
updateTimeEntryEpic,
|
|
1561
1570
|
updateTransactionDetailEpic,
|
|
1562
1571
|
updateVendorContactEpic,
|
|
1563
1572
|
uploadAccountStatementEpic,
|
package/lib/esm/index.js
CHANGED
|
@@ -75,6 +75,7 @@ import { getTaskGroupById } from './entity/taskGroup/taskGroupSelector';
|
|
|
75
75
|
import { deleteConnection, doMagicLinkSignIn, doSignIn, doSignOut, fetchActiveTenant, fetchAllTenants, fetchExcludedResources as fetchExcludedResourcesForTenant, fetchExternalConnections as fetchExternalConnectionsForTenant, fetchSubscriptionSummaryForTenant, initEmailConnectOAuth, resendVerifyDeviceOTP, resetSignInState, saveAPIKeyConnection, saveConnectorCredentials, saveExternalConnection as saveExternalConnectionForTenant, saveOAuthConnection, sendEmailMagicLinkToUser, sendSessionHeartbeat, toExternalIntegrationType, updateCurrentTenant, updateSignInState, verifyDeviceWithTwoFA, } from './entity/tenant/tenantReducer';
|
|
76
76
|
import { DEFAULT_RECEIPTS_EMAIL, getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getIsAutoReviewIncomeWithoutCustomerEnabled, getReceiptsEmail, getTenantBaseById, getTenantBaseViewForTenantId, getTenantBaseViewForTenantView, getTenantsBaseByCheckInDateSelector, getTenantsByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, isOtherProductsEnabledForTenant, isTenantBankingOnly, isTenantBookkeepingEnabled, isTenantCardsOnly, isTenantUsingZeniCOA, isZeniDomainTenant, toTenantCoreDetailsView, } from './entity/tenant/tenantSelector';
|
|
77
77
|
import { toExternalSupportedTool } from './entity/tenant/tenantTypes';
|
|
78
|
+
import { getTaskCategories, getTaskCategoryById, } from './entity/timeEntry/timeEntrySelector';
|
|
78
79
|
import { pushToastNotification } from './entity/toastNotification/toastNotificationReducer';
|
|
79
80
|
import { getLastNotificationTime, getNotifications, } from './entity/toastNotification/toastNotificationSelector';
|
|
80
81
|
import { mapFileEntityToAttachment, toContentType, toContentTypeStrict, } from './entity/transaction/stateTypes/attachment';
|
|
@@ -423,6 +424,8 @@ import { ALL_TASK_LIST_TABS, TASK_LIST_FILTER_CATEGORIES, TASK_LIST_GROUP_BY_CAT
|
|
|
423
424
|
import { bulkUpdateTaskList, dragNDropTasks, fetchTaskList, fetchTaskListPage, initiateTaskListLocalData, removeTaskFromList, updateTaskFilters, updateTaskFromListView, updateTaskListLocalData, updateTaskListSearchText, updateTaskListTab, updateTaskListUIState, } from './view/taskManager/taskListView/taskListReducer';
|
|
424
425
|
import { getAllTasks, } from './view/taskManager/taskListView/taskListSelector';
|
|
425
426
|
import { getDueDateValueFromDueDateGroupId, getTaskUpdates, sortSubtasks, } from './view/taskManager/taskListView/taskListViewHelpers';
|
|
427
|
+
import { deleteTimeEntry, fetchTaskCategories, fetchTimeEntriesForRange, fetchTimeEntriesForTask, logTime, updateTimeEntryMinutes, } from './view/taskManager/timeEntryView/timeEntryViewReducer';
|
|
428
|
+
import { getTimeEntriesForRange, getTimeEntriesForTask, getTimeEntrySaveState, } from './view/taskManager/timeEntryView/timeEntryViewSelector';
|
|
426
429
|
import { fetchTasksCard } from './view/tasksCard/tasksCardReducer';
|
|
427
430
|
import { fetchTopEx, updateTopExDownloadState, updateTopExTimeFrame, } from './view/topEx/topExReducer';
|
|
428
431
|
import { getAllTopExpenses, getTop6Expenses, getTopExpenses, } from './view/topEx/topExSelector';
|
|
@@ -681,7 +684,7 @@ export { fetchGlobalMerchantRecommendation, createGlobalMerchant, updateCreateGl
|
|
|
681
684
|
export { approveVendorGlobalReview, rejectVendorGlobalReview, fetchVendorGlobalReviewView, updateSelectedGlobalMerchant, updateVendorGlobalReviewViewUIState, getVendorGlobalReviewView, getTenantMerchantByMerchantId, toVendorGlobalReviewColumnSortKeyType, updateVendorGlobalReviewViewLocalData, };
|
|
682
685
|
export { fetchArAging, updateArAgingUIState, fetchArAgingDetail, updateArAgingDetailUIState, getArAgingReport, getArAgingDetailForCustomer, updateArAgingNodeCollapseState, };
|
|
683
686
|
export { toRecurringBillFrequency, toRecurringBillFrequencyStrict, };
|
|
684
|
-
export { getTaskGroupById, getAllTasks, fetchTaskList, fetchTaskListPage, getTaskDetail, fetchTaskDetailPage, saveTaskUpdatesToLocalStore, saveTaskDetail, createSubTask, resetSubTaskCreateStatus, fetchSubTasks, archiveTask, fetchCannedResponses, saveCannedResponse, deleteCannedResponse, getCannedResponsesView, updateTaskListSearchText, updateTaskListUIState, allTaskStatus, allTaskPriority, updateTaskFilters, TASK_LIST_FILTER_CATEGORIES, TASK_LIST_GROUP_BY_CATEGORIES, deleteTask, discardTaskUpdatesInLocalStore, bulkUpdateTaskList, createNewTaskGroup, initiateTaskListLocalData, deleteTaskGroup, updateTaskListLocalData, dragNDropTasks, updateTaskGroupName, toTaskListGroupByKeyType, toTaskListGroupByKeyTypeStrict, getTaskUpdates, sortSubtasks, fetchAllTaskGroups, initialTaskDetail, getDueDateValueFromDueDateGroupId, toPriorityCodeType, toTaskStatusCodeType, toDueDateGroupKeyType, updateTaskFromListView, updateTaskListTab, removeTaskFromList, snoozeTask, unsnoozeTask, convertHHMMStrToMinutes, initialTaskDetailLocalData, ALL_TASK_LIST_TABS, };
|
|
687
|
+
export { getTaskGroupById, getAllTasks, getTaskCategories, getTaskCategoryById, getTimeEntriesForRange, getTimeEntriesForTask, getTimeEntrySaveState, fetchTimeEntriesForTask, fetchTimeEntriesForRange, fetchTaskCategories, logTime, updateTimeEntryMinutes, deleteTimeEntry, fetchTaskList, fetchTaskListPage, getTaskDetail, fetchTaskDetailPage, saveTaskUpdatesToLocalStore, saveTaskDetail, createSubTask, resetSubTaskCreateStatus, fetchSubTasks, archiveTask, fetchCannedResponses, saveCannedResponse, deleteCannedResponse, getCannedResponsesView, updateTaskListSearchText, updateTaskListUIState, allTaskStatus, allTaskPriority, updateTaskFilters, TASK_LIST_FILTER_CATEGORIES, TASK_LIST_GROUP_BY_CATEGORIES, deleteTask, discardTaskUpdatesInLocalStore, bulkUpdateTaskList, createNewTaskGroup, initiateTaskListLocalData, deleteTaskGroup, updateTaskListLocalData, dragNDropTasks, updateTaskGroupName, toTaskListGroupByKeyType, toTaskListGroupByKeyTypeStrict, getTaskUpdates, sortSubtasks, fetchAllTaskGroups, initialTaskDetail, getDueDateValueFromDueDateGroupId, toPriorityCodeType, toTaskStatusCodeType, toDueDateGroupKeyType, updateTaskFromListView, updateTaskListTab, removeTaskFromList, snoozeTask, unsnoozeTask, convertHHMMStrToMinutes, initialTaskDetailLocalData, ALL_TASK_LIST_TABS, };
|
|
685
688
|
export { getAllTags, fetchTagList, createTag, deleteTag, };
|
|
686
689
|
export { getAllCardsAndBankPaymentMethods, createCardSetupIntent, confirmCardSetupIntent, addCardPaymentSource, fetchPaymentSources, resetCardPaymentErrorStatuses, clearCardPaymentView, };
|
|
687
690
|
export { getAuditReportGroupViewSelectorView, getAuditRuleGroupViewSelectorView, getUserFromAllUsers, fetchAuditRuleGroupView, fetchAuditReportGroupView, saveReasonForAuditRule, clearAuditReportGroupViewByCompanyId, };
|
package/lib/esm/reducer.js
CHANGED
|
@@ -70,6 +70,7 @@ import taskGroup, { initialState as initialTaskGroupState, } from './entity/task
|
|
|
70
70
|
import taskGroupTemplate, { initialState as initialTaskGroupTemplateState, } from './entity/taskGroupTemplate/taskGroupTemplateReducer';
|
|
71
71
|
import taskSummary, { initialState as initailTaskSummaryState, } from './entity/taskSummary/taskSummaryReducer';
|
|
72
72
|
import tenant, { initialState as initialTenantState, } from './entity/tenant/tenantReducer';
|
|
73
|
+
import timeEntry, { initialState as initialTimeEntryState, } from './entity/timeEntry/timeEntryReducer';
|
|
73
74
|
import toastNotification, { initialState as initialToastNotificationState, } from './entity/toastNotification/toastNotificationReducer';
|
|
74
75
|
import transaction, { initialState as initialTransactionState, } from './entity/transaction/transactionReducer';
|
|
75
76
|
import transactionActivityLog, { initialState as initialTransactionActivityLogState, } from './entity/transactionActivityLog/transactionActivityLogReducer';
|
|
@@ -256,6 +257,7 @@ import taskDetailView, { initialState as initialTaskDetailState, } from './view/
|
|
|
256
257
|
import taskGroupTemplateView, { initialState as initialTaskGroupTemplateViewState, } from './view/taskManager/taskGroupTemplateView/taskGroupTemplateViewReducer';
|
|
257
258
|
import taskGroupView, { initialState as initialTaskGroupViewState, } from './view/taskManager/taskGroupView/taskGroupViewReducer';
|
|
258
259
|
import taskList, { initialState as initialTaskListState, } from './view/taskManager/taskListView/taskListReducer';
|
|
260
|
+
import timeEntryView, { initialState as initialTimeEntryViewState, } from './view/taskManager/timeEntryView/timeEntryViewReducer';
|
|
259
261
|
import tasksCard, { initialState as initialTasksCardState, } from './view/tasksCard/tasksCardReducer';
|
|
260
262
|
import topEx, { initialState as initialTopExState, } from './view/topEx/topExReducer';
|
|
261
263
|
import transactionActivityLogView, { initialState as initialTransactionActivityLogViewState, } from './view/transactionActivityLogView/transactionActivityLogViewReducer';
|
|
@@ -344,6 +346,7 @@ const initialEntitiesState = {
|
|
|
344
346
|
subscriptionState: initialSubscriptionState,
|
|
345
347
|
subscriptionSummaryState: initialSubscriptionSummaryState,
|
|
346
348
|
tagState: initialTagState,
|
|
349
|
+
timeEntryState: initialTimeEntryState,
|
|
347
350
|
taskGroupState: initialTaskGroupState,
|
|
348
351
|
taskGroupTemplateState: initialTaskGroupTemplateState,
|
|
349
352
|
taskState: initialTaskState,
|
|
@@ -526,6 +529,7 @@ const initialViewsState = {
|
|
|
526
529
|
taskGroupTemplateViewState: initialTaskGroupTemplateViewState,
|
|
527
530
|
taskGroupViewState: initialTaskGroupViewState,
|
|
528
531
|
taskListState: initialTaskListState,
|
|
532
|
+
timeEntryViewState: initialTimeEntryViewState,
|
|
529
533
|
tasksCardState: initialTasksCardState,
|
|
530
534
|
topExState: initialTopExState,
|
|
531
535
|
transactionActivityLogViewState: initialTransactionActivityLogViewState,
|
|
@@ -634,6 +638,7 @@ const entityReducers = {
|
|
|
634
638
|
subscriptionState: subscription,
|
|
635
639
|
subscriptionSummaryState: subscriptionSummary,
|
|
636
640
|
tagState: tag,
|
|
641
|
+
timeEntryState: timeEntry,
|
|
637
642
|
taskGroupState: taskGroup,
|
|
638
643
|
taskGroupTemplateState: taskGroupTemplate,
|
|
639
644
|
taskState: task,
|
|
@@ -815,6 +820,7 @@ const viewReducers = {
|
|
|
815
820
|
taskGroupTemplateViewState: taskGroupTemplateView,
|
|
816
821
|
taskGroupViewState: taskGroupView,
|
|
817
822
|
taskListState: taskList,
|
|
823
|
+
timeEntryViewState: timeEntryView,
|
|
818
824
|
tagViewState: tagView,
|
|
819
825
|
tasksCardState: tasksCard,
|
|
820
826
|
topExState: topEx,
|
|
@@ -26,6 +26,7 @@ export const getTaskDetail = (state, taskId) => {
|
|
|
26
26
|
let isDeleted = undefined;
|
|
27
27
|
let depth = undefined;
|
|
28
28
|
let recurringSourceTaskId = undefined;
|
|
29
|
+
let taskCategoryId = undefined;
|
|
29
30
|
let snoozedUntil = undefined;
|
|
30
31
|
let taskGroupId = undefined;
|
|
31
32
|
let subtasks = [];
|
|
@@ -39,6 +40,7 @@ export const getTaskDetail = (state, taskId) => {
|
|
|
39
40
|
if (taskEntity != null) {
|
|
40
41
|
createdByUser = getUserAndUserRole(userState, userRoleState, addressState, taskEntity.createdBy);
|
|
41
42
|
depth = taskEntity.depth;
|
|
43
|
+
taskCategoryId = taskEntity.taskCategoryId;
|
|
42
44
|
isArchived = taskEntity.isArchived;
|
|
43
45
|
isDeleted = taskEntity.isDeleted;
|
|
44
46
|
recurringSourceTaskId = taskEntity.recurringSourceTaskId;
|
|
@@ -103,6 +105,7 @@ export const getTaskDetail = (state, taskId) => {
|
|
|
103
105
|
version: 0,
|
|
104
106
|
createdByUser,
|
|
105
107
|
depth,
|
|
108
|
+
taskCategoryId,
|
|
106
109
|
isArchived,
|
|
107
110
|
isDeleted,
|
|
108
111
|
recurringSourceTaskId,
|
|
@@ -47,7 +47,13 @@ export const TASK_LIST_FILTER_CATEGORIES = [
|
|
|
47
47
|
type: 'user',
|
|
48
48
|
multiple: true,
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
value: 'createdBy',
|
|
52
|
+
type: 'user',
|
|
53
|
+
multiple: true,
|
|
54
|
+
},
|
|
50
55
|
{ value: 'creationDate', type: 'dateRange', multiple: true },
|
|
56
|
+
{ value: 'billing', type: 'dropdown', multiple: true },
|
|
51
57
|
];
|
|
52
58
|
export const TASK_LIST_GROUP_BY_CATEGORIES = [
|
|
53
59
|
{ value: 'status' },
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** Billing lives on the task's category, not the task, so it needs the catalog. */
|
|
2
|
+
export const buildCategoryValueGetter = (taskCategoriesById) => (key, task) => key === 'billing'
|
|
3
|
+
? (taskCategoriesById[task.taskCategoryId ?? '']?.billingType ??
|
|
4
|
+
'non_billable')
|
|
5
|
+
: getCategoryValueForTaskList(key, task);
|
|
1
6
|
export const getCategoryValueForTaskList = (key, task) => {
|
|
2
7
|
switch (key) {
|
|
3
8
|
case 'dueDate':
|
|
@@ -12,6 +17,8 @@ export const getCategoryValueForTaskList = (key, task) => {
|
|
|
12
17
|
return task.createTime;
|
|
13
18
|
case 'assignee':
|
|
14
19
|
return task.assignees;
|
|
20
|
+
case 'createdBy':
|
|
21
|
+
return task.createdBy;
|
|
15
22
|
default:
|
|
16
23
|
return undefined;
|
|
17
24
|
}
|
|
@@ -15,7 +15,7 @@ import { getUserGroupList } from '../../userGroupListView/userGroupListViewSelec
|
|
|
15
15
|
import { getUserList, } from '../../userListView/userListViewSelector';
|
|
16
16
|
import { allTaskPriority, allTaskStatus, } from '../taskDetailView/taskDetailSelector';
|
|
17
17
|
import { DUE_DATE_GROUP_KEYS, } from './taskList';
|
|
18
|
-
import {
|
|
18
|
+
import { buildCategoryValueGetter } from './taskListFilterHelpers';
|
|
19
19
|
const sortTasksList = (tagState, tasksList, sortKey, sortOrder) => {
|
|
20
20
|
const tasksInOrder = orderBy(tasksList, (task) => {
|
|
21
21
|
switch (sortKey) {
|
|
@@ -47,7 +47,7 @@ const sortTasksList = (tagState, tasksList, sortKey, sortOrder) => {
|
|
|
47
47
|
}, sortOrder);
|
|
48
48
|
return tasksInOrder;
|
|
49
49
|
};
|
|
50
|
-
const getTasksWithUserDetails = ({ taskListState, taskState, userState, userRoleState, addressState, taskIds, tagState, userGroupsState, }) => {
|
|
50
|
+
const getTasksWithUserDetails = ({ taskListState, taskState, userState, userRoleState, addressState, taskIds, tagState, userGroupsState, taskCategoriesById, }) => {
|
|
51
51
|
const { sortKey, searchText } = taskListState.uiState;
|
|
52
52
|
const sortOrder = getSortOrder(taskListState.uiState.sortOrder);
|
|
53
53
|
const tasks = taskIds
|
|
@@ -70,11 +70,11 @@ const getTasksWithUserDetails = ({ taskListState, taskState, userState, userRole
|
|
|
70
70
|
return undefined;
|
|
71
71
|
})
|
|
72
72
|
.filter((value) => value?.name.toLowerCase().includes(searchText.toLowerCase()) === true);
|
|
73
|
-
const filteredTaskList = applyAdvancedFiltersOnList(
|
|
73
|
+
const filteredTaskList = applyAdvancedFiltersOnList(buildCategoryValueGetter(taskCategoriesById), tasks, taskListState.filters);
|
|
74
74
|
const sortedTaskList = sortTasksList(tagState, filteredTaskList, sortKey, sortOrder);
|
|
75
75
|
return sortedTaskList;
|
|
76
76
|
};
|
|
77
|
-
const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupState, taskGroupsIds, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
|
|
77
|
+
const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupState, taskGroupsIds, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
|
|
78
78
|
const taskGroupsWithTasksList = taskGroupsIds
|
|
79
79
|
.map((taskGroupId) => {
|
|
80
80
|
const taskGroup = getTaskGroupById(taskGroupState, taskGroupId);
|
|
@@ -88,6 +88,7 @@ const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupStat
|
|
|
88
88
|
taskIds,
|
|
89
89
|
tagState,
|
|
90
90
|
userGroupsState,
|
|
91
|
+
taskCategoriesById,
|
|
91
92
|
});
|
|
92
93
|
if (taskGroup != null) {
|
|
93
94
|
return {
|
|
@@ -119,7 +120,7 @@ const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupStat
|
|
|
119
120
|
// Also hides deleted groups, which are empty as a side effect.
|
|
120
121
|
return sortedTaskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
|
|
121
122
|
};
|
|
122
|
-
export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
|
|
123
|
+
export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
|
|
123
124
|
const assigneeGroups = Object.keys(taskListState.taskIdsByAssignees);
|
|
124
125
|
const taskGroupsWithTasksList = assigneeGroups.map((groupKey) => {
|
|
125
126
|
const taskIds = taskListState.taskIdsByAssignees[groupKey];
|
|
@@ -132,6 +133,7 @@ export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userSt
|
|
|
132
133
|
taskIds,
|
|
133
134
|
tagState,
|
|
134
135
|
userGroupsState,
|
|
136
|
+
taskCategoriesById,
|
|
135
137
|
});
|
|
136
138
|
const assigneeIds = groupKey.split(',');
|
|
137
139
|
const groupName = getUsersByUserIds(userState, assigneeIds)
|
|
@@ -149,7 +151,7 @@ export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userSt
|
|
|
149
151
|
});
|
|
150
152
|
return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
|
|
151
153
|
};
|
|
152
|
-
export const getTaskListGroupedByPriority = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
|
|
154
|
+
export const getTaskListGroupedByPriority = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
|
|
153
155
|
const taskGroupsWithTasksList = ALL_PRIORITY_CODE.map((groupKey) => {
|
|
154
156
|
const taskIds = taskListState.taskIdsByPriority[groupKey] ?? [];
|
|
155
157
|
const tasksWithUserDetails = getTasksWithUserDetails({
|
|
@@ -161,6 +163,7 @@ export const getTaskListGroupedByPriority = ({ taskListState, taskState, userSta
|
|
|
161
163
|
taskIds,
|
|
162
164
|
tagState,
|
|
163
165
|
userGroupsState,
|
|
166
|
+
taskCategoriesById,
|
|
164
167
|
});
|
|
165
168
|
return {
|
|
166
169
|
groupName: allTaskPriority.find((priority) => priority.code === groupKey)?.name ??
|
|
@@ -172,7 +175,7 @@ export const getTaskListGroupedByPriority = ({ taskListState, taskState, userSta
|
|
|
172
175
|
});
|
|
173
176
|
return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
|
|
174
177
|
};
|
|
175
|
-
const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
|
|
178
|
+
const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
|
|
176
179
|
const taskGroupsWithTasksList = ALL_TASK_STATUS_CODE.map((groupKey) => {
|
|
177
180
|
const taskIds = taskListState.taskIdsByStatus[groupKey] ?? [];
|
|
178
181
|
const tasksWithUserDetails = getTasksWithUserDetails({
|
|
@@ -184,6 +187,7 @@ const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userR
|
|
|
184
187
|
taskIds,
|
|
185
188
|
tagState,
|
|
186
189
|
userGroupsState,
|
|
190
|
+
taskCategoriesById,
|
|
187
191
|
});
|
|
188
192
|
return {
|
|
189
193
|
groupName: allTaskStatus.find((taskStatus) => taskStatus.code === groupKey)
|
|
@@ -195,7 +199,7 @@ const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userR
|
|
|
195
199
|
});
|
|
196
200
|
return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
|
|
197
201
|
};
|
|
198
|
-
const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
|
|
202
|
+
const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
|
|
199
203
|
const tagGroups = Object.keys(taskListState.taskIdsByTags);
|
|
200
204
|
const taskGroupsWithTasksList = tagGroups.map((groupKey) => {
|
|
201
205
|
const taskIds = taskListState.taskIdsByTags[groupKey];
|
|
@@ -208,6 +212,7 @@ const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRol
|
|
|
208
212
|
taskIds,
|
|
209
213
|
tagState,
|
|
210
214
|
userGroupsState,
|
|
215
|
+
taskCategoriesById,
|
|
211
216
|
});
|
|
212
217
|
const tagIds = groupKey.split(',');
|
|
213
218
|
const groupName = getTagsByIds(tagState, tagIds)
|
|
@@ -224,7 +229,7 @@ const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRol
|
|
|
224
229
|
});
|
|
225
230
|
return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
|
|
226
231
|
};
|
|
227
|
-
const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
|
|
232
|
+
const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, taskCategoriesById, }) => {
|
|
228
233
|
const taskGroupsWithTasksList = DUE_DATE_GROUP_KEYS.map((groupKey) => {
|
|
229
234
|
const taskIds = taskListState.taskIdsByDueDate[groupKey];
|
|
230
235
|
const tasksWithUserDetails = getTasksWithUserDetails({
|
|
@@ -236,6 +241,7 @@ const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, user
|
|
|
236
241
|
taskIds,
|
|
237
242
|
tagState,
|
|
238
243
|
userGroupsState,
|
|
244
|
+
taskCategoriesById,
|
|
239
245
|
});
|
|
240
246
|
return {
|
|
241
247
|
groupName: groupKey,
|
|
@@ -246,7 +252,7 @@ const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, user
|
|
|
246
252
|
});
|
|
247
253
|
return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
|
|
248
254
|
};
|
|
249
|
-
const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState, }) => {
|
|
255
|
+
const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState, taskCategoriesById, }) => {
|
|
250
256
|
const { groupByKey } = taskListState.uiState;
|
|
251
257
|
if (groupByKey === 'groupName') {
|
|
252
258
|
return getTaskListGroupedByGroupName({
|
|
@@ -259,6 +265,7 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
|
|
|
259
265
|
taskGroupsIds,
|
|
260
266
|
tagState,
|
|
261
267
|
userGroupsState,
|
|
268
|
+
taskCategoriesById,
|
|
262
269
|
});
|
|
263
270
|
}
|
|
264
271
|
if (groupByKey === 'assignee') {
|
|
@@ -270,6 +277,7 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
|
|
|
270
277
|
addressState,
|
|
271
278
|
tagState,
|
|
272
279
|
userGroupsState,
|
|
280
|
+
taskCategoriesById,
|
|
273
281
|
});
|
|
274
282
|
}
|
|
275
283
|
if (groupByKey === 'priority') {
|
|
@@ -281,6 +289,7 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
|
|
|
281
289
|
addressState,
|
|
282
290
|
tagState,
|
|
283
291
|
userGroupsState,
|
|
292
|
+
taskCategoriesById,
|
|
284
293
|
});
|
|
285
294
|
}
|
|
286
295
|
if (groupByKey === 'status') {
|
|
@@ -292,6 +301,7 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
|
|
|
292
301
|
addressState,
|
|
293
302
|
tagState,
|
|
294
303
|
userGroupsState,
|
|
304
|
+
taskCategoriesById,
|
|
295
305
|
});
|
|
296
306
|
}
|
|
297
307
|
if (groupByKey === 'tag') {
|
|
@@ -303,6 +313,7 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
|
|
|
303
313
|
addressState,
|
|
304
314
|
tagState,
|
|
305
315
|
userGroupsState,
|
|
316
|
+
taskCategoriesById,
|
|
306
317
|
});
|
|
307
318
|
}
|
|
308
319
|
return getTaskListGroupedByDueDate({
|
|
@@ -313,6 +324,7 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
|
|
|
313
324
|
addressState,
|
|
314
325
|
tagState,
|
|
315
326
|
userGroupsState,
|
|
327
|
+
taskCategoriesById,
|
|
316
328
|
});
|
|
317
329
|
};
|
|
318
330
|
/**
|
|
@@ -337,7 +349,10 @@ const getTaskGroupsWithTaskListMemoized = createSelector((state) => state.taskLi
|
|
|
337
349
|
// invalidate this memo and rebuild a board the user is already using.
|
|
338
350
|
(state) => tagsAreStructural(state.taskListState.uiState)
|
|
339
351
|
? state.tagState
|
|
340
|
-
: EMPTY_TAG_STATE, (state) => state.userGroupsState,
|
|
352
|
+
: EMPTY_TAG_STATE, (state) => state.userGroupsState,
|
|
353
|
+
// Only the catalog, not the whole timeEntry slice: billing is read off
|
|
354
|
+
// the task's category, and a timer tick must not rebuild the board.
|
|
355
|
+
(state) => state.timeEntryState.taskCategoriesById, (uiState, filters, taskIdsByGroupsIds, taskIdsByAssignees, taskIdsByPriority, taskIdsByStatus, taskIdsByTags, taskIdsByDueDate, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState, taskCategoriesById) => getTaskGroupsWithTaskList({
|
|
341
356
|
taskListState: {
|
|
342
357
|
uiState,
|
|
343
358
|
filters,
|
|
@@ -356,6 +371,7 @@ const getTaskGroupsWithTaskListMemoized = createSelector((state) => state.taskLi
|
|
|
356
371
|
taskGroupsIds,
|
|
357
372
|
tagState,
|
|
358
373
|
userGroupsState,
|
|
374
|
+
taskCategoriesById,
|
|
359
375
|
}));
|
|
360
376
|
export const getAllTasks = createSelector(
|
|
361
377
|
// `taskState`, `taskGroupState`, `addressState` and `tagState` are gone from
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { convertHHMMStrToMinutes } from '../../../commonStateTypes/fiscalYearHelpers/formatMinutesToFromHHMM';
|
|
1
2
|
import { dateNow } from '../../../zeniDayJS';
|
|
2
3
|
import { allTaskPriority, allTaskStatus, } from '../taskDetailView/taskDetailSelector';
|
|
3
4
|
import { toDueDateGroupKeyType } from './taskList';
|
|
@@ -63,6 +64,11 @@ export const sortSubtasks = (subtasks, sortKey, sortOrder, userList, tagList) =>
|
|
|
63
64
|
return acc;
|
|
64
65
|
}, {});
|
|
65
66
|
const unknownStatusRank = allTaskStatus.length;
|
|
67
|
+
const priorityOrder = allTaskPriority.reduce((acc, priority, index) => {
|
|
68
|
+
acc[priority.code] = index;
|
|
69
|
+
return acc;
|
|
70
|
+
}, {});
|
|
71
|
+
const unknownPriorityRank = allTaskPriority.length;
|
|
66
72
|
const cmpStr = (a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' });
|
|
67
73
|
const keyFn = (task) => {
|
|
68
74
|
if (sortKey === 'name') {
|
|
@@ -89,6 +95,37 @@ export const sortSubtasks = (subtasks, sortKey, sortOrder, userList, tagList) =>
|
|
|
89
95
|
const tag = tagList.find((candidate) => candidate.tagId === firstTagId);
|
|
90
96
|
return tag?.name ?? '';
|
|
91
97
|
}
|
|
98
|
+
if (sortKey === 'priority') {
|
|
99
|
+
// Rank by the canonical order, not the code string: alphabetical would
|
|
100
|
+
// sort "high" before "urgent" and read as wrong to anyone scanning it.
|
|
101
|
+
return priorityOrder[task.priority?.code] ?? unknownPriorityRank;
|
|
102
|
+
}
|
|
103
|
+
if (sortKey === 'dueDate') {
|
|
104
|
+
// A task with no date is not "earliest", so undated rows belong at the
|
|
105
|
+
// bottom in BOTH directions. The comparator negates for 'desc', so one
|
|
106
|
+
// fixed sentinel would flip them to the top — the sentinel has to flip
|
|
107
|
+
// with the order to survive that negation.
|
|
108
|
+
// `dueDate` is already a ZeniDate (dayjs), so valueOf() is the epoch.
|
|
109
|
+
// An unparseable date yields NaN, and a NaN comparator leaves the array
|
|
110
|
+
// untouched -- the same silent failure the timeSpent branch guards.
|
|
111
|
+
// Treat it as undated rather than as a date.
|
|
112
|
+
const due = task.dueDate;
|
|
113
|
+
const epoch = due?.valueOf();
|
|
114
|
+
if (epoch == null || Number.isNaN(epoch)) {
|
|
115
|
+
return sortOrder === 'asc'
|
|
116
|
+
? Number.MAX_SAFE_INTEGER
|
|
117
|
+
: Number.MIN_SAFE_INTEGER;
|
|
118
|
+
}
|
|
119
|
+
return epoch;
|
|
120
|
+
}
|
|
121
|
+
if (sortKey === 'timeSpent') {
|
|
122
|
+
// `timeSpent` is an "HHMM" STRING (no colon). Comparing it as text puts
|
|
123
|
+
// "1000" before "0900" only by luck of padding, and any malformed value
|
|
124
|
+
// makes the converter return NaN -- which a comparator turns into "no
|
|
125
|
+
// sort at all", silently. Coerce that to 0.
|
|
126
|
+
const minutes = convertHHMMStrToMinutes(task.timeSpent ?? '');
|
|
127
|
+
return Number.isFinite(minutes) ? minutes : 0;
|
|
128
|
+
}
|
|
92
129
|
return '';
|
|
93
130
|
};
|
|
94
131
|
const copy = [...subtasks];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { of } from 'rxjs';
|
|
2
|
+
import { catchError, filter, switchMap } from 'rxjs/operators';
|
|
3
|
+
import { mergeMap } from 'rxjs/operators';
|
|
4
|
+
import { updateTaskCategories } from '../../../../entity/timeEntry/timeEntryReducer';
|
|
5
|
+
import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
|
|
6
|
+
import { fetchTaskCategories, fetchTaskCategoriesFailure, fetchTaskCategoriesSuccess, } from '../timeEntryViewReducer';
|
|
7
|
+
/**
|
|
8
|
+
* The 156-category catalog that fills the Category dropdown.
|
|
9
|
+
*
|
|
10
|
+
* `switchMap` here, unlike the entry epics: there is one catalog, so a second
|
|
11
|
+
* request supersedes the first rather than racing it.
|
|
12
|
+
*
|
|
13
|
+
* Zeni-only on the server (the billing marker is commercially sensitive), so
|
|
14
|
+
* a customer user gets a 401 — surfaced as Error rather than an empty list,
|
|
15
|
+
* because an empty catalog looks like "no categories exist".
|
|
16
|
+
*/
|
|
17
|
+
export const fetchTaskCategoriesEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchTaskCategories.match), switchMap(() => zeniAPI
|
|
18
|
+
.getJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/task-categories`)
|
|
19
|
+
.pipe(mergeMap((response) => {
|
|
20
|
+
if (isSuccessResponse(response) && response.data != null) {
|
|
21
|
+
return of(updateTaskCategories(response.data.task_categories ?? []), fetchTaskCategoriesSuccess());
|
|
22
|
+
}
|
|
23
|
+
return of(fetchTaskCategoriesFailure(response.status));
|
|
24
|
+
}), catchError((error) => of(fetchTaskCategoriesFailure(createZeniAPIStatus('Unexpected Error', 'Fetch task categories errored out' + JSON.stringify(error))))))));
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { of } from 'rxjs';
|
|
2
|
+
import { catchError, filter, mergeMap } from 'rxjs/operators';
|
|
3
|
+
import { updateTimeEntries } from '../../../../entity/timeEntry/timeEntryReducer';
|
|
4
|
+
import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
|
|
5
|
+
import { fetchTimeEntriesForRange, fetchTimeEntriesForRangeFailure, fetchTimeEntriesForRangeSuccess, fetchTimeEntriesForTask, fetchTimeEntriesForTaskFailure, fetchTimeEntriesForTaskSuccess, } from '../timeEntryViewReducer';
|
|
6
|
+
import { toTimeEntryRangeKey } from '../timeEntryViewState';
|
|
7
|
+
/**
|
|
8
|
+
* Every entry on one task, all users — the task detail breakdown.
|
|
9
|
+
*
|
|
10
|
+
* `mergeMap`, not `switchMap`: a detail page and an expanded board row can
|
|
11
|
+
* ask for two different tasks at once, and cancelling the first would leave
|
|
12
|
+
* its breakdown stuck In-Progress forever.
|
|
13
|
+
*/
|
|
14
|
+
export const fetchTimeEntriesForTaskEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchTimeEntriesForTask.match), mergeMap((action) => {
|
|
15
|
+
const { taskId, cacheOverride } = action.payload;
|
|
16
|
+
const cached = state$.value.timeEntryViewState.timeEntryIdsByTaskId[taskId];
|
|
17
|
+
const status = state$.value.timeEntryViewState.fetchStateByTaskId[taskId];
|
|
18
|
+
// The reducer has already flipped this request to In-Progress, so the
|
|
19
|
+
// guard reads the COMPLETED cache, never the fetch state -- a fetch
|
|
20
|
+
// state is not an in-flight signal.
|
|
21
|
+
if (cacheOverride !== true &&
|
|
22
|
+
cached != null &&
|
|
23
|
+
status?.fetchState === 'In-Progress') {
|
|
24
|
+
return of(fetchTimeEntriesForTaskSuccess({ taskId, timeEntryIds: cached }));
|
|
25
|
+
}
|
|
26
|
+
return zeniAPI
|
|
27
|
+
.getJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks/${taskId}/time-entries`)
|
|
28
|
+
.pipe(mergeMap((response) => {
|
|
29
|
+
if (isSuccessResponse(response) && response.data != null) {
|
|
30
|
+
const entries = response.data.time_entries ?? [];
|
|
31
|
+
return of(updateTimeEntries(entries), fetchTimeEntriesForTaskSuccess({
|
|
32
|
+
taskId,
|
|
33
|
+
timeEntryIds: entries.map((entry) => entry.time_entry_id),
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
return of(fetchTimeEntriesForTaskFailure({ taskId, status: response.status }));
|
|
37
|
+
}), catchError((error) => of(fetchTimeEntriesForTaskFailure({
|
|
38
|
+
taskId,
|
|
39
|
+
status: createZeniAPIStatus('Unexpected Error', 'Fetch time entries for task errored out' +
|
|
40
|
+
JSON.stringify(error)),
|
|
41
|
+
}))));
|
|
42
|
+
}));
|
|
43
|
+
/**
|
|
44
|
+
* One user's entries in an inclusive date range — the weekly timesheet.
|
|
45
|
+
*
|
|
46
|
+
* Needs the `is_deleted + user_id + entry_date` composite index (api #593).
|
|
47
|
+
* Without it the server returns 500, not an empty list, so the failure is
|
|
48
|
+
* surfaced rather than rendered as a blank week.
|
|
49
|
+
*/
|
|
50
|
+
export const fetchTimeEntriesForRangeEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchTimeEntriesForRange.match), mergeMap((action) => {
|
|
51
|
+
const { userId, startDate, endDate } = action.payload;
|
|
52
|
+
const rangeKey = toTimeEntryRangeKey(userId, startDate, endDate);
|
|
53
|
+
const query = `user_id=${encodeURIComponent(userId)}&start_date=${encodeURIComponent(startDate)}&end_date=${encodeURIComponent(endDate)}`;
|
|
54
|
+
return zeniAPI
|
|
55
|
+
.getJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/time-entries?${query}`)
|
|
56
|
+
.pipe(mergeMap((response) => {
|
|
57
|
+
if (isSuccessResponse(response) && response.data != null) {
|
|
58
|
+
const entries = response.data.time_entries ?? [];
|
|
59
|
+
return of(updateTimeEntries(entries), fetchTimeEntriesForRangeSuccess({
|
|
60
|
+
rangeKey,
|
|
61
|
+
timeEntryIds: entries.map((entry) => entry.time_entry_id),
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
return of(fetchTimeEntriesForRangeFailure({
|
|
65
|
+
rangeKey,
|
|
66
|
+
status: response.status,
|
|
67
|
+
}));
|
|
68
|
+
}), catchError((error) => of(fetchTimeEntriesForRangeFailure({
|
|
69
|
+
rangeKey,
|
|
70
|
+
status: createZeniAPIStatus('Unexpected Error', 'Fetch time entries for range errored out' +
|
|
71
|
+
JSON.stringify(error)),
|
|
72
|
+
}))));
|
|
73
|
+
}));
|