@mj-biz-apps/common-ng 5.43.0 → 5.45.0

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 (59) hide show
  1. package/dist/lib/components/related-chips/__tests__/related-links.test.d.ts +2 -0
  2. package/dist/lib/components/related-chips/__tests__/related-links.test.d.ts.map +1 -0
  3. package/dist/lib/components/related-chips/__tests__/related-links.test.js +183 -0
  4. package/dist/lib/components/related-chips/__tests__/related-links.test.js.map +1 -0
  5. package/dist/lib/components/related-chips/related-chips.component.d.ts +132 -0
  6. package/dist/lib/components/related-chips/related-chips.component.d.ts.map +1 -0
  7. package/dist/lib/components/related-chips/related-chips.component.js +241 -0
  8. package/dist/lib/components/related-chips/related-chips.component.js.map +1 -0
  9. package/dist/lib/components/related-chips/related-links.d.ts +151 -0
  10. package/dist/lib/components/related-chips/related-links.d.ts.map +1 -0
  11. package/dist/lib/components/related-chips/related-links.js +178 -0
  12. package/dist/lib/components/related-chips/related-links.js.map +1 -0
  13. package/dist/lib/components/selling-company-field/__tests__/selling-company-confirm.test.d.ts +2 -0
  14. package/dist/lib/components/selling-company-field/__tests__/selling-company-confirm.test.d.ts.map +1 -0
  15. package/dist/lib/components/selling-company-field/__tests__/selling-company-confirm.test.js +53 -0
  16. package/dist/lib/components/selling-company-field/__tests__/selling-company-confirm.test.js.map +1 -0
  17. package/dist/lib/components/selling-company-field/__tests__/selling-company-field.component.test.d.ts +2 -0
  18. package/dist/lib/components/selling-company-field/__tests__/selling-company-field.component.test.d.ts.map +1 -0
  19. package/dist/lib/components/selling-company-field/__tests__/selling-company-field.component.test.js +166 -0
  20. package/dist/lib/components/selling-company-field/__tests__/selling-company-field.component.test.js.map +1 -0
  21. package/dist/lib/components/selling-company-field/selling-company-confirm.d.ts +36 -0
  22. package/dist/lib/components/selling-company-field/selling-company-confirm.d.ts.map +1 -0
  23. package/dist/lib/components/selling-company-field/selling-company-confirm.js +54 -0
  24. package/dist/lib/components/selling-company-field/selling-company-confirm.js.map +1 -0
  25. package/dist/lib/components/selling-company-field/selling-company-field.component.d.ts +63 -0
  26. package/dist/lib/components/selling-company-field/selling-company-field.component.d.ts.map +1 -0
  27. package/dist/lib/components/selling-company-field/selling-company-field.component.js +221 -0
  28. package/dist/lib/components/selling-company-field/selling-company-field.component.js.map +1 -0
  29. package/dist/lib/data/__tests__/directory-headline.test.d.ts +2 -0
  30. package/dist/lib/data/__tests__/directory-headline.test.d.ts.map +1 -0
  31. package/dist/lib/data/__tests__/directory-headline.test.js +102 -0
  32. package/dist/lib/data/__tests__/directory-headline.test.js.map +1 -0
  33. package/dist/lib/data/__tests__/party-signal-store.test.d.ts +2 -0
  34. package/dist/lib/data/__tests__/party-signal-store.test.d.ts.map +1 -0
  35. package/dist/lib/data/__tests__/party-signal-store.test.js +98 -0
  36. package/dist/lib/data/__tests__/party-signal-store.test.js.map +1 -0
  37. package/dist/lib/data/common-settings.d.ts +18 -0
  38. package/dist/lib/data/common-settings.d.ts.map +1 -0
  39. package/dist/lib/data/common-settings.js +39 -0
  40. package/dist/lib/data/common-settings.js.map +1 -0
  41. package/dist/lib/data/directory-stats.d.ts +23 -1
  42. package/dist/lib/data/directory-stats.d.ts.map +1 -1
  43. package/dist/lib/data/directory-stats.js +40 -0
  44. package/dist/lib/data/directory-stats.js.map +1 -1
  45. package/dist/lib/data/directory-types.d.ts +34 -0
  46. package/dist/lib/data/directory-types.d.ts.map +1 -1
  47. package/dist/lib/data/party-signal-store.d.ts +39 -0
  48. package/dist/lib/data/party-signal-store.d.ts.map +1 -0
  49. package/dist/lib/data/party-signal-store.js +101 -0
  50. package/dist/lib/data/party-signal-store.js.map +1 -0
  51. package/dist/lib/pages/common-dashboard.page.d.ts +7 -7
  52. package/dist/lib/pages/common-dashboard.page.d.ts.map +1 -1
  53. package/dist/lib/pages/common-dashboard.page.js +228 -202
  54. package/dist/lib/pages/common-dashboard.page.js.map +1 -1
  55. package/dist/public-api.d.ts +6 -0
  56. package/dist/public-api.d.ts.map +1 -1
  57. package/dist/public-api.js +8 -0
  58. package/dist/public-api.js.map +1 -1
  59. package/package.json +2 -2
