@zeniai/client-epic-state 5.2.49 → 5.2.50-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.
Files changed (87) hide show
  1. package/lib/entity/notificationRegistry/notificationRegistryReducer.d.ts +2 -1
  2. package/lib/entity/notificationRegistry/notificationRegistryReducer.js +20 -9
  3. package/lib/entity/notificationRegistry/notificationRegistrySelector.d.ts +2 -1
  4. package/lib/entity/notificationRegistry/notificationRegistrySelector.js +7 -1
  5. package/lib/entity/notificationRegistry/notificationRegistryState.d.ts +5 -0
  6. package/lib/entity/notificationRegistry/notificationRegistryState.js +7 -1
  7. package/lib/entity/sectionProjectView/sectionProjectViewReducer.js +3 -1
  8. package/lib/epic.js +4 -4
  9. package/lib/esm/entity/notificationRegistry/notificationRegistryReducer.js +20 -9
  10. package/lib/esm/entity/notificationRegistry/notificationRegistrySelector.js +5 -0
  11. package/lib/esm/entity/notificationRegistry/notificationRegistryState.js +5 -0
  12. package/lib/esm/entity/sectionProjectView/sectionProjectViewReducer.js +3 -1
  13. package/lib/esm/epic.js +4 -4
  14. package/lib/esm/index.js +4 -4
  15. package/lib/esm/view/aiCfoView/aiCfoViewReducer.js +4 -2
  16. package/lib/esm/view/aiCfoView/epics/fetchRoutineRunsEpic.js +3 -1
  17. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer.js +19 -11
  18. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +4 -1
  19. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +3 -2
  20. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/seedInvoicingCatalogItemFormDraftEpic.js +26 -7
  21. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewReducer.js +19 -11
  22. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +3 -1
  23. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +1 -1
  24. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/seedInvoicingCouponFormDraftEpic.js +25 -6
  25. package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer.js +19 -11
  26. package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.js +4 -2
  27. package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/saveInvoicingCustomerEpic.js +1 -1
  28. package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/seedInvoicingCustomerFormDraftEpic.js +25 -6
  29. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.js +21 -13
  30. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +3 -2
  31. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionFormPageEpic.js +4 -1
  32. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +1 -1
  33. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/seedInvoicingSubscriptionFormDraftEpic.js +29 -8
  34. package/lib/esm/view/invoicing/invoicingFormDraft.js +65 -0
  35. package/lib/esm/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +20 -7
  36. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewReducer.js +43 -17
  37. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewSelector.js +24 -4
  38. package/lib/esm/view/settingsView/epic/fetchNotificationSettingsEpic.js +12 -2
  39. package/lib/index.d.ts +4 -4
  40. package/lib/index.js +21 -14
  41. package/lib/view/aiCfoView/aiCfoViewReducer.js +4 -2
  42. package/lib/view/aiCfoView/aiCfoViewSelector.d.ts +1 -1
  43. package/lib/view/aiCfoView/epics/fetchRoutineRunsEpic.js +3 -1
  44. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer.d.ts +3 -1
  45. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer.js +19 -11
  46. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.d.ts +10 -0
  47. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +4 -1
  48. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewState.d.ts +2 -1
  49. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +3 -2
  50. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/seedInvoicingCatalogItemFormDraftEpic.d.ts +10 -6
  51. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/seedInvoicingCatalogItemFormDraftEpic.js +25 -6
  52. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewReducer.d.ts +3 -1
  53. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewReducer.js +19 -11
  54. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.d.ts +13 -0
  55. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +3 -1
  56. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewState.d.ts +2 -1
  57. package/lib/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +1 -1
  58. package/lib/view/invoicing/editInvoicingCouponDetailView/seedInvoicingCouponFormDraftEpic.d.ts +10 -6
  59. package/lib/view/invoicing/editInvoicingCouponDetailView/seedInvoicingCouponFormDraftEpic.js +24 -5
  60. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer.d.ts +3 -1
  61. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer.js +19 -11
  62. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.d.ts +9 -0
  63. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.js +4 -2
  64. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewState.d.ts +2 -1
  65. package/lib/view/invoicing/editInvoicingCustomerDetailView/saveInvoicingCustomerEpic.js +1 -1
  66. package/lib/view/invoicing/editInvoicingCustomerDetailView/seedInvoicingCustomerFormDraftEpic.d.ts +10 -6
  67. package/lib/view/invoicing/editInvoicingCustomerDetailView/seedInvoicingCustomerFormDraftEpic.js +24 -5
  68. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.d.ts +3 -1
  69. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewReducer.js +21 -13
  70. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.d.ts +9 -0
  71. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +3 -2
  72. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewState.d.ts +2 -1
  73. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/fetchInvoicingSubscriptionFormPageEpic.js +4 -1
  74. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +1 -1
  75. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/seedInvoicingSubscriptionFormDraftEpic.d.ts +10 -6
  76. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/seedInvoicingSubscriptionFormDraftEpic.js +28 -7
  77. package/lib/view/invoicing/invoicingFormDraft.d.ts +54 -0
  78. package/lib/view/invoicing/invoicingFormDraft.js +72 -0
  79. package/lib/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +19 -6
  80. package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.d.ts +8 -5
  81. package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.js +43 -17
  82. package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.d.ts +4 -0
  83. package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.js +30 -5
  84. package/lib/view/notificationPreferencesView/notificationPreferencesViewState.d.ts +4 -3
  85. package/lib/view/settingsView/epic/fetchNotificationSettingsEpic.js +12 -2
  86. package/lib/view/settingsView/settingsViewPayload.d.ts +2 -0
  87. package/package.json +1 -1
