@zeniai/client-epic-state 5.2.3 → 5.2.5

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 (105) hide show
  1. package/lib/entity/invoicing/invoicingCommonPayload.d.ts +27 -0
  2. package/lib/entity/invoicing/invoicingCommonPayload.js +52 -1
  3. package/lib/epic.js +12 -0
  4. package/lib/esm/entity/invoicing/invoicingCommonPayload.js +50 -0
  5. package/lib/esm/epic.js +12 -0
  6. package/lib/esm/index.js +3 -3
  7. package/lib/esm/view/aiCfoView/aiCfoViewReducer.js +411 -9
  8. package/lib/esm/view/aiCfoView/aiCfoViewSelector.js +49 -0
  9. package/lib/esm/view/aiCfoView/epics/deleteSkillEpic.js +23 -0
  10. package/lib/esm/view/aiCfoView/epics/fetchSkillDetailEpic.js +35 -0
  11. package/lib/esm/view/aiCfoView/epics/fetchSkillVersionBodyEpic.js +45 -0
  12. package/lib/esm/view/aiCfoView/epics/fetchSkillVersionsEpic.js +37 -0
  13. package/lib/esm/view/aiCfoView/epics/fetchSkillsEpic.js +11 -6
  14. package/lib/esm/view/aiCfoView/epics/saveSkillEpic.js +72 -0
  15. package/lib/esm/view/aiCfoView/epics/setSkillVisibilityEpic.js +32 -0
  16. package/lib/esm/view/aiCfoView/epics/toSkillDetail.js +102 -0
  17. package/lib/esm/view/invoicing/auditView/auditViewPayload.js +11 -0
  18. package/lib/esm/view/invoicing/createInvoice/createInvoiceSelector.js +2 -0
  19. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +2 -1
  20. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +2 -1
  21. package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.js +2 -1
  22. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewPayload.js +7 -4
  23. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +3 -5
  24. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/invoicingSubscriptionFormConfig.js +0 -31
  25. package/lib/esm/view/invoicing/invoiceDetail/invoiceDetailSelector.js +2 -0
  26. package/lib/esm/view/invoicing/invoicingCatalogItemDetailView/invoicingCatalogItemDetailViewSelector.js +2 -1
  27. package/lib/esm/view/invoicing/invoicingConfigView/invoicingConfigViewReducer.js +5 -1
  28. package/lib/esm/view/invoicing/invoicingConfigView/invoicingConfigViewSelector.js +7 -0
  29. package/lib/esm/view/invoicing/invoicingCouponDetailView/invoicingCouponDetailViewSelector.js +2 -0
  30. package/lib/esm/view/invoicing/invoicingCreditNoteDetailView/invoicingCreditNoteDetailViewSelector.js +2 -0
  31. package/lib/esm/view/invoicing/invoicingCustomerDetailView/invoicingCustomerDetailViewSelector.js +2 -0
  32. package/lib/esm/view/invoicing/invoicingDunningCaseDetailView/invoicingDunningCaseDetailViewSelector.js +2 -0
  33. package/lib/esm/view/invoicing/invoicingSubscriptionDetailView/invoicingSubscriptionDetailViewSelector.js +2 -0
  34. package/lib/esm/view/invoicing/issueCreditNote/issueCreditNoteSelector.js +2 -1
  35. package/lib/esm/view/invoicing/recordPayment/recordPaymentSelector.js +2 -1
  36. package/lib/index.d.ts +5 -5
  37. package/lib/index.js +26 -12
  38. package/lib/view/aiCfoView/aiCfoViewPayload.d.ts +85 -0
  39. package/lib/view/aiCfoView/aiCfoViewReducer.d.ts +67 -2
  40. package/lib/view/aiCfoView/aiCfoViewReducer.js +413 -10
  41. package/lib/view/aiCfoView/aiCfoViewSelector.d.ts +7 -1
  42. package/lib/view/aiCfoView/aiCfoViewSelector.js +54 -0
  43. package/lib/view/aiCfoView/aiCfoViewState.d.ts +143 -1
  44. package/lib/view/aiCfoView/epics/deleteSkillEpic.d.ts +8 -0
  45. package/lib/view/aiCfoView/epics/deleteSkillEpic.js +27 -0
  46. package/lib/view/aiCfoView/epics/fetchSkillDetailEpic.d.ts +8 -0
  47. package/lib/view/aiCfoView/epics/fetchSkillDetailEpic.js +39 -0
  48. package/lib/view/aiCfoView/epics/fetchSkillVersionBodyEpic.d.ts +13 -0
  49. package/lib/view/aiCfoView/epics/fetchSkillVersionBodyEpic.js +49 -0
  50. package/lib/view/aiCfoView/epics/fetchSkillVersionsEpic.d.ts +8 -0
  51. package/lib/view/aiCfoView/epics/fetchSkillVersionsEpic.js +41 -0
  52. package/lib/view/aiCfoView/epics/fetchSkillsEpic.js +11 -6
  53. package/lib/view/aiCfoView/epics/saveSkillEpic.d.ts +14 -0
  54. package/lib/view/aiCfoView/epics/saveSkillEpic.js +76 -0
  55. package/lib/view/aiCfoView/epics/setSkillVisibilityEpic.d.ts +14 -0
  56. package/lib/view/aiCfoView/epics/setSkillVisibilityEpic.js +36 -0
  57. package/lib/view/aiCfoView/epics/toSkillDetail.d.ts +88 -0
  58. package/lib/view/aiCfoView/epics/toSkillDetail.js +110 -0
  59. package/lib/view/companyView/types/cockpitTabsID.d.ts +1 -1
  60. package/lib/view/companyView/types/cockpitTypes.d.ts +2 -2
  61. package/lib/view/invoicing/auditView/auditViewPayload.d.ts +2 -0
  62. package/lib/view/invoicing/auditView/auditViewPayload.js +11 -0
  63. package/lib/view/invoicing/couponView/couponViewState.d.ts +1 -1
  64. package/lib/view/invoicing/createInvoice/createInvoiceSelector.d.ts +6 -1
  65. package/lib/view/invoicing/createInvoice/createInvoiceSelector.js +2 -0
  66. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.d.ts +6 -1
  67. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +1 -0
  68. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.d.ts +6 -1
  69. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +1 -0
  70. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.d.ts +6 -1
  71. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.js +1 -0
  72. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewPayload.d.ts +6 -0
  73. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewPayload.js +6 -3
  74. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.d.ts +6 -2
  75. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewSelector.js +1 -3
  76. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/editInvoicingSubscriptionDetailViewState.d.ts +0 -1
  77. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/invoicingSubscriptionFormConfig.d.ts +0 -13
  78. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/invoicingSubscriptionFormConfig.js +1 -34
  79. package/lib/view/invoicing/invoiceDetail/invoiceDetailSelector.d.ts +6 -0
  80. package/lib/view/invoicing/invoiceDetail/invoiceDetailSelector.js +2 -0
  81. package/lib/view/invoicing/invoicingCatalogItemDetailView/invoicingCatalogItemDetailViewSelector.d.ts +6 -1
  82. package/lib/view/invoicing/invoicingCatalogItemDetailView/invoicingCatalogItemDetailViewSelector.js +1 -0
  83. package/lib/view/invoicing/invoicingConfigView/invoicingConfigViewPayload.d.ts +2 -1
  84. package/lib/view/invoicing/invoicingConfigView/invoicingConfigViewReducer.d.ts +1 -1
  85. package/lib/view/invoicing/invoicingConfigView/invoicingConfigViewReducer.js +4 -0
  86. package/lib/view/invoicing/invoicingConfigView/invoicingConfigViewSelector.d.ts +8 -0
  87. package/lib/view/invoicing/invoicingConfigView/invoicingConfigViewSelector.js +9 -1
  88. package/lib/view/invoicing/invoicingConfigView/invoicingConfigViewState.d.ts +3 -1
  89. package/lib/view/invoicing/invoicingCouponDetailView/invoicingCouponDetailViewSelector.d.ts +6 -0
  90. package/lib/view/invoicing/invoicingCouponDetailView/invoicingCouponDetailViewSelector.js +2 -0
  91. package/lib/view/invoicing/invoicingCreditNoteDetailView/invoicingCreditNoteDetailViewSelector.d.ts +6 -0
  92. package/lib/view/invoicing/invoicingCreditNoteDetailView/invoicingCreditNoteDetailViewSelector.js +2 -0
  93. package/lib/view/invoicing/invoicingCreditNoteListView/invoicingCreditNoteListViewState.d.ts +1 -1
  94. package/lib/view/invoicing/invoicingCustomerDetailView/invoicingCustomerDetailViewSelector.d.ts +6 -0
  95. package/lib/view/invoicing/invoicingCustomerDetailView/invoicingCustomerDetailViewSelector.js +2 -0
  96. package/lib/view/invoicing/invoicingDunningCaseDetailView/invoicingDunningCaseDetailViewSelector.d.ts +6 -0
  97. package/lib/view/invoicing/invoicingDunningCaseDetailView/invoicingDunningCaseDetailViewSelector.js +2 -0
  98. package/lib/view/invoicing/invoicingSubscriptionDetailView/invoicingSubscriptionDetailViewSelector.d.ts +6 -0
  99. package/lib/view/invoicing/invoicingSubscriptionDetailView/invoicingSubscriptionDetailViewSelector.js +2 -0
  100. package/lib/view/invoicing/issueCreditNote/issueCreditNoteSelector.d.ts +6 -1
  101. package/lib/view/invoicing/issueCreditNote/issueCreditNoteSelector.js +1 -0
  102. package/lib/view/invoicing/recordPayment/recordPaymentSelector.d.ts +6 -1
  103. package/lib/view/invoicing/recordPayment/recordPaymentSelector.js +1 -0
  104. package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
  105. package/package.json +1 -1
@@ -0,0 +1,37 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
4
+ import { fetchSkillVersions, fetchSkillVersionsFailure, fetchSkillVersionsSuccess, } from '../aiCfoViewReducer';
5
+ const toVersion = (payload) => ({
6
+ version: payload.version,
7
+ semver: payload.semver ?? null,
8
+ displayName: payload.display_name ?? null,
9
+ updatedAt: payload.updated_at ?? null,
10
+ // Defaulted defensively: these are booleans the server always sends, but a malformed
11
+ // 2xx should render a row as inactive-and-not-latest rather than as the live one.
12
+ isActive: payload.is_active === true,
13
+ isLatest: payload.is_latest === true,
14
+ instructionsCharCount: payload.instructions_char_count ?? 0,
15
+ });
16
+ /** GET /1.0/skills/{macroId}/versions — the history panel. */
17
+ export const fetchSkillVersionsEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchSkillVersions.match),
18
+ // switchMap: a read, and only the skill currently open matters. The reducer also
19
+ // drops responses whose macroId no longer matches, so a cancelled-but-in-flight
20
+ // request cannot render one skill's history under another's title.
21
+ switchMap((action) => {
22
+ const { macroId } = action.payload;
23
+ const url = `${zeniAPI.apiEndPoints.aiCfoMicroServiceBaseUrl}/1.0/skills/${encodeURIComponent(macroId)}/versions`;
24
+ return zeniAPI.getJSON(url).pipe(mergeMap((response) => {
25
+ if (isSuccessResponse(response) && response.data != null) {
26
+ return of(fetchSkillVersionsSuccess({
27
+ macroId,
28
+ // Guard the array: a malformed 2xx without `versions` must not crash .map.
29
+ versions: (response.data.versions ?? []).map(toVersion),
30
+ }));
31
+ }
32
+ return of(fetchSkillVersionsFailure({ macroId, error: response.status }));
33
+ }), catchError((error) => of(fetchSkillVersionsFailure({
34
+ macroId,
35
+ error: createZeniAPIStatus('Unexpected Error', 'Fetch skill versions REST API call failed', error instanceof Error ? { message: error.message } : undefined),
36
+ }))));
37
+ }));
@@ -2,6 +2,7 @@ import { of } from 'rxjs';
2
2
  import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
4
4
  import { fetchSkills, fetchSkillsFailure, fetchSkillsSuccess, } from '../aiCfoViewReducer';
5
+ import { toBaseSkill } from './toSkillDetail';
5
6
  const isNonBlankString = (value) => typeof value === 'string' && value.trim() !== '';
6
7
  // parameter_hints is a free-form {param: exampleValue(s)} map; flatten its non-blank
7
8
  // string values (a single string or an array of them) into display-ready "try: …"
@@ -27,14 +28,12 @@ const toExampleHints = (parameterHints) => {
27
28
  // a broken duplicate to the user — dedupe across parameter keys and array entries.
28
29
  return Array.from(new Set(hints));
29
30
  };
31
+ // Only the LIST route returns parameter_hints, so this is the one call site that has
32
+ // hints to spread in. Everything else about a summary comes from the shared base mapper,
33
+ // so a new `Skill` field cannot land here and be missed on the detail path.
30
34
  const toSkill = (skillSummaryPayload) => ({
31
- macroId: skillSummaryPayload.macro_id,
32
- name: skillSummaryPayload.name,
33
- displayName: skillSummaryPayload.display_name,
34
- description: skillSummaryPayload.description,
35
- semver: skillSummaryPayload.semver,
35
+ ...toBaseSkill(skillSummaryPayload),
36
36
  exampleHints: toExampleHints(skillSummaryPayload.parameter_hints),
37
- hasScripts: skillSummaryPayload.has_scripts,
38
37
  });
39
38
  /** GET {aiCfoMicroServiceBaseUrl}/1.0/skills — chat-visible skills for the `/` menu. */
40
39
  export const fetchSkillsEpic = (actions$,
@@ -52,6 +51,12 @@ switchMap(() => {
52
51
  // Guard the array: a malformed 2xx body without `skills` must not crash .map.
53
52
  return of(fetchSkillsSuccess({
54
53
  skills: (response.data.skills ?? []).map(toSkill),
54
+ // === true, not a truthy check: a body missing the field entirely
55
+ // must read as "off" so the Skills entry stays hidden by default.
56
+ skillsEnabled: response.data.skills_enabled === true,
57
+ // Same discipline: a body without the field must withhold the publish
58
+ // control, not grant it.
59
+ canPublish: response.data.can_publish === true,
55
60
  }));
56
61
  }
57
62
  return of(fetchSkillsFailure(response.status));
@@ -0,0 +1,72 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, filter, mergeMap } from 'rxjs/operators';
3
+ import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
4
+ import { fetchSkillVersions, fetchSkills, saveSkill, saveSkillFailure, saveSkillSuccess, } from '../aiCfoViewReducer';
5
+ import { getSkillDetail } from '../aiCfoViewSelector';
6
+ import { isSkillDetailPayload, toSkillDetail } from './toSkillDetail';
7
+ /**
8
+ * POST /1.0/skills (create) or PUT /1.0/skills/{macroId} (new version).
9
+ *
10
+ * One epic for both because they are the same action to a user — "save what I wrote" —
11
+ * and the server answers both with the detail payload. Splitting them would duplicate
12
+ * the payload mapping and the error handling for a difference the form does not have.
13
+ */
14
+ export const saveSkillEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(saveSkill.match),
15
+ // mergeMap, NOT switchMap: a save is a write. Cancelling an in-flight one because
16
+ // another started does not un-send it, it just hides the outcome — and for a
17
+ // create that means a skill exists that the user was never told about.
18
+ mergeMap((action) => {
19
+ const { draft, macroId } = action.payload;
20
+ const base = `${zeniAPI.apiEndPoints.aiCfoMicroServiceBaseUrl}/1.0/skills`;
21
+ const body = {
22
+ name: draft.name,
23
+ instructions: draft.instructions,
24
+ description: draft.description,
25
+ // Sent as the menu name too. The server derives the unique `/` slug from it, so
26
+ // letting the two drift would make the command stop matching the row's title.
27
+ display_name: draft.name,
28
+ // Spread, not `visibility: draft.visibility`: the server distinguishes an ABSENT
29
+ // visibility ("leave it alone") from an explicit null or 'private' ("make it
30
+ // private"). Sending the key with a null value would unpublish a tenant-wide
31
+ // skill on any edit by someone who cannot publish.
32
+ //
33
+ // `!= null`, not `!== null`: the type says this is always present, but the value
34
+ // arrives from a component, and an untyped caller omitting it would spread
35
+ // `{visibility: undefined}` — a key present with no value, which is the one shape
36
+ // the rule above is about. The looser check costs nothing and closes it.
37
+ ...(draft.visibility != null && { visibility: draft.visibility }),
38
+ };
39
+ const request$ = macroId != null && macroId !== ''
40
+ ? zeniAPI.putAndGetJSON(`${base}/${encodeURIComponent(macroId)}`, body)
41
+ : zeniAPI.postAndGetJSON(base, body);
42
+ return request$.pipe(mergeMap((response) => {
43
+ if (isSuccessResponse(response) &&
44
+ isSkillDetailPayload(response.data)) {
45
+ const saved = response.data;
46
+ const savedMacroId = saved.skill.macro_id;
47
+ const emitted = [
48
+ saveSkillSuccess({
49
+ macroId: savedMacroId,
50
+ // The response is a full detail read — carried through so an editor left
51
+ // on this skill shows what it just saved rather than the previous prompt.
52
+ detail: toSkillDetail(saved),
53
+ }),
54
+ // Refetch rather than splice the response in: a create changes the list's
55
+ // ordering and an update can change the name it is sorted by, so a local
56
+ // insert would put the row somewhere the next load moves it away from.
57
+ fetchSkills(),
58
+ ];
59
+ // A save adds a version, so the history panel is one behind — but only
60
+ // refetch when this skill is still the one on screen. `fetchSkillVersions`
61
+ // takes ownership of the versions slice, so firing it for a skill the user
62
+ // has already left would cancel the history of whatever they opened next.
63
+ if (getSkillDetail(state$.value).macroId === savedMacroId) {
64
+ emitted.push(fetchSkillVersions({ macroId: savedMacroId }));
65
+ }
66
+ return of(...emitted);
67
+ }
68
+ // A malformed 2xx is a failure, not a silent success: reporting "saved" when
69
+ // no skill came back leaves the user believing work was persisted.
70
+ return of(saveSkillFailure(response.status));
71
+ }), catchError((error) => of(saveSkillFailure(createZeniAPIStatus('Unexpected Error', 'Save skill REST API call failed', error instanceof Error ? { message: error.message } : undefined)))));
72
+ }));
@@ -0,0 +1,32 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, filter, mergeMap } from 'rxjs/operators';
3
+ import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
4
+ import { fetchSkills, setSkillVisibility, setSkillVisibilityFailure, setSkillVisibilitySuccess, } from '../aiCfoViewReducer';
5
+ import { isSkillDetailPayload, toSkillDetail } from './toSkillDetail';
6
+ /**
7
+ * PUT /1.0/skills/{macroId}/visibility — publish or unpublish, without a new version.
8
+ *
9
+ * Its own route rather than the prompt PUT: that one requires name and instructions, so a
10
+ * visibility-only change cannot be expressed through it, and sending the whole prompt back
11
+ * to flip one field would create a version whose prompt is identical to its parent's.
12
+ */
13
+ export const setSkillVisibilityEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(setSkillVisibility.match),
14
+ // mergeMap, like the other writes: cancelling a write in flight does not un-send it,
15
+ // it only hides the outcome — and for a publish that means a skill sitting in every
16
+ // colleague's menu with nobody told it got there.
17
+ mergeMap((action) => {
18
+ const { macroId, visibility } = action.payload;
19
+ const url = `${zeniAPI.apiEndPoints.aiCfoMicroServiceBaseUrl}/1.0/skills/${encodeURIComponent(macroId)}/visibility`;
20
+ return zeniAPI.putAndGetJSON(url, { visibility }).pipe(mergeMap((response) => {
21
+ if (isSuccessResponse(response) &&
22
+ isSkillDetailPayload(response.data)) {
23
+ return of(setSkillVisibilitySuccess({
24
+ macroId,
25
+ detail: toSkillDetail(response.data),
26
+ }),
27
+ // The list carries the badge that shows the audience, so it is now stale.
28
+ fetchSkills());
29
+ }
30
+ return of(setSkillVisibilityFailure(response.status));
31
+ }), catchError((error) => of(setSkillVisibilityFailure(createZeniAPIStatus('Unexpected Error', 'Set skill visibility REST API call failed', error instanceof Error ? { message: error.message } : undefined)))));
32
+ }));
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Every value `SkillVisibility` admits, as data so the converter below cannot drift from
3
+ * the type. Adding a tier to the union without adding it here is a compile error.
4
+ */
5
+ const SKILL_VISIBILITIES = ['private', 'tenant'];
6
+ /**
7
+ * A wire string narrowed to {@link SkillVisibility}, or null.
8
+ *
9
+ * A converter rather than passing the raw string through, because `Skill.visibility` is a
10
+ * union on the state type and this is the boundary where an unvalidated payload becomes
11
+ * it — the repo's rule about explicit converters at scalar boundaries instead of a bare
12
+ * cast.
13
+ *
14
+ * Unrecognised maps to NULL, not to `'private'`. Null already means "does not apply" for a
15
+ * Zeni default, and consumers show no audience badge for it — so an unknown tier renders
16
+ * as no badge. Defaulting to `'private'` would instead label a skill the whole tenant can
17
+ * run as author-only, which is a confident lie rather than a visible gap.
18
+ */
19
+ export const toSkillVisibility = (raw) => SKILL_VISIBILITIES.find((known) => known === raw) ?? null;
20
+ /**
21
+ * Everything about a skill summary EXCEPT `exampleHints`, which no shared mapper can
22
+ * honestly supply.
23
+ *
24
+ * Only the LIST route returns `parameter_hints`; the detail and write routes do not. A
25
+ * shared mapper hardcoding `exampleHints: []` would put a "try: …" line under a skill
26
+ * that has hints, or drop them from the menu — so the field is `Omit`ted and each caller
27
+ * spreads in what its own route actually knows.
28
+ *
29
+ * Returning `Omit<Skill, 'exampleHints'>` rather than duplicating the mapper is what
30
+ * makes a new `Skill` field land in one place: this function stops compiling until the
31
+ * field is mapped, and both callers inherit it. The previous shape — two full copies —
32
+ * is how `isMine` was nearly added to only one of them, which would have silently
33
+ * withheld the edit and delete controls on whichever surface was missed.
34
+ */
35
+ export const toBaseSkill = (payload) => ({
36
+ macroId: payload.macro_id,
37
+ name: payload.name,
38
+ displayName: payload.display_name,
39
+ description: payload.description,
40
+ semver: payload.semver,
41
+ hasScripts: payload.has_scripts === true,
42
+ // === true, so an absent field withholds the write controls rather than offering ones
43
+ // the API refuses.
44
+ isMine: payload.is_mine === true,
45
+ isOwnedByTenant: payload.is_owned_by_tenant === true,
46
+ updatedAt: payload.updated_at ?? null,
47
+ visibility: toSkillVisibility(payload.visibility),
48
+ });
49
+ /**
50
+ * Whether a 2xx body carries a prompt.
51
+ *
52
+ * Narrows to {@link PromptBody} and NOT to the whole payload, because the only consumer —
53
+ * the version-body read — dereferences `instructions` and nothing else. Asserting the
54
+ * whole shape here would claim `skill` was checked when it was not, and tying the read to
55
+ * it would fail a response whose prompt was fine and whose `skill` block was malformed,
56
+ * which is exactly what happened when this was one predicate instead of two.
57
+ *
58
+ * Takes `unknown`: the input is an unvalidated network body, and typing the parameter as
59
+ * the payload interface would assume the very thing being checked.
60
+ */
61
+ export const hasPromptBody = (data) => typeof data?.instructions ===
62
+ 'string';
63
+ /**
64
+ * Whether a 2xx body can build a whole {@link SkillDetail}.
65
+ *
66
+ * Narrows to the payload type itself, which is now truthful: the two fields this checks
67
+ * are the two the interface still declares required, and the other three are optional
68
+ * there. Stricter than {@link hasPromptBody} by exactly one field. Four epics read this shape
69
+ * and each used to guard it differently — `data != null`, `data?.skill != null`, and a
70
+ * real `instructions` check — so one malformed response became a failure in one place, an
71
+ * empty prompt in another, and `undefined` in a third.
72
+ */
73
+ export const isSkillDetailPayload = (data) => hasPromptBody(data) && data.skill != null;
74
+ /**
75
+ * The detail payload every write route answers with.
76
+ *
77
+ * Shared because three epics need it — save, visibility, and the detail read — and a
78
+ * fourth copy is how one of them quietly stops carrying a field the others do.
79
+ *
80
+ * The coercions are not defensive habit; the payload type makes them mandatory — the
81
+ * three optional fields cannot be assigned to a non-optional `SkillDetail` without one.
82
+ * Which fields get coerced and which do not is the deliberate part:
83
+ *
84
+ * - `instructions` is load-bearing and already proven, so it passes through. Defaulting a
85
+ * missing prompt to `''` is the one thing this module exists to prevent: the user
86
+ * cannot tell an empty prompt from a skill that has one, and may save over the real
87
+ * thing.
88
+ * - `instructionsCharCount` falls back to the prompt's own length, NOT to 0. A 12,000
89
+ * character prompt displayed as "0 characters" is a lie the user can see, and the
90
+ * prompt is right here to measure. Approximate only for astral-plane characters, where
91
+ * UTF-16 units and the server's count differ — a rounding error, against a guaranteed
92
+ * wrong answer.
93
+ * - `artifact` and `artifactTitle` degrade to empty. They render a preview card, so
94
+ * losing them costs a card; failing the whole read would cost the user the skill.
95
+ */
96
+ export const toSkillDetail = (payload) => ({
97
+ skill: { ...toBaseSkill(payload.skill), exampleHints: [] },
98
+ instructions: payload.instructions,
99
+ instructionsCharCount: payload.instructions_char_count ?? payload.instructions.length,
100
+ artifact: payload.artifact ?? '',
101
+ artifactTitle: payload.artifact_title ?? '',
102
+ });
@@ -39,6 +39,16 @@ function formatActor(actor, sourceLabel) {
39
39
  }
40
40
  return name;
41
41
  }
42
+ /**
43
+ * The backend's human-readable action text, or `undefined` when it carries none.
44
+ * Blank copy collapses to `undefined` so the UI's `humanizeToken` fallback runs
45
+ * instead of rendering an empty Action cell, and surrounding whitespace is
46
+ * trimmed so the stored value is what should be displayed verbatim.
47
+ */
48
+ function invoicingAuditActionLabel(actionLabel) {
49
+ const trimmed = actionLabel?.trim() ?? '';
50
+ return trimmed !== '' ? trimmed : undefined;
51
+ }
42
52
  export function mapAuditLogEntry(entry) {
43
53
  const sourceLabel = entry.source_label ?? '';
44
54
  return {
@@ -49,6 +59,7 @@ export function mapAuditLogEntry(entry) {
49
59
  source: entry.source,
50
60
  sourceLabel: sourceLabel || undefined,
51
61
  action: entry.action,
62
+ actionLabel: invoicingAuditActionLabel(entry.action_label),
52
63
  entityId: entry.entity_id,
53
64
  entityType: entry.entity_type,
54
65
  customerId: entry.customer_id ?? undefined,
@@ -1,5 +1,6 @@
1
1
  import { getAddressByAddressId } from '../../../entity/address/addressSelector';
2
2
  import { getInvoicingCatalogListView, getInvoicingCatalogPrices, } from '../invoicingCatalogListView/invoicingCatalogListViewSelector';
3
+ import { invoicingFieldHelpForGroup } from '../invoicingConfigView/invoicingConfigViewSelector';
3
4
  import { getInvoicingCustomerListView } from '../invoicingCustomerListView/invoicingCustomerListViewSelector';
4
5
  import { getInvoicingSettings } from '../settingsView/settingsViewSelector';
5
6
  /**
@@ -32,6 +33,7 @@ export const getCreateInvoiceFormView = (state) => {
32
33
  const createInvoiceState = state.invoicingCreateInvoiceState;
33
34
  const catalogListView = getInvoicingCatalogListView(state);
34
35
  return {
36
+ fieldHelp: invoicingFieldHelpForGroup(state, 'invoice'),
35
37
  customers: getInvoicingCustomerListView(state).customers.map((customer) => ({
36
38
  billingAddress: customer.billingAddressId != null
37
39
  ? getAddressByAddressId(state.addressState, customer.billingAddressId)
@@ -1,6 +1,6 @@
1
1
  import { createSelector } from '@reduxjs/toolkit';
2
2
  import { getInvoicingPlanById, getInvoicingPlansByIds, } from '../../../entity/invoicing/plan/planSelector';
3
- import { getInvoicingConfigView } from '../invoicingConfigView/invoicingConfigViewSelector';
3
+ import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
4
4
  import { INVOICING_CATALOG_APPLICABILITY_VALUES, INVOICING_CATALOG_PRICING_MODEL_VALUES, INVOICING_CATALOG_PRORATION_VALUES, INVOICING_CATALOG_TIER_TYPE_VALUES, entityToInvoicingCatalogItemFormLocalData, } from './invoicingCatalogItemFormConfig';
5
5
  export const getEditInvoicingCatalogItemDetailViewState = (state) => ({
6
6
  fetchState: state.editInvoicingCatalogItemDetailViewState.fetchState,
@@ -50,6 +50,7 @@ export const getInvoicingCatalogItemFormView = (state, id) => {
50
50
  : allApplicableItemOptions;
51
51
  const { billingPeriodUnitOptions, catalogItemTypeOptions, catalogTierTypeOptionsByPricingModel, catalogTrialEndActionOptions, catalogTrialUnitOptions, } = getInvoicingConfigView(state);
52
52
  return {
53
+ fieldHelp: invoicingFieldHelpForGroup(state, 'catalog'),
53
54
  applicabilityValues: INVOICING_CATALOG_APPLICABILITY_VALUES,
54
55
  applicableItemOptions,
55
56
  localData,
@@ -1,6 +1,6 @@
1
1
  import { getInvoicingCouponById } from '../../../entity/invoicing/coupon/couponSelector';
2
2
  import { toInvoicingFormOption, } from '../../../entity/invoicing/invoicingCommonPayload';
3
- import { getInvoicingConfigView } from '../invoicingConfigView/invoicingConfigViewSelector';
3
+ import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
4
4
  import { entityToInvoicingCouponFormLocalData, } from './editInvoicingCouponDetailViewState';
5
5
  /**
6
6
  * Bundled view for the discount create/edit form: the save fetch state/error,
@@ -37,6 +37,7 @@ export const getInvoicingCouponFormView = (state, couponId) => {
37
37
  : newCouponDraft;
38
38
  const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, } = getInvoicingConfigView(state);
39
39
  return {
40
+ fieldHelp: invoicingFieldHelpForGroup(state, 'discount'),
40
41
  ...base,
41
42
  applyOnOptions: discountApplyOnOptions.map(toInvoicingFormOption),
42
43
  discountTypeOptions: discountTypeOptions.map(toInvoicingFormOption),
@@ -1,6 +1,6 @@
1
1
  import { getInvoicingCustomerById } from '../../../entity/invoicing/invoicingCustomer/invoicingCustomerSelector';
2
2
  import { getFormattedAddress } from '../../addressView/addressViewSelector';
3
- import { getInvoicingConfigView } from '../invoicingConfigView/invoicingConfigViewSelector';
3
+ import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
4
4
  import { INVOICING_CUSTOMER_BILLING_ADDRESS_TYPE, entityToInvoicingCustomerFormLocalData, } from './invoicingCustomerFormConfig';
5
5
  /**
6
6
  * Bundled view for the customer create/edit form. Exposes the draft `localData`
@@ -29,6 +29,7 @@ export const getInvoicingCustomerFormView = (state, invoicingCustomerID) => {
29
29
  : undefined;
30
30
  const { customerTypeOptions, netTermDayOptions, taxabilityOptions } = getInvoicingConfigView(state);
31
31
  return {
32
+ fieldHelp: invoicingFieldHelpForGroup(state, 'customer'),
32
33
  customerTypeOptions,
33
34
  localData,
34
35
  mode,
@@ -1,5 +1,5 @@
1
1
  import { toAddressUpdatableInfoPayload } from '../../addressView/addressViewPayload';
2
- import { billingFrequencyToPeriod, invoicingSubscriptionTodayIso, toInvoicingSubscriptionBillingCycles, toInvoicingSubscriptionQuantity, } from './invoicingSubscriptionFormConfig';
2
+ import { invoicingSubscriptionTodayIso, toInvoicingSubscriptionBillingCycles, toInvoicingSubscriptionQuantity, } from './invoicingSubscriptionFormConfig';
3
3
  /**
4
4
  * Build the create/update subscription request body from the form draft. The
5
5
  * shipping address is sent as an object (captured via the shared Address
@@ -8,9 +8,14 @@ import { billingFrequencyToPeriod, invoicingSubscriptionTodayIso, toInvoicingSub
8
8
  * `remaining_billing_cycles` is omitted when the user left the field empty, so
9
9
  * the backend applies the plan's own `billing_cycles` (or renews forever when
10
10
  * the plan sets no limit) rather than being pinned to a value the form invented.
11
+ *
12
+ * `billing_period` / `billing_period_unit` are deliberately absent. Billing
13
+ * cadence is a property of the selected price point, not of the subscription,
14
+ * and the backend derives it from that price point when the fields are omitted.
15
+ * Sending them made every price point whose cadence the form could not express
16
+ * — a daily plan, for instance — fail with a 422.
11
17
  */
12
18
  export const localDataToSaveInvoicingSubscriptionPayload = (localData, shippingAddress) => {
13
- const frequency = billingFrequencyToPeriod(localData.billingFrequency);
14
19
  const items = [
15
20
  {
16
21
  item_price_code: localData.planPriceCode,
@@ -35,8 +40,6 @@ export const localDataToSaveInvoicingSubscriptionPayload = (localData, shippingA
35
40
  const remainingBillingCycles = toInvoicingSubscriptionBillingCycles(localData.remainingBillingCycles);
36
41
  return {
37
42
  auto_collection: localData.autoCollection ? 'on' : 'off',
38
- billing_period: frequency.period,
39
- billing_period_unit: frequency.unit,
40
43
  currency_code: localData.preferredCurrencyCode,
41
44
  customer_id: localData.customerId,
42
45
  started_at: startedAt !== '' ? startedAt : invoicingSubscriptionTodayIso(),
@@ -2,9 +2,9 @@ import { getInvoicingSubscriptionById } from '../../../entity/invoicing/invoicin
2
2
  import { getFormattedAddress } from '../../addressView/addressViewSelector';
3
3
  import { getInvoicingCouponListItems } from '../couponView/couponViewSelector';
4
4
  import { getInvoicingCatalogListView, getInvoicingCatalogPrices, } from '../invoicingCatalogListView/invoicingCatalogListViewSelector';
5
- import { getInvoicingConfigView } from '../invoicingConfigView/invoicingConfigViewSelector';
5
+ import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
6
6
  import { getInvoicingCustomerListView } from '../invoicingCustomerListView/invoicingCustomerListViewSelector';
7
- import { INVOICING_SUBSCRIPTION_BILLING_FREQUENCY_VALUES, INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE, entityToInvoicingSubscriptionFormLocalData, } from './invoicingSubscriptionFormConfig';
7
+ import { INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE, entityToInvoicingSubscriptionFormLocalData, } from './invoicingSubscriptionFormConfig';
8
8
  export const getEditInvoicingSubscriptionDetailViewState = (state, invoicingSubscriptionID) => {
9
9
  const { error, fetchState, savedSubscriptionId } = state.editInvoicingSubscriptionDetailViewState;
10
10
  const { subscriptionCommercialEditAllowedFromStatuses, subscriptionLifecycleActionOptions, subscriptionMetadataEditAllowedFromStatuses, } = getInvoicingConfigView(state);
@@ -42,9 +42,7 @@ export const getInvoicingSubscriptionFormView = (state, invoicingSubscriptionID)
42
42
  const planPrices = getInvoicingCatalogPrices(catalogListView.plans);
43
43
  const addonAndChargePrices = getInvoicingCatalogPrices(catalogListView.products);
44
44
  return {
45
- billingFrequencyValues: [
46
- ...INVOICING_SUBSCRIPTION_BILLING_FREQUENCY_VALUES,
47
- ],
45
+ fieldHelp: invoicingFieldHelpForGroup(state, 'subscription'),
48
46
  coupons: getInvoicingCouponListItems(state),
49
47
  customers: getInvoicingCustomerListView(state).customers,
50
48
  localData,
@@ -1,35 +1,6 @@
1
1
  import { dateNow } from '../../../zeniDayJS';
2
2
  /** `AddressType` key used when the subscription form opens the shared Address screen. */
3
3
  export const INVOICING_SUBSCRIPTION_SHIPPING_ADDRESS_TYPE = 'invoicing_subscription_shipping_address';
4
- /**
5
- * Canonical billing-frequency value set for the subscription form. CES owns the
6
- * values (and their period/unit mapping); web-components maps each value to a
7
- * localized label (same split as the customer form's value sets).
8
- */
9
- export const INVOICING_SUBSCRIPTION_BILLING_FREQUENCY_VALUES = [
10
- 'weekly',
11
- 'monthly',
12
- 'quarterly',
13
- 'half_yearly',
14
- 'yearly',
15
- ];
16
- const BILLING_FREQUENCY_TO_PERIOD = {
17
- weekly: { period: 1, unit: 'week' },
18
- monthly: { period: 1, unit: 'month' },
19
- quarterly: { period: 3, unit: 'month' },
20
- half_yearly: { period: 6, unit: 'month' },
21
- yearly: { period: 1, unit: 'year' },
22
- };
23
- /** Map a billing-frequency value to its API `billing_period` / `billing_period_unit`. */
24
- export const billingFrequencyToPeriod = (value) => BILLING_FREQUENCY_TO_PERIOD[value] ?? BILLING_FREQUENCY_TO_PERIOD.monthly;
25
- /** Map an existing subscription's period/unit back to a billing-frequency value. */
26
- export const periodToBillingFrequency = (period, unit) => {
27
- const match = INVOICING_SUBSCRIPTION_BILLING_FREQUENCY_VALUES.find((value) => {
28
- const mapped = BILLING_FREQUENCY_TO_PERIOD[value];
29
- return mapped.period === period && mapped.unit === unit;
30
- });
31
- return match ?? 'monthly';
32
- };
33
4
  const subscriptionItemPriceCodes = (subscription, itemType) => subscription?.subscriptionItems
34
5
  ?.filter((item) => item.itemType === itemType)
35
6
  .map((item) => item.itemPriceCode)
@@ -80,7 +51,6 @@ export const initialInvoicingSubscriptionFormLocalData = {
80
51
  addonPriceCodes: [],
81
52
  addonQuantities: {},
82
53
  autoCollection: true,
83
- billingFrequency: 'monthly',
84
54
  chargePriceCodes: [],
85
55
  chargeQuantities: {},
86
56
  couponId: '',
@@ -103,7 +73,6 @@ export const entityToInvoicingSubscriptionFormLocalData = (subscription) => {
103
73
  addonPriceCodes: subscriptionItemPriceCodes(subscription, 'addon'),
104
74
  addonQuantities: subscriptionItemQuantities(subscription, 'addon'),
105
75
  autoCollection: (subscription.autoCollection ?? 'on') === 'on',
106
- billingFrequency: periodToBillingFrequency(subscription.billingPeriod, subscription.billingPeriodUnit),
107
76
  chargePriceCodes: subscriptionItemPriceCodes(subscription, 'charge'),
108
77
  chargeQuantities: subscriptionItemQuantities(subscription, 'charge'),
109
78
  couponId: subscription.coupons?.[0]?.couponCode ??
@@ -2,10 +2,12 @@ import recordGet from 'lodash/get';
2
2
  import { getAddressByAddressId } from '../../../entity/address/addressSelector';
3
3
  import { getInvoiceById } from '../../../entity/invoicing/invoice/invoiceSelector';
4
4
  import { getInvoicingInvoiceActionState } from '../invoiceAction/invoiceActionSelector';
5
+ import { invoicingFieldHelpForGroup } from '../invoicingConfigView/invoicingConfigViewSelector';
5
6
  export const getInvoiceDetail = (state, invoiceId) => {
6
7
  const detail = recordGet(state.invoicingInvoiceDetailState.detailById, invoiceId, undefined);
7
8
  const invoice = getInvoiceById(state.invoicingInvoiceState, invoiceId);
8
9
  return {
10
+ fieldHelp: invoicingFieldHelpForGroup(state, 'invoice'),
9
11
  invoice,
10
12
  billingAddress: invoice?.billingAddressId != null
11
13
  ? getAddressByAddressId(state.addressState, invoice.billingAddressId)
@@ -1,9 +1,10 @@
1
1
  import { getInvoicingPlanById } from '../../../entity/invoicing/plan/planSelector';
2
- import { getInvoicingConfigView } from '../invoicingConfigView/invoicingConfigViewSelector';
2
+ import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
3
3
  export const getInvoicingCatalogItemDetail = (state, _kind, id) => {
4
4
  const { error, fetchState } = state.invoicingCatalogItemDetailViewState;
5
5
  const { billingPeriodUnitOptions, catalogItemLifecycleActionOptions, catalogItemTypeOptions, catalogProductLifecycleActionOptions, catalogTierTypeOptionsByPricingModel, catalogTrialEndActionOptions, catalogTrialUnitOptions, fetchState: configFetchState, } = getInvoicingConfigView(state);
6
6
  return {
7
+ fieldHelp: invoicingFieldHelpForGroup(state, 'catalog'),
7
8
  catalogItemLifecycleActionOptions,
8
9
  catalogProductLifecycleActionOptions,
9
10
  catalogTierTypeOptionsByPricingModel,
@@ -1,5 +1,5 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
- import { toAuditLogEntityRouteOption, toInvoicingCatalogItemLifecycleActionOption, toInvoicingCatalogProductLifecycleActionOption, toInvoicingDiscountLifecycleActionOption, toInvoicingDunningAction, toInvoicingNamedOption, toInvoicingNamedOptionsByKey, toInvoicingQboAccountMappingKindOption, toInvoicingSubscriptionEditFieldOption, toInvoicingSubscriptionLifecycleActionOption, } from '../../../entity/invoicing/invoicingCommonPayload';
2
+ import { toAuditLogEntityRouteOption, toInvoicingCatalogItemLifecycleActionOption, toInvoicingCatalogProductLifecycleActionOption, toInvoicingDiscountLifecycleActionOption, toInvoicingDunningAction, toInvoicingFieldHelp, toInvoicingNamedOption, toInvoicingNamedOptionsByKey, toInvoicingQboAccountMappingKindOption, toInvoicingSubscriptionEditFieldOption, toInvoicingSubscriptionLifecycleActionOption, } from '../../../entity/invoicing/invoicingCommonPayload';
3
3
  import { clearAll } from '../../../rootActions';
4
4
  export const initialState = {
5
5
  fetchState: 'Not-Started',
@@ -15,6 +15,7 @@ export const initialState = {
15
15
  catalogTrialUnitOptions: [],
16
16
  customerTypeOptions: [],
17
17
  dateFormatOptions: [],
18
+ fieldHelp: {},
18
19
  discountApplyOnOptions: [],
19
20
  discountDurationTypeOptions: [],
20
21
  discountTypeOptions: [],
@@ -75,6 +76,9 @@ export function mapInvoicingConfigPayloadToState(payload) {
75
76
  })),
76
77
  autoCollectionOptions: (payload.auto_collection_options ?? []).map(toInvoicingNamedOption),
77
78
  billingPeriodUnitOptions: (payload.billing_period_unit_options ?? []).map(toInvoicingNamedOption),
79
+ fieldHelp: payload.field_help != null
80
+ ? toInvoicingFieldHelp(payload.field_help)
81
+ : {},
78
82
  catalogTierTypeOptionsByPricingModel: payload.catalog_tier_type_options_by_pricing_model != null
79
83
  ? toInvoicingNamedOptionsByKey(payload.catalog_tier_type_options_by_pricing_model)
80
84
  : undefined,
@@ -17,3 +17,10 @@ export const getInvoicingOnboardingGateView = (state) => {
17
17
  status: deriveInvoicingOnboardingStatus(state.invoicingConfigViewState.isInvoicingTosAccepted, state.invoicingConfigViewState.isInvoicingEnabled),
18
18
  };
19
19
  };
20
+ /**
21
+ * One group's help copy, keyed by field code. Composed by the page-level view
22
+ * selectors rather than exported to the UI: a screen receives the slice for its
23
+ * own group, so no screen needs the config shape. Empty when the backend has
24
+ * not populated the group, which the UI reads as "render no icon".
25
+ */
26
+ export const invoicingFieldHelpForGroup = (state, group) => state.invoicingConfigViewState.fieldHelp[group] ?? {};
@@ -1,8 +1,10 @@
1
1
  import recordGet from 'lodash/get';
2
2
  import { getInvoicingCouponById } from '../../../entity/invoicing/coupon/couponSelector';
3
+ import { invoicingFieldHelpForGroup } from '../invoicingConfigView/invoicingConfigViewSelector';
3
4
  export const getInvoicingCouponDetail = (state, id) => {
4
5
  const detail = recordGet(state.invoicingCouponDetailViewState.detailById, id, undefined);
5
6
  return {
7
+ fieldHelp: invoicingFieldHelpForGroup(state, 'discount'),
6
8
  coupon: getInvoicingCouponById(state.invoicingCouponState, id),
7
9
  fetchState: detail?.fetchState ?? 'Not-Started',
8
10
  error: detail?.error,
@@ -1,8 +1,10 @@
1
1
  import { getInvoicingCreditNoteById as getInvoicingCreditNoteByIdFromState } from '../../../entity/invoicing/creditNote/creditNoteSelector';
2
+ import { invoicingFieldHelpForGroup } from '../invoicingConfigView/invoicingConfigViewSelector';
2
3
  export const getInvoicingCreditNoteById = (state, id) => getInvoicingCreditNoteByIdFromState(state.invoicingCreditNoteState, id);
3
4
  export const getInvoicingCreditNoteDetail = (state, invoicingCreditNoteID) => {
4
5
  const { error, fetchState } = state.invoicingCreditNoteDetailViewState;
5
6
  return {
7
+ fieldHelp: invoicingFieldHelpForGroup(state, 'credit_note'),
6
8
  creditNote: getInvoicingCreditNoteById(state, invoicingCreditNoteID),
7
9
  fetchState,
8
10
  error,
@@ -2,6 +2,7 @@ import recordGet from 'lodash/get';
2
2
  import { getInvoicesByIds } from '../../../entity/invoicing/invoice/invoiceSelector';
3
3
  import { getInvoicingCustomerById, invoicingCustomerPaymentMethods, } from '../../../entity/invoicing/invoicingCustomer/invoicingCustomerSelector';
4
4
  import { getInvoicingPaymentLinkSendState, getInvoicingPaymentMethodSaveState, getInvoicingPlaidLinkToken, getInvoicingSentPaymentLinkEmail, getInvoicingSentPaymentLinkMagicUrl, getInvoicingSetupIntent, getInvoicingSetupIntentFetchState, } from '../customerPaymentMethod/customerPaymentMethodSelector';
5
+ import { invoicingFieldHelpForGroup } from '../invoicingConfigView/invoicingConfigViewSelector';
5
6
  import { isStripeConnectedFromSettings } from '../settingsView/invoicingSettingsHelpers';
6
7
  import { getInvoicingSettings } from '../settingsView/settingsViewSelector';
7
8
  export const getInvoicingCustomerDetail = (state, invoicingCustomerID) => {
@@ -10,6 +11,7 @@ export const getInvoicingCustomerDetail = (state, invoicingCustomerID) => {
10
11
  const settings = getInvoicingSettings(state);
11
12
  const invoicingCustomer = getInvoicingCustomerById(invoicingCustomerState, invoicingCustomerID);
12
13
  return {
14
+ fieldHelp: invoicingFieldHelpForGroup(state, 'customer'),
13
15
  invoicingCustomer,
14
16
  invoices: getInvoicesByIds(state.invoicingInvoiceState, state.invoicingInvoiceListState.invoiceByTab[state.invoicingInvoiceListState.activeTab]?.invoiceIds ?? []),
15
17
  paymentMethods: invoicingCustomerPaymentMethods(invoicingCustomer),
@@ -1,5 +1,6 @@
1
1
  import { getInvoicingDunningCaseById as getInvoicingDunningCaseByIdFromState } from '../../../entity/invoicing/dunningCase/dunningCaseSelector';
2
2
  import { getInvoicingDunningActionState } from '../dunningAction/dunningActionSelector';
3
+ import { invoicingFieldHelpForGroup } from '../invoicingConfigView/invoicingConfigViewSelector';
3
4
  const EMAIL_PREVIEW_NOT_STARTED = {
4
5
  error: undefined,
5
6
  fetchState: 'Not-Started',
@@ -9,6 +10,7 @@ export const getInvoicingDunningCaseById = (state, id) => getInvoicingDunningCas
9
10
  export const getInvoicingDunningCaseDetail = (state, invoicingDunningCaseID) => {
10
11
  const { emailPreview, error, fetchState, id } = state.invoicingDunningCaseDetailViewState;
11
12
  return {
13
+ fieldHelp: invoicingFieldHelpForGroup(state, 'dunning'),
12
14
  actionState: getInvoicingDunningActionState(invoicingDunningCaseID)(state),
13
15
  dunningCase: getInvoicingDunningCaseById(state, invoicingDunningCaseID),
14
16
  emailPreview: id === invoicingDunningCaseID ? emailPreview : EMAIL_PREVIEW_NOT_STARTED,
@@ -1,9 +1,11 @@
1
1
  import { getInvoicingSubscriptionById as getInvoicingSubscriptionByIdFromState } from '../../../entity/invoicing/invoicingSubscription/invoicingSubscriptionSelector';
2
+ import { invoicingFieldHelpForGroup } from '../invoicingConfigView/invoicingConfigViewSelector';
2
3
  import { getInvoicingSubscriptionActionState } from '../subscriptionAction/subscriptionActionSelector';
3
4
  export const getInvoicingSubscriptionById = (state, id) => getInvoicingSubscriptionByIdFromState(state.invoicingSubscriptionState, id);
4
5
  export const getInvoicingSubscriptionDetail = (state, invoicingSubscriptionID) => {
5
6
  const { error, fetchState } = state.invoicingSubscriptionDetailViewState;
6
7
  return {
8
+ fieldHelp: invoicingFieldHelpForGroup(state, 'subscription'),
7
9
  actionState: getInvoicingSubscriptionActionState(invoicingSubscriptionID)(state),
8
10
  subscription: getInvoicingSubscriptionById(state, invoicingSubscriptionID),
9
11
  fetchState,