@zeniai/client-epic-state 5.3.8-beta0DI → 5.3.9

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 (78) hide show
  1. package/lib/entity/task/taskPayload.d.ts +0 -1
  2. package/lib/entity/task/taskPayload.js +0 -1
  3. package/lib/entity/task/taskReducer.d.ts +1 -4
  4. package/lib/entity/task/taskReducer.js +2 -19
  5. package/lib/entity/task/taskState.d.ts +0 -3
  6. package/lib/entity/tenant/epic/fetchAllTenantsEpic.js +45 -38
  7. package/lib/epic.d.ts +1 -4
  8. package/lib/epic.js +0 -9
  9. package/lib/esm/entity/task/taskPayload.js +0 -1
  10. package/lib/esm/entity/task/taskReducer.js +1 -18
  11. package/lib/esm/entity/tenant/epic/fetchAllTenantsEpic.js +46 -39
  12. package/lib/esm/epic.js +0 -9
  13. package/lib/esm/index.js +3 -6
  14. package/lib/esm/init.js +14 -3
  15. package/lib/esm/reducer.js +0 -6
  16. package/lib/esm/sessionEndpointsLoader.js +7 -0
  17. package/lib/esm/view/taskManager/taskDetailView/taskDetailSelector.js +0 -3
  18. package/lib/esm/view/taskManager/taskListView/taskList.js +0 -5
  19. package/lib/esm/view/taskManager/taskListView/taskListFilterHelpers.js +0 -2
  20. package/lib/esm/view/taskManager/taskListView/taskListViewHelpers.js +0 -37
  21. package/lib/esm/view/transactionDetail/createTransaction/createTransactionPayload.js +14 -2
  22. package/lib/esm/view/transactionDetail/createTransaction/createTransactionSelector.js +21 -2
  23. package/lib/index.d.ts +4 -7
  24. package/lib/index.js +53 -66
  25. package/lib/init.d.ts +9 -0
  26. package/lib/init.js +17 -4
  27. package/lib/reducer.d.ts +0 -2
  28. package/lib/reducer.js +0 -6
  29. package/lib/rootStateTypes.d.ts +0 -4
  30. package/lib/sessionEndpointsLoader.d.ts +9 -0
  31. package/lib/sessionEndpointsLoader.js +11 -0
  32. package/lib/view/taskManager/taskDetailView/taskDetailSelector.d.ts +0 -4
  33. package/lib/view/taskManager/taskDetailView/taskDetailSelector.js +0 -3
  34. package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
  35. package/lib/view/taskManager/taskListView/taskList.js +0 -5
  36. package/lib/view/taskManager/taskListView/taskListFilterHelpers.js +0 -2
  37. package/lib/view/taskManager/taskListView/taskListViewHelpers.d.ts +1 -1
  38. package/lib/view/taskManager/taskListView/taskListViewHelpers.js +0 -37
  39. package/lib/view/transactionDetail/createTransaction/createTransactionPayload.d.ts +10 -6
  40. package/lib/view/transactionDetail/createTransaction/createTransactionPayload.js +14 -2
  41. package/lib/view/transactionDetail/createTransaction/createTransactionSelector.d.ts +4 -1
  42. package/lib/view/transactionDetail/createTransaction/createTransactionSelector.js +21 -2
  43. package/lib/view/transactionDetail/createTransaction/createTransactionState.d.ts +1 -1
  44. package/lib/zeniAPI.d.ts +2 -2
  45. package/package.json +1 -1
  46. package/lib/entity/timeEntry/timeEntryPayload.d.ts +0 -26
  47. package/lib/entity/timeEntry/timeEntryPayload.js +0 -2
  48. package/lib/entity/timeEntry/timeEntryReducer.d.ts +0 -8
  49. package/lib/entity/timeEntry/timeEntryReducer.js +0 -76
  50. package/lib/entity/timeEntry/timeEntrySelector.d.ts +0 -10
  51. package/lib/entity/timeEntry/timeEntrySelector.js +0 -32
  52. package/lib/entity/timeEntry/timeEntryState.d.ts +0 -43
  53. package/lib/entity/timeEntry/timeEntryState.js +0 -13
  54. package/lib/esm/entity/timeEntry/timeEntryPayload.js +0 -1
  55. package/lib/esm/entity/timeEntry/timeEntryReducer.js +0 -70
  56. package/lib/esm/entity/timeEntry/timeEntrySelector.js +0 -23
  57. package/lib/esm/entity/timeEntry/timeEntryState.js +0 -8
  58. package/lib/esm/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.js +0 -24
  59. package/lib/esm/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.js +0 -73
  60. package/lib/esm/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.js +0 -77
  61. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewPayload.js +0 -1
  62. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewReducer.js +0 -160
  63. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewSelector.js +0 -41
  64. package/lib/esm/view/taskManager/timeEntryView/timeEntryViewState.js +0 -1
  65. package/lib/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.d.ts +0 -18
  66. package/lib/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.js +0 -28
  67. package/lib/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.d.ts +0 -23
  68. package/lib/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.js +0 -78
  69. package/lib/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.d.ts +0 -23
  70. package/lib/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.js +0 -83
  71. package/lib/view/taskManager/timeEntryView/timeEntryViewPayload.d.ts +0 -27
  72. package/lib/view/taskManager/timeEntryView/timeEntryViewPayload.js +0 -2
  73. package/lib/view/taskManager/timeEntryView/timeEntryViewReducer.d.ts +0 -48
  74. package/lib/view/taskManager/timeEntryView/timeEntryViewReducer.js +0 -164
  75. package/lib/view/taskManager/timeEntryView/timeEntryViewSelector.d.ts +0 -24
  76. package/lib/view/taskManager/timeEntryView/timeEntryViewSelector.js +0 -46
  77. package/lib/view/taskManager/timeEntryView/timeEntryViewState.d.ts +0 -21
  78. package/lib/view/taskManager/timeEntryView/timeEntryViewState.js +0 -5
@@ -1,76 +0,0 @@
1
- "use strict";
2
- var _a;
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.updateTaskCategories = exports.removeTimeEntry = exports.updateTimeEntries = exports.initialState = void 0;
5
- exports.mapTimeEntryPayloadToTimeEntry = mapTimeEntryPayloadToTimeEntry;
6
- exports.mapTaskCategoryPayloadToTaskCategory = mapTaskCategoryPayloadToTaskCategory;
7
- const toolkit_1 = require("@reduxjs/toolkit");
8
- const rootActions_1 = require("../../rootActions");
9
- const zeniDayJS_1 = require("../../zeniDayJS");
10
- const timeEntryState_1 = require("./timeEntryState");
11
- exports.initialState = {
12
- timeEntriesById: {},
13
- taskCategoriesById: {},
14
- };
15
- const timeEntry = (0, toolkit_1.createSlice)({
16
- name: 'timeEntry',
17
- initialState: exports.initialState,
18
- reducers: {
19
- updateTimeEntries(draft, action) {
20
- action.payload.forEach((payload) => {
21
- const latest = mapTimeEntryPayloadToTimeEntry(payload);
22
- const inStore = draft.timeEntriesById[latest.timeEntryId];
23
- // Last-write-wins by updateTime, same as `address`. A weekly fetch and
24
- // a single-entry edit can land in either order.
25
- if (inStore == null || latest.updateTime.isAfter(inStore.updateTime)) {
26
- draft.timeEntriesById[latest.timeEntryId] = latest;
27
- }
28
- });
29
- },
30
- /** A soft delete comes back as a normal entry; the server has already
31
- * dropped it from the totals, so the client drops it from the store
32
- * rather than keeping a row it must then remember to filter. */
33
- removeTimeEntry(draft, action) {
34
- delete draft.timeEntriesById[action.payload];
35
- },
36
- updateTaskCategories(draft, action) {
37
- action.payload.forEach((payload) => {
38
- const category = mapTaskCategoryPayloadToTaskCategory(payload);
39
- draft.taskCategoriesById[category.taskCategoryId] = category;
40
- });
41
- },
42
- },
43
- extraReducers: (builder) => {
44
- builder.addCase(rootActions_1.clearAll, (draft) => {
45
- Object.assign(draft, exports.initialState);
46
- });
47
- },
48
- });
49
- _a = timeEntry.actions, exports.updateTimeEntries = _a.updateTimeEntries, exports.removeTimeEntry = _a.removeTimeEntry, exports.updateTaskCategories = _a.updateTaskCategories;
50
- exports.default = timeEntry.reducer;
51
- function mapTimeEntryPayloadToTimeEntry(payload) {
52
- return {
53
- timeEntryId: payload.time_entry_id,
54
- taskId: payload.task_id,
55
- userId: payload.user_id,
56
- minutes: payload.minutes,
57
- entryDate: (0, zeniDayJS_1.date)(payload.entry_date),
58
- source: (0, timeEntryState_1.toTimeEntrySource)(payload.source),
59
- createTime: (0, zeniDayJS_1.date)(payload.create_time),
60
- updateTime: (0, zeniDayJS_1.date)(payload.update_time),
61
- billingType: (0, timeEntryState_1.toBillingType)(payload.billing_type),
62
- createdBy: payload.created_by ?? undefined,
63
- department: payload.department ?? undefined,
64
- taskCategoryId: payload.task_category_id ?? undefined,
65
- };
66
- }
67
- function mapTaskCategoryPayloadToTaskCategory(payload) {
68
- return {
69
- taskCategoryId: payload.task_category_id,
70
- name: payload.name,
71
- isActive: payload.is_active,
72
- billingType: (0, timeEntryState_1.toBillingType)(payload.billing_type),
73
- service: payload.service ?? undefined,
74
- vertical: payload.vertical ?? undefined,
75
- };
76
- }
@@ -1,10 +0,0 @@
1
- import { ID } from '../../commonStateTypes/common';
2
- import { TaskCategory, TimeEntry, TimeEntryState } from './timeEntryState';
3
- export declare function getTimeEntryById(timeEntryState: TimeEntryState, timeEntryId: ID): TimeEntry | undefined;
4
- export declare function getTimeEntriesByIds(timeEntryState: TimeEntryState, timeEntryIds: ID[]): TimeEntry[];
5
- export declare function getTaskCategoryById(timeEntryState: TimeEntryState, taskCategoryId: ID): TaskCategory | undefined;
6
- /**
7
- * The catalog, ordered service → vertical → name, matching the order the
8
- * server sends so the dropdown reads the same on both sides.
9
- */
10
- export declare function getTaskCategories(timeEntryState: TimeEntryState, includeInactive?: boolean): TaskCategory[];
@@ -1,32 +0,0 @@
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.getTimeEntryById = getTimeEntryById;
7
- exports.getTimeEntriesByIds = getTimeEntriesByIds;
8
- exports.getTaskCategoryById = getTaskCategoryById;
9
- exports.getTaskCategories = getTaskCategories;
10
- const get_1 = __importDefault(require("lodash/get"));
11
- function getTimeEntryById(timeEntryState, timeEntryId) {
12
- return (0, get_1.default)(timeEntryState.timeEntriesById, timeEntryId, undefined);
13
- }
14
- function getTimeEntriesByIds(timeEntryState, timeEntryIds) {
15
- return timeEntryIds
16
- .map((timeEntryId) => getTimeEntryById(timeEntryState, timeEntryId))
17
- .filter((entry) => entry != null);
18
- }
19
- function getTaskCategoryById(timeEntryState, taskCategoryId) {
20
- return (0, get_1.default)(timeEntryState.taskCategoriesById, taskCategoryId, undefined);
21
- }
22
- /**
23
- * The catalog, ordered service → vertical → name, matching the order the
24
- * server sends so the dropdown reads the same on both sides.
25
- */
26
- function getTaskCategories(timeEntryState, includeInactive = false) {
27
- return Object.values(timeEntryState.taskCategoriesById)
28
- .filter((category) => includeInactive || category.isActive)
29
- .sort((left, right) => (left.service ?? '').localeCompare(right.service ?? '') ||
30
- (left.vertical ?? '').localeCompare(right.vertical ?? '') ||
31
- left.name.localeCompare(right.name));
32
- }
@@ -1,43 +0,0 @@
1
- import { ID } from '../../commonStateTypes/common';
2
- import { ZeniDate } from '../../zeniDayJS';
3
- export declare const ALL_TIME_ENTRY_SOURCES: readonly ["timer", "manual", "migration"];
4
- export type TimeEntrySource = (typeof ALL_TIME_ENTRY_SOURCES)[number];
5
- export declare const ALL_BILLING_TYPES: readonly ["billable", "non_billable"];
6
- export type BillingType = (typeof ALL_BILLING_TYPES)[number];
7
- /** Unknown values become `undefined` rather than being cast through.
8
- * A server that gains a source we do not know about must not land an
9
- * unmatched string in a union the UI switches on. */
10
- export declare const toTimeEntrySource: (raw: string | null | undefined) => "manual" | "timer" | "migration" | undefined;
11
- export declare const toBillingType: (raw: string | null | undefined) => "billable" | "non_billable" | undefined;
12
- export interface TimeEntry {
13
- createTime: ZeniDate;
14
- /** The day the work happened, not the day the row was written. The grain is
15
- * a DAY: one row per user per task per day. */
16
- entryDate: ZeniDate;
17
- minutes: number;
18
- taskId: ID;
19
- timeEntryId: ID;
20
- updateTime: ZeniDate;
21
- userId: ID;
22
- /** Snapshotted at write time, never refreshed — reclassifying a category
23
- * must not rewrite an approved week. Read-only to the client. */
24
- billingType?: BillingType;
25
- createdBy?: ID;
26
- department?: string;
27
- source?: TimeEntrySource;
28
- taskCategoryId?: ID;
29
- }
30
- export interface TaskCategory {
31
- isActive: boolean;
32
- name: string;
33
- taskCategoryId: ID;
34
- billingType?: BillingType;
35
- service?: string;
36
- vertical?: string;
37
- }
38
- export interface TimeEntryState {
39
- taskCategoriesById: Record<ID, TaskCategory>;
40
- /** Keyed by id, like every other entity slice. The per-task and per-user
41
- * groupings live in the VIEW slice, because they are queries over this. */
42
- timeEntriesById: Record<ID, TimeEntry>;
43
- }
@@ -1,13 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toBillingType = exports.toTimeEntrySource = exports.ALL_BILLING_TYPES = exports.ALL_TIME_ENTRY_SOURCES = void 0;
4
- const stringToUnion_1 = require("../../commonStateTypes/stringToUnion");
5
- exports.ALL_TIME_ENTRY_SOURCES = ['timer', 'manual', 'migration'];
6
- exports.ALL_BILLING_TYPES = ['billable', 'non_billable'];
7
- /** Unknown values become `undefined` rather than being cast through.
8
- * A server that gains a source we do not know about must not land an
9
- * unmatched string in a union the UI switches on. */
10
- const toTimeEntrySource = (raw) => raw == null ? undefined : (0, stringToUnion_1.stringToUnionStrict)(raw, exports.ALL_TIME_ENTRY_SOURCES);
11
- exports.toTimeEntrySource = toTimeEntrySource;
12
- const toBillingType = (raw) => raw == null ? undefined : (0, stringToUnion_1.stringToUnionStrict)(raw, exports.ALL_BILLING_TYPES);
13
- exports.toBillingType = toBillingType;
@@ -1 +0,0 @@
1
- export {};
@@ -1,70 +0,0 @@
1
- import { createSlice } from '@reduxjs/toolkit';
2
- import { clearAll } from '../../rootActions';
3
- import { date } from '../../zeniDayJS';
4
- import { toBillingType, toTimeEntrySource, } from './timeEntryState';
5
- export const initialState = {
6
- timeEntriesById: {},
7
- taskCategoriesById: {},
8
- };
9
- const timeEntry = createSlice({
10
- name: 'timeEntry',
11
- initialState,
12
- reducers: {
13
- updateTimeEntries(draft, action) {
14
- action.payload.forEach((payload) => {
15
- const latest = mapTimeEntryPayloadToTimeEntry(payload);
16
- const inStore = draft.timeEntriesById[latest.timeEntryId];
17
- // Last-write-wins by updateTime, same as `address`. A weekly fetch and
18
- // a single-entry edit can land in either order.
19
- if (inStore == null || latest.updateTime.isAfter(inStore.updateTime)) {
20
- draft.timeEntriesById[latest.timeEntryId] = latest;
21
- }
22
- });
23
- },
24
- /** A soft delete comes back as a normal entry; the server has already
25
- * dropped it from the totals, so the client drops it from the store
26
- * rather than keeping a row it must then remember to filter. */
27
- removeTimeEntry(draft, action) {
28
- delete draft.timeEntriesById[action.payload];
29
- },
30
- updateTaskCategories(draft, action) {
31
- action.payload.forEach((payload) => {
32
- const category = mapTaskCategoryPayloadToTaskCategory(payload);
33
- draft.taskCategoriesById[category.taskCategoryId] = category;
34
- });
35
- },
36
- },
37
- extraReducers: (builder) => {
38
- builder.addCase(clearAll, (draft) => {
39
- Object.assign(draft, initialState);
40
- });
41
- },
42
- });
43
- export const { updateTimeEntries, removeTimeEntry, updateTaskCategories } = timeEntry.actions;
44
- export default timeEntry.reducer;
45
- export function mapTimeEntryPayloadToTimeEntry(payload) {
46
- return {
47
- timeEntryId: payload.time_entry_id,
48
- taskId: payload.task_id,
49
- userId: payload.user_id,
50
- minutes: payload.minutes,
51
- entryDate: date(payload.entry_date),
52
- source: toTimeEntrySource(payload.source),
53
- createTime: date(payload.create_time),
54
- updateTime: date(payload.update_time),
55
- billingType: toBillingType(payload.billing_type),
56
- createdBy: payload.created_by ?? undefined,
57
- department: payload.department ?? undefined,
58
- taskCategoryId: payload.task_category_id ?? undefined,
59
- };
60
- }
61
- export function mapTaskCategoryPayloadToTaskCategory(payload) {
62
- return {
63
- taskCategoryId: payload.task_category_id,
64
- name: payload.name,
65
- isActive: payload.is_active,
66
- billingType: toBillingType(payload.billing_type),
67
- service: payload.service ?? undefined,
68
- vertical: payload.vertical ?? undefined,
69
- };
70
- }
@@ -1,23 +0,0 @@
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
- }
@@ -1,8 +0,0 @@
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);
@@ -1,24 +0,0 @@
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))))))));
@@ -1,73 +0,0 @@
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
- }));
@@ -1,77 +0,0 @@
1
- import { of } from 'rxjs';
2
- import { catchError, filter, mergeMap } from 'rxjs/operators';
3
- import { updateTaskTimeSpentMinutes } from '../../../../entity/task/taskReducer';
4
- import { removeTimeEntry, updateTimeEntries, } from '../../../../entity/timeEntry/timeEntryReducer';
5
- import { getTimeEntryById } from '../../../../entity/timeEntry/timeEntrySelector';
6
- import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
7
- import { deleteTimeEntry, logTime, logTimeFailure, logTimeSuccess, timeEntryWriteFailure, timeEntryWriteSuccess, updateTimeEntryMinutes, } from '../timeEntryViewReducer';
8
- const unexpected = (what, error) => createZeniAPIStatus('Unexpected Error', `${what} errored out${JSON.stringify(error)}`);
9
- /**
10
- * Log time against a task.
11
- *
12
- * `mergeMap`, not `switchMap`: two rows on the board can be edited in quick
13
- * succession and cancelling the first would silently drop that entry.
14
- */
15
- export const logTimeEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(logTime.match), mergeMap((action) => {
16
- const { taskId, minutes, entryDate, source, identifier } = action.payload;
17
- return zeniAPI
18
- .postAndGetJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks/${taskId}/time-entries`, {
19
- minutes,
20
- entry_date: entryDate,
21
- source,
22
- // Sent only when the caller has one. The server treats it as the
23
- // entity key, which is what makes a retried timer stop idempotent.
24
- ...(identifier != null ? { identifier } : {}),
25
- })
26
- .pipe(mergeMap((response) => {
27
- if (isSuccessResponse(response) && response.data != null) {
28
- const { time_entry, task_time_spent_minutes } = response.data;
29
- return of(updateTimeEntries([time_entry]),
30
- // The board reads `Task.time_spent` per row, so it has to be
31
- // told the new total; otherwise the row keeps the old number
32
- // until something refetches the whole list.
33
- updateTaskTimeSpentMinutes({
34
- taskId,
35
- minutes: task_time_spent_minutes ?? 0,
36
- }), logTimeSuccess({ taskId, timeEntryId: time_entry.time_entry_id }));
37
- }
38
- return of(logTimeFailure({ taskId, status: response.status }));
39
- }), catchError((error) => of(logTimeFailure({ taskId, status: unexpected('Log time', error) }))));
40
- }));
41
- /** Correct an entry's minutes. Owner, or a Zeni user as the admin path. */
42
- export const updateTimeEntryEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(updateTimeEntryMinutes.match), mergeMap((action) => {
43
- const { timeEntryId, minutes } = action.payload;
44
- return zeniAPI
45
- .putAndGetJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/time-entries/${timeEntryId}`, { minutes })
46
- .pipe(mergeMap((response) => {
47
- if (isSuccessResponse(response) && response.data != null) {
48
- return of(updateTimeEntries([response.data.time_entry]), timeEntryWriteSuccess({ timeEntryId }));
49
- }
50
- return of(timeEntryWriteFailure({ timeEntryId, status: response.status }));
51
- }), catchError((error) => of(timeEntryWriteFailure({
52
- timeEntryId,
53
- status: unexpected('Update time entry', error),
54
- }))));
55
- }));
56
- /**
57
- * Soft delete. The row stays in Datastore for the audit trail but leaves the
58
- * totals, so the client drops it from the store rather than keeping something
59
- * every reader would then have to remember to filter.
60
- */
61
- export const deleteTimeEntryEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(deleteTimeEntry.match), mergeMap((action) => {
62
- const timeEntryId = action.payload;
63
- // Read the task id BEFORE the delete: the entry is about to leave the
64
- // store, and the view slice needs to know which task's id list to prune.
65
- const taskId = getTimeEntryById(state$.value.timeEntryState, timeEntryId)?.taskId;
66
- return zeniAPI
67
- .deleteAndGetJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/time-entries/${timeEntryId}`)
68
- .pipe(mergeMap((response) => {
69
- if (isSuccessResponse(response)) {
70
- return of(removeTimeEntry(timeEntryId), timeEntryWriteSuccess({ timeEntryId, removedFromTaskId: taskId }));
71
- }
72
- return of(timeEntryWriteFailure({ timeEntryId, status: response.status }));
73
- }), catchError((error) => of(timeEntryWriteFailure({
74
- timeEntryId,
75
- status: unexpected('Delete time entry', error),
76
- }))));
77
- }));
@@ -1,160 +0,0 @@
1
- import { createSlice } from '@reduxjs/toolkit';
2
- import { clearAll } from '../../../rootActions';
3
- const NOT_STARTED = { fetchState: 'Not-Started', error: undefined };
4
- export const initialState = {
5
- timeEntryIdsByTaskId: {},
6
- fetchStateByTaskId: {},
7
- timeEntryIdsByRangeKey: {},
8
- fetchStateByRangeKey: {},
9
- saveStateByTaskId: {},
10
- saveStateByTimeEntryId: {},
11
- taskCategoryFetchState: NOT_STARTED,
12
- };
13
- const timeEntryView = createSlice({
14
- name: 'timeEntryView',
15
- initialState,
16
- reducers: {
17
- fetchTimeEntriesForTask: {
18
- prepare(taskId, cacheOverride = false) {
19
- return { payload: { taskId, cacheOverride } };
20
- },
21
- reducer(draft, action) {
22
- draft.fetchStateByTaskId[action.payload.taskId] = {
23
- fetchState: 'In-Progress',
24
- error: undefined,
25
- };
26
- },
27
- },
28
- fetchTimeEntriesForTaskSuccess(draft, action) {
29
- const { taskId, timeEntryIds } = action.payload;
30
- // Replaced, not merged: the server is authoritative about which entries
31
- // a task has, and a merge would resurrect one that was just deleted.
32
- draft.timeEntryIdsByTaskId[taskId] = timeEntryIds;
33
- draft.fetchStateByTaskId[taskId] = {
34
- fetchState: 'Completed',
35
- error: undefined,
36
- };
37
- },
38
- fetchTimeEntriesForTaskFailure(draft, action) {
39
- draft.fetchStateByTaskId[action.payload.taskId] = {
40
- fetchState: 'Error',
41
- error: action.payload.status,
42
- };
43
- },
44
- fetchTimeEntriesForRange: {
45
- prepare(userId, startDate, endDate, cacheOverride = false) {
46
- return { payload: { userId, startDate, endDate, cacheOverride } };
47
- },
48
- reducer(draft, action) {
49
- const { userId, startDate, endDate } = action.payload;
50
- draft.fetchStateByRangeKey[`${userId}_${startDate}_${endDate}`] = {
51
- fetchState: 'In-Progress',
52
- error: undefined,
53
- };
54
- },
55
- },
56
- fetchTimeEntriesForRangeSuccess(draft, action) {
57
- const { rangeKey, timeEntryIds } = action.payload;
58
- draft.timeEntryIdsByRangeKey[rangeKey] = timeEntryIds;
59
- draft.fetchStateByRangeKey[rangeKey] = {
60
- fetchState: 'Completed',
61
- error: undefined,
62
- };
63
- },
64
- fetchTimeEntriesForRangeFailure(draft, action) {
65
- draft.fetchStateByRangeKey[action.payload.rangeKey] = {
66
- fetchState: 'Error',
67
- error: action.payload.status,
68
- };
69
- },
70
- logTime: {
71
- prepare(taskId, minutes, entryDate, source = 'manual', identifier) {
72
- return { payload: { taskId, minutes, entryDate, source, identifier } };
73
- },
74
- reducer(draft, action) {
75
- draft.saveStateByTaskId[action.payload.taskId] = {
76
- fetchState: 'In-Progress',
77
- error: undefined,
78
- };
79
- },
80
- },
81
- logTimeSuccess(draft, action) {
82
- const { taskId, timeEntryId } = action.payload;
83
- draft.saveStateByTaskId[taskId] = {
84
- fetchState: 'Completed',
85
- error: undefined,
86
- };
87
- const existing = draft.timeEntryIdsByTaskId[taskId] ?? [];
88
- // Guarded against a duplicate id: a retried write with the same
89
- // deterministic identifier returns the SAME entry, and pushing it twice
90
- // would double it in the breakdown even though the server has one row.
91
- if (!existing.includes(timeEntryId)) {
92
- draft.timeEntryIdsByTaskId[taskId] = [...existing, timeEntryId];
93
- }
94
- },
95
- logTimeFailure(draft, action) {
96
- draft.saveStateByTaskId[action.payload.taskId] = {
97
- fetchState: 'Error',
98
- error: action.payload.status,
99
- };
100
- },
101
- updateTimeEntryMinutes: {
102
- prepare(timeEntryId, minutes) {
103
- return { payload: { timeEntryId, minutes } };
104
- },
105
- reducer(draft, action) {
106
- draft.saveStateByTimeEntryId[action.payload.timeEntryId] = {
107
- fetchState: 'In-Progress',
108
- error: undefined,
109
- };
110
- },
111
- },
112
- deleteTimeEntry(draft, action) {
113
- draft.saveStateByTimeEntryId[action.payload] = {
114
- fetchState: 'In-Progress',
115
- error: undefined,
116
- };
117
- },
118
- timeEntryWriteSuccess(draft, action) {
119
- const { timeEntryId, removedFromTaskId } = action.payload;
120
- draft.saveStateByTimeEntryId[timeEntryId] = {
121
- fetchState: 'Completed',
122
- error: undefined,
123
- };
124
- if (removedFromTaskId != null) {
125
- draft.timeEntryIdsByTaskId[removedFromTaskId] = (draft.timeEntryIdsByTaskId[removedFromTaskId] ?? []).filter((candidate) => candidate !== timeEntryId);
126
- }
127
- },
128
- timeEntryWriteFailure(draft, action) {
129
- draft.saveStateByTimeEntryId[action.payload.timeEntryId] = {
130
- fetchState: 'Error',
131
- error: action.payload.status,
132
- };
133
- },
134
- fetchTaskCategories(draft) {
135
- draft.taskCategoryFetchState = {
136
- fetchState: 'In-Progress',
137
- error: undefined,
138
- };
139
- },
140
- fetchTaskCategoriesSuccess(draft) {
141
- draft.taskCategoryFetchState = {
142
- fetchState: 'Completed',
143
- error: undefined,
144
- };
145
- },
146
- fetchTaskCategoriesFailure(draft, action) {
147
- draft.taskCategoryFetchState = {
148
- fetchState: 'Error',
149
- error: action.payload,
150
- };
151
- },
152
- },
153
- extraReducers: (builder) => {
154
- builder.addCase(clearAll, (draft) => {
155
- Object.assign(draft, initialState);
156
- });
157
- },
158
- });
159
- export const { fetchTimeEntriesForTask, fetchTimeEntriesForTaskSuccess, fetchTimeEntriesForTaskFailure, fetchTimeEntriesForRange, fetchTimeEntriesForRangeSuccess, fetchTimeEntriesForRangeFailure, logTime, logTimeSuccess, logTimeFailure, updateTimeEntryMinutes, deleteTimeEntry, timeEntryWriteSuccess, timeEntryWriteFailure, fetchTaskCategories, fetchTaskCategoriesSuccess, fetchTaskCategoriesFailure, } = timeEntryView.actions;
160
- export default timeEntryView.reducer;