@@ -7,14 +7,35 @@ const editInvoicingSubscriptionDetailViewReducer_1 = require("./editInvoicingSub
7
7
  const invoicingSubscriptionDetailHelper_1 = require("./invoicingSubscriptionDetailHelper");
8
8
  const invoicingSubscriptionFormConfig_1 = require("./invoicingSubscriptionFormConfig");
9
9
  /**
10
- * Seeds the edit draft from the record the moment the detail fetch lands, so
11
- * the form has its values before it mounts. The reducer ignores the seed when a
12
- * draft already exists, which is what stops a background refetch from
13
- * overwriting edits in progress.
10
+ * Seeds the edit draft from the record, so the form has its values before it
11
+ * mounts. The reducer ignores a seed once the draft holds the record, which is
12
+ * what stops a background refetch from overwriting edits in progress.
13
+ *
14
+ * Opening the page counts as a trigger, not just the detail response: the page
15
+ * fetch is cache-guarded, so re-opening a form for a record already loaded this
16
+ * session issues no request at all — and a draft cleared in between (by a save
17
+ * or a cancel) would have nothing left to seed it.
14
18
  */
15
- const seedInvoicingSubscriptionFormDraftEpic = (actions$, state$) => actions$.pipe((0, operators_1.filter)(editInvoicingSubscriptionDetailViewReducer_1.updateInvoicingSubscriptionDetail.match), (0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(([action, state]) => {
16
- const { id } = action.payload;
17
- const subscription = (0, invoicingSubscriptionDetailHelper_1.getInvoicingSubscriptionById)(state, id);
19
+ const seedInvoicingSubscriptionFormDraftEpic = (actions$, state$) => actions$.pipe((0, operators_1.filter)((action) => editInvoicingSubscriptionDetailViewReducer_1.updateInvoicingSubscriptionDetail.match(action) ||
20
+ editInvoicingSubscriptionDetailViewReducer_1.fetchInvoicingSubscriptionFormPage.match(action)), (0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(([action, state]) => {
21
+ const id = editInvoicingSubscriptionDetailViewReducer_1.updateInvoicingSubscriptionDetail.match(action)
22
+ ? action.payload.id
23
+ : action.payload.subscriptionId;
24
+ if (id == null) {
25
+ return rxjs_1.EMPTY;
26
+ }
27
+ // The entity alone does not say the whole record is loaded — the list
28
+ // endpoints merge their own list-shaped rows into the same store. Only a
29
+ // completed detail load does, which is also exactly when the page's
30
+ // cache-guarded fetch issues no request and this is the sole seed.
31
+ if (editInvoicingSubscriptionDetailViewReducer_1.fetchInvoicingSubscriptionFormPage.match(action) &&
32
+ state.editInvoicingSubscriptionDetailViewState.recordStateById[id]
33
+ ?.fetchState !== 'Completed') {
34
+ return rxjs_1.EMPTY;
35
+ }
36
+ // Nothing to seed from on a cold open; the detail response triggers this
37
+ // again once it lands.
38
+ const subscription = id != null ? (0, invoicingSubscriptionDetailHelper_1.getInvoicingSubscriptionById)(state, id) : undefined;
18
39
  if (subscription == null) {
19
40
  return rxjs_1.EMPTY;
20
41
  }
@@ -0,0 +1,54 @@
1
+ import { FetchState, ID } from '../../commonStateTypes/common';
2
+ /**
3
+ * A create/edit form's draft, plus where its values came from.
4
+ *
5
+ * The status is what lets an edit form tell "these are the record's values"
6
+ * from "these are whatever the form had at first render". It cannot be read off
7
+ * the presence of a draft, because the form writes drafts too: it pushes its
8
+ * whole state back on every change, so a change made before the record landed
9
+ * leaves a draft that looks seeded but holds nothing.
10
+ *
11
+ * `Not-Started` — written by the form, never filled from a record.
12
+ * `Completed` — filled from the loaded record; the form may have edited it since.
13
+ * `Error` — the record's fetch failed, so the form starts blank and a
14
+ * later successful fetch is still allowed to seed it.
15
+ */
16
+ export interface InvoicingFormDraft<TLocalData> {
17
+ fetchState: FetchState;
18
+ localData: TLocalData;
19
+ }
20
+ export type InvoicingFormDraftById<TLocalData> = Record<ID, InvoicingFormDraft<TLocalData>>;
21
+ /**
22
+ * The form's own write. Keeps the status: an edit on top of a seeded draft is
23
+ * still the record's draft, and an edit made before the record arrived must not
24
+ * pass for one.
25
+ */
26
+ export declare function applyInvoicingFormDraftEdit<TLocalData>(draftById: InvoicingFormDraftById<TLocalData>, id: ID, localData: TLocalData): void;
27
+ /**
28
+ * Fill the draft from the loaded record, once. A second seed is ignored so a
29
+ * late refetch cannot overwrite edits in progress — but a draft the form wrote
30
+ * before the record landed is replaced, since it was never the record's.
31
+ */
32
+ export declare function applyInvoicingFormDraftSeed<TLocalData>(draftById: InvoicingFormDraftById<TLocalData>, id: ID, localData: TLocalData): void;
33
+ /**
34
+ * Record that the fetch failed, so the form renders instead of waiting behind a
35
+ * skeleton that will never resolve.
36
+ *
37
+ * Mirrors the seed: a draft holding the record is left alone, since a
38
+ * background refetch failing is no reason to discard it, and anything else is
39
+ * marked failed. A draft the form wrote before the record landed keeps its
40
+ * values — there is no reason to throw away what the user typed — but stops
41
+ * counting as still loading, and a later successful fetch can still seed it.
42
+ */
43
+ export declare function applyInvoicingFormDraftFailure<TLocalData>(draftById: InvoicingFormDraftById<TLocalData>, id: ID, blankLocalData: TLocalData): void;
44
+ /** The draft's values, or the blank form when nothing is drafted yet. */
45
+ export declare function getInvoicingFormDraftLocalData<TLocalData>(draftById: InvoicingFormDraftById<TLocalData>, id: ID | undefined, blankLocalData: TLocalData): TLocalData;
46
+ /**
47
+ * Whether the form should still be showing its skeleton — the record has
48
+ * neither reached the draft nor failed trying.
49
+ *
50
+ * Settles once and stays settled, which is what keeps a later refetch from
51
+ * swapping a form the user is typing into back for a skeleton. Create mode
52
+ * (no id) has no record to wait for.
53
+ */
54
+ export declare function isInvoicingFormDraftPending<TLocalData>(draftById: InvoicingFormDraftById<TLocalData>, id: ID | undefined): boolean;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyInvoicingFormDraftEdit = applyInvoicingFormDraftEdit;
4
+ exports.applyInvoicingFormDraftSeed = applyInvoicingFormDraftSeed;
5
+ exports.applyInvoicingFormDraftFailure = applyInvoicingFormDraftFailure;
6
+ exports.getInvoicingFormDraftLocalData = getInvoicingFormDraftLocalData;
7
+ exports.isInvoicingFormDraftPending = isInvoicingFormDraftPending;
8
+ /**
9
+ * The form's own write. Keeps the status: an edit on top of a seeded draft is
10
+ * still the record's draft, and an edit made before the record arrived must not
11
+ * pass for one.
12
+ */
13
+ function applyInvoicingFormDraftEdit(draftById, id, localData) {
14
+ draftById[id] = {
15
+ fetchState: draftById[id]?.fetchState ?? 'Not-Started',
16
+ localData,
17
+ };
18
+ }
19
+ /**
20
+ * Fill the draft from the loaded record, once. A second seed is ignored so a
21
+ * late refetch cannot overwrite edits in progress — but a draft the form wrote
22
+ * before the record landed is replaced, since it was never the record's.
23
+ */
24
+ function applyInvoicingFormDraftSeed(draftById, id, localData) {
25
+ if (draftById[id]?.fetchState === 'Completed') {
26
+ return;
27
+ }
28
+ draftById[id] = { fetchState: 'Completed', localData };
29
+ }
30
+ /**
31
+ * Record that the fetch failed, so the form renders instead of waiting behind a
32
+ * skeleton that will never resolve.
33
+ *
34
+ * Mirrors the seed: a draft holding the record is left alone, since a
35
+ * background refetch failing is no reason to discard it, and anything else is
36
+ * marked failed. A draft the form wrote before the record landed keeps its
37
+ * values — there is no reason to throw away what the user typed — but stops
38
+ * counting as still loading, and a later successful fetch can still seed it.
39
+ */
40
+ function applyInvoicingFormDraftFailure(draftById, id, blankLocalData) {
41
+ const existing = draftById[id];
42
+ if (existing == null) {
43
+ draftById[id] = { fetchState: 'Error', localData: blankLocalData };
44
+ return;
45
+ }
46
+ if (existing.fetchState === 'Completed') {
47
+ return;
48
+ }
49
+ existing.fetchState = 'Error';
50
+ }
51
+ /** The draft's values, or the blank form when nothing is drafted yet. */
52
+ function getInvoicingFormDraftLocalData(draftById, id, blankLocalData) {
53
+ if (id == null) {
54
+ return blankLocalData;
55
+ }
56
+ return draftById[id]?.localData ?? blankLocalData;
57
+ }
58
+ /**
59
+ * Whether the form should still be showing its skeleton — the record has
60
+ * neither reached the draft nor failed trying.
61
+ *
62
+ * Settles once and stays settled, which is what keeps a later refetch from
63
+ * swapping a form the user is typing into back for a skeleton. Create mode
64
+ * (no id) has no record to wait for.
65
+ */
66
+ function isInvoicingFormDraftPending(draftById, id) {
67
+ if (id == null) {
68
+ return false;
69
+ }
70
+ const fetchState = draftById[id]?.fetchState;
71
+ return fetchState !== 'Completed' && fetchState !== 'Error';
72
+ }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.saveNotificationPreferencesEpic = void 0;
4
4
  const rxjs_1 = require("rxjs");
5
5
  const operators_1 = require("rxjs/operators");
6
+ const notificationRegistryState_1 = require("../../../entity/notificationRegistry/notificationRegistryState");
6
7
  const snackbarReducer_1 = require("../../../entity/snackbar/snackbarReducer");
7
8
  const responsePayload_1 = require("../../../responsePayload");
8
9
  const notificationPreferencesEndpoint_1 = require("../notificationPreferencesEndpoint");
@@ -16,9 +17,19 @@ const saveNotificationPreferencesEpic = (actions$, state$, zeniAPI) => actions$.
16
17
  // ignored, so stale success/failure actions cannot clobber the newer
17
18
  // batch's state. Server-side ordering is arrival-order LWW.
18
19
  (0, operators_1.switchMap)(([, state]) => {
19
- const overrides = (0, notificationPreferencesViewSelector_1.getNotificationLocalOverrides)(state);
20
- const preferencesPatch = (0, notificationPreferencesViewPayload_1.mapPreferencesToPayload)(overrides);
21
- if (Object.keys(preferencesPatch).length === 0) {
20
+ // A clean domain is omitted entirely: an empty patch is still a write against it.
21
+ const overridesByDomain = {};
22
+ const body = {};
23
+ notificationRegistryState_1.NOTIFICATION_DOMAINS.forEach((domain) => {
24
+ const overrides = (0, notificationPreferencesViewSelector_1.getNotificationLocalOverridesForDomain)(state, domain);
25
+ const preferencesPatch = (0, notificationPreferencesViewPayload_1.mapPreferencesToPayload)(overrides);
26
+ if (Object.keys(preferencesPatch).length === 0) {
27
+ return;
28
+ }
29
+ overridesByDomain[domain] = overrides;
30
+ body[`${domain}_preferences`] = preferencesPatch;
31
+ });
32
+ if (Object.keys(body).length === 0) {
22
33
  // No diff → do not dispatch a synthetic success (that would wipe
23
34
  // stored preferences via the reducer). Simply no-op.
24
35
  return rxjs_1.EMPTY;
@@ -31,7 +42,6 @@ const saveNotificationPreferencesEpic = (actions$, state$, zeniAPI) => actions$.
31
42
  // in-flight save's response (Success/Failure never dispatched, so
32
43
  // the counter never drained and future saves got their merge dropped).
33
44
  const cancelEpochAtDispatch = state.notificationPreferencesViewState.cancelEpoch;
34
- const body = { task_checklist_preferences: preferencesPatch };
35
45
  // Suppress the error snackbar when the response lands AFTER the user
36
46
  // pressed Cancel — the Failure reducer already swallows the state
37
47
  // update, so the toast would be a stale error for a save the user
@@ -60,8 +70,11 @@ const saveNotificationPreferencesEpic = (actions$, state$, zeniAPI) => actions$.
60
70
  if ((0, responsePayload_1.isSuccessResponse)(response)) {
61
71
  return (0, rxjs_1.of)((0, notificationPreferencesViewReducer_1.saveNotificationPreferencesSuccess)({
62
72
  cancelEpochAtDispatch,
63
- savedOverrides: overrides,
64
- serverEcho: response.data?.task_checklist_preferences,
73
+ savedOverridesByDomain: overridesByDomain,
74
+ serverEchoByDomain: {
75
+ task_checklist: response.data?.task_checklist_preferences,
76
+ commenting: response.data?.commenting_preferences,
77
+ },
65
78
  }));
66
79
  }
67
80
  const actions = [
@@ -1,30 +1,33 @@
1
- import { NotificationChannel, NotificationFrequency } from '../../entity/notificationRegistry/notificationRegistryState';
1
+ import { NotificationChannel, NotificationDomain, NotificationFrequency } from '../../entity/notificationRegistry/notificationRegistryState';
2
2
  import { ZeniAPIStatus } from '../../responsePayload';
3
3
  import { NotificationPreferencesPayload } from './notificationPreferencesViewPayload';
4
- import { NotificationPreferences, NotificationPreferencesViewState } from './notificationPreferencesViewState';
4
+ import { NotificationPreferencesByDomain, NotificationPreferencesViewState } from './notificationPreferencesViewState';
5
5
  export declare const initialState: NotificationPreferencesViewState;
6
6
  export interface SetFrequencyPayload {
7
7
  frequency: NotificationFrequency;
8
8
  groupId: string;
9
+ domain?: NotificationDomain;
9
10
  }
10
11
  export interface TogglePreferencePayload {
11
12
  channel: NotificationChannel;
12
13
  enabled: boolean;
13
14
  eventId: string;
15
+ domain?: NotificationDomain;
14
16
  }
15
17
  export interface SetGroupChannelMasterPayload {
16
18
  channel: NotificationChannel;
17
19
  enabled: boolean;
18
20
  groupId: string;
21
+ domain?: NotificationDomain;
19
22
  }
20
23
  export interface FetchNotificationPreferencesSuccessPayload {
21
- preferences: NotificationPreferencesPayload | undefined;
24
+ preferencesByDomain: Partial<Record<NotificationDomain, NotificationPreferencesPayload | undefined>>;
22
25
  saveEpochAtDispatch: number;
23
26
  }
24
27
  export interface SaveNotificationPreferencesSuccessPayload {
25
28
  cancelEpochAtDispatch: number;
26
- savedOverrides: NotificationPreferences;
27
- serverEcho: NotificationPreferencesPayload | undefined;
29
+ savedOverridesByDomain: NotificationPreferencesByDomain;
30
+ serverEchoByDomain: Partial<Record<NotificationDomain, NotificationPreferencesPayload | undefined>>;
28
31
  }
29
32
  export interface SaveNotificationPreferencesFailurePayload {
30
33
  cancelEpochAtDispatch: number;
@@ -3,16 +3,27 @@ var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.toggleEventChannel = exports.setGroupFrequency = exports.setGroupChannelMaster = exports.saveNotificationPreferencesSuccess = exports.saveNotificationPreferencesFailure = exports.saveNotificationPreferences = exports.fetchNotificationPreferencesSuccess = exports.clearNotificationPreferencesLocalOverrides = exports.clearAllNotificationPreferencesView = exports.initialState = void 0;
5
5
  const toolkit_1 = require("@reduxjs/toolkit");
6
+ const notificationRegistryState_1 = require("../../entity/notificationRegistry/notificationRegistryState");
6
7
  const rootActions_1 = require("../../rootActions");
7
8
  const notificationPreferencesViewPayload_1 = require("./notificationPreferencesViewPayload");
8
9
  const notificationPreferencesViewState_1 = require("./notificationPreferencesViewState");
9
10
  exports.initialState = {
10
11
  cancelEpoch: 0,
11
- localOverrides: (0, notificationPreferencesViewState_1.emptyNotificationPreferences)(),
12
- preferences: (0, notificationPreferencesViewState_1.emptyNotificationPreferences)(),
12
+ localOverridesByDomain: {},
13
+ preferencesByDomain: {},
13
14
  saveEpoch: 0,
14
15
  savePreferencesState: { fetchState: 'Not-Started' },
15
16
  };
17
+ // Lazy: a gate-off tenant never receives that domain's key at all.
18
+ const overridesFor = (byDomain, domain) => {
19
+ const existing = byDomain[domain];
20
+ if (existing != null) {
21
+ return existing;
22
+ }
23
+ const created = (0, notificationPreferencesViewState_1.emptyNotificationPreferences)();
24
+ byDomain[domain] = created;
25
+ return created;
26
+ };
16
27
  const mergePreferences = (base, patch) => {
17
28
  const eventEnabledByChannel = {
18
29
  ...base.eventEnabledByChannel,
@@ -150,7 +161,8 @@ const notificationPreferencesView = (0, toolkit_1.createSlice)({
150
161
  // Success/Failure never dispatches, subsequent legitimate saves
151
162
  // still work because their captured epoch matches the current one.
152
163
  draft.cancelEpoch += 1;
153
- draft.localOverrides = (0, notificationPreferencesViewState_1.emptyNotificationPreferences)();
164
+ // One shared Save/Cancel footer, so Cancel discards every domain.
165
+ draft.localOverridesByDomain = {};
154
166
  // Also drop `savePreferencesState` back to Not-Started so any
155
167
  // "Saving…" affordance bound to `getNotificationPreferencesSaveState`
156
168
  // clears on Cancel. Without this, a Cancel-during-save whose stale
@@ -167,9 +179,6 @@ const notificationPreferencesView = (0, toolkit_1.createSlice)({
167
179
  // them; the save-success path is the only clearer.
168
180
  // A `null`/`undefined` payload isn't a snapshot at all (e.g. the
169
181
  // envelope key was omitted); no-op instead of wiping preferences.
170
- if (action.payload.preferences == null) {
171
- return;
172
- }
173
182
  // Stale-GET guard (Cursor Bugbot 3632510237): if the fetch epic
174
183
  // captured a `saveEpoch` lower than the current one, a save has
175
184
  // succeeded since this GET dispatched — the response is carrying
@@ -179,7 +188,13 @@ const notificationPreferencesView = (0, toolkit_1.createSlice)({
179
188
  if (action.payload.saveEpochAtDispatch < draft.saveEpoch) {
180
189
  return;
181
190
  }
182
- draft.preferences = (0, notificationPreferencesViewPayload_1.mapPayloadToPreferences)(action.payload.preferences);
191
+ Object.entries(action.payload.preferencesByDomain).forEach(([domain, payload]) => {
192
+ if (payload == null) {
193
+ return;
194
+ }
195
+ draft.preferencesByDomain[domain] =
196
+ (0, notificationPreferencesViewPayload_1.mapPayloadToPreferences)(payload);
197
+ });
183
198
  },
184
199
  saveNotificationPreferences(draft) {
185
200
  draft.savePreferencesState = { fetchState: 'In-Progress' };
@@ -238,33 +253,44 @@ const notificationPreferencesView = (0, toolkit_1.createSlice)({
238
253
  // skip the `preferences` replace. Cursor Bugbot 3632510237.
239
254
  draft.saveEpoch += 1;
240
255
  draft.savePreferencesState = { fetchState: 'Completed' };
241
- const patch = action.payload.serverEcho == null
242
- ? action.payload.savedOverrides
243
- : (0, notificationPreferencesViewPayload_1.mapPayloadToPreferences)(action.payload.serverEcho);
244
- draft.preferences = mergePreferences(draft.preferences, patch);
245
- draft.localOverrides = subtractSavedOverrides(draft.localOverrides, action.payload.savedOverrides);
256
+ Object.entries(action.payload.savedOverridesByDomain).forEach(([rawDomain, savedOverrides]) => {
257
+ if (savedOverrides == null) {
258
+ return;
259
+ }
260
+ const domain = rawDomain;
261
+ const serverEcho = action.payload.serverEchoByDomain[domain];
262
+ const patch = serverEcho == null
263
+ ? savedOverrides
264
+ : (0, notificationPreferencesViewPayload_1.mapPayloadToPreferences)(serverEcho);
265
+ draft.preferencesByDomain[domain] = mergePreferences(draft.preferencesByDomain[domain] ?? (0, notificationPreferencesViewState_1.emptyNotificationPreferences)(), patch);
266
+ draft.localOverridesByDomain[domain] = subtractSavedOverrides(draft.localOverridesByDomain[domain] ??
267
+ (0, notificationPreferencesViewState_1.emptyNotificationPreferences)(), savedOverrides);
268
+ });
246
269
  },
247
270
  setGroupChannelMaster(draft, action) {
248
271
  // Mirror of `toggleEventChannel` but scoped to the group-channel master
249
272
  // held under `groupChannelEnabledByGroupId[groupId][channel]`.
250
273
  const { channel, enabled, groupId } = action.payload;
274
+ const overrides = overridesFor(draft.localOverridesByDomain, action.payload.domain ?? notificationRegistryState_1.DEFAULT_NOTIFICATION_DOMAIN);
251
275
  const groupMap = {
252
- ...(draft.localOverrides.groupChannelEnabledByGroupId[groupId] ?? {}),
276
+ ...(overrides.groupChannelEnabledByGroupId[groupId] ?? {}),
253
277
  };
254
278
  groupMap[channel] = enabled;
255
- draft.localOverrides.groupChannelEnabledByGroupId[groupId] = groupMap;
279
+ overrides.groupChannelEnabledByGroupId[groupId] = groupMap;
256
280
  },
257
281
  setGroupFrequency(draft, action) {
258
- draft.localOverrides.groupFrequencyByGroupId[action.payload.groupId] =
282
+ const overrides = overridesFor(draft.localOverridesByDomain, action.payload.domain ?? notificationRegistryState_1.DEFAULT_NOTIFICATION_DOMAIN);
283
+ overrides.groupFrequencyByGroupId[action.payload.groupId] =
259
284
  action.payload.frequency;
260
285
  },
261
286
  toggleEventChannel(draft, action) {
262
287
  const { channel, enabled, eventId } = action.payload;
288
+ const overrides = overridesFor(draft.localOverridesByDomain, action.payload.domain ?? notificationRegistryState_1.DEFAULT_NOTIFICATION_DOMAIN);
263
289
  const eventMap = {
264
- ...(draft.localOverrides.eventEnabledByChannel[eventId] ?? {}),
290
+ ...(overrides.eventEnabledByChannel[eventId] ?? {}),
265
291
  };
266
292
  eventMap[channel] = enabled;
267
- draft.localOverrides.eventEnabledByChannel[eventId] = eventMap;
293
+ overrides.eventEnabledByChannel[eventId] = eventMap;
268
294
  },
269
295
  },
270
296
  extraReducers: (builder) => {
@@ -1,7 +1,11 @@
1
1
  import { FetchStateAndError } from '../../commonStateTypes/common';
2
+ import { NotificationDomain } from '../../entity/notificationRegistry/notificationRegistryState';
2
3
  import { RootState } from '../../rootStateTypes';
3
4
  import { NotificationPreferences } from './notificationPreferencesViewState';
4
5
  export declare const getNotificationPreferencesSaveState: (state: RootState) => FetchStateAndError;
6
+ export declare const getEffectiveNotificationPreferencesForDomain: (state: RootState, domain: NotificationDomain) => NotificationPreferences;
5
7
  export declare const getEffectiveNotificationPreferences: (state: RootState) => NotificationPreferences;
8
+ export declare const getNotificationLocalOverridesForDomain: (state: RootState, domain: NotificationDomain) => NotificationPreferences;
6
9
  export declare const getNotificationLocalOverrides: (state: RootState) => NotificationPreferences;
10
+ export declare const hasUnsavedNotificationPreferencesForDomain: (state: RootState, domain: NotificationDomain) => boolean;
7
11
  export declare const hasUnsavedNotificationPreferences: (state: RootState) => boolean;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hasUnsavedNotificationPreferences = exports.getNotificationLocalOverrides = exports.getEffectiveNotificationPreferences = exports.getNotificationPreferencesSaveState = void 0;
3
+ exports.hasUnsavedNotificationPreferences = exports.hasUnsavedNotificationPreferencesForDomain = exports.getNotificationLocalOverrides = exports.getNotificationLocalOverridesForDomain = exports.getEffectiveNotificationPreferences = exports.getEffectiveNotificationPreferencesForDomain = exports.getNotificationPreferencesSaveState = void 0;
4
4
  const toolkit_1 = require("@reduxjs/toolkit");
5
+ const notificationRegistryState_1 = require("../../entity/notificationRegistry/notificationRegistryState");
6
+ const notificationPreferencesViewState_1 = require("./notificationPreferencesViewState");
5
7
  const getView = (state) => state.notificationPreferencesViewState;
6
8
  const getNotificationPreferencesSaveState = (state) => getView(state).savePreferencesState;
7
9
  exports.getNotificationPreferencesSaveState = getNotificationPreferencesSaveState;
@@ -9,7 +11,12 @@ exports.getNotificationPreferencesSaveState = getNotificationPreferencesSaveStat
9
11
  // stable reference while neither input changes — a plain function would
10
12
  // allocate a fresh object on every call and re-render every `useSelector`
11
13
  // consumer on any unrelated dispatch.
12
- exports.getEffectiveNotificationPreferences = (0, toolkit_1.createSelector)((state) => getView(state).preferences, (state) => getView(state).localOverrides, (preferences, localOverrides) => {
14
+ const EMPTY_PREFERENCES = (0, notificationPreferencesViewState_1.emptyNotificationPreferences)();
15
+ // One per domain: a shared createSelector has a cache of one and would thrash across two.
16
+ const effectivePreferencesSelectorByDomain = new Map();
17
+ const createEffectivePreferencesSelector = (domain) => (0, toolkit_1.createSelector)((state) => getView(state).preferencesByDomain[domain], (state) => getView(state).localOverridesByDomain[domain], (storedPreferences, storedOverrides) => {
18
+ const preferences = storedPreferences ?? EMPTY_PREFERENCES;
19
+ const localOverrides = storedOverrides ?? EMPTY_PREFERENCES;
13
20
  const mergedEventEnabledByChannel = { ...preferences.eventEnabledByChannel };
14
21
  Object.entries(localOverrides.eventEnabledByChannel).forEach(([eventId, channels]) => {
15
22
  mergedEventEnabledByChannel[eventId] = {
@@ -33,8 +40,26 @@ exports.getEffectiveNotificationPreferences = (0, toolkit_1.createSelector)((sta
33
40
  },
34
41
  };
35
42
  });
36
- const getNotificationLocalOverrides = (state) => getView(state).localOverrides;
43
+ const getEffectiveNotificationPreferencesForDomain = (state, domain) => {
44
+ let selector = effectivePreferencesSelectorByDomain.get(domain);
45
+ if (selector == null) {
46
+ selector = createEffectivePreferencesSelector(domain);
47
+ effectivePreferencesSelectorByDomain.set(domain, selector);
48
+ }
49
+ return selector(state);
50
+ };
51
+ exports.getEffectiveNotificationPreferencesForDomain = getEffectiveNotificationPreferencesForDomain;
52
+ const getEffectiveNotificationPreferences = (state) => (0, exports.getEffectiveNotificationPreferencesForDomain)(state, notificationRegistryState_1.DEFAULT_NOTIFICATION_DOMAIN);
53
+ exports.getEffectiveNotificationPreferences = getEffectiveNotificationPreferences;
54
+ const getNotificationLocalOverridesForDomain = (state, domain) => getView(state).localOverridesByDomain[domain] ?? EMPTY_PREFERENCES;
55
+ exports.getNotificationLocalOverridesForDomain = getNotificationLocalOverridesForDomain;
56
+ const getNotificationLocalOverrides = (state) => (0, exports.getNotificationLocalOverridesForDomain)(state, notificationRegistryState_1.DEFAULT_NOTIFICATION_DOMAIN);
37
57
  exports.getNotificationLocalOverrides = getNotificationLocalOverrides;
38
- exports.hasUnsavedNotificationPreferences = (0, toolkit_1.createSelector)((state) => getView(state).localOverrides, (localOverrides) => Object.keys(localOverrides.eventEnabledByChannel).length > 0 ||
58
+ const isDirty = (localOverrides) => Object.keys(localOverrides.eventEnabledByChannel).length > 0 ||
39
59
  Object.keys(localOverrides.groupChannelEnabledByGroupId).length > 0 ||
40
- Object.keys(localOverrides.groupFrequencyByGroupId).length > 0);
60
+ Object.keys(localOverrides.groupFrequencyByGroupId).length > 0;
61
+ const hasUnsavedNotificationPreferencesForDomain = (state, domain) => isDirty((0, exports.getNotificationLocalOverridesForDomain)(state, domain));
62
+ exports.hasUnsavedNotificationPreferencesForDomain = hasUnsavedNotificationPreferencesForDomain;
63
+ // Drives the shared page-level Save/Cancel footer.
64
+ const hasUnsavedNotificationPreferences = (state) => Object.values(getView(state).localOverridesByDomain).some((localOverrides) => localOverrides != null && isDirty(localOverrides));
65
+ exports.hasUnsavedNotificationPreferences = hasUnsavedNotificationPreferences;
@@ -1,5 +1,5 @@
1
1
  import { FetchStateAndError } from '../../commonStateTypes/common';
2
- import { NotificationChannel, NotificationFrequency } from '../../entity/notificationRegistry/notificationRegistryState';
2
+ import { NotificationChannel, NotificationDomain, NotificationFrequency } from '../../entity/notificationRegistry/notificationRegistryState';
3
3
  export interface NotificationPreferences {
4
4
  eventEnabledByChannel: Record<string, Partial<Record<NotificationChannel, boolean>>>;
5
5
  groupChannelEnabledByGroupId: Record<string, Partial<Record<NotificationChannel, boolean>>>;
@@ -7,9 +7,10 @@ export interface NotificationPreferences {
7
7
  }
8
8
  export interface NotificationPreferencesViewState {
9
9
  cancelEpoch: number;
10
- localOverrides: NotificationPreferences;
11
- preferences: NotificationPreferences;
10
+ localOverridesByDomain: NotificationPreferencesByDomain;
11
+ preferencesByDomain: NotificationPreferencesByDomain;
12
12
  saveEpoch: number;
13
13
  savePreferencesState: FetchStateAndError;
14
14
  }
15
+ export type NotificationPreferencesByDomain = Partial<Record<NotificationDomain, NotificationPreferences>>;
15
16
  export declare const emptyNotificationPreferences: () => NotificationPreferences;
@@ -41,11 +41,21 @@ const fetchNotificationSettingsEpic = (actions$, state$, zeniAPI) => actions$.pi
41
41
  // 67d6918e.
42
42
  updateActions.push((0, notificationRegistryReducer_1.updateNotificationRegistry)({
43
43
  registry: response.data.task_checklist_registry ?? undefined,
44
+ // An absent `<domain>_registry` means that domain's gate is off.
45
+ registriesByDomain: {
46
+ task_checklist: response.data.task_checklist_registry ?? undefined,
47
+ commenting: response.data.commenting_registry ?? undefined,
48
+ },
44
49
  registryEpochAtDispatch,
45
50
  }));
46
- if (response.data.task_checklist_preferences != null) {
51
+ // One action so the domains cannot land on different store epochs.
52
+ const preferencesByDomain = {
53
+ task_checklist: response.data.task_checklist_preferences ?? undefined,
54
+ commenting: response.data.commenting_preferences ?? undefined,
55
+ };
56
+ if (Object.values(preferencesByDomain).some((preferences) => preferences != null)) {
47
57
  updateActions.push((0, notificationPreferencesViewReducer_1.fetchNotificationPreferencesSuccess)({
48
- preferences: response.data.task_checklist_preferences,
58
+ preferencesByDomain,
49
59
  saveEpochAtDispatch,
50
60
  }));
51
61
  }
@@ -37,6 +37,8 @@ export interface NotificationSettingsPayload {
37
37
  pending_bill_approval_notification: RemindersPayload | null;
38
38
  pending_reimbursement_approval_notification: RemindersPayload | null;
39
39
  uncategorized_transaction_notification: RemindersPayload | null;
40
+ commenting_preferences?: NotificationPreferencesPayload;
41
+ commenting_registry?: NotificationRegistryPayload;
40
42
  month_end_email_recipient_email_ids?: string[];
41
43
  task_checklist_preferences?: NotificationPreferencesPayload;
42
44
  task_checklist_registry?: NotificationRegistryPayload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.2.49",
3
+ "version": "5.2.50-beta0ND",
4
4
  "description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/esm/index.js",