@zeniai/client-epic-state 5.2.52 → 5.2.53-beta0ND
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/notificationRegistry/notificationRegistryReducer.d.ts +2 -1
- package/lib/entity/notificationRegistry/notificationRegistryReducer.js +20 -9
- package/lib/entity/notificationRegistry/notificationRegistrySelector.d.ts +2 -1
- package/lib/entity/notificationRegistry/notificationRegistrySelector.js +7 -1
- package/lib/entity/notificationRegistry/notificationRegistryState.d.ts +5 -0
- package/lib/entity/notificationRegistry/notificationRegistryState.js +7 -1
- package/lib/entity/sectionProjectView/sectionProjectViewReducer.js +3 -1
- package/lib/epic.js +4 -4
- package/lib/esm/entity/notificationRegistry/notificationRegistryReducer.js +20 -9
- package/lib/esm/entity/notificationRegistry/notificationRegistrySelector.js +5 -0
- package/lib/esm/entity/notificationRegistry/notificationRegistryState.js +5 -0
- package/lib/esm/entity/sectionProjectView/sectionProjectViewReducer.js +3 -1
- package/lib/esm/epic.js +4 -4
- package/lib/esm/index.js +4 -4
- package/lib/esm/view/aiCfoView/aiCfoViewReducer.js +4 -2
- package/lib/esm/view/aiCfoView/epics/fetchRoutineRunsEpic.js +3 -1
- package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +15 -4
- package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +10 -0
- package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +28 -11
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +19 -3
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +21 -21
- package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +57 -9
- package/lib/esm/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +20 -7
- package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewReducer.js +43 -17
- package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewSelector.js +24 -4
- package/lib/esm/view/settingsView/epic/fetchNotificationSettingsEpic.js +12 -2
- package/lib/index.d.ts +4 -4
- package/lib/index.js +21 -14
- package/lib/view/aiCfoView/aiCfoViewReducer.js +4 -2
- package/lib/view/aiCfoView/aiCfoViewSelector.d.ts +1 -1
- package/lib/view/aiCfoView/epics/fetchRoutineRunsEpic.js +3 -1
- package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +15 -4
- package/lib/view/expenseAutomationView/payload/transactionCategorizationPayload.d.ts +18 -7
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +10 -0
- package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +8 -4
- package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +27 -10
- package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +8 -5
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.d.ts +7 -0
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +18 -2
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.d.ts +3 -2
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +19 -19
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +49 -0
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +59 -10
- package/lib/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +19 -6
- package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.d.ts +8 -5
- package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.js +43 -17
- package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.d.ts +4 -0
- package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.js +30 -5
- package/lib/view/notificationPreferencesView/notificationPreferencesViewState.d.ts +4 -3
- package/lib/view/settingsView/epic/fetchNotificationSettingsEpic.js +12 -2
- package/lib/view/settingsView/settingsViewPayload.d.ts +2 -0
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { NotificationRegistryPayload } from './notificationRegistryPayload';
|
|
2
|
-
import { NotificationRegistryState } from './notificationRegistryState';
|
|
2
|
+
import { NotificationDomain, NotificationRegistryState } from './notificationRegistryState';
|
|
3
3
|
export declare const initialState: NotificationRegistryState;
|
|
4
4
|
export interface UpdateNotificationRegistryPayload {
|
|
5
5
|
registry: NotificationRegistryPayload | undefined;
|
|
6
6
|
registryEpochAtDispatch: number;
|
|
7
|
+
registriesByDomain?: Partial<Record<NotificationDomain, NotificationRegistryPayload | undefined>>;
|
|
7
8
|
}
|
|
8
9
|
export declare const updateNotificationRegistry: import("@reduxjs/toolkit").ActionCreatorWithPayload<UpdateNotificationRegistryPayload, "notificationRegistry/updateNotificationRegistry">;
|
|
9
10
|
declare const _default: import("redux").Reducer<NotificationRegistryState>;
|
|
@@ -4,6 +4,7 @@ exports.updateNotificationRegistry = exports.initialState = void 0;
|
|
|
4
4
|
const toolkit_1 = require("@reduxjs/toolkit");
|
|
5
5
|
const rootActions_1 = require("../../rootActions");
|
|
6
6
|
const notificationRegistryPayload_1 = require("./notificationRegistryPayload");
|
|
7
|
+
const notificationRegistryState_1 = require("./notificationRegistryState");
|
|
7
8
|
exports.initialState = { registryEpoch: 0 };
|
|
8
9
|
const notificationRegistry = (0, toolkit_1.createSlice)({
|
|
9
10
|
name: 'notificationRegistry',
|
|
@@ -22,16 +23,24 @@ const notificationRegistry = (0, toolkit_1.createSlice)({
|
|
|
22
23
|
// `task_checklist_registry` CLEARS so tenant B doesn't inherit A's
|
|
23
24
|
// Task Checklist section. Guarded by the epoch check above so a stale
|
|
24
25
|
// absent-response can't wipe a fresh registry.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const { registriesByDomain } = action.payload;
|
|
27
|
+
// Absent `registriesByDomain` means a pre-domain caller: treat `registry`
|
|
28
|
+
// as the task_checklist snapshot so their behaviour is unchanged.
|
|
29
|
+
const snapshots = registriesByDomain ?? { task_checklist: action.payload.registry };
|
|
30
|
+
draft.registryByDomain = {};
|
|
31
|
+
for (const domain of notificationRegistryState_1.NOTIFICATION_DOMAINS) {
|
|
32
|
+
const payload = snapshots[domain];
|
|
33
|
+
// Full-replace per domain, and an absent snapshot CLEARS that domain so
|
|
34
|
+
// tenant B never inherits A's section. The gates are independent
|
|
35
|
+
// server-side, so one domain clearing must not touch the other.
|
|
36
|
+
if (payload != null) {
|
|
37
|
+
draft.registryByDomain[domain] =
|
|
38
|
+
(0, notificationRegistryPayload_1.mapNotificationRegistryPayloadToRegistry)(payload);
|
|
39
|
+
}
|
|
28
40
|
}
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// the preferences slice's `mergePreferences`). Do not assume the
|
|
33
|
-
// current payload is partial without also updating this reducer.
|
|
34
|
-
draft.registry = (0, notificationRegistryPayload_1.mapNotificationRegistryPayloadToRegistry)(action.payload.registry);
|
|
41
|
+
// Mirrored for consumers that predate the domain-keyed map.
|
|
42
|
+
draft.registry = draft.registryByDomain.task_checklist;
|
|
43
|
+
return;
|
|
35
44
|
},
|
|
36
45
|
},
|
|
37
46
|
extraReducers: (builder) => {
|
|
@@ -44,6 +53,8 @@ const notificationRegistry = (0, toolkit_1.createSlice)({
|
|
|
44
53
|
// — Cursor Bugbot 67d6918e. Returning `initialState` verbatim would
|
|
45
54
|
// rewind the counter and let a stale in-flight GET restore A's
|
|
46
55
|
// registry over B's.
|
|
56
|
+
// Drops `registry` and `registryByDomain` together: a tenant switch must
|
|
57
|
+
// not leave one domain's section behind.
|
|
47
58
|
return { registryEpoch: draft.registryEpoch + 1 };
|
|
48
59
|
});
|
|
49
60
|
},
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import { NotificationRegistry, NotificationRegistryState } from './notificationRegistryState';
|
|
1
|
+
import { NotificationDomain, NotificationRegistry, NotificationRegistryState } from './notificationRegistryState';
|
|
2
2
|
export declare const getNotificationRegistry: (slice: NotificationRegistryState) => NotificationRegistry | undefined;
|
|
3
|
+
export declare const getNotificationRegistryForDomain: (slice: NotificationRegistryState, domain: NotificationDomain) => NotificationRegistry | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getNotificationRegistry = void 0;
|
|
3
|
+
exports.getNotificationRegistryForDomain = exports.getNotificationRegistry = void 0;
|
|
4
4
|
// Current behavior: a null / undefined registry means the whole notification
|
|
5
5
|
// section is hidden by consumers. This conflates two distinct states —
|
|
6
6
|
// "user has no saved preferences yet" vs "the Task Checklist feature gate is
|
|
@@ -8,3 +8,9 @@ exports.getNotificationRegistry = void 0;
|
|
|
8
8
|
// them (e.g. an explicit `gate_off` signal from the server) is a follow-up.
|
|
9
9
|
const getNotificationRegistry = (slice) => slice.registry;
|
|
10
10
|
exports.getNotificationRegistry = getNotificationRegistry;
|
|
11
|
+
// `undefined` conflates gate-off with no-preferences-yet; consumers hide the
|
|
12
|
+
// section either way. Falls back to `registry` for task_checklist so state built
|
|
13
|
+
// in the pre-domain shape (consumer mocks, older callers) still resolves.
|
|
14
|
+
const getNotificationRegistryForDomain = (slice, domain) => slice.registryByDomain?.[domain] ??
|
|
15
|
+
(domain === 'task_checklist' ? slice.registry : undefined);
|
|
16
|
+
exports.getNotificationRegistryForDomain = getNotificationRegistryForDomain;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export declare const NOTIFICATION_DOMAINS: readonly ["task_checklist", "commenting"];
|
|
2
|
+
export type NotificationDomain = (typeof NOTIFICATION_DOMAINS)[number];
|
|
3
|
+
export declare const DEFAULT_NOTIFICATION_DOMAIN: NotificationDomain;
|
|
4
|
+
export declare const toNotificationDomain: (raw: string) => NotificationDomain;
|
|
1
5
|
export declare const NOTIFICATION_CHANNELS: readonly ["email", "slack", "in_app"];
|
|
2
6
|
export type NotificationChannel = (typeof NOTIFICATION_CHANNELS)[number];
|
|
3
7
|
export declare const toNotificationChannel: (raw: string) => NotificationChannel;
|
|
@@ -30,4 +34,5 @@ export interface NotificationRegistry {
|
|
|
30
34
|
export interface NotificationRegistryState {
|
|
31
35
|
registryEpoch: number;
|
|
32
36
|
registry?: NotificationRegistry;
|
|
37
|
+
registryByDomain?: Partial<Record<NotificationDomain, NotificationRegistry>>;
|
|
33
38
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.toNotificationFrequency = exports.NOTIFICATION_FREQUENCIES = exports.toNotificationChannel = exports.NOTIFICATION_CHANNELS = void 0;
|
|
3
|
+
exports.toNotificationFrequency = exports.NOTIFICATION_FREQUENCIES = exports.toNotificationChannel = exports.NOTIFICATION_CHANNELS = exports.toNotificationDomain = exports.DEFAULT_NOTIFICATION_DOMAIN = exports.NOTIFICATION_DOMAINS = void 0;
|
|
4
4
|
const stringToUnion_1 = require("../../commonStateTypes/stringToUnion");
|
|
5
|
+
// Wire vocabulary owned by the notifications service, not by this slice.
|
|
6
|
+
exports.NOTIFICATION_DOMAINS = ['task_checklist', 'commenting'];
|
|
7
|
+
// Callers that predate the domain split mean Task Checklist.
|
|
8
|
+
exports.DEFAULT_NOTIFICATION_DOMAIN = 'task_checklist';
|
|
9
|
+
const toNotificationDomain = (raw) => (0, stringToUnion_1.stringToUnion)(raw, exports.NOTIFICATION_DOMAINS);
|
|
10
|
+
exports.toNotificationDomain = toNotificationDomain;
|
|
5
11
|
exports.NOTIFICATION_CHANNELS = ['email', 'slack', 'in_app'];
|
|
6
12
|
const toNotificationChannel = (raw) => (0, stringToUnion_1.stringToUnion)(raw, exports.NOTIFICATION_CHANNELS);
|
|
7
13
|
exports.toNotificationChannel = toNotificationChannel;
|
|
@@ -35,7 +35,9 @@ const sectionProjectView = (0, toolkit_1.createSlice)({
|
|
|
35
35
|
Object.keys(sectionPayload.projects).forEach((projectId) => {
|
|
36
36
|
const projectPayload = sectionPayload.projects[projectId];
|
|
37
37
|
// A calculated section is grouped by project with no accounts under it.
|
|
38
|
-
const projectAccounts = 'accounts' in projectPayload
|
|
38
|
+
const projectAccounts = 'accounts' in projectPayload
|
|
39
|
+
? projectPayload.accounts
|
|
40
|
+
: undefined;
|
|
39
41
|
const nestedAccountIds = projectAccounts != null
|
|
40
42
|
? (0, getNestedAccountIDHierarchyForSection_1.getNestedAccountIDHierarchyForAccounts)(Object.keys(projectAccounts), accountsMetadata)
|
|
41
43
|
: [];
|
package/lib/epic.js
CHANGED
|
@@ -53,21 +53,21 @@ const acceptMasterTOSEpic_1 = require("./view/aiCfoView/epics/acceptMasterTOSEpi
|
|
|
53
53
|
const createSessionAndSubmitEpic_1 = require("./view/aiCfoView/epics/createSessionAndSubmitEpic");
|
|
54
54
|
const createSessionEpic_1 = require("./view/aiCfoView/epics/createSessionEpic");
|
|
55
55
|
const deleteChatSessionEpic_1 = require("./view/aiCfoView/epics/deleteChatSessionEpic");
|
|
56
|
+
const deleteRoutineEpic_1 = require("./view/aiCfoView/epics/deleteRoutineEpic");
|
|
56
57
|
const deleteSkillEpic_1 = require("./view/aiCfoView/epics/deleteSkillEpic");
|
|
57
58
|
const fetchChatHistoryEpic_1 = require("./view/aiCfoView/epics/fetchChatHistoryEpic");
|
|
58
59
|
const fetchChatSessionsForUserEpic_1 = require("./view/aiCfoView/epics/fetchChatSessionsForUserEpic");
|
|
60
|
+
const fetchRoutineRunsEpic_1 = require("./view/aiCfoView/epics/fetchRoutineRunsEpic");
|
|
61
|
+
const fetchRoutinesEpic_1 = require("./view/aiCfoView/epics/fetchRoutinesEpic");
|
|
59
62
|
const fetchSkillDetailEpic_1 = require("./view/aiCfoView/epics/fetchSkillDetailEpic");
|
|
60
63
|
const fetchSkillVersionBodyEpic_1 = require("./view/aiCfoView/epics/fetchSkillVersionBodyEpic");
|
|
61
64
|
const fetchSkillVersionsEpic_1 = require("./view/aiCfoView/epics/fetchSkillVersionsEpic");
|
|
62
65
|
const fetchSkillsEpic_1 = require("./view/aiCfoView/epics/fetchSkillsEpic");
|
|
63
66
|
const fetchSuggestedQuestionsEpic_1 = require("./view/aiCfoView/epics/fetchSuggestedQuestionsEpic");
|
|
64
|
-
const deleteRoutineEpic_1 = require("./view/aiCfoView/epics/deleteRoutineEpic");
|
|
65
|
-
const fetchRoutineRunsEpic_1 = require("./view/aiCfoView/epics/fetchRoutineRunsEpic");
|
|
66
|
-
const fetchRoutinesEpic_1 = require("./view/aiCfoView/epics/fetchRoutinesEpic");
|
|
67
67
|
const runRoutineEpic_1 = require("./view/aiCfoView/epics/runRoutineEpic");
|
|
68
68
|
const saveRoutineEpic_1 = require("./view/aiCfoView/epics/saveRoutineEpic");
|
|
69
|
-
const setRoutineEnabledEpic_1 = require("./view/aiCfoView/epics/setRoutineEnabledEpic");
|
|
70
69
|
const saveSkillEpic_1 = require("./view/aiCfoView/epics/saveSkillEpic");
|
|
70
|
+
const setRoutineEnabledEpic_1 = require("./view/aiCfoView/epics/setRoutineEnabledEpic");
|
|
71
71
|
const setSkillVisibilityEpic_1 = require("./view/aiCfoView/epics/setSkillVisibilityEpic");
|
|
72
72
|
const stopSubmitEpic_1 = require("./view/aiCfoView/epics/stopSubmitEpic");
|
|
73
73
|
const stopSubmitQuestionEpic_1 = require("./view/aiCfoView/epics/stopSubmitQuestionEpic");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createSlice } from '@reduxjs/toolkit';
|
|
2
2
|
import { clearAll } from '../../rootActions';
|
|
3
3
|
import { mapNotificationRegistryPayloadToRegistry, } from './notificationRegistryPayload';
|
|
4
|
+
import { NOTIFICATION_DOMAINS, } from './notificationRegistryState';
|
|
4
5
|
export const initialState = { registryEpoch: 0 };
|
|
5
6
|
const notificationRegistry = createSlice({
|
|
6
7
|
name: 'notificationRegistry',
|
|
@@ -19,16 +20,24 @@ const notificationRegistry = createSlice({
|
|
|
19
20
|
// `task_checklist_registry` CLEARS so tenant B doesn't inherit A's
|
|
20
21
|
// Task Checklist section. Guarded by the epoch check above so a stale
|
|
21
22
|
// absent-response can't wipe a fresh registry.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const { registriesByDomain } = action.payload;
|
|
24
|
+
// Absent `registriesByDomain` means a pre-domain caller: treat `registry`
|
|
25
|
+
// as the task_checklist snapshot so their behaviour is unchanged.
|
|
26
|
+
const snapshots = registriesByDomain ?? { task_checklist: action.payload.registry };
|
|
27
|
+
draft.registryByDomain = {};
|
|
28
|
+
for (const domain of NOTIFICATION_DOMAINS) {
|
|
29
|
+
const payload = snapshots[domain];
|
|
30
|
+
// Full-replace per domain, and an absent snapshot CLEARS that domain so
|
|
31
|
+
// tenant B never inherits A's section. The gates are independent
|
|
32
|
+
// server-side, so one domain clearing must not touch the other.
|
|
33
|
+
if (payload != null) {
|
|
34
|
+
draft.registryByDomain[domain] =
|
|
35
|
+
mapNotificationRegistryPayloadToRegistry(payload);
|
|
36
|
+
}
|
|
25
37
|
}
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// the preferences slice's `mergePreferences`). Do not assume the
|
|
30
|
-
// current payload is partial without also updating this reducer.
|
|
31
|
-
draft.registry = mapNotificationRegistryPayloadToRegistry(action.payload.registry);
|
|
38
|
+
// Mirrored for consumers that predate the domain-keyed map.
|
|
39
|
+
draft.registry = draft.registryByDomain.task_checklist;
|
|
40
|
+
return;
|
|
32
41
|
},
|
|
33
42
|
},
|
|
34
43
|
extraReducers: (builder) => {
|
|
@@ -41,6 +50,8 @@ const notificationRegistry = createSlice({
|
|
|
41
50
|
// — Cursor Bugbot 67d6918e. Returning `initialState` verbatim would
|
|
42
51
|
// rewind the counter and let a stale in-flight GET restore A's
|
|
43
52
|
// registry over B's.
|
|
53
|
+
// Drops `registry` and `registryByDomain` together: a tenant switch must
|
|
54
|
+
// not leave one domain's section behind.
|
|
44
55
|
return { registryEpoch: draft.registryEpoch + 1 };
|
|
45
56
|
});
|
|
46
57
|
},
|
|
@@ -4,3 +4,8 @@
|
|
|
4
4
|
// off for this tenant". Both currently surface as `undefined`. Distinguishing
|
|
5
5
|
// them (e.g. an explicit `gate_off` signal from the server) is a follow-up.
|
|
6
6
|
export const getNotificationRegistry = (slice) => slice.registry;
|
|
7
|
+
// `undefined` conflates gate-off with no-preferences-yet; consumers hide the
|
|
8
|
+
// section either way. Falls back to `registry` for task_checklist so state built
|
|
9
|
+
// in the pre-domain shape (consumer mocks, older callers) still resolves.
|
|
10
|
+
export const getNotificationRegistryForDomain = (slice, domain) => slice.registryByDomain?.[domain] ??
|
|
11
|
+
(domain === 'task_checklist' ? slice.registry : undefined);
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { stringToUnion } from '../../commonStateTypes/stringToUnion';
|
|
2
|
+
// Wire vocabulary owned by the notifications service, not by this slice.
|
|
3
|
+
export const NOTIFICATION_DOMAINS = ['task_checklist', 'commenting'];
|
|
4
|
+
// Callers that predate the domain split mean Task Checklist.
|
|
5
|
+
export const DEFAULT_NOTIFICATION_DOMAIN = 'task_checklist';
|
|
6
|
+
export const toNotificationDomain = (raw) => stringToUnion(raw, NOTIFICATION_DOMAINS);
|
|
2
7
|
export const NOTIFICATION_CHANNELS = ['email', 'slack', 'in_app'];
|
|
3
8
|
export const toNotificationChannel = (raw) => stringToUnion(raw, NOTIFICATION_CHANNELS);
|
|
4
9
|
export const NOTIFICATION_FREQUENCIES = ['instant', 'daily'];
|
|
@@ -32,7 +32,9 @@ const sectionProjectView = createSlice({
|
|
|
32
32
|
Object.keys(sectionPayload.projects).forEach((projectId) => {
|
|
33
33
|
const projectPayload = sectionPayload.projects[projectId];
|
|
34
34
|
// A calculated section is grouped by project with no accounts under it.
|
|
35
|
-
const projectAccounts = 'accounts' in projectPayload
|
|
35
|
+
const projectAccounts = 'accounts' in projectPayload
|
|
36
|
+
? projectPayload.accounts
|
|
37
|
+
: undefined;
|
|
36
38
|
const nestedAccountIds = projectAccounts != null
|
|
37
39
|
? getNestedAccountIDHierarchyForAccounts(Object.keys(projectAccounts), accountsMetadata)
|
|
38
40
|
: [];
|
package/lib/esm/epic.js
CHANGED
|
@@ -50,21 +50,21 @@ import { acceptMasterTOSEpic, } from './view/aiCfoView/epics/acceptMasterTOSEpic
|
|
|
50
50
|
import { createSessionAndSubmitEpic, } from './view/aiCfoView/epics/createSessionAndSubmitEpic';
|
|
51
51
|
import { createSessionEpic, } from './view/aiCfoView/epics/createSessionEpic';
|
|
52
52
|
import { deleteChatSessionEpic, } from './view/aiCfoView/epics/deleteChatSessionEpic';
|
|
53
|
+
import { deleteRoutineEpic } from './view/aiCfoView/epics/deleteRoutineEpic';
|
|
53
54
|
import { deleteSkillEpic } from './view/aiCfoView/epics/deleteSkillEpic';
|
|
54
55
|
import { fetchChatHistoryEpic, } from './view/aiCfoView/epics/fetchChatHistoryEpic';
|
|
55
56
|
import { fetchChatSessionsForUserEpic, } from './view/aiCfoView/epics/fetchChatSessionsForUserEpic';
|
|
57
|
+
import { fetchRoutineRunsEpic } from './view/aiCfoView/epics/fetchRoutineRunsEpic';
|
|
58
|
+
import { fetchRoutinesEpic } from './view/aiCfoView/epics/fetchRoutinesEpic';
|
|
56
59
|
import { fetchSkillDetailEpic } from './view/aiCfoView/epics/fetchSkillDetailEpic';
|
|
57
60
|
import { fetchSkillVersionBodyEpic } from './view/aiCfoView/epics/fetchSkillVersionBodyEpic';
|
|
58
61
|
import { fetchSkillVersionsEpic } from './view/aiCfoView/epics/fetchSkillVersionsEpic';
|
|
59
62
|
import { fetchSkillsEpic, } from './view/aiCfoView/epics/fetchSkillsEpic';
|
|
60
63
|
import { fetchSuggestedQuestionsEpic, } from './view/aiCfoView/epics/fetchSuggestedQuestionsEpic';
|
|
61
|
-
import { deleteRoutineEpic } from './view/aiCfoView/epics/deleteRoutineEpic';
|
|
62
|
-
import { fetchRoutineRunsEpic } from './view/aiCfoView/epics/fetchRoutineRunsEpic';
|
|
63
|
-
import { fetchRoutinesEpic } from './view/aiCfoView/epics/fetchRoutinesEpic';
|
|
64
64
|
import { runRoutineEpic } from './view/aiCfoView/epics/runRoutineEpic';
|
|
65
65
|
import { saveRoutineEpic } from './view/aiCfoView/epics/saveRoutineEpic';
|
|
66
|
-
import { setRoutineEnabledEpic } from './view/aiCfoView/epics/setRoutineEnabledEpic';
|
|
67
66
|
import { saveSkillEpic } from './view/aiCfoView/epics/saveSkillEpic';
|
|
67
|
+
import { setRoutineEnabledEpic } from './view/aiCfoView/epics/setRoutineEnabledEpic';
|
|
68
68
|
import { setSkillVisibilityEpic } from './view/aiCfoView/epics/setSkillVisibilityEpic';
|
|
69
69
|
import { stopSubmitEpic, } from './view/aiCfoView/epics/stopSubmitEpic';
|
|
70
70
|
import { stopSubmitQuestionEpic, } from './view/aiCfoView/epics/stopSubmitQuestionEpic';
|
package/lib/esm/index.js
CHANGED
|
@@ -64,8 +64,8 @@ import { ALL_JE_SCHEDULE_IGNORE_REASONS, ALL_JE_SCHEDULE_REVERT_REASONS, ALL_ONE
|
|
|
64
64
|
import { ALL_MONTH_END_CLOSE_CHECKS_FREQUENCY, } from './entity/monthEndCloseChecks/monthEndCloseChecksState';
|
|
65
65
|
import { updateCommentsNotifications, updateCommentsNotificationsStatuses, } from './entity/notification/notificationReducer';
|
|
66
66
|
import { toNotificationModeStrict, } from './entity/notification/types/notificationTypes';
|
|
67
|
-
import { getNotificationRegistry } from './entity/notificationRegistry/notificationRegistrySelector';
|
|
68
|
-
import { toNotificationChannel, toNotificationFrequency, } from './entity/notificationRegistry/notificationRegistryState';
|
|
67
|
+
import { getNotificationRegistry, getNotificationRegistryForDomain, } from './entity/notificationRegistry/notificationRegistrySelector';
|
|
68
|
+
import { DEFAULT_NOTIFICATION_DOMAIN, NOTIFICATION_DOMAINS, toNotificationChannel, toNotificationDomain, toNotificationFrequency, } from './entity/notificationRegistry/notificationRegistryState';
|
|
69
69
|
import { getPlaidPaymentAccounts } from './entity/paymentAccount/paymentAccountSelector';
|
|
70
70
|
import { VERIFIED_PAYMENT_ACCOUNT_PROVIDER_VERIFICATION_STATUS, } from './entity/paymentAccount/paymentAccountState';
|
|
71
71
|
import { closeSnackbar, openSnackbar } from './entity/snackbar/snackbarReducer';
|
|
@@ -236,7 +236,7 @@ import { toAverageMonthsCount } from './view/netBurnOrIncomeStoryCard/epic/updat
|
|
|
236
236
|
import { fetchNetBurnOrIncomeStoryCard, updateNetBurnOrIncomeStoryCardSettings, } from './view/netBurnOrIncomeStoryCard/netBurnOrIncomeStoryCardReducer';
|
|
237
237
|
import { getNetBurnOrIncomeStoryCardReport } from './view/netBurnOrIncomeStoryCard/netBurnOrIncomeStoryCardSelector';
|
|
238
238
|
import { clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, saveNotificationPreferences, setGroupChannelMaster, setGroupFrequency, toggleEventChannel, } from './view/notificationPreferencesView/notificationPreferencesViewReducer';
|
|
239
|
-
import { getEffectiveNotificationPreferences, getNotificationLocalOverrides, getNotificationPreferencesSaveState, hasUnsavedNotificationPreferences, } from './view/notificationPreferencesView/notificationPreferencesViewSelector';
|
|
239
|
+
import { getEffectiveNotificationPreferences, getEffectiveNotificationPreferencesForDomain, getNotificationLocalOverrides, getNotificationLocalOverridesForDomain, getNotificationPreferencesSaveState, hasUnsavedNotificationPreferences, hasUnsavedNotificationPreferencesForDomain, } from './view/notificationPreferencesView/notificationPreferencesViewSelector';
|
|
240
240
|
import { fetchNotificationUnreadCount, fetchNotificationUnreadCountSuccess, fetchNotificationView, updateNotificationViewAllNotificationsStatus, updateNotificationViewCurrentTabAndSubTab, updateNotificationViewNotificationStatus, updateNotificationViewSubTab, updateNotificationViewTabState, updateNotificationViewUIState, } from './view/notificationView/notificationViewReducer';
|
|
241
241
|
import { getExternalNotificationsForSelectedSubTab, getNotificationView, getNotificationsForSelectedSubTab, } from './view/notificationView/notificationViewSelector';
|
|
242
242
|
import { toNotificationSubTabTypeStrict, toNotificationTabTypeStrict, } from './view/notificationView/notificationViewState';
|
|
@@ -680,7 +680,7 @@ export { toNotificationModeStrict, updateCommentsNotifications, updateCommentsNo
|
|
|
680
680
|
export { toNotificationSubTabTypeStrict, toNotificationTabTypeStrict, fetchNotificationView, fetchNotificationUnreadCount, fetchNotificationUnreadCountSuccess, updateNotificationViewAllNotificationsStatus, updateNotificationViewNotificationStatus, updateNotificationViewTabState, updateNotificationViewCurrentTabAndSubTab, updateNotificationViewSubTab, updateNotificationViewUIState, getNotificationView, getExternalNotificationsForSelectedSubTab, getNotificationsForSelectedSubTab, };
|
|
681
681
|
export { fetchRegisteredInterests, notifyMeForFeature, getFeatureNotificationView, getRegisteredInterests, getRegisteredInterestsByFeature, isFeatureInterestRegistered, };
|
|
682
682
|
export { pushToastNotification, getLastNotificationTime, getNotifications, };
|
|
683
|
-
export { getEffectiveNotificationPreferences, getNotificationLocalOverrides, getNotificationPreferencesSaveState, getNotificationRegistry, hasUnsavedNotificationPreferences, clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, saveNotificationPreferences, setGroupChannelMaster, setGroupFrequency, toggleEventChannel, toNotificationChannel, toNotificationFrequency, };
|
|
683
|
+
export { getEffectiveNotificationPreferences, getEffectiveNotificationPreferencesForDomain, getNotificationLocalOverrides, getNotificationLocalOverridesForDomain, getNotificationPreferencesSaveState, getNotificationRegistry, getNotificationRegistryForDomain, hasUnsavedNotificationPreferences, hasUnsavedNotificationPreferencesForDomain, NOTIFICATION_DOMAINS, DEFAULT_NOTIFICATION_DOMAIN, clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, saveNotificationPreferences, setGroupChannelMaster, setGroupFrequency, toggleEventChannel, toNotificationChannel, toNotificationDomain, toNotificationFrequency, };
|
|
684
684
|
export { getReferralListView, getInviteFormView, toReferralListViewSortKeyType, StatusTypes, AmountStatusTypes, DEFAULT_REFERRER_AMOUNT, fetchReferrals, sendReferralInvite, clearReferrals, saveReferralFormDataInLocalStore, updateReferralListSortUiState, resendReferralInvite, fetchRewardsPlan, getRewardsPlanCard, updateReferViewed, };
|
|
685
685
|
export { ALL_WEEK_DAYS, SEMI_WEEKLY_REQUIRED_DAYS_COUNT, getMinAllowedEndDate, getRecurringEndDateFromCount, toDayOfWeek, toRecurringFrequency, };
|
|
686
686
|
export { fetchCockpitContext, fetchCompanyTaskManagerView, fetchTaskManagerMetrics, getCompanyTaskManagerView, createTaskFromTaskGroupTemplate, };
|
|
@@ -835,7 +835,8 @@ const aiCfoView = createSlice({
|
|
|
835
835
|
// The server has it. Nothing it sends from here on will contain this row,
|
|
836
836
|
// so the tombstone has done its job and holding it any longer is what
|
|
837
837
|
// makes a routine recreated under the same id invisible forever.
|
|
838
|
-
draft.routines.deletedScheduleIds =
|
|
838
|
+
draft.routines.deletedScheduleIds =
|
|
839
|
+
draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
|
|
839
840
|
draft.routineMutation = {
|
|
840
841
|
fetchState: 'Completed',
|
|
841
842
|
error: undefined,
|
|
@@ -847,7 +848,8 @@ const aiCfoView = createSlice({
|
|
|
847
848
|
// is what lets the refetch put the row back — leaving it hidden would
|
|
848
849
|
// tell the user they had stopped something they had not, which is the
|
|
849
850
|
// worst outcome this list can produce.
|
|
850
|
-
draft.routines.deletedScheduleIds =
|
|
851
|
+
draft.routines.deletedScheduleIds =
|
|
852
|
+
draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
|
|
851
853
|
// Its own slice: the refetch that puts the row back would otherwise wipe
|
|
852
854
|
// this error, leaving the row silently reappearing with no explanation.
|
|
853
855
|
draft.routineMutation = {
|
|
@@ -27,7 +27,9 @@ export const fetchRoutineRunsEpic = (actions$, _state$, zeniAPI) => actions$.pip
|
|
|
27
27
|
// Filtered, not trusted: one malformed row must not take the
|
|
28
28
|
// whole history down, and a run without an id is not addressable
|
|
29
29
|
// by anything the UI does with it.
|
|
30
|
-
runs: (data.runs ?? [])
|
|
30
|
+
runs: (data.runs ?? [])
|
|
31
|
+
.filter(isRoutineRunPayload)
|
|
32
|
+
.map(toRoutineRun),
|
|
31
33
|
scheduleId,
|
|
32
34
|
sessionId: data.session_id ?? null,
|
|
33
35
|
});
|
|
@@ -6,7 +6,6 @@ import { updateTransactions } from '../../../../entity/transaction/transactionRe
|
|
|
6
6
|
import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
|
|
7
7
|
import { getLineItemsByTransactionsIdsFromResponse } from '../../helpers/transactionCategorizationLocalDataHelper';
|
|
8
8
|
import { fetchTransactionCategorization, fetchTransactionCategorizationFailure, fetchTransactionCategorizationSuccess, initializeTransactionCategorizationViewLocalData, resetOtherTabsFetchState, updateParentTotalCountForTab, updateTotalCountForTransactionCategorization, updateTransactionCategorizationUIState, } from '../../reducers/transactionsViewReducer';
|
|
9
|
-
import { DEFAULT_COMPLETED_SUB_TAB } from '../../types/completedSubTab';
|
|
10
9
|
import { TRANSACTIONS_TABS, toTransactionsSortKey, } from '../../types/transactionsViewState';
|
|
11
10
|
export const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchTransactionCategorization.match),
|
|
12
11
|
// Per-tab switchMap: cancels in-flight requests for the same tab on a new
|
|
@@ -33,9 +32,10 @@ groupBy((action) => action.payload.selectedTab), mergeMap((group$) => group$.pip
|
|
|
33
32
|
// cross-status search (omit or true) is reserved for Receipts
|
|
34
33
|
// manual-match.
|
|
35
34
|
search_all_statuses: false,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
// Sent on both tabs: the backend ignores it for Review's own rows but uses
|
|
36
|
+
// it to scope the Completed badge it returns alongside them.
|
|
37
|
+
sub_tab: expenseAutomationTransactionsViewState
|
|
38
|
+
.selectedTransactionCategorizationCompletedSubTab,
|
|
39
39
|
transaction_group: expenseAutomationTransactionsViewState.selectedTransactionGroup,
|
|
40
40
|
};
|
|
41
41
|
return zeniAPI
|
|
@@ -87,6 +87,17 @@ groupBy((action) => action.payload.selectedTab), mergeMap((group$) => group$.pip
|
|
|
87
87
|
parentTotalCount: response.data.parent_tab_total_count ??
|
|
88
88
|
response.data.total_count,
|
|
89
89
|
}));
|
|
90
|
+
// The inactive tab's badge, counted under the same filters. Absent on
|
|
91
|
+
// pages after the first and on the cross-tab search path, where the
|
|
92
|
+
// last known value stays correct.
|
|
93
|
+
const otherTabTotalCount = response.data.other_tab_total_count;
|
|
94
|
+
if (otherTabTotalCount != null) {
|
|
95
|
+
updateActions.push(updateParentTotalCountForTab({
|
|
96
|
+
selectedPeriod,
|
|
97
|
+
selectedTab: selectedTab === 'review' ? 'autoCategorized' : 'review',
|
|
98
|
+
parentTotalCount: otherTabTotalCount,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
90
101
|
updateActions.push(resetOtherTabsFetchState({
|
|
91
102
|
refreshViewInBackground,
|
|
92
103
|
tabs: TRANSACTIONS_TABS.filter((tab) => selectedTab !== tab),
|
|
@@ -373,6 +373,16 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
373
373
|
if (cached != null) {
|
|
374
374
|
autoCat.uiState.pageToken = cached.pageToken;
|
|
375
375
|
autoCat.uiState.totalCount = { ...cached.totalCount };
|
|
376
|
+
// Keep the stored badge in step with the restored sub-tab: the selector
|
|
377
|
+
// falls back to it for any period this sub-tab has not loaded. Merged,
|
|
378
|
+
// not replaced -- the snapshot only holds periods this sub-tab actually
|
|
379
|
+
// loaded, while the badge map also carries periods filled from a Review
|
|
380
|
+
// response's `other_tab_total_count`. Overwriting would drop those and
|
|
381
|
+
// leave the badge at 0 for any period this sub-tab never visited.
|
|
382
|
+
draft.parentTotalCountByTab.autoCategorized = {
|
|
383
|
+
...draft.parentTotalCountByTab.autoCategorized,
|
|
384
|
+
...cached.totalCount,
|
|
385
|
+
};
|
|
376
386
|
autoCat.uiState.scrollPosition = cached.scrollPosition;
|
|
377
387
|
autoCat.transactionIdsBySelectedPeriod = {
|
|
378
388
|
...cached.transactionIdsBySelectedPeriod,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSelector } from '@reduxjs/toolkit';
|
|
2
2
|
import omit from 'lodash/omit';
|
|
3
|
-
import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
|
|
3
|
+
import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
|
|
4
4
|
import { getCurrentTenant, getIsAccountingProjectsEnabled, } from '../../../entity/tenant/tenantSelector';
|
|
5
5
|
import { getTransactionWithCOT } from '../../../entity/transaction/transactionHelper';
|
|
6
6
|
import { getSupportedTransactionsByIds } from '../../../entity/transaction/transactionSelector';
|
|
@@ -172,18 +172,35 @@ export function getExpenseAutomationTransactionView(state) {
|
|
|
172
172
|
review: monthYearPeriodId != null ? totalCountByReview[monthYearPeriodId] : 0,
|
|
173
173
|
autoCategorized: monthYearPeriodId != null ? totalCountByAutoCat[monthYearPeriodId] : 0,
|
|
174
174
|
};
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
175
|
+
// Tab badge counts, used exclusively by the navbar / tab strip badges. Both
|
|
176
|
+
// tabs are counted under the filters applied to the list, so a badge always
|
|
177
|
+
// equals the number of rows that tab shows.
|
|
178
|
+
//
|
|
179
|
+
// The ACTIVE tab prefers its own list count: the two are the same number (the
|
|
180
|
+
// backend's parent_tab_total_count mirrors total_count), but `uiState.totalCount`
|
|
181
|
+
// is restored by every cache path that skips a fetch -- switching back to an
|
|
182
|
+
// already-loaded period, or to a cached Completed sub-tab -- whereas the stored
|
|
183
|
+
// badge is only written when a response lands, so on those paths it would keep
|
|
184
|
+
// showing whatever was fetched last.
|
|
185
|
+
//
|
|
186
|
+
// The `in` check is load-bearing, not defensive: `totalCount` is a per-period
|
|
187
|
+
// map typed as `Record<MonthYearPeriodId, number>`, so indexing it is typed
|
|
188
|
+
// `number` but is `undefined` for any period the active (sub-)tab has not
|
|
189
|
+
// loaded -- the window between switching to a fresh sub-tab and its response
|
|
190
|
+
// landing. Falling back to the stored badge there keeps a real number in the
|
|
191
|
+
// label instead of blanking it.
|
|
179
192
|
const parentTotalCountByTab = expenseAutomationTransactionsViewState.parentTotalCountByTab;
|
|
193
|
+
const storedBadgeByTab = (tab) => monthYearPeriodId != null
|
|
194
|
+
? (parentTotalCountByTab[tab][monthYearPeriodId] ?? 0)
|
|
195
|
+
: 0;
|
|
196
|
+
const badgeForTab = (tab, listCountByPeriod) => selectedTransactionCategorizationTab === tab &&
|
|
197
|
+
monthYearPeriodId != null &&
|
|
198
|
+
monthYearPeriodId in listCountByPeriod
|
|
199
|
+
? listCountByPeriod[monthYearPeriodId]
|
|
200
|
+
: storedBadgeByTab(tab);
|
|
180
201
|
const parentTabTotalCountByTab = {
|
|
181
|
-
review:
|
|
182
|
-
|
|
183
|
-
: 0,
|
|
184
|
-
autoCategorized: monthYearPeriodId != null
|
|
185
|
-
? (parentTotalCountByTab.autoCategorized[monthYearPeriodId] ?? 0)
|
|
186
|
-
: 0,
|
|
202
|
+
review: badgeForTab('review', totalCountByReview),
|
|
203
|
+
autoCategorized: badgeForTab('autoCategorized', totalCountByAutoCat),
|
|
187
204
|
};
|
|
188
205
|
const fetchStateByTab = {
|
|
189
206
|
review: {
|
|
@@ -3,7 +3,7 @@ import { reduceAllFetchState } from '../../../commonStateTypes/reduceFetchState'
|
|
|
3
3
|
import { getInvoicingPlanById, getInvoicingPlansByIds, } from '../../../entity/invoicing/plan/planSelector';
|
|
4
4
|
import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
|
|
5
5
|
import { getInvoicingFormDraftLocalData, isInvoicingFormDraftPending, } from '../invoicingFormDraft';
|
|
6
|
-
import { getInvoicingQboView, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
|
|
6
|
+
import { INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES, INVOICING_INCOME_ACCOUNT_MAPPING_SLOT, NO_INVOICING_ACCOUNT_HIERARCHY, NO_INVOICING_ACCOUNT_IDS, NO_INVOICING_CLASS_HIERARCHY, getInvoicingQboAccountOptions, getInvoicingQboView, invoicingQboAccountIdsForMappingSlot, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
|
|
7
7
|
import { INVOICING_CATALOG_APPLICABILITY_VALUES, INVOICING_CATALOG_PRICING_MODEL_VALUES, INVOICING_CATALOG_PRORATION_VALUES, INVOICING_CATALOG_TIER_TYPE_VALUES, initialInvoicingCatalogItemFormLocalData, } from './invoicingCatalogItemFormHelper';
|
|
8
8
|
export const getEditInvoicingCatalogItemDetailViewState = (state) => ({
|
|
9
9
|
fetchState: state.editInvoicingCatalogItemDetailViewState.fetchState,
|
|
@@ -44,6 +44,21 @@ const NOT_STARTED = {
|
|
|
44
44
|
error: undefined,
|
|
45
45
|
fetchState: 'Not-Started',
|
|
46
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* The QuickBooks ids a price point's income account may be picked from.
|
|
49
|
+
*
|
|
50
|
+
* Memoized, and collapsing to one shared empty array while QBO is disconnected:
|
|
51
|
+
* the pages turn this into their picker options and re-seed their form defaults
|
|
52
|
+
* when those change, so a fresh array on every store change would reset the form
|
|
53
|
+
* under the user — including mid-save.
|
|
54
|
+
*/
|
|
55
|
+
const getInvoicingCatalogIncomeAccountIds = createSelector([
|
|
56
|
+
getInvoicingQboAccountOptions,
|
|
57
|
+
(state) => getInvoicingConfigView(state).qboAccountMappingKindOptions,
|
|
58
|
+
isInvoicingQboConnected,
|
|
59
|
+
], (accounts, qboAccountMappingKindOptions, isQboConnected) => isQboConnected
|
|
60
|
+
? invoicingQboAccountIdsForMappingSlot(accounts, qboAccountMappingKindOptions, INVOICING_INCOME_ACCOUNT_MAPPING_SLOT, INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES)
|
|
61
|
+
: NO_INVOICING_ACCOUNT_IDS);
|
|
47
62
|
export const getInvoicingCatalogItemFormView = (state, id) => {
|
|
48
63
|
const { editDraftById, error, fetchState, newItemDraft, savedItemId, savedKind, } = state.editInvoicingCatalogItemDetailViewState;
|
|
49
64
|
const plan = id != null ? getInvoicingPlanById(state.invoicingPlanState, id) : undefined;
|
|
@@ -94,8 +109,9 @@ export const getInvoicingCatalogItemFormView = (state, id) => {
|
|
|
94
109
|
isFormLoading: isInvoicingAccountingPickerDataLoading(state) ||
|
|
95
110
|
isInvoicingFormDraftPending(editDraftById, id),
|
|
96
111
|
isQboPickerDataLoading: isInvoicingAccountingPickerDataLoading(state),
|
|
97
|
-
qboAccountHierarchy: qboView?.nestedAccountHierarchy ??
|
|
98
|
-
qboClassHierarchy: qboView?.nestedClassHierarchy ??
|
|
112
|
+
qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? NO_INVOICING_ACCOUNT_HIERARCHY,
|
|
113
|
+
qboClassHierarchy: qboView?.nestedClassHierarchy ?? NO_INVOICING_CLASS_HIERARCHY,
|
|
114
|
+
qboIncomeAccountIds: getInvoicingCatalogIncomeAccountIds(state),
|
|
99
115
|
applicabilityValues: INVOICING_CATALOG_APPLICABILITY_VALUES,
|
|
100
116
|
applicableItemOptions,
|
|
101
117
|
localData,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { createSelector } from '@reduxjs/toolkit';
|
|
1
2
|
import { reduceAllFetchState } from '../../../commonStateTypes/reduceFetchState';
|
|
2
3
|
import { getInvoicingCouponById } from '../../../entity/invoicing/coupon/couponSelector';
|
|
3
|
-
import { toInvoicingFormOption,
|
|
4
|
+
import { toInvoicingFormOption, } from '../../../entity/invoicing/invoicingCommonPayload';
|
|
4
5
|
import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
|
|
5
6
|
import { getInvoicingFormDraftLocalData, isInvoicingFormDraftPending, } from '../invoicingFormDraft';
|
|
6
|
-
import {
|
|
7
|
+
import { INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES, INVOICING_DISCOUNTS_ACCOUNT_MAPPING_SLOT, NO_INVOICING_ACCOUNT_HIERARCHY, NO_INVOICING_ACCOUNT_IDS, getInvoicingQboAccountOptions, getInvoicingQboView, invoicingQboAccountByQboId, invoicingQboAccountIdsForMappingSlot, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
|
|
7
8
|
import { getInvoicingSettings, getInvoicingSettingsFetchState, } from '../settingsView/settingsViewSelector';
|
|
8
9
|
import { initialInvoicingCouponFormLocalData, invoicingCouponDiscountsAccountIdForCreate, } from './editInvoicingCouponDetailViewState';
|
|
9
10
|
/**
|
|
@@ -11,6 +12,21 @@ import { initialInvoicingCouponFormLocalData, invoicingCouponDiscountsAccountIdF
|
|
|
11
12
|
* the id of the coupon last saved, and — when a `couponId` is passed (edit
|
|
12
13
|
* mode) — the existing coupon entity to prefill the form.
|
|
13
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* The QuickBooks ids the discounts account may be picked from.
|
|
17
|
+
*
|
|
18
|
+
* Memoized, and collapsing to one shared empty array while QBO is disconnected:
|
|
19
|
+
* the form turns this into its picker options and re-seeds its defaults when
|
|
20
|
+
* those change, so a fresh array on every store change would reset the form
|
|
21
|
+
* under the user until their first edit.
|
|
22
|
+
*/
|
|
23
|
+
const getInvoicingCouponDiscountsAccountIds = createSelector([
|
|
24
|
+
getInvoicingQboAccountOptions,
|
|
25
|
+
(state) => getInvoicingConfigView(state).qboAccountMappingKindOptions,
|
|
26
|
+
isInvoicingQboConnected,
|
|
27
|
+
], (accounts, qboAccountMappingKindOptions, isQboConnected) => isQboConnected
|
|
28
|
+
? invoicingQboAccountIdsForMappingSlot(accounts, qboAccountMappingKindOptions, INVOICING_DISCOUNTS_ACCOUNT_MAPPING_SLOT, INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES)
|
|
29
|
+
: NO_INVOICING_ACCOUNT_IDS);
|
|
14
30
|
const NOT_STARTED = {
|
|
15
31
|
error: undefined,
|
|
16
32
|
fetchState: 'Not-Started',
|
|
@@ -51,14 +67,6 @@ export const getEditInvoicingCouponDetailViewState = (state, couponId) => {
|
|
|
51
67
|
version: 1.0,
|
|
52
68
|
};
|
|
53
69
|
};
|
|
54
|
-
/**
|
|
55
|
-
* The account types the discounts slot falls back to when the config has no
|
|
56
|
-
* `discounts_account` kind. A QuickBooks discount line posts to an income
|
|
57
|
-
* account; offering every account instead would let a save be rejected.
|
|
58
|
-
*/
|
|
59
|
-
const DEFAULT_DISCOUNTS_ACCOUNT_TYPES = 'Income';
|
|
60
|
-
/** The account-mapping slot a coupon's discounts account shares with settings. */
|
|
61
|
-
const DISCOUNTS_ACCOUNT_MAPPING_SLOT = 'discounts_account';
|
|
62
70
|
/**
|
|
63
71
|
* Bundled view for the discount form screen: the edit view (save state + the
|
|
64
72
|
* coupon to prefill in edit mode), the draft `localData` (seeded from the
|
|
@@ -74,7 +82,7 @@ export const getInvoicingCouponFormView = (state, couponId) => {
|
|
|
74
82
|
const localData = couponId != null
|
|
75
83
|
? getInvoicingFormDraftLocalData(editDraftById, couponId, initialInvoicingCouponFormLocalData)
|
|
76
84
|
: newCouponDraft;
|
|
77
|
-
const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions,
|
|
85
|
+
const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, } = getInvoicingConfigView(state);
|
|
78
86
|
const isQboConnected = isInvoicingQboConnected(state);
|
|
79
87
|
const qboView = isQboConnected ? getInvoicingQboView(state) : undefined;
|
|
80
88
|
const isQboPickerDataLoading = isInvoicingAccountingPickerDataLoading(state);
|
|
@@ -84,14 +92,6 @@ export const getInvoicingCouponFormView = (state, couponId) => {
|
|
|
84
92
|
const isCreateSeedPending = couponId == null &&
|
|
85
93
|
(settingsFetchState === 'Not-Started' ||
|
|
86
94
|
settingsFetchState === 'In-Progress');
|
|
87
|
-
// The account types the discounts slot accepts come from the same config the
|
|
88
|
-
// QBO settings page reads, so the two pickers offer the same accounts and
|
|
89
|
-
// widening the slot stays a config change. Matched on the slot rather than
|
|
90
|
-
// the code as written: a config that names the kind `discounts_account_id`
|
|
91
|
-
// is the same slot, and missing it here would quietly fall back to the
|
|
92
|
-
// default and offer a different set than the settings page does.
|
|
93
|
-
const discountsAccountTypes = qboAccountMappingKindOptions.find((kindOption) => toInvoicingQboAccountMappingSlot(kindOption.code) ===
|
|
94
|
-
DISCOUNTS_ACCOUNT_MAPPING_SLOT)?.accountTypes ?? DEFAULT_DISCOUNTS_ACCOUNT_TYPES;
|
|
95
95
|
return {
|
|
96
96
|
...base,
|
|
97
97
|
applyOnOptions: discountApplyOnOptions.map(toInvoicingFormOption),
|
|
@@ -130,7 +130,7 @@ export const getInvoicingCouponFormView = (state, couponId) => {
|
|
|
130
130
|
},
|
|
131
131
|
mode: couponId != null ? 'edit' : 'create',
|
|
132
132
|
periodUnitOptions: billingPeriodUnitOptions.map(toInvoicingFormOption),
|
|
133
|
-
qboAccountHierarchy: qboView?.nestedAccountHierarchy ??
|
|
134
|
-
qboIncomeAccountIds:
|
|
133
|
+
qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? NO_INVOICING_ACCOUNT_HIERARCHY,
|
|
134
|
+
qboIncomeAccountIds: getInvoicingCouponDiscountsAccountIds(state),
|
|
135
135
|
};
|
|
136
136
|
};
|