@zeniai/client-epic-state 5.2.49-beta0ND → 5.2.49-betaAK0
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 +1 -2
- package/lib/entity/notificationRegistry/notificationRegistryReducer.js +9 -20
- package/lib/entity/notificationRegistry/notificationRegistrySelector.d.ts +1 -2
- package/lib/entity/notificationRegistry/notificationRegistrySelector.js +1 -7
- package/lib/entity/notificationRegistry/notificationRegistryState.d.ts +0 -5
- package/lib/entity/notificationRegistry/notificationRegistryState.js +1 -7
- package/lib/entity/sectionProjectView/sectionProjectViewReducer.js +1 -3
- package/lib/epic.js +4 -4
- package/lib/esm/entity/notificationRegistry/notificationRegistryReducer.js +9 -20
- package/lib/esm/entity/notificationRegistry/notificationRegistrySelector.js +0 -5
- package/lib/esm/entity/notificationRegistry/notificationRegistryState.js +0 -5
- package/lib/esm/entity/sectionProjectView/sectionProjectViewReducer.js +1 -3
- package/lib/esm/epic.js +4 -4
- package/lib/esm/index.js +4 -4
- package/lib/esm/view/aiCfoView/aiCfoViewReducer.js +2 -4
- package/lib/esm/view/aiCfoView/epics/fetchRoutineRunsEpic.js +1 -3
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer.js +19 -11
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +4 -1
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +3 -2
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/seedInvoicingCatalogItemFormDraftEpic.js +18 -9
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewReducer.js +19 -11
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +3 -1
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +1 -1
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/seedInvoicingCouponFormDraftEpic.js +16 -7
- package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer.js +19 -11
- package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.js +4 -2
- package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/saveInvoicingCustomerEpic.js +1 -1
- package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.js +21 -13
- package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +3 -2
- package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionFormPageEpic.js +4 -1
- package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +1 -1
- package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/seedInvoicingSubscriptionFormDraftEpic.js +18 -9
- package/lib/esm/view/invoicing/invoicingFormDraft.js +56 -0
- package/lib/esm/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +7 -20
- package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewReducer.js +17 -43
- package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewSelector.js +4 -24
- package/lib/esm/view/settingsView/epic/fetchNotificationSettingsEpic.js +2 -12
- package/lib/index.d.ts +4 -4
- package/lib/index.js +14 -21
- package/lib/view/aiCfoView/aiCfoViewReducer.js +2 -4
- package/lib/view/aiCfoView/aiCfoViewSelector.d.ts +1 -1
- package/lib/view/aiCfoView/epics/fetchRoutineRunsEpic.js +1 -3
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer.d.ts +3 -1
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer.js +19 -11
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.d.ts +10 -0
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +4 -1
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewState.d.ts +2 -1
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +3 -2
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/seedInvoicingCatalogItemFormDraftEpic.d.ts +10 -6
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/seedInvoicingCatalogItemFormDraftEpic.js +17 -8
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewReducer.d.ts +3 -1
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewReducer.js +19 -11
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.d.ts +13 -0
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +3 -1
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.d.ts +2 -1
- package/lib/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +1 -1
- package/lib/view/invoicing/editInvoicingCouponDetailView/seedInvoicingCouponFormDraftEpic.d.ts +10 -6
- package/lib/view/invoicing/editInvoicingCouponDetailView/seedInvoicingCouponFormDraftEpic.js +15 -6
- package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer.d.ts +3 -1
- package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer.js +19 -11
- package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.d.ts +9 -0
- package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.js +4 -2
- package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewState.d.ts +2 -1
- package/lib/view/invoicing/editInvoicingCustomerDetailView/saveInvoicingCustomerEpic.js +1 -1
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.d.ts +3 -1
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.js +21 -13
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.d.ts +9 -0
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +3 -2
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewState.d.ts +2 -1
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionFormPageEpic.js +4 -1
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +1 -1
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/seedInvoicingSubscriptionFormDraftEpic.d.ts +10 -6
- package/lib/view/invoicing/editInvoicingSubscriptionDetailView/seedInvoicingSubscriptionFormDraftEpic.js +17 -8
- package/lib/view/invoicing/invoicingFormDraft.d.ts +50 -0
- package/lib/view/invoicing/invoicingFormDraft.js +63 -0
- package/lib/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +6 -19
- package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.d.ts +5 -8
- package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.js +17 -43
- package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.d.ts +0 -4
- package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.js +5 -30
- package/lib/view/notificationPreferencesView/notificationPreferencesViewState.d.ts +3 -4
- package/lib/view/settingsView/epic/fetchNotificationSettingsEpic.js +2 -12
- package/lib/view/settingsView/settingsViewPayload.d.ts +0 -2
- package/package.json +1 -1
package/lib/esm/view/invoicing/editInvoicingCouponDetailView/seedInvoicingCouponFormDraftEpic.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { EMPTY, of } from 'rxjs';
|
|
2
2
|
import { filter, mergeMap, withLatestFrom } from 'rxjs/operators';
|
|
3
3
|
import { getInvoicingCouponById } from '../../../entity/invoicing/coupon/couponSelector';
|
|
4
|
-
import { seedInvoicingCouponFormDraft, updateInvoicingCouponDetail, } from './editInvoicingCouponDetailViewReducer';
|
|
4
|
+
import { fetchInvoicingCouponDetailPage, seedInvoicingCouponFormDraft, updateInvoicingCouponDetail, } from './editInvoicingCouponDetailViewReducer';
|
|
5
5
|
import { entityToInvoicingCouponFormLocalData } from './editInvoicingCouponDetailViewState';
|
|
6
6
|
/**
|
|
7
|
-
* Seeds the edit draft from the record the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Seeds the edit draft from the record, so the form has its values before it
|
|
8
|
+
* mounts. The reducer ignores a seed once the draft holds the record, which is
|
|
9
|
+
* what stops a background refetch from overwriting edits in progress.
|
|
10
|
+
*
|
|
11
|
+
* Opening the page counts as a trigger, not just the detail response: the page
|
|
12
|
+
* fetch is cache-guarded, so re-opening a form for a record already loaded this
|
|
13
|
+
* session issues no request at all — and a draft cleared in between (by a save
|
|
14
|
+
* or a cancel) would have nothing left to seed it.
|
|
11
15
|
*/
|
|
12
|
-
export const seedInvoicingCouponFormDraftEpic = (actions$, state$) => actions$.pipe(filter(updateInvoicingCouponDetail.match
|
|
16
|
+
export const seedInvoicingCouponFormDraftEpic = (actions$, state$) => actions$.pipe(filter((action) => updateInvoicingCouponDetail.match(action) ||
|
|
17
|
+
fetchInvoicingCouponDetailPage.match(action)), withLatestFrom(state$), mergeMap(([action, state]) => {
|
|
13
18
|
const { id } = action.payload;
|
|
14
|
-
|
|
19
|
+
// Nothing to seed from on a cold open; the detail response triggers this
|
|
20
|
+
// again once it lands.
|
|
21
|
+
const coupon = id != null
|
|
22
|
+
? getInvoicingCouponById(state.invoicingCouponState, id)
|
|
23
|
+
: undefined;
|
|
15
24
|
if (coupon == null) {
|
|
16
25
|
return EMPTY;
|
|
17
26
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createSlice } from '@reduxjs/toolkit';
|
|
2
2
|
import { clearAll } from '../../../rootActions';
|
|
3
|
+
import { applyInvoicingFormDraftEdit, applyInvoicingFormDraftFailure, applyInvoicingFormDraftSeed, } from '../invoicingFormDraft';
|
|
3
4
|
import { initialInvoicingCustomerFormLocalData } from './invoicingCustomerFormConfig';
|
|
4
5
|
export const initialState = {
|
|
5
6
|
emailPreviewFetchState: { fetchState: 'Not-Started', error: undefined },
|
|
@@ -123,20 +124,17 @@ const editInvoicingCustomerDetailView = createSlice({
|
|
|
123
124
|
fetchState: 'Error',
|
|
124
125
|
error: action.payload.status,
|
|
125
126
|
};
|
|
127
|
+
applyInvoicingFormDraftFailure(draft.editDraftById, action.payload.id, initialInvoicingCustomerFormLocalData);
|
|
126
128
|
},
|
|
127
|
-
/**
|
|
128
|
-
* Seed the draft from the loaded record, once. Skipped when a draft already
|
|
129
|
-
* exists so a late refetch cannot overwrite in-progress edits.
|
|
130
|
-
*/
|
|
129
|
+
/** Fill the draft from the loaded record — see `applyInvoicingFormDraftSeed`. */
|
|
131
130
|
seedInvoicingCustomerFormDraft(draft, action) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
131
|
+
const { id, localData } = action.payload;
|
|
132
|
+
applyInvoicingFormDraftSeed(draft.editDraftById, id, localData);
|
|
135
133
|
},
|
|
136
134
|
updateInvoicingCustomerFormDraft(draft, action) {
|
|
137
135
|
const { id, localData } = action.payload;
|
|
138
136
|
if (id != null) {
|
|
139
|
-
draft.editDraftById
|
|
137
|
+
applyInvoicingFormDraftEdit(draft.editDraftById, id, localData);
|
|
140
138
|
}
|
|
141
139
|
else {
|
|
142
140
|
draft.newCustomerDraft = localData;
|
|
@@ -161,12 +159,22 @@ const editInvoicingCustomerDetailView = createSlice({
|
|
|
161
159
|
draft.fetchState = 'Error';
|
|
162
160
|
draft.error = action.payload;
|
|
163
161
|
},
|
|
164
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Discard a form's draft — the customer's own when `id` is given,
|
|
164
|
+
* the create draft otherwise. Scoped to the one form on purpose: another
|
|
165
|
+
* customer's half-finished edit is not this form's to throw away.
|
|
166
|
+
* Clearing every draft at once is what `clearAll` is for.
|
|
167
|
+
*/
|
|
168
|
+
resetEditInvoicingCustomerDetailView(draft, action) {
|
|
165
169
|
draft.fetchState = 'Not-Started';
|
|
166
170
|
draft.error = undefined;
|
|
167
171
|
draft.savedCustomerId = undefined;
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
const id = action.payload?.id;
|
|
173
|
+
if (id == null) {
|
|
174
|
+
draft.newCustomerDraft = { ...initialInvoicingCustomerFormLocalData };
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
delete draft.editDraftById[id];
|
|
170
178
|
},
|
|
171
179
|
// Trigger only — handled by initializeInvoicingCustomerAddressEpic.
|
|
172
180
|
initializeInvoicingCustomerAddress: {
|
|
@@ -5,6 +5,7 @@ import { invoicingCustomerPaymentMethods } from '../../../entity/invoicing/invoi
|
|
|
5
5
|
import { getFormattedAddress } from '../../addressView/addressViewSelector';
|
|
6
6
|
import { getInvoicingPaymentLinkSendState, getInvoicingPaymentMethodSaveState, getInvoicingPlaidLinkToken, getInvoicingPlaidLinkTokenFetchState, getInvoicingSentPaymentLinkEmail, getInvoicingSentPaymentLinkMagicUrl, getInvoicingSetupIntent, getInvoicingSetupIntentFetchState, } from '../customerPaymentMethod/customerPaymentMethodSelector';
|
|
7
7
|
import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
|
|
8
|
+
import { getInvoicingFormDraftLocalData, isInvoicingFormDraftPending, } from '../invoicingFormDraft';
|
|
8
9
|
import { isStripeConnectedFromSettings } from '../settingsView/invoicingSettingsHelpers';
|
|
9
10
|
import { getInvoicingSettings } from '../settingsView/settingsViewSelector';
|
|
10
11
|
import { INVOICING_CUSTOMER_BILLING_ADDRESS_TYPE, initialInvoicingCustomerFormLocalData, } from './invoicingCustomerFormConfig';
|
|
@@ -59,8 +60,7 @@ export const getInvoicingCustomerFormView = (state, invoicingCustomerID) => {
|
|
|
59
60
|
// `seedInvoicingCustomerFormDraftEpic` when the detail load lands. Until then
|
|
60
61
|
// the page is showing its skeleton.
|
|
61
62
|
const localData = invoicingCustomerID != null
|
|
62
|
-
? (editDraftById
|
|
63
|
-
initialInvoicingCustomerFormLocalData)
|
|
63
|
+
? getInvoicingFormDraftLocalData(editDraftById, invoicingCustomerID, initialInvoicingCustomerFormLocalData)
|
|
64
64
|
: newCustomerDraft;
|
|
65
65
|
// Billing address is captured via the shared Address screen and seeded there
|
|
66
66
|
// (from the address entity) on detail load, so the formatted label comes from
|
|
@@ -78,6 +78,7 @@ export const getInvoicingCustomerFormView = (state, invoicingCustomerID) => {
|
|
|
78
78
|
? { error: undefined, fetchState: 'Completed' }
|
|
79
79
|
: (state.editInvoicingCustomerDetailViewState.recordStateById[invoicingCustomerID] ?? { error: undefined, fetchState: 'Not-Started' });
|
|
80
80
|
const { emailPreview, emailPreviewCustomerId, emailPreviewFetchState } = state.editInvoicingCustomerDetailViewState;
|
|
81
|
+
const isFormLoading = isInvoicingFormDraftPending(editDraftById, invoicingCustomerID);
|
|
81
82
|
return {
|
|
82
83
|
// A preview cached for another customer is not this screen's.
|
|
83
84
|
emailPreview: emailPreviewCustomerId === invoicingCustomerID ? emailPreview : undefined,
|
|
@@ -92,6 +93,7 @@ export const getInvoicingCustomerFormView = (state, invoicingCustomerID) => {
|
|
|
92
93
|
promotionalCreditSubmitState: state.editInvoicingCustomerDetailViewState.promotionalCreditSubmitState,
|
|
93
94
|
record: getInvoicingCustomerRecord(state, invoicingCustomerID),
|
|
94
95
|
fieldHelp: invoicingFieldHelpForGroup(state, 'customer'),
|
|
96
|
+
isFormLoading,
|
|
95
97
|
customerTypeOptions,
|
|
96
98
|
localData,
|
|
97
99
|
mode,
|
|
@@ -16,7 +16,7 @@ export const saveInvoicingCustomerEpic = (actions$, state$, zeniAPI) => actions$
|
|
|
16
16
|
const rootState = state$.value;
|
|
17
17
|
const viewState = rootState.editInvoicingCustomerDetailViewState;
|
|
18
18
|
const localData = (id != null
|
|
19
|
-
? viewState.editDraftById[id]
|
|
19
|
+
? viewState.editDraftById[id]?.localData
|
|
20
20
|
: viewState.newCustomerDraft) ?? viewState.newCustomerDraft;
|
|
21
21
|
// Billing address captured via the shared Address screen is sent as an
|
|
22
22
|
// object in the payload; the backend creates it and returns its id
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createSlice } from '@reduxjs/toolkit';
|
|
2
2
|
import { clearAll } from '../../../rootActions';
|
|
3
|
+
import { applyInvoicingFormDraftEdit, applyInvoicingFormDraftFailure, applyInvoicingFormDraftSeed, } from '../invoicingFormDraft';
|
|
3
4
|
import { initialInvoicingSubscriptionFormLocalData } from './invoicingSubscriptionFormConfig';
|
|
4
5
|
export const initialState = {
|
|
5
6
|
fetchState: 'Not-Started',
|
|
@@ -65,15 +66,12 @@ const editInvoicingSubscriptionDetailView = createSlice({
|
|
|
65
66
|
fetchState: 'Error',
|
|
66
67
|
error: action.payload.error,
|
|
67
68
|
};
|
|
69
|
+
applyInvoicingFormDraftFailure(draft.editDraftById, action.payload.id, initialInvoicingSubscriptionFormLocalData);
|
|
68
70
|
},
|
|
69
|
-
/**
|
|
70
|
-
* Seed the draft from the loaded record, once. Skipped when a draft already
|
|
71
|
-
* exists so a late refetch cannot overwrite in-progress edits.
|
|
72
|
-
*/
|
|
71
|
+
/** Fill the draft from the loaded record — see `applyInvoicingFormDraftSeed`. */
|
|
73
72
|
seedInvoicingSubscriptionFormDraft(draft, action) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
73
|
+
const { id, localData } = action.payload;
|
|
74
|
+
applyInvoicingFormDraftSeed(draft.editDraftById, id, localData);
|
|
77
75
|
},
|
|
78
76
|
fetchInvoicingSubscriptionFormPage: {
|
|
79
77
|
reducer() {
|
|
@@ -87,7 +85,7 @@ const editInvoicingSubscriptionDetailView = createSlice({
|
|
|
87
85
|
updateInvoicingSubscriptionFormDraft(draft, action) {
|
|
88
86
|
const { id, localData } = action.payload;
|
|
89
87
|
if (id != null) {
|
|
90
|
-
draft.editDraftById
|
|
88
|
+
applyInvoicingFormDraftEdit(draft.editDraftById, id, localData);
|
|
91
89
|
}
|
|
92
90
|
else {
|
|
93
91
|
draft.newSubscriptionDraft = localData;
|
|
@@ -112,14 +110,24 @@ const editInvoicingSubscriptionDetailView = createSlice({
|
|
|
112
110
|
draft.fetchState = 'Error';
|
|
113
111
|
draft.error = action.payload;
|
|
114
112
|
},
|
|
115
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Discard a form's draft — the subscription's own when `id` is given,
|
|
115
|
+
* the create draft otherwise. Scoped to the one form on purpose: another
|
|
116
|
+
* subscription's half-finished edit is not this form's to throw away.
|
|
117
|
+
* Clearing every draft at once is what `clearAll` is for.
|
|
118
|
+
*/
|
|
119
|
+
resetEditInvoicingSubscriptionDetailView(draft, action) {
|
|
116
120
|
draft.fetchState = 'Not-Started';
|
|
117
121
|
draft.error = undefined;
|
|
118
122
|
draft.savedSubscriptionId = undefined;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
const id = action.payload?.id;
|
|
124
|
+
if (id == null) {
|
|
125
|
+
draft.newSubscriptionDraft = {
|
|
126
|
+
...initialInvoicingSubscriptionFormLocalData,
|
|
127
|
+
};
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
delete draft.editDraftById[id];
|
|
123
131
|
},
|
|
124
132
|
// Trigger only — handled by initializeInvoicingSubscriptionAddressEpic.
|
|
125
133
|
initializeInvoicingSubscriptionAddress: {
|
|
@@ -5,6 +5,7 @@ import { getInvoicingCouponListItems } from '../couponView/couponViewSelector';
|
|
|
5
5
|
import { getInvoicingCatalogListView, getInvoicingCatalogPrices, } from '../invoicingCatalogListView/invoicingCatalogListViewSelector';
|
|
6
6
|
import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
|
|
7
7
|
import { getInvoicingCustomerListView } from '../invoicingCustomerListView/invoicingCustomerListViewSelector';
|
|
8
|
+
import { getInvoicingFormDraftLocalData, isInvoicingFormDraftPending, } from '../invoicingFormDraft';
|
|
8
9
|
import { initialInvoicingSubscriptionFormLocalData } from './invoicingSubscriptionFormConfig';
|
|
9
10
|
import { INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE } from './invoicingSubscriptionFormConfig';
|
|
10
11
|
export const getEditInvoicingSubscriptionDetailViewState = (state, invoicingSubscriptionID) => {
|
|
@@ -33,8 +34,7 @@ export const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID)
|
|
|
33
34
|
// `seedInvoicingSubscriptionFormDraftEpic` when the detail load lands. Until
|
|
34
35
|
// then the page is showing its skeleton.
|
|
35
36
|
const localData = invoicingSubscriptionID != null
|
|
36
|
-
? (editDraftById
|
|
37
|
-
initialInvoicingSubscriptionFormLocalData)
|
|
37
|
+
? getInvoicingFormDraftLocalData(editDraftById, invoicingSubscriptionID, initialInvoicingSubscriptionFormLocalData)
|
|
38
38
|
: newSubscriptionDraft;
|
|
39
39
|
// Shipping address is captured via the shared Address screen and seeded there
|
|
40
40
|
// (looked up in the shared address store by `shippingAddressId`) on detail
|
|
@@ -65,6 +65,7 @@ export const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID)
|
|
|
65
65
|
subscriptionLifecycleActionOptions: configState.subscriptionLifecycleActionOptions,
|
|
66
66
|
subscriptionMetadataEditAllowedFromStatuses: configState.subscriptionMetadataEditAllowedFromStatuses,
|
|
67
67
|
recordState,
|
|
68
|
+
isFormLoading: isInvoicingFormDraftPending(editDraftById, invoicingSubscriptionID),
|
|
68
69
|
fieldHelp: invoicingFieldHelpForGroup(state, 'subscription'),
|
|
69
70
|
coupons: getInvoicingCouponListItems(state),
|
|
70
71
|
customers: getInvoicingCustomerListView(state).customers,
|
|
@@ -16,8 +16,11 @@ export const fetchInvoicingSubscriptionFormPageEpic = (actions$) => actions$.pip
|
|
|
16
16
|
const { subscriptionId } = action.payload;
|
|
17
17
|
const pageActions = [
|
|
18
18
|
fetchInvoicingCustomerList({ filters: {}, keepExistingListItems: false }),
|
|
19
|
+
// Plans only: the form's plan picker is the one consumer, and an
|
|
20
|
+
// unfiltered list would replace the store's plans with every catalog
|
|
21
|
+
// item.
|
|
19
22
|
fetchInvoicingCatalogPlanList({
|
|
20
|
-
filters: {},
|
|
23
|
+
filters: { type: 'plan' },
|
|
21
24
|
keepExistingListItems: false,
|
|
22
25
|
}),
|
|
23
26
|
fetchInvoicingCouponList({ filters: {}, keepExistingListItems: false }),
|
package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js
CHANGED
|
@@ -17,7 +17,7 @@ export const saveInvoicingSubscriptionEpic = (actions$, state$, zeniAPI) => acti
|
|
|
17
17
|
const rootState = state$.value;
|
|
18
18
|
const viewState = rootState.editInvoicingSubscriptionDetailViewState;
|
|
19
19
|
const localData = (id != null
|
|
20
|
-
? viewState.editDraftById[id]
|
|
20
|
+
? viewState.editDraftById[id]?.localData
|
|
21
21
|
: viewState.newSubscriptionDraft) ?? viewState.newSubscriptionDraft;
|
|
22
22
|
// Shipping address captured via the shared Address screen is sent as an
|
|
23
23
|
// object in the payload (same pattern as the customer save flow).
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import { EMPTY, of } from 'rxjs';
|
|
2
2
|
import { filter, mergeMap, withLatestFrom } from 'rxjs/operators';
|
|
3
|
-
import { seedInvoicingSubscriptionFormDraft, updateInvoicingSubscriptionDetail, } from './editInvoicingSubscriptionDetailViewReducer';
|
|
3
|
+
import { fetchInvoicingSubscriptionFormPage, seedInvoicingSubscriptionFormDraft, updateInvoicingSubscriptionDetail, } from './editInvoicingSubscriptionDetailViewReducer';
|
|
4
4
|
import { getInvoicingSubscriptionById } from './invoicingSubscriptionDetailHelper';
|
|
5
5
|
import { entityToInvoicingSubscriptionFormLocalData } from './invoicingSubscriptionFormConfig';
|
|
6
6
|
/**
|
|
7
|
-
* Seeds the edit draft from the record the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Seeds the edit draft from the record, so the form has its values before it
|
|
8
|
+
* mounts. The reducer ignores a seed once the draft holds the record, which is
|
|
9
|
+
* what stops a background refetch from overwriting edits in progress.
|
|
10
|
+
*
|
|
11
|
+
* Opening the page counts as a trigger, not just the detail response: the page
|
|
12
|
+
* fetch is cache-guarded, so re-opening a form for a record already loaded this
|
|
13
|
+
* session issues no request at all — and a draft cleared in between (by a save
|
|
14
|
+
* or a cancel) would have nothing left to seed it.
|
|
11
15
|
*/
|
|
12
|
-
export const seedInvoicingSubscriptionFormDraftEpic = (actions$, state$) => actions$.pipe(filter(updateInvoicingSubscriptionDetail.match
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
+
export const seedInvoicingSubscriptionFormDraftEpic = (actions$, state$) => actions$.pipe(filter((action) => updateInvoicingSubscriptionDetail.match(action) ||
|
|
17
|
+
fetchInvoicingSubscriptionFormPage.match(action)), withLatestFrom(state$), mergeMap(([action, state]) => {
|
|
18
|
+
const id = updateInvoicingSubscriptionDetail.match(action)
|
|
19
|
+
? action.payload.id
|
|
20
|
+
: action.payload.subscriptionId;
|
|
21
|
+
// Nothing to seed from on a cold open; the detail response triggers this
|
|
22
|
+
// again once it lands.
|
|
23
|
+
const subscription = id != null ? getInvoicingSubscriptionById(state, id) : undefined;
|
|
24
|
+
if (id == null || subscription == null) {
|
|
16
25
|
return EMPTY;
|
|
17
26
|
}
|
|
18
27
|
return of(seedInvoicingSubscriptionFormDraft({
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The form's own write. Keeps the status: an edit on top of a seeded draft is
|
|
3
|
+
* still the record's draft, and an edit made before the record arrived must not
|
|
4
|
+
* pass for one.
|
|
5
|
+
*/
|
|
6
|
+
export function applyInvoicingFormDraftEdit(draftById, id, localData) {
|
|
7
|
+
draftById[id] = {
|
|
8
|
+
fetchState: draftById[id]?.fetchState ?? 'Not-Started',
|
|
9
|
+
localData,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Fill the draft from the loaded record, once. A second seed is ignored so a
|
|
14
|
+
* late refetch cannot overwrite edits in progress — but a draft the form wrote
|
|
15
|
+
* before the record landed is replaced, since it was never the record's.
|
|
16
|
+
*/
|
|
17
|
+
export function applyInvoicingFormDraftSeed(draftById, id, localData) {
|
|
18
|
+
if (draftById[id]?.fetchState === 'Completed') {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
draftById[id] = { fetchState: 'Completed', localData };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Stand the draft up blank when the record's fetch fails, so the form renders
|
|
25
|
+
* instead of waiting behind a skeleton that will never resolve. A draft that
|
|
26
|
+
* already exists is left alone: a background refetch failing is no reason to
|
|
27
|
+
* discard what the user has.
|
|
28
|
+
*/
|
|
29
|
+
export function applyInvoicingFormDraftFailure(draftById, id, blankLocalData) {
|
|
30
|
+
if (draftById[id] != null) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
draftById[id] = { fetchState: 'Error', localData: blankLocalData };
|
|
34
|
+
}
|
|
35
|
+
/** The draft's values, or the blank form when nothing is drafted yet. */
|
|
36
|
+
export function getInvoicingFormDraftLocalData(draftById, id, blankLocalData) {
|
|
37
|
+
if (id == null) {
|
|
38
|
+
return blankLocalData;
|
|
39
|
+
}
|
|
40
|
+
return draftById[id]?.localData ?? blankLocalData;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether the form should still be showing its skeleton — the record has
|
|
44
|
+
* neither reached the draft nor failed trying.
|
|
45
|
+
*
|
|
46
|
+
* Settles once and stays settled, which is what keeps a later refetch from
|
|
47
|
+
* swapping a form the user is typing into back for a skeleton. Create mode
|
|
48
|
+
* (no id) has no record to wait for.
|
|
49
|
+
*/
|
|
50
|
+
export function isInvoicingFormDraftPending(draftById, id) {
|
|
51
|
+
if (id == null) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const fetchState = draftById[id]?.fetchState;
|
|
55
|
+
return fetchState !== 'Completed' && fetchState !== 'Error';
|
|
56
|
+
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { EMPTY, of } from 'rxjs';
|
|
2
2
|
import { catchError, filter, finalize, mergeMap, switchMap, takeUntil, withLatestFrom, } from 'rxjs/operators';
|
|
3
|
-
import { NOTIFICATION_DOMAINS } from '../../../entity/notificationRegistry/notificationRegistryState';
|
|
4
3
|
import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
|
|
5
4
|
import { createZeniAPIStatus, isSuccessResponse, } from '../../../responsePayload';
|
|
6
5
|
import { notificationPreferencesUrl } from '../notificationPreferencesEndpoint';
|
|
7
6
|
import { mapPreferencesToPayload, } from '../notificationPreferencesViewPayload';
|
|
8
7
|
import { clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, saveNotificationPreferences, saveNotificationPreferencesFailure, saveNotificationPreferencesSuccess, } from '../notificationPreferencesViewReducer';
|
|
9
|
-
import {
|
|
8
|
+
import { getNotificationLocalOverrides } from '../notificationPreferencesViewSelector';
|
|
10
9
|
export const saveNotificationPreferencesEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter((action) => saveNotificationPreferences.match(action)), withLatestFrom(state$),
|
|
11
10
|
// `switchMap` unsubscribes from any in-flight PUT observable when a newer
|
|
12
11
|
// Save fires. The prior HTTP request has already left the
|
|
@@ -14,19 +13,9 @@ export const saveNotificationPreferencesEpic = (actions$, state$, zeniAPI) => ac
|
|
|
14
13
|
// ignored, so stale success/failure actions cannot clobber the newer
|
|
15
14
|
// batch's state. Server-side ordering is arrival-order LWW.
|
|
16
15
|
switchMap(([, state]) => {
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
NOTIFICATION_DOMAINS.forEach((domain) => {
|
|
21
|
-
const overrides = getNotificationLocalOverridesForDomain(state, domain);
|
|
22
|
-
const preferencesPatch = mapPreferencesToPayload(overrides);
|
|
23
|
-
if (Object.keys(preferencesPatch).length === 0) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
overridesByDomain[domain] = overrides;
|
|
27
|
-
body[`${domain}_preferences`] = preferencesPatch;
|
|
28
|
-
});
|
|
29
|
-
if (Object.keys(body).length === 0) {
|
|
16
|
+
const overrides = getNotificationLocalOverrides(state);
|
|
17
|
+
const preferencesPatch = mapPreferencesToPayload(overrides);
|
|
18
|
+
if (Object.keys(preferencesPatch).length === 0) {
|
|
30
19
|
// No diff → do not dispatch a synthetic success (that would wipe
|
|
31
20
|
// stored preferences via the reducer). Simply no-op.
|
|
32
21
|
return EMPTY;
|
|
@@ -39,6 +28,7 @@ switchMap(([, state]) => {
|
|
|
39
28
|
// in-flight save's response (Success/Failure never dispatched, so
|
|
40
29
|
// the counter never drained and future saves got their merge dropped).
|
|
41
30
|
const cancelEpochAtDispatch = state.notificationPreferencesViewState.cancelEpoch;
|
|
31
|
+
const body = { task_checklist_preferences: preferencesPatch };
|
|
42
32
|
// Suppress the error snackbar when the response lands AFTER the user
|
|
43
33
|
// pressed Cancel — the Failure reducer already swallows the state
|
|
44
34
|
// update, so the toast would be a stale error for a save the user
|
|
@@ -67,11 +57,8 @@ switchMap(([, state]) => {
|
|
|
67
57
|
if (isSuccessResponse(response)) {
|
|
68
58
|
return of(saveNotificationPreferencesSuccess({
|
|
69
59
|
cancelEpochAtDispatch,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
task_checklist: response.data?.task_checklist_preferences,
|
|
73
|
-
commenting: response.data?.commenting_preferences,
|
|
74
|
-
},
|
|
60
|
+
savedOverrides: overrides,
|
|
61
|
+
serverEcho: response.data?.task_checklist_preferences,
|
|
75
62
|
}));
|
|
76
63
|
}
|
|
77
64
|
const actions = [
|
|
@@ -1,25 +1,14 @@
|
|
|
1
1
|
import { createSlice } from '@reduxjs/toolkit';
|
|
2
|
-
import { DEFAULT_NOTIFICATION_DOMAIN, } from '../../entity/notificationRegistry/notificationRegistryState';
|
|
3
2
|
import { clearAll } from '../../rootActions';
|
|
4
3
|
import { mapPayloadToPreferences, } from './notificationPreferencesViewPayload';
|
|
5
4
|
import { emptyNotificationPreferences, } from './notificationPreferencesViewState';
|
|
6
5
|
export const initialState = {
|
|
7
6
|
cancelEpoch: 0,
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
localOverrides: emptyNotificationPreferences(),
|
|
8
|
+
preferences: emptyNotificationPreferences(),
|
|
10
9
|
saveEpoch: 0,
|
|
11
10
|
savePreferencesState: { fetchState: 'Not-Started' },
|
|
12
11
|
};
|
|
13
|
-
// Lazy: a gate-off tenant never receives that domain's key at all.
|
|
14
|
-
const overridesFor = (byDomain, domain) => {
|
|
15
|
-
const existing = byDomain[domain];
|
|
16
|
-
if (existing != null) {
|
|
17
|
-
return existing;
|
|
18
|
-
}
|
|
19
|
-
const created = emptyNotificationPreferences();
|
|
20
|
-
byDomain[domain] = created;
|
|
21
|
-
return created;
|
|
22
|
-
};
|
|
23
12
|
const mergePreferences = (base, patch) => {
|
|
24
13
|
const eventEnabledByChannel = {
|
|
25
14
|
...base.eventEnabledByChannel,
|
|
@@ -157,8 +146,7 @@ const notificationPreferencesView = createSlice({
|
|
|
157
146
|
// Success/Failure never dispatches, subsequent legitimate saves
|
|
158
147
|
// still work because their captured epoch matches the current one.
|
|
159
148
|
draft.cancelEpoch += 1;
|
|
160
|
-
|
|
161
|
-
draft.localOverridesByDomain = {};
|
|
149
|
+
draft.localOverrides = emptyNotificationPreferences();
|
|
162
150
|
// Also drop `savePreferencesState` back to Not-Started so any
|
|
163
151
|
// "Saving…" affordance bound to `getNotificationPreferencesSaveState`
|
|
164
152
|
// clears on Cancel. Without this, a Cancel-during-save whose stale
|
|
@@ -175,6 +163,9 @@ const notificationPreferencesView = createSlice({
|
|
|
175
163
|
// them; the save-success path is the only clearer.
|
|
176
164
|
// A `null`/`undefined` payload isn't a snapshot at all (e.g. the
|
|
177
165
|
// envelope key was omitted); no-op instead of wiping preferences.
|
|
166
|
+
if (action.payload.preferences == null) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
178
169
|
// Stale-GET guard (Cursor Bugbot 3632510237): if the fetch epic
|
|
179
170
|
// captured a `saveEpoch` lower than the current one, a save has
|
|
180
171
|
// succeeded since this GET dispatched — the response is carrying
|
|
@@ -184,13 +175,7 @@ const notificationPreferencesView = createSlice({
|
|
|
184
175
|
if (action.payload.saveEpochAtDispatch < draft.saveEpoch) {
|
|
185
176
|
return;
|
|
186
177
|
}
|
|
187
|
-
|
|
188
|
-
if (payload == null) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
draft.preferencesByDomain[domain] =
|
|
192
|
-
mapPayloadToPreferences(payload);
|
|
193
|
-
});
|
|
178
|
+
draft.preferences = mapPayloadToPreferences(action.payload.preferences);
|
|
194
179
|
},
|
|
195
180
|
saveNotificationPreferences(draft) {
|
|
196
181
|
draft.savePreferencesState = { fetchState: 'In-Progress' };
|
|
@@ -249,44 +234,33 @@ const notificationPreferencesView = createSlice({
|
|
|
249
234
|
// skip the `preferences` replace. Cursor Bugbot 3632510237.
|
|
250
235
|
draft.saveEpoch += 1;
|
|
251
236
|
draft.savePreferencesState = { fetchState: 'Completed' };
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const serverEcho = action.payload.serverEchoByDomain[domain];
|
|
258
|
-
const patch = serverEcho == null
|
|
259
|
-
? savedOverrides
|
|
260
|
-
: mapPayloadToPreferences(serverEcho);
|
|
261
|
-
draft.preferencesByDomain[domain] = mergePreferences(draft.preferencesByDomain[domain] ?? emptyNotificationPreferences(), patch);
|
|
262
|
-
draft.localOverridesByDomain[domain] = subtractSavedOverrides(draft.localOverridesByDomain[domain] ??
|
|
263
|
-
emptyNotificationPreferences(), savedOverrides);
|
|
264
|
-
});
|
|
237
|
+
const patch = action.payload.serverEcho == null
|
|
238
|
+
? action.payload.savedOverrides
|
|
239
|
+
: mapPayloadToPreferences(action.payload.serverEcho);
|
|
240
|
+
draft.preferences = mergePreferences(draft.preferences, patch);
|
|
241
|
+
draft.localOverrides = subtractSavedOverrides(draft.localOverrides, action.payload.savedOverrides);
|
|
265
242
|
},
|
|
266
243
|
setGroupChannelMaster(draft, action) {
|
|
267
244
|
// Mirror of `toggleEventChannel` but scoped to the group-channel master
|
|
268
245
|
// held under `groupChannelEnabledByGroupId[groupId][channel]`.
|
|
269
246
|
const { channel, enabled, groupId } = action.payload;
|
|
270
|
-
const overrides = overridesFor(draft.localOverridesByDomain, action.payload.domain ?? DEFAULT_NOTIFICATION_DOMAIN);
|
|
271
247
|
const groupMap = {
|
|
272
|
-
...(
|
|
248
|
+
...(draft.localOverrides.groupChannelEnabledByGroupId[groupId] ?? {}),
|
|
273
249
|
};
|
|
274
250
|
groupMap[channel] = enabled;
|
|
275
|
-
|
|
251
|
+
draft.localOverrides.groupChannelEnabledByGroupId[groupId] = groupMap;
|
|
276
252
|
},
|
|
277
253
|
setGroupFrequency(draft, action) {
|
|
278
|
-
|
|
279
|
-
overrides.groupFrequencyByGroupId[action.payload.groupId] =
|
|
254
|
+
draft.localOverrides.groupFrequencyByGroupId[action.payload.groupId] =
|
|
280
255
|
action.payload.frequency;
|
|
281
256
|
},
|
|
282
257
|
toggleEventChannel(draft, action) {
|
|
283
258
|
const { channel, enabled, eventId } = action.payload;
|
|
284
|
-
const overrides = overridesFor(draft.localOverridesByDomain, action.payload.domain ?? DEFAULT_NOTIFICATION_DOMAIN);
|
|
285
259
|
const eventMap = {
|
|
286
|
-
...(
|
|
260
|
+
...(draft.localOverrides.eventEnabledByChannel[eventId] ?? {}),
|
|
287
261
|
};
|
|
288
262
|
eventMap[channel] = enabled;
|
|
289
|
-
|
|
263
|
+
draft.localOverrides.eventEnabledByChannel[eventId] = eventMap;
|
|
290
264
|
},
|
|
291
265
|
},
|
|
292
266
|
extraReducers: (builder) => {
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { createSelector } from '@reduxjs/toolkit';
|
|
2
|
-
import { DEFAULT_NOTIFICATION_DOMAIN, } from '../../entity/notificationRegistry/notificationRegistryState';
|
|
3
|
-
import { emptyNotificationPreferences, } from './notificationPreferencesViewState';
|
|
4
2
|
const getView = (state) => state.notificationPreferencesViewState;
|
|
5
3
|
export const getNotificationPreferencesSaveState = (state) => getView(state).savePreferencesState;
|
|
6
4
|
// Memoised on `preferences` + `localOverrides` so the merged shape keeps a
|
|
7
5
|
// stable reference while neither input changes — a plain function would
|
|
8
6
|
// allocate a fresh object on every call and re-render every `useSelector`
|
|
9
7
|
// consumer on any unrelated dispatch.
|
|
10
|
-
const
|
|
11
|
-
// One per domain: a shared createSelector has a cache of one and would thrash across two.
|
|
12
|
-
const effectivePreferencesSelectorByDomain = new Map();
|
|
13
|
-
const createEffectivePreferencesSelector = (domain) => createSelector((state) => getView(state).preferencesByDomain[domain], (state) => getView(state).localOverridesByDomain[domain], (storedPreferences, storedOverrides) => {
|
|
14
|
-
const preferences = storedPreferences ?? EMPTY_PREFERENCES;
|
|
15
|
-
const localOverrides = storedOverrides ?? EMPTY_PREFERENCES;
|
|
8
|
+
export const getEffectiveNotificationPreferences = createSelector((state) => getView(state).preferences, (state) => getView(state).localOverrides, (preferences, localOverrides) => {
|
|
16
9
|
const mergedEventEnabledByChannel = { ...preferences.eventEnabledByChannel };
|
|
17
10
|
Object.entries(localOverrides.eventEnabledByChannel).forEach(([eventId, channels]) => {
|
|
18
11
|
mergedEventEnabledByChannel[eventId] = {
|
|
@@ -36,20 +29,7 @@ const createEffectivePreferencesSelector = (domain) => createSelector((state) =>
|
|
|
36
29
|
},
|
|
37
30
|
};
|
|
38
31
|
});
|
|
39
|
-
export const
|
|
40
|
-
|
|
41
|
-
if (selector == null) {
|
|
42
|
-
selector = createEffectivePreferencesSelector(domain);
|
|
43
|
-
effectivePreferencesSelectorByDomain.set(domain, selector);
|
|
44
|
-
}
|
|
45
|
-
return selector(state);
|
|
46
|
-
};
|
|
47
|
-
export const getEffectiveNotificationPreferences = (state) => getEffectiveNotificationPreferencesForDomain(state, DEFAULT_NOTIFICATION_DOMAIN);
|
|
48
|
-
export const getNotificationLocalOverridesForDomain = (state, domain) => getView(state).localOverridesByDomain[domain] ?? EMPTY_PREFERENCES;
|
|
49
|
-
export const getNotificationLocalOverrides = (state) => getNotificationLocalOverridesForDomain(state, DEFAULT_NOTIFICATION_DOMAIN);
|
|
50
|
-
const isDirty = (localOverrides) => Object.keys(localOverrides.eventEnabledByChannel).length > 0 ||
|
|
32
|
+
export const getNotificationLocalOverrides = (state) => getView(state).localOverrides;
|
|
33
|
+
export const hasUnsavedNotificationPreferences = createSelector((state) => getView(state).localOverrides, (localOverrides) => Object.keys(localOverrides.eventEnabledByChannel).length > 0 ||
|
|
51
34
|
Object.keys(localOverrides.groupChannelEnabledByGroupId).length > 0 ||
|
|
52
|
-
Object.keys(localOverrides.groupFrequencyByGroupId).length > 0;
|
|
53
|
-
export const hasUnsavedNotificationPreferencesForDomain = (state, domain) => isDirty(getNotificationLocalOverridesForDomain(state, domain));
|
|
54
|
-
// Drives the shared page-level Save/Cancel footer.
|
|
55
|
-
export const hasUnsavedNotificationPreferences = (state) => Object.values(getView(state).localOverridesByDomain).some((localOverrides) => localOverrides != null && isDirty(localOverrides));
|
|
35
|
+
Object.keys(localOverrides.groupFrequencyByGroupId).length > 0);
|
|
@@ -38,21 +38,11 @@ export const fetchNotificationSettingsEpic = (actions$, state$, zeniAPI) => acti
|
|
|
38
38
|
// 67d6918e.
|
|
39
39
|
updateActions.push(updateNotificationRegistry({
|
|
40
40
|
registry: response.data.task_checklist_registry ?? undefined,
|
|
41
|
-
// An absent `<domain>_registry` means that domain's gate is off.
|
|
42
|
-
registriesByDomain: {
|
|
43
|
-
task_checklist: response.data.task_checklist_registry ?? undefined,
|
|
44
|
-
commenting: response.data.commenting_registry ?? undefined,
|
|
45
|
-
},
|
|
46
41
|
registryEpochAtDispatch,
|
|
47
42
|
}));
|
|
48
|
-
|
|
49
|
-
const preferencesByDomain = {
|
|
50
|
-
task_checklist: response.data.task_checklist_preferences ?? undefined,
|
|
51
|
-
commenting: response.data.commenting_preferences ?? undefined,
|
|
52
|
-
};
|
|
53
|
-
if (Object.values(preferencesByDomain).some((preferences) => preferences != null)) {
|
|
43
|
+
if (response.data.task_checklist_preferences != null) {
|
|
54
44
|
updateActions.push(fetchNotificationPreferencesSuccess({
|
|
55
|
-
|
|
45
|
+
preferences: response.data.task_checklist_preferences,
|
|
56
46
|
saveEpochAtDispatch,
|
|
57
47
|
}));
|
|
58
48
|
}
|