@@ -0,0 +1,151 @@
1
+ import type { RecordNavigationEvent } from '@memberjunction/ng-base-forms';
2
+ /**
3
+ * One relationship a record header offers to follow.
4
+ *
5
+ * Give it either a {@link RecordID} (the ordinary case — this record holds the id) or a
6
+ * {@link Filter} (the reverse case — the other record holds the id, as with an Order whose Deal
7
+ * points at it). A link with neither names nothing and is dropped.
8
+ */
9
+ export interface BizAppsRelatedLink {
10
+ /** Stable identity, used to track the chip and to correlate it back to the caller's own model. */
11
+ Key: string;
12
+ /**
13
+ * Entity that owns the linked record, as a name — e.g. `'MJ_BizApps_Orders: Order Headers'`.
14
+ *
15
+ * An entity that is not in the catalog produces NO chip. That covers both "the app that owns
16
+ * this record is not installed here" and "this user may not read it", and the two are
17
+ * deliberately one outcome: the entity catalog is already permission-filtered, and a reader who
18
+ * cannot open the record is no better served by a chip than one whose host never had the app.
19
+ * `bizapps-sales`' `OrdersIsInstalled()` draws the same line for the same reason.
20
+ */
21
+ EntityName: string;
22
+ /** Forward link: the linked record's primary key value, as held on the record being displayed. */
23
+ RecordID?: string | null;
24
+ /**
25
+ * Reverse link: a filter that selects the linked record. The first row wins, so pass something
26
+ * that identifies one record. Composed by the caller, which is the only party that knows the
27
+ * other entity's column names.
28
+ */
29
+ Filter?: string;
30
+ /** Names the relationship outright. Wins over {@link LabelPrefix}. */
31
+ Label?: string;
32
+ /**
33
+ * Builds the label as `<prefix> <entity's singular name>` — "Source Deal", "Source Order".
34
+ *
35
+ * For a polymorphic link, where the caller knows the ROLE the other record plays but not which
36
+ * entity fills it until the id is resolved. Hardcoding "Deal" is what this avoids.
37
+ */
38
+ LabelPrefix?: string;
39
+ /** Font Awesome class for a small leading icon. Optional. */
40
+ Icon?: string;
41
+ }
42
+ /** The part of `EntityInfo` a link needs. `EntityInfo` satisfies this. */
43
+ export interface RelatedLinkEntity {
44
+ Name: string;
45
+ BaseTableDisplayName: string;
46
+ NameField: {
47
+ Name: string;
48
+ } | null;
49
+ PrimaryKeys: {
50
+ Name: string;
51
+ }[];
52
+ }
53
+ /** The part of `RunViewResult` a link needs. `RunViewResult<Record<string, unknown>>` satisfies this. */
54
+ export interface RelatedLinkReadResult {
55
+ Success: boolean;
56
+ Results?: Record<string, unknown>[];
57
+ }
58
+ /** Reads one row for a link. The component supplies a provider-scoped `RunView`. */
59
+ export type RelatedLinkReader = (params: {
60
+ EntityName: string;
61
+ Fields: string[];
62
+ ExtraFilter: string;
63
+ MaxRows: number;
64
+ }) => Promise<RelatedLinkReadResult>;
65
+ /** A link that resolved to something openable. Anything that did not resolve is absent, not blank. */
66
+ export interface ResolvedRelatedChip {
67
+ Key: string;
68
+ /** What the relationship is: "Source Deal", "Order". */
69
+ Label: string;
70
+ /** The linked record's name. Empty ONLY when the record exists but could not be read — see below. */
71
+ Name: string;
72
+ Icon: string | null;
73
+ EntityName: string;
74
+ PrimaryKeyField: string;
75
+ RecordID: string;
76
+ /** Accessible name for the chip's button, composed once here so the template stays declarative. */
77
+ AriaLabel: string;
78
+ }
79
+ /**
80
+ * The filter that selects the linked record, or `null` when the link names nothing to select.
81
+ *
82
+ * A `RecordID` is preferred over a `Filter` when both are given: the caller holding the id is the
83
+ * more direct statement of the same fact.
84
+ */
85
+ export declare function FilterForRelatedLink(link: BizAppsRelatedLink, entity: RelatedLinkEntity): string | null;
86
+ /** The entity's primary key column, defaulting to `ID` as every other link in these apps does. */
87
+ export declare function PrimaryKeyFieldFor(entity: RelatedLinkEntity): string;
88
+ /**
89
+ * The chip's label: what the caller called it, else `<prefix> <entity>`, else the entity's own name.
90
+ *
91
+ * `BaseTableDisplayName` rather than `DisplayName` because a chip labels ONE record and the entity
92
+ * name is plural — it is what turns a Deals link into "Source Deal" without guessing at plurals.
93
+ */
94
+ export declare function LabelForRelatedLink(link: BizAppsRelatedLink, entity: RelatedLinkEntity): string;
95
+ /**
96
+ * Resolve one link to a chip, or to `null` for "show nothing".
97
+ *
98
+ * ## The two rules this function exists to hold
99
+ *
100
+ * **A CHIP NEVER SHOWS A RAW ID.** The whole point of the row is that a reader recognises what they
101
+ * are about to open. So the name is read from the entity's own name field, and an entity with no
102
+ * name field produces no chip rather than a GUID wearing a label.
103
+ *
104
+ * **A CHIP THAT CANNOT NAVIGATE IS NOT RENDERED.** A link that goes nowhere is worse than an absent
105
+ * one: it invites a click and spends the reader's trust. Five ways a link produces nothing — the
106
+ * entity is not in the catalog, the entity has no name field, the link names neither a record nor a
107
+ * filter, the read succeeded while matching zero rows, and the read was refused.
108
+ *
109
+ * ## Why a read that THREW is treated differently from one that FAILED or found nothing
110
+ *
111
+ * Three different facts, and the line between them is whether the server answered.
112
+ *
113
+ * **Zero rows from a successful read** means the record is not there — `bizapps-contracts` hit
114
+ * exactly this with CTR-000026, whose hand-typed provenance pair names a row that does not exist,
115
+ * and whose header rendered an "Open" button that navigated nowhere. No chip.
116
+ *
117
+ * **`Success: false`** is the server answering and refusing. In MJ that is the channel for a
118
+ * permission denial as much as for a bad filter: `GraphQLDataProvider.InternalRunView` throws when
119
+ * the transport fails and returns `Success: false` with an `ErrorMessage` when the request reached
120
+ * the server and was turned down. A reader who may not read Orders would otherwise get an
121
+ * "Order · Open" chip that navigates to a record that will not open — the dead link this component
122
+ * exists to prevent. So a refusal produces NO chip.
123
+ *
124
+ * **A thrown read** means we do not know: the record may well exist and simply be unreachable this
125
+ * moment. So a throw keeps the chip when we already hold the id to open — labelled, with "Open"
126
+ * where the name would go — and drops it for a reverse link, where the throw also cost us the id.
127
+ */
128
+ export declare function ResolveRelatedChip(link: BizAppsRelatedLink, entities: RelatedLinkEntity[], read: RelatedLinkReader): Promise<ResolvedRelatedChip | null>;
129
+ /** The part of a `MouseEvent` that decides where a chip opens. `MouseEvent` satisfies this. */
130
+ export interface RelatedChipClick {
131
+ ctrlKey: boolean;
132
+ metaKey: boolean;
133
+ }
134
+ /**
135
+ * The navigation event a chip click produces.
136
+ *
137
+ * Ctrl (Windows/Linux) and Cmd (macOS) both mean "new tab", which is the idiom MJ uses everywhere a
138
+ * record link is clicked.
139
+ *
140
+ * `OpenInNewTab` is OMITTED rather than set to `false` on a plain click, and that is the whole of
141
+ * what makes shift-click work. `NavigationService.shouldForceNewTab` returns `options.forceNewTab`
142
+ * whenever it is `!== undefined` and only otherwise consults its global shift-key state, and
143
+ * Explorer passes `forceNewTab: event.OpenInNewTab` straight through. An explicit `false` therefore
144
+ * does not mean "no opinion" — it means "same tab", and it suppresses the shift detection outright.
145
+ *
146
+ * `CompositeKey.FromKeyValuePair` rather than `FromID`: the latter assumes the target's primary key
147
+ * is literally called `ID`, which is true of these apps today and is not a thing a shared component
148
+ * gets to assume.
149
+ */
150
+ export declare function RelatedChipNavigation(chip: ResolvedRelatedChip, event: RelatedChipClick): RecordNavigationEvent;
151
+ //# sourceMappingURL=related-links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"related-links.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/related-chips/related-links.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAE3E;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IAC/B,kGAAkG;IAClG,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;;OAQG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,kGAAkG;IAClG,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,EAAE,MAAM,CAAC;IAC7B,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACnC,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACnC;AAED,yGAAyG;AACzG,MAAM,WAAW,qBAAqB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;CACvC;AAED,oFAAoF;AACpF,MAAM,MAAM,iBAAiB,GAAG,CAAC,MAAM,EAAE;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACnB,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;AAErC,sGAAsG;AACtG,MAAM,WAAW,mBAAmB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,qGAAqG;IACrG,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,mGAAmG;IACnG,SAAS,EAAE,MAAM,CAAC;CACrB;AAYD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAKvG;AAED,kGAAkG;AAClG,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAQ/F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAsB,kBAAkB,CACpC,IAAI,EAAE,kBAAkB,EACxB,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,IAAI,EAAE,iBAAiB,GACxB,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CA4CrC;AAmCD,+FAA+F;AAC/F,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,mBAAmB,EAAE,KAAK,EAAE,gBAAgB,GAAG,qBAAqB,CAU/G"}
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @fileoverview The resolve-and-hide rules behind `bizapps-related-chips`, kept out of the component.
3
+ *
4
+ * WHY THIS IS NOT IN THE COMPONENT. Everything here is a decision about whether a chip may be shown
5
+ * at all — "is the app installed", "does the record still exist", "did the read fail or did it
6
+ * succeed and find nothing" — and those are exactly the things that must be pinned by tests. A rule
7
+ * declared inside an Angular component cannot be exercised without standing up DI, which this
8
+ * package's node-environment vitest setup deliberately does not do. Same split as
9
+ * `bizapps-contracts`' `source-record-candidates.ts`.
10
+ *
11
+ * The dependencies below are narrowed to the shape each function actually reads rather than taking
12
+ * `IMetadataProvider` / `RunView`. `EntityInfo[]` and `RunViewResult` satisfy them structurally, so
13
+ * the component passes the real objects while a test passes a literal.
14
+ *
15
+ * @module @mj-biz-apps/common-ng
16
+ */
17
+ import { CompositeKey } from '@memberjunction/core';
18
+ /** Single-quote escaping for a value going into an `ExtraFilter`. */
19
+ function quote(value) {
20
+ return value.replace(/'/g, "''");
21
+ }
22
+ /** `true` for a value that actually names a record — not null, not undefined, not blank. */
23
+ function isPresent(value) {
24
+ return value !== null && value !== undefined && String(value).trim().length > 0;
25
+ }
26
+ /**
27
+ * The filter that selects the linked record, or `null` when the link names nothing to select.
28
+ *
29
+ * A `RecordID` is preferred over a `Filter` when both are given: the caller holding the id is the
30
+ * more direct statement of the same fact.
31
+ */
32
+ export function FilterForRelatedLink(link, entity) {
33
+ if (isPresent(link.RecordID)) {
34
+ return `${PrimaryKeyFieldFor(entity)} = '${quote(String(link.RecordID).trim())}'`;
35
+ }
36
+ return isPresent(link.Filter) ? link.Filter.trim() : null;
37
+ }
38
+ /** The entity's primary key column, defaulting to `ID` as every other link in these apps does. */
39
+ export function PrimaryKeyFieldFor(entity) {
40
+ return entity.PrimaryKeys?.[0]?.Name ?? 'ID';
41
+ }
42
+ /**
43
+ * The chip's label: what the caller called it, else `<prefix> <entity>`, else the entity's own name.
44
+ *
45
+ * `BaseTableDisplayName` rather than `DisplayName` because a chip labels ONE record and the entity
46
+ * name is plural — it is what turns a Deals link into "Source Deal" without guessing at plurals.
47
+ */
48
+ export function LabelForRelatedLink(link, entity) {
49
+ if (isPresent(link.Label)) {
50
+ return link.Label.trim();
51
+ }
52
+ if (isPresent(link.LabelPrefix)) {
53
+ return `${link.LabelPrefix.trim()} ${entity.BaseTableDisplayName}`;
54
+ }
55
+ return entity.BaseTableDisplayName;
56
+ }
57
+ /**
58
+ * Resolve one link to a chip, or to `null` for "show nothing".
59
+ *
60
+ * ## The two rules this function exists to hold
61
+ *
62
+ * **A CHIP NEVER SHOWS A RAW ID.** The whole point of the row is that a reader recognises what they
63
+ * are about to open. So the name is read from the entity's own name field, and an entity with no
64
+ * name field produces no chip rather than a GUID wearing a label.
65
+ *
66
+ * **A CHIP THAT CANNOT NAVIGATE IS NOT RENDERED.** A link that goes nowhere is worse than an absent
67
+ * one: it invites a click and spends the reader's trust. Five ways a link produces nothing — the
68
+ * entity is not in the catalog, the entity has no name field, the link names neither a record nor a
69
+ * filter, the read succeeded while matching zero rows, and the read was refused.
70
+ *
71
+ * ## Why a read that THREW is treated differently from one that FAILED or found nothing
72
+ *
73
+ * Three different facts, and the line between them is whether the server answered.
74
+ *
75
+ * **Zero rows from a successful read** means the record is not there — `bizapps-contracts` hit
76
+ * exactly this with CTR-000026, whose hand-typed provenance pair names a row that does not exist,
77
+ * and whose header rendered an "Open" button that navigated nowhere. No chip.
78
+ *
79
+ * **`Success: false`** is the server answering and refusing. In MJ that is the channel for a
80
+ * permission denial as much as for a bad filter: `GraphQLDataProvider.InternalRunView` throws when
81
+ * the transport fails and returns `Success: false` with an `ErrorMessage` when the request reached
82
+ * the server and was turned down. A reader who may not read Orders would otherwise get an
83
+ * "Order · Open" chip that navigates to a record that will not open — the dead link this component
84
+ * exists to prevent. So a refusal produces NO chip.
85
+ *
86
+ * **A thrown read** means we do not know: the record may well exist and simply be unreachable this
87
+ * moment. So a throw keeps the chip when we already hold the id to open — labelled, with "Open"
88
+ * where the name would go — and drops it for a reverse link, where the throw also cost us the id.
89
+ */
90
+ export async function ResolveRelatedChip(link, entities, read) {
91
+ const entity = entities?.find((e) => e.Name === link.EntityName) ?? null;
92
+ if (!entity) {
93
+ return null;
94
+ }
95
+ const nameField = entity.NameField?.Name;
96
+ if (!isPresent(nameField)) {
97
+ return null;
98
+ }
99
+ const filter = FilterForRelatedLink(link, entity);
100
+ if (!filter) {
101
+ return null;
102
+ }
103
+ const pkField = PrimaryKeyFieldFor(entity);
104
+ const label = LabelForRelatedLink(link, entity);
105
+ try {
106
+ const result = await read({
107
+ EntityName: entity.Name,
108
+ Fields: [pkField, nameField],
109
+ ExtraFilter: filter,
110
+ MaxRows: 1,
111
+ });
112
+ if (result?.Success === false) {
113
+ return null;
114
+ }
115
+ const row = result?.Success ? result.Results?.[0] : undefined;
116
+ if (result?.Success && !row) {
117
+ return null;
118
+ }
119
+ if (!row) {
120
+ return unreadableChip(link, entity, label, pkField);
121
+ }
122
+ const recordID = String(row[pkField] ?? link.RecordID ?? '');
123
+ if (!isPresent(recordID)) {
124
+ return null;
125
+ }
126
+ return chip(link, entity, label, pkField, recordID, String(row[nameField] ?? ''));
127
+ }
128
+ catch {
129
+ return unreadableChip(link, entity, label, pkField);
130
+ }
131
+ }
132
+ /**
133
+ * The chip for a record we could not read but can still open — only possible when the link carried
134
+ * the id itself. A reverse link has nothing left to open, so it renders nothing.
135
+ */
136
+ function unreadableChip(link, entity, label, pkField) {
137
+ return isPresent(link.RecordID) ? chip(link, entity, label, pkField, String(link.RecordID).trim(), '') : null;
138
+ }
139
+ function chip(link, entity, label, pkField, recordID, name) {
140
+ return {
141
+ Key: link.Key,
142
+ Label: label,
143
+ Name: name,
144
+ Icon: link.Icon ?? null,
145
+ EntityName: entity.Name,
146
+ PrimaryKeyField: pkField,
147
+ RecordID: recordID,
148
+ AriaLabel: name ? `Open ${label} ${name}` : `Open ${label}`,
149
+ };
150
+ }
151
+ /**
152
+ * The navigation event a chip click produces.
153
+ *
154
+ * Ctrl (Windows/Linux) and Cmd (macOS) both mean "new tab", which is the idiom MJ uses everywhere a
155
+ * record link is clicked.
156
+ *
157
+ * `OpenInNewTab` is OMITTED rather than set to `false` on a plain click, and that is the whole of
158
+ * what makes shift-click work. `NavigationService.shouldForceNewTab` returns `options.forceNewTab`
159
+ * whenever it is `!== undefined` and only otherwise consults its global shift-key state, and
160
+ * Explorer passes `forceNewTab: event.OpenInNewTab` straight through. An explicit `false` therefore
161
+ * does not mean "no opinion" — it means "same tab", and it suppresses the shift detection outright.
162
+ *
163
+ * `CompositeKey.FromKeyValuePair` rather than `FromID`: the latter assumes the target's primary key
164
+ * is literally called `ID`, which is true of these apps today and is not a thing a shared component
165
+ * gets to assume.
166
+ */
167
+ export function RelatedChipNavigation(chip, event) {
168
+ const navigation = {
169
+ Kind: 'record',
170
+ EntityName: chip.EntityName,
171
+ PrimaryKey: CompositeKey.FromKeyValuePair(chip.PrimaryKeyField, chip.RecordID),
172
+ };
173
+ if (event.ctrlKey || event.metaKey) {
174
+ navigation.OpenInNewTab = true;
175
+ }
176
+ return navigation;
177
+ }
178
+ //# sourceMappingURL=related-links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"related-links.js","sourceRoot":"","sources":["../../../../src/lib/components/related-chips/related-links.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=selling-company-confirm.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selling-company-confirm.test.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/selling-company-field/__tests__/selling-company-confirm.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { SellingCompanyConfirm } from '../selling-company-confirm';
3
+ describe('SellingCompanyConfirm', () => {
4
+ it('writes the default without asking', () => {
5
+ const confirm = new SellingCompanyConfirm('default-id');
6
+ expect(confirm.Propose('default-id')).toEqual({ Write: 'default-id', Ask: null });
7
+ expect(confirm.Pending).toBeNull();
8
+ });
9
+ it('treats the default as the same company whatever case the ID arrives in', () => {
10
+ const confirm = new SellingCompanyConfirm('AbC-123');
11
+ expect(confirm.Propose('abc-123')).toEqual({ Write: 'abc-123', Ask: null });
12
+ });
13
+ it('holds any other company for confirmation instead of writing it', () => {
14
+ const confirm = new SellingCompanyConfirm('default-id');
15
+ expect(confirm.Propose('other-id')).toEqual({ Write: null, Ask: 'other-id' });
16
+ expect(confirm.Pending).toBe('other-id');
17
+ });
18
+ it('writes the held value on confirm and clears the pending state', () => {
19
+ const confirm = new SellingCompanyConfirm('default-id');
20
+ confirm.Propose('other-id');
21
+ expect(confirm.Confirm()).toBe('other-id');
22
+ expect(confirm.Pending).toBeNull();
23
+ });
24
+ it('restores the caller-supplied previous value on revert', () => {
25
+ const confirm = new SellingCompanyConfirm('default-id');
26
+ confirm.Propose('other-id');
27
+ expect(confirm.Revert('previous-id')).toBe('previous-id');
28
+ expect(confirm.Pending).toBeNull();
29
+ });
30
+ it('replaces an unanswered question rather than stacking two', () => {
31
+ const confirm = new SellingCompanyConfirm('default-id');
32
+ confirm.Propose('first-id');
33
+ expect(confirm.Propose('second-id').Ask).toBe('second-id');
34
+ expect(confirm.Confirm()).toBe('second-id');
35
+ });
36
+ it('clears a pending question when the user goes back to the default', () => {
37
+ const confirm = new SellingCompanyConfirm('default-id');
38
+ confirm.Propose('other-id');
39
+ expect(confirm.Propose('default-id')).toEqual({ Write: 'default-id', Ask: null });
40
+ expect(confirm.Pending).toBeNull();
41
+ });
42
+ it('always allows clearing the field, since an empty value books nothing', () => {
43
+ const confirm = new SellingCompanyConfirm('default-id');
44
+ expect(confirm.Propose(null)).toEqual({ Write: null, Ask: null });
45
+ expect(confirm.Propose('')).toEqual({ Write: '', Ask: null });
46
+ });
47
+ it('never asks when the instance has no default configured', () => {
48
+ const confirm = new SellingCompanyConfirm(null);
49
+ expect(confirm.Propose('anything')).toEqual({ Write: 'anything', Ask: null });
50
+ expect(confirm.Pending).toBeNull();
51
+ });
52
+ });
53
+ //# sourceMappingURL=selling-company-confirm.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selling-company-confirm.test.js","sourceRoot":"","sources":["../../../../../src/lib/components/selling-company-field/__tests__/selling-company-confirm.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import '@angular/compiler';
2
+ //# sourceMappingURL=selling-company-field.component.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selling-company-field.component.test.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/selling-company-field/__tests__/selling-company-field.component.test.ts"],"names":[],"mappings":"AAAA,OAAO,mBAAmB,CAAC"}
@@ -0,0 +1,166 @@
1
+ import '@angular/compiler';
2
+ import { ChangeDetectorRef, Injector, runInInjectionContext } from '@angular/core';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ const H = vi.hoisted(() => ({
5
+ settingValue: 'default-co',
6
+ employeeID: null,
7
+ companies: [
8
+ { ID: 'default-co', Name: 'Default Selling Co' },
9
+ { ID: 'other-co', Name: 'Other Co' },
10
+ { ID: 'employee-co', Name: 'Employee Co' },
11
+ ],
12
+ employeeRows: [],
13
+ runView: vi.fn(),
14
+ }));
15
+ vi.mock('@memberjunction/core', async (importOriginal) => {
16
+ const actual = await importOriginal();
17
+ class MockMetadata {
18
+ static Provider = {};
19
+ get CurrentUser() {
20
+ return { ID: 'u1', EmployeeID: H.employeeID };
21
+ }
22
+ get Applications() {
23
+ return [{ ID: 'app-common', Name: 'Common' }];
24
+ }
25
+ }
26
+ return {
27
+ ...actual,
28
+ Metadata: MockMetadata,
29
+ RunView: { FromMetadataProvider: () => ({ RunView: H.runView }) },
30
+ };
31
+ });
32
+ vi.mock('@memberjunction/core-entities', async (importOriginal) => ({
33
+ ...(await importOriginal()),
34
+ ApplicationSettingEngine: {
35
+ Instance: {
36
+ Config: async () => undefined,
37
+ GetSetting: (name) => (name === 'DefaultSellingCompanyID' ? H.settingValue : undefined),
38
+ },
39
+ },
40
+ }));
41
+ vi.mock('@memberjunction/ng-base-forms', () => ({ BaseFormsModule: class {
42
+ } }));
43
+ import { SellingCompanyFieldComponent } from '../selling-company-field.component';
44
+ function record(isSaved, companyID = null) {
45
+ const values = { CompanyID: companyID };
46
+ return {
47
+ IsSaved: isSaved,
48
+ values,
49
+ Get: (field) => values[field],
50
+ Set: (field, value) => {
51
+ values[field] = value;
52
+ },
53
+ };
54
+ }
55
+ /**
56
+ * The component resolves ChangeDetectorRef with `inject()`, which needs an injection context.
57
+ * A one-provider injector gives it one without standing up TestBed — these are unit tests of the
58
+ * default-and-confirm behaviour, not of rendering.
59
+ */
60
+ async function build(rec) {
61
+ const injector = Injector.create({
62
+ providers: [{ provide: ChangeDetectorRef, useValue: { markForCheck: () => undefined } }],
63
+ });
64
+ const component = runInInjectionContext(injector, () => new SellingCompanyFieldComponent());
65
+ component.Record = rec;
66
+ await component.ngOnInit();
67
+ return component;
68
+ }
69
+ describe('SellingCompanyFieldComponent', () => {
70
+ beforeEach(() => {
71
+ H.settingValue = 'default-co';
72
+ H.employeeID = null;
73
+ H.employeeRows = [];
74
+ H.runView.mockImplementation(async (params) => params.EntityName === 'MJ: Employees'
75
+ ? { Success: true, Results: H.employeeRows }
76
+ : { Success: true, Results: H.companies });
77
+ });
78
+ it('defaults a new record to the configured company', async () => {
79
+ const rec = record(false);
80
+ await build(rec);
81
+ expect(rec.values['CompanyID']).toBe('default-co');
82
+ });
83
+ it('prefers the user own company over the configured setting', async () => {
84
+ H.employeeID = 'emp-1';
85
+ H.employeeRows = [{ CompanyID: 'employee-co' }];
86
+ const rec = record(false);
87
+ const component = await build(rec);
88
+ expect(rec.values['CompanyID']).toBe('employee-co');
89
+ expect(component.DefaultName).toBe('Employee Co');
90
+ });
91
+ it('ignores an employee company this instance does not know', async () => {
92
+ H.employeeID = 'emp-1';
93
+ H.employeeRows = [{ CompanyID: 'some-unknown-co' }];
94
+ const rec = record(false);
95
+ await build(rec);
96
+ expect(rec.values['CompanyID']).toBe('default-co');
97
+ });
98
+ it('never overwrites a company already on the record', async () => {
99
+ const rec = record(false, 'other-co');
100
+ await build(rec);
101
+ expect(rec.values['CompanyID']).toBe('other-co');
102
+ });
103
+ it('leaves a saved record alone', async () => {
104
+ const rec = record(true);
105
+ await build(rec);
106
+ expect(rec.values['CompanyID']).toBeNull();
107
+ });
108
+ it('writes nothing and asks nothing when the instance configured no default', async () => {
109
+ H.settingValue = null;
110
+ const rec = record(false);
111
+ const component = await build(rec);
112
+ expect(rec.values['CompanyID']).toBeNull();
113
+ rec.Set('CompanyID', 'other-co');
114
+ component.OnValueChange('other-co');
115
+ expect(component.AskName).toBeNull();
116
+ expect(rec.values['CompanyID']).toBe('other-co');
117
+ });
118
+ it('holds another company, restoring the previous value while the question is open', async () => {
119
+ const rec = record(false);
120
+ const component = await build(rec);
121
+ rec.Set('CompanyID', 'other-co'); // mj-form-field writes before it emits
122
+ component.OnValueChange('other-co');
123
+ expect(component.AskName).toBe('Other Co');
124
+ expect(component.DefaultName).toBe('Default Selling Co');
125
+ expect(rec.values['CompanyID']).toBe('default-co');
126
+ });
127
+ it('writes the held company on confirm', async () => {
128
+ const rec = record(false);
129
+ const component = await build(rec);
130
+ rec.Set('CompanyID', 'other-co');
131
+ component.OnValueChange('other-co');
132
+ component.OnConfirm();
133
+ expect(rec.values['CompanyID']).toBe('other-co');
134
+ expect(component.AskName).toBeNull();
135
+ });
136
+ it('keeps the previous company on revert', async () => {
137
+ const rec = record(false);
138
+ const component = await build(rec);
139
+ rec.Set('CompanyID', 'other-co');
140
+ component.OnValueChange('other-co');
141
+ component.OnRevert();
142
+ expect(rec.values['CompanyID']).toBe('default-co');
143
+ expect(component.AskName).toBeNull();
144
+ });
145
+ it('asks again after a confirmed choice, using the confirmed value as what to revert to', async () => {
146
+ const rec = record(false);
147
+ const component = await build(rec);
148
+ rec.Set('CompanyID', 'other-co');
149
+ component.OnValueChange('other-co');
150
+ component.OnConfirm();
151
+ rec.Set('CompanyID', 'employee-co');
152
+ component.OnValueChange('employee-co');
153
+ expect(component.AskName).toBe('Employee Co');
154
+ component.OnRevert();
155
+ expect(rec.values['CompanyID']).toBe('other-co');
156
+ });
157
+ it('names a company by ID rather than hiding the question when the company list cannot be read', async () => {
158
+ H.runView.mockImplementation(async () => ({ Success: false, ErrorMessage: 'denied', Results: [] }));
159
+ const rec = record(false);
160
+ const component = await build(rec);
161
+ rec.Set('CompanyID', 'other-co');
162
+ component.OnValueChange('other-co');
163
+ expect(component.AskName).toBe('other-co');
164
+ });
165
+ });
166
+ //# sourceMappingURL=selling-company-field.component.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selling-company-field.component.test.js","sourceRoot":"","sources":["../../../../../src/lib/components/selling-company-field/__tests__/selling-company-field.component.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Which selling-company choices go straight through and which pause for a confirmation.
3
+ *
4
+ * The selling company decides which legal entity books the revenue on an order or a contract, and
5
+ * the picker can offer dozens of companies whose names differ by a word. Getting it wrong is silent
6
+ * — the record looks correct afterwards. So the default is written without ceremony and anything
7
+ * else is held until the user says yes.
8
+ *
9
+ * Pure and synchronous on purpose: the rule is the part worth testing, and it is testable here
10
+ * without a record, a provider or a rendered field.
11
+ */
12
+ export declare class SellingCompanyConfirm {
13
+ private readonly defaultID;
14
+ private pending;
15
+ /**
16
+ * @param defaultID the resolved default company, or null when this instance has none
17
+ * configured — in which case nothing is ever held back, because there is no
18
+ * expectation to depart from.
19
+ */
20
+ constructor(defaultID: string | null);
21
+ /** The choice currently awaiting an answer, or null when nothing is pending. */
22
+ get Pending(): string | null;
23
+ /**
24
+ * Offer a choice. Returns what to write now (`Write`) and what to ask about (`Ask`); exactly
25
+ * one of them is non-null. Clearing the field is always allowed — an empty value books nothing.
26
+ */
27
+ Propose(id: string | null): {
28
+ Write: string | null;
29
+ Ask: string | null;
30
+ };
31
+ /** The user said yes. Returns the value to write. */
32
+ Confirm(): string | null;
33
+ /** The user said no, or moved on without answering. Returns the value to restore. */
34
+ Revert(previous: string | null): string | null;
35
+ }
36
+ //# sourceMappingURL=selling-company-confirm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selling-company-confirm.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/selling-company-field/selling-company-confirm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,qBAAa,qBAAqB;IAQlB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAPtC,OAAO,CAAC,OAAO,CAAuB;IAEtC;;;;OAIG;gBAC0B,SAAS,EAAE,MAAM,GAAG,IAAI;IAErD,gFAAgF;IAChF,IAAW,OAAO,IAAI,MAAM,GAAG,IAAI,CAElC;IAED;;;OAGG;IACI,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;IAS/E,qDAAqD;IAC9C,OAAO,IAAI,MAAM,GAAG,IAAI;IAM/B,qFAAqF;IAC9E,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI;CAIxD"}