@globalpayments-private/gpds-pattern-web-react 0.0.0-stage → 0.1.1-pr.50.g2356b27

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 (40) hide show
  1. package/dist/components.d.ts +12 -0
  2. package/dist/components.js +13 -0
  3. package/dist/composition/admin.d.ts +60 -0
  4. package/dist/composition/admin.js +29 -0
  5. package/dist/composition/children.d.ts +3 -0
  6. package/dist/composition/children.js +7 -0
  7. package/dist/composition/custom-routes.d.ts +88 -0
  8. package/dist/composition/custom-routes.js +76 -0
  9. package/dist/composition/index.d.ts +55 -0
  10. package/dist/composition/index.js +48 -0
  11. package/dist/composition/list.d.ts +220 -0
  12. package/dist/composition/list.js +90 -0
  13. package/dist/composition/record.d.ts +137 -0
  14. package/dist/composition/record.js +257 -0
  15. package/dist/composition/reference-many.d.ts +86 -0
  16. package/dist/composition/reference-many.js +60 -0
  17. package/dist/composition/reference.d.ts +13 -0
  18. package/dist/composition/reference.js +17 -0
  19. package/dist/composition/resource.d.ts +36 -0
  20. package/dist/composition/resource.js +52 -0
  21. package/dist/composition/simple-form.d.ts +96 -0
  22. package/dist/composition/simple-form.js +82 -0
  23. package/dist/gp-admin.d.ts +9 -0
  24. package/dist/gp-admin.js +20 -0
  25. package/dist/gp-pattern-list.d.ts +11 -0
  26. package/dist/gp-pattern-list.js +22 -0
  27. package/dist/gp-pattern-record.d.ts +12 -0
  28. package/dist/gp-pattern-record.js +23 -0
  29. package/dist/gp-pattern-reference-input.d.ts +8 -0
  30. package/dist/gp-pattern-reference-input.js +19 -0
  31. package/dist/gp-pattern-wizard.d.ts +11 -0
  32. package/dist/gp-pattern-wizard.js +22 -0
  33. package/dist/gp-resource.d.ts +5 -0
  34. package/dist/gp-resource.js +17 -0
  35. package/dist/uds-pattern-login-card.d.ts +13 -0
  36. package/dist/uds-pattern-login-card.js +22 -0
  37. package/dist/uds-pattern-login-page.d.ts +5 -0
  38. package/dist/uds-pattern-login-page.js +17 -0
  39. package/package.json +54 -4
  40. package/README.md +0 -3
@@ -0,0 +1,90 @@
1
+ import { resolveReference } from './reference';
2
+ import { reactChildrenToArray } from './children';
3
+ /** Marker component for one table column. Never rendered — see the module doc. */
4
+ export function DataTableCol(_props) {
5
+ return null;
6
+ }
7
+ /** Marker component for the table view. Never rendered. */
8
+ export function DataTable(_props) {
9
+ return null;
10
+ }
11
+ DataTable.Col = DataTableCol;
12
+ /** Marker component for the board view. Never rendered — see the module doc. */
13
+ export function Board(_props) {
14
+ return null;
15
+ }
16
+ /** Marker component for the card-grid view. Never rendered — see the module doc. */
17
+ export function CardGrid(_props) {
18
+ return null;
19
+ }
20
+ /** Marker component for the list workflow. Never rendered — see the module doc. */
21
+ export function List(_props) {
22
+ return null;
23
+ }
24
+ function columnFrom(element) {
25
+ const { field, label, sortable, align, width, reference, children } = element.props;
26
+ const base = {
27
+ field,
28
+ label,
29
+ sortable,
30
+ align,
31
+ width,
32
+ reference: resolveReference(reference),
33
+ };
34
+ return children ? { ...base, ...children(field, label) } : base;
35
+ }
36
+ function columnsFrom(children) {
37
+ const nodes = [];
38
+ reactChildrenToArray(children).forEach((child) => {
39
+ if (isElementOfType(child, DataTableCol)) {
40
+ nodes.push(child);
41
+ }
42
+ });
43
+ return nodes.map(columnFrom);
44
+ }
45
+ /**
46
+ * Reads a `<List>` element's props and view child into a `UdsAdminListConfig`
47
+ * — the exact shape `GpResource`'s `config.list` takes (ADR 0022).
48
+ *
49
+ * `perPage` is a scalar, forwarded as `UdsAdminResource.perPage` rather than
50
+ * as a member of this config (see `admin-config.ts`'s `perPage` doc) — the
51
+ * caller (`resource.tsx`) is responsible for placing it there; this function
52
+ * only returns what belongs under `list`.
53
+ *
54
+ * The view child decides the `layout` and nothing else: all three read the
55
+ * same `<DataTable.Col>` children into the same `columns`, which is what
56
+ * makes swapping one for another a one-tag change (ADR 0016). A `<List>` with
57
+ * no recognised view child keeps the table default rather than rendering
58
+ * nothing, matching the behaviour before the board and grid views existed.
59
+ */
60
+ export function listConfigFromChildren(listChildren) {
61
+ const view = reactChildrenToArray(listChildren).find((child) => isElementOfType(child, DataTable) || isElementOfType(child, Board) || isElementOfType(child, CardGrid));
62
+ const columns = columnsFrom(view?.props.children);
63
+ const layout = layoutFrom(view);
64
+ const { rowActions, bulkActions } = view?.props ?? {};
65
+ // Written only when present, so an unconfigured member reaches the pattern
66
+ // as absent rather than as an explicit `undefined` — the same rule
67
+ // `listConfigFor` follows for the shorthands it hoists.
68
+ const config = columns.length ? { columns, layout } : { layout };
69
+ if (rowActions !== undefined)
70
+ config.rowActions = rowActions;
71
+ if (bulkActions !== undefined)
72
+ config.bulkActions = bulkActions;
73
+ return config;
74
+ }
75
+ /** The `layout` a view child stands for. `<DataTable>` and an absent child both mean a table. */
76
+ function layoutFrom(view) {
77
+ if (view && isElementOfType(view, Board)) {
78
+ const { groupBy, lanes, title, fields, minLaneWidth } = view.props;
79
+ return { kind: 'board', groupBy, lanes, title, fields, minLaneWidth };
80
+ }
81
+ if (view && isElementOfType(view, CardGrid)) {
82
+ const { title, media, fields, minColumnWidth } = view.props;
83
+ return { kind: 'grid', title, media, fields, minColumnWidth };
84
+ }
85
+ return { kind: 'table' };
86
+ }
87
+ /** A React element whose `type` is exactly this marker function — not a subclass, not a lookalike. */
88
+ function isElementOfType(node, type) {
89
+ return typeof node === 'object' && node !== null && 'type' in node && node.type === type;
90
+ }
@@ -0,0 +1,137 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { UdsAdminRecordConfig, UdsRecordField, UdsRecordGroup, UdsRecordRelatedPanel } from '@globalpayments-private/gpds-pattern-web';
3
+ /**
4
+ * `Show`, `Edit`, `Create` and `TabbedShowLayout`: non-rendering marker
5
+ * components read by `Resource` (`resource.tsx`) to build a
6
+ * `UdsAdminRecordConfig` before anything reaches `gp-resource`/
7
+ * `gp-pattern-record`. Same "why these never render" reasoning as
8
+ * `list.tsx` and `simple-form.tsx`.
9
+ *
10
+ * Named after real react-admin's own view components
11
+ * (https://marmelab.com/react-admin/EditTutorial.html,
12
+ * https://marmelab.com/react-admin/Create.html,
13
+ * https://marmelab.com/react-admin/TabbedShowLayout.html):
14
+ *
15
+ * ```tsx
16
+ * <Resource name="deals">
17
+ * <Edit>
18
+ * <SimpleForm>
19
+ * <TextInput field="reference" label="Reference" />
20
+ * </SimpleForm>
21
+ * </Edit>
22
+ * </Resource>
23
+ * ```
24
+ *
25
+ * ## What these wrappers do, and the one thing they still cannot
26
+ *
27
+ * In real react-admin `<Edit>` and `<Create>` are *routed views*, and a
28
+ * resource may give each a **different** field set. That is now expressible:
29
+ * `UdsRecordField.modes` gates a field to the modes that have it (ADR 0031),
30
+ * and these wrappers are read into it — a field written under one wrapper
31
+ * only is gated to that wrapper's mode, and a field every wrapper present
32
+ * declares identically is left ungated, which is what a resource authored
33
+ * before `modes` existed already meant.
34
+ *
35
+ * Three things remain unexpressible, because the shell renders all three
36
+ * modes from one `fields` array, one `groups` array and one `layout`:
37
+ *
38
+ * - **Contradictory field order.** `<Show>` ordering `a, b` and `<Edit>`
39
+ * ordering `b, a` cannot both be honoured by one array.
40
+ * - **A tab renamed between modes**, or a layout that is `tabs` in one
41
+ * wrapper and a flat `<SimpleForm>` in another.
42
+ * - **Different related panels per mode.** A wrapper that declares no
43
+ * `<ReferenceManyField>` is read as silent on the subject rather than as
44
+ * saying there are none — otherwise the natural authoring, panels under
45
+ * `<Show>` and not under `<Edit>`, would be an error. Two wrappers that
46
+ * each declare panels and disagree is the real contradiction, and that is
47
+ * rejected.
48
+ *
49
+ * Each is rejected loudly at read time rather than silently resolved to
50
+ * whichever wrapper happened to be read last. An affordance that quietly
51
+ * drops half of what it was given is the failure mode this layer's
52
+ * predecessor comment warned about; an error that names the limitation is
53
+ * not that.
54
+ *
55
+ * ## `<ReferenceManyField>` inside a view
56
+ *
57
+ * A `<ReferenceManyField>` written beside the form — react-admin's own
58
+ * placement — is read into `UdsRecordConfig.related`, the record-level
59
+ * related panel ADR 0031 adds, and takes its id from the record on screen.
60
+ * See `reference-many.tsx`.
61
+ */
62
+ export interface ShowProps {
63
+ /** A `<SimpleForm>` or a `<TabbedShowLayout>`. */
64
+ children?: ReactNode;
65
+ }
66
+ /** Marker component for the show view. Never rendered — see the module doc. */
67
+ export declare function Show(_props: ShowProps): null;
68
+ export type EditProps = ShowProps;
69
+ /** Marker component for the edit view. Never rendered — see the module doc. */
70
+ export declare function Edit(_props: EditProps): null;
71
+ export type CreateProps = ShowProps;
72
+ /** Marker component for the create view. Never rendered — see the module doc. */
73
+ export declare function Create(_props: CreateProps): null;
74
+ /**
75
+ * One tab of a `<TabbedShowLayout>`, mirroring react-admin's
76
+ * `<TabbedShowLayout.Tab label="summary" path="summary">`.
77
+ */
78
+ export interface TabbedShowLayoutTabProps {
79
+ label: string;
80
+ /**
81
+ * The tab's stable key, written to the URL and used as the panel's DOM id
82
+ * (`UdsRecordGroup.id`).
83
+ *
84
+ * Mirrors react-admin's `path`, including its default: omit it on the first
85
+ * tab and that tab becomes the index. Here the fallback is a slug of
86
+ * `label` rather than a positional index, because `UdsRecordGroup.id`'s own
87
+ * doc rules an index out — reordering tabs would silently re-point every
88
+ * saved link.
89
+ */
90
+ path?: string;
91
+ /** The input markers whose fields this tab renders, in order. */
92
+ children?: ReactNode;
93
+ }
94
+ /** Marker component for one tab. Never rendered — see the module doc. */
95
+ export declare function TabbedShowLayoutTab(_props: TabbedShowLayoutTabProps): null;
96
+ export interface TabbedShowLayoutProps {
97
+ /** `<TabbedShowLayout.Tab>` children, in order. */
98
+ children?: ReactNode;
99
+ }
100
+ /**
101
+ * Marker component for a tabbed record, read into `UdsRecordConfig.groups`
102
+ * plus `layout: 'tabs'`. Never rendered — see the module doc.
103
+ */
104
+ export declare function TabbedShowLayout(_props: TabbedShowLayoutProps): null;
105
+ export declare namespace TabbedShowLayout {
106
+ var Tab: typeof TabbedShowLayoutTab;
107
+ }
108
+ /** Everything a record view child resolves to. */
109
+ interface RecordShape {
110
+ fields: UdsRecordField[];
111
+ groups?: UdsRecordGroup[];
112
+ layout?: 'tabs';
113
+ /** The `<ReferenceManyField>` panels written beside the form. */
114
+ panels: UdsRecordRelatedPanel[];
115
+ }
116
+ /**
117
+ * Reads a `<Show>`/`<Edit>`/`<Create>` element's view child into the fields,
118
+ * groups and layout they stand for.
119
+ *
120
+ * A `<SimpleForm>` yields fields and no groups. A `<TabbedShowLayout>` yields
121
+ * the concatenation of its tabs' fields — the flat array the shell renders —
122
+ * plus the `groups` that say which tab each one belongs to, plus the `tabs`
123
+ * layout that makes them tabs rather than stacked fieldsets.
124
+ */
125
+ export declare function recordShapeFromView(viewChildren: ReactNode): RecordShape;
126
+ /**
127
+ * Reads every `<Show>`/`<Edit>`/`<Create>` child of a `<Resource>` into the
128
+ * one `UdsAdminRecordConfig` the shell renders for all three modes.
129
+ *
130
+ * Returns `undefined` when the resource has none, so a list-only resource
131
+ * stays list-only rather than gaining an empty record config.
132
+ *
133
+ * @throws when the wrappers describe something one `fields`/`groups`/`layout`
134
+ * set cannot hold — see the module doc.
135
+ */
136
+ export declare function recordConfigFromChildren(children: ReactNode): UdsAdminRecordConfig | undefined;
137
+ export {};
@@ -0,0 +1,257 @@
1
+ import { SimpleForm, fieldsFromChildren } from './simple-form';
2
+ import { findReferenceManyFields, relatedPanelFrom } from './reference-many';
3
+ import { reactChildrenToArray } from './children';
4
+ /** Marker component for the show view. Never rendered — see the module doc. */
5
+ export function Show(_props) {
6
+ return null;
7
+ }
8
+ /** Marker component for the edit view. Never rendered — see the module doc. */
9
+ export function Edit(_props) {
10
+ return null;
11
+ }
12
+ /** Marker component for the create view. Never rendered — see the module doc. */
13
+ export function Create(_props) {
14
+ return null;
15
+ }
16
+ /** Marker component for one tab. Never rendered — see the module doc. */
17
+ export function TabbedShowLayoutTab(_props) {
18
+ return null;
19
+ }
20
+ /**
21
+ * Marker component for a tabbed record, read into `UdsRecordConfig.groups`
22
+ * plus `layout: 'tabs'`. Never rendered — see the module doc.
23
+ */
24
+ export function TabbedShowLayout(_props) {
25
+ return null;
26
+ }
27
+ TabbedShowLayout.Tab = TabbedShowLayoutTab;
28
+ /**
29
+ * Reads a `<Show>`/`<Edit>`/`<Create>` element's view child into the fields,
30
+ * groups and layout they stand for.
31
+ *
32
+ * A `<SimpleForm>` yields fields and no groups. A `<TabbedShowLayout>` yields
33
+ * the concatenation of its tabs' fields — the flat array the shell renders —
34
+ * plus the `groups` that say which tab each one belongs to, plus the `tabs`
35
+ * layout that makes them tabs rather than stacked fieldsets.
36
+ */
37
+ export function recordShapeFromView(viewChildren) {
38
+ const children = reactChildrenToArray(viewChildren);
39
+ const panels = findReferenceManyFields(viewChildren).map(relatedPanelFrom);
40
+ const form = children.find((child) => isElementOfType(child, SimpleForm));
41
+ if (form) {
42
+ return { fields: fieldsFromChildren(form.props.children), panels };
43
+ }
44
+ const tabbed = children.find((child) => isElementOfType(child, TabbedShowLayout));
45
+ if (!tabbed) {
46
+ return { fields: [], panels };
47
+ }
48
+ const tabs = reactChildrenToArray(tabbed.props.children).filter((child) => isElementOfType(child, TabbedShowLayoutTab));
49
+ const fields = [];
50
+ const groups = tabs.map((tab) => {
51
+ const tabFields = fieldsFromChildren(tab.props.children);
52
+ fields.push(...tabFields);
53
+ return {
54
+ id: tab.props.path ?? slug(tab.props.label),
55
+ label: tab.props.label,
56
+ fields: tabFields.map((field) => field.field),
57
+ };
58
+ });
59
+ return { fields, groups, layout: 'tabs', panels };
60
+ }
61
+ /**
62
+ * Reads every `<Show>`/`<Edit>`/`<Create>` child of a `<Resource>` into the
63
+ * one `UdsAdminRecordConfig` the shell renders for all three modes.
64
+ *
65
+ * Returns `undefined` when the resource has none, so a list-only resource
66
+ * stays list-only rather than gaining an empty record config.
67
+ *
68
+ * @throws when the wrappers describe something one `fields`/`groups`/`layout`
69
+ * set cannot hold — see the module doc.
70
+ */
71
+ export function recordConfigFromChildren(children) {
72
+ const views = reactChildrenToArray(children).filter((child) => isRecordViewElement(child));
73
+ if (!views.length) {
74
+ return undefined;
75
+ }
76
+ const read = views.map((view) => ({
77
+ name: nameOf(view),
78
+ mode: modeOf(view),
79
+ shape: recordShapeFromView(view.props.children),
80
+ }));
81
+ // Only wrappers that actually hold a form have a layout to disagree about.
82
+ // A `<Show>` carrying nothing but a `<ReferenceManyField>` is silent here,
83
+ // the same way it is silent about fields.
84
+ const withFields = read.filter(({ shape }) => shape.fields.length > 0);
85
+ const layouts = new Set(withFields.map(({ shape }) => shape.layout ?? 'flat'));
86
+ if (layouts.size > 1) {
87
+ throw divergent(read, 'use different layouts, and the shell has one layout for all three modes');
88
+ }
89
+ const fields = mergeFields(read);
90
+ const groups = mergeGroups(read);
91
+ const related = mergePanels(read);
92
+ const layout = withFields.find(({ shape }) => shape.layout)?.shape.layout;
93
+ return {
94
+ fields,
95
+ ...(groups ? { groups } : {}),
96
+ ...(layout ? { layout } : {}),
97
+ ...(related.length ? { related } : {}),
98
+ };
99
+ }
100
+ /**
101
+ * The one `fields` array every wrapper agrees with, with `modes` written on
102
+ * the entries that are not in all of them.
103
+ *
104
+ * A field is identified by its *whole definition*, not by its name, so the
105
+ * same field carrying a different label or input per mode is two gated
106
+ * entries rather than a conflict — the shell narrows by mode before anything
107
+ * reads them, so only one is ever live at a time.
108
+ *
109
+ * A field every declared mode has is left **ungated**, which is what makes
110
+ * this backward compatible: two identical wrappers produce exactly the config
111
+ * they produced before `modes` existed, and a mode no wrapper declares still
112
+ * renders the fields they all agree on rather than nothing at all.
113
+ */
114
+ function mergeFields(read) {
115
+ const merged = mergeOrder(read.map(({ shape }) => shape.fields), (field) => JSON.stringify(field));
116
+ if (!merged) {
117
+ throw divergent(read, 'order the same fields differently, and the shell renders one ordered array');
118
+ }
119
+ const modes = new Set(read.map(({ mode }) => mode));
120
+ return merged.map((field) => {
121
+ const declaredIn = read
122
+ .filter(({ shape }) => shape.fields.some((candidate) => sameField(candidate, field)))
123
+ .map(({ mode }) => mode);
124
+ const distinct = [...new Set(declaredIn)];
125
+ return distinct.length === modes.size ? field : { ...field, modes: distinct };
126
+ });
127
+ }
128
+ /**
129
+ * The one `groups` array, merged the same way.
130
+ *
131
+ * A group's *fields* may legitimately differ between modes — that is the
132
+ * per-mode field set arriving inside a tab — so they are merged and the shell
133
+ * narrows each group to the fields the mode actually has, dropping a group
134
+ * left empty. A group's **label** may not differ: one tab strip is rendered
135
+ * for all three modes, and there is nowhere to put the second name.
136
+ */
137
+ function mergeGroups(read) {
138
+ const lists = read.map(({ shape }) => shape.groups ?? []);
139
+ if (lists.every((groups) => groups.length === 0)) {
140
+ return undefined;
141
+ }
142
+ // Every wrapper that tabs its fields must name the *same* tabs: the shell
143
+ // renders one tab strip, so a tab present in one mode and renamed in
144
+ // another would quietly become a tab holding nothing.
145
+ const declared = lists.filter((groups) => groups.length > 0);
146
+ const ids = declared.map((groups) => [...groups.map((group) => group.id)].sort().join());
147
+ if (ids.some((set) => set !== ids[0])) {
148
+ throw divergent(read, 'name different tabs, and the shell renders one tab strip');
149
+ }
150
+ const merged = mergeOrder(lists, (group) => group.id);
151
+ if (!merged) {
152
+ throw divergent(read, 'order the same tabs differently, and the shell renders one tab strip');
153
+ }
154
+ return merged.map((group) => {
155
+ const declarations = lists.flat().filter((candidate) => candidate.id === group.id);
156
+ if (declarations.some((candidate) => candidate.label !== group.label)) {
157
+ throw divergent(read, `label the "${group.id}" tab differently, and the shell renders one tab strip`);
158
+ }
159
+ const fields = mergeOrder(declarations.map((candidate) => candidate.fields), (field) => field);
160
+ if (!fields) {
161
+ throw divergent(read, `order the "${group.id}" tab's fields differently`);
162
+ }
163
+ return { ...group, fields };
164
+ });
165
+ }
166
+ /**
167
+ * The related panels, taken from whichever wrappers declared any.
168
+ *
169
+ * A wrapper declaring none is silent rather than empty: panels under `<Show>`
170
+ * and not under `<Edit>` is the natural authoring, and reading the `<Edit>` as
171
+ * "there are no panels" would make it an error. Two wrappers that both declare
172
+ * panels and disagree is a real contradiction — `UdsRecordConfig.related` is
173
+ * one array for the whole record — and is rejected.
174
+ */
175
+ function mergePanels(read) {
176
+ const declared = read.filter(({ shape }) => shape.panels.length > 0);
177
+ if (!declared.length) {
178
+ return [];
179
+ }
180
+ const [first, ...rest] = declared;
181
+ if (rest.some(({ shape }) => JSON.stringify(shape.panels) !== JSON.stringify(first.shape.panels))) {
182
+ throw divergent(read, 'declare different related panels, and the shell renders one set beneath the record');
183
+ }
184
+ return first.shape.panels;
185
+ }
186
+ /**
187
+ * Merge ordered lists into the one order every input agrees with, or
188
+ * `undefined` when they contradict each other.
189
+ *
190
+ * First appearance wins the position; a list whose own items then run
191
+ * backwards through that order is the contradiction. Cheap, and it catches
192
+ * the case a set comparison would miss.
193
+ */
194
+ function mergeOrder(lists, keyOf) {
195
+ const merged = [];
196
+ const positions = new Map();
197
+ for (const list of lists) {
198
+ for (const item of list) {
199
+ const key = keyOf(item);
200
+ if (!positions.has(key)) {
201
+ positions.set(key, merged.length);
202
+ merged.push(item);
203
+ }
204
+ }
205
+ }
206
+ for (const list of lists) {
207
+ const seen = list.map((item) => positions.get(keyOf(item)));
208
+ for (let i = 1; i < seen.length; i += 1) {
209
+ if (seen[i] <= seen[i - 1]) {
210
+ return undefined;
211
+ }
212
+ }
213
+ }
214
+ return merged;
215
+ }
216
+ /** Two fields are the same field when their whole definitions match. */
217
+ function sameField(a, b) {
218
+ return JSON.stringify(a) === JSON.stringify(b);
219
+ }
220
+ /** The one error shape, naming the wrappers and what they asked for. */
221
+ function divergent(read, problem) {
222
+ const names = read.map(({ name }) => `<${name}>`).join(' and ');
223
+ return new Error(`${names} ${problem}. A field set may differ per mode — write the differing fields under one ` +
224
+ 'wrapper each and they are gated with `modes` — but this cannot be. See ' +
225
+ 'docs/adr/0031-the-record-pattern-gains-modes-and-related-panels.md.');
226
+ }
227
+ /** `'Billing details'` → `'billing-details'`. */
228
+ function slug(label) {
229
+ return label
230
+ .trim()
231
+ .toLowerCase()
232
+ .replace(/[^a-z0-9]+/g, '-')
233
+ .replace(/^-+|-+$/g, '');
234
+ }
235
+ const RECORD_VIEWS = new Map([
236
+ [Show, { name: 'Show', mode: 'show' }],
237
+ [Edit, { name: 'Edit', mode: 'edit' }],
238
+ [Create, { name: 'Create', mode: 'create' }],
239
+ ]);
240
+ function isRecordViewElement(node) {
241
+ return (typeof node === 'object' &&
242
+ node !== null &&
243
+ 'type' in node &&
244
+ RECORD_VIEWS.has(node.type));
245
+ }
246
+ /** The wrapper's tag name, for the error message. Only ever called on an element `isRecordViewElement` passed. */
247
+ function nameOf(view) {
248
+ return RECORD_VIEWS.get(view.type).name;
249
+ }
250
+ /** The mode the wrapper stands for, which is what its fields are gated to. */
251
+ function modeOf(view) {
252
+ return RECORD_VIEWS.get(view.type).mode;
253
+ }
254
+ /** A React element whose `type` is exactly this marker function. */
255
+ function isElementOfType(node, type) {
256
+ return typeof node === 'object' && node !== null && 'type' in node && node.type === type;
257
+ }
@@ -0,0 +1,86 @@
1
+ import type { ReactElement, ReactNode } from 'react';
2
+ import type { UdsAdminListConfig, UdsRecordId, UdsRecordRelatedPanel } from '@globalpayments-private/gpds-pattern-web';
3
+ /**
4
+ * `ReferenceManyField`: the composition spelling of a related-records panel,
5
+ * mirroring real react-admin's
6
+ * https://marmelab.com/react-admin/ReferenceManyField.html:
7
+ *
8
+ * ```tsx
9
+ * <Show>
10
+ * <SimpleForm>
11
+ * <TextInput field="number" label="Invoice" />
12
+ * </SimpleForm>
13
+ * <ReferenceManyField reference="invoice-lines" target="invoiceId" label="Lines">
14
+ * <DataTable>
15
+ * <DataTable.Col field="sku" label="SKU" />
16
+ * </DataTable>
17
+ * </ReferenceManyField>
18
+ * </Show>
19
+ * ```
20
+ *
21
+ * Rows are read with `getManyReference` rather than `getList`, `target` names
22
+ * the field on the referenced resource holding the other record's id, and the
23
+ * child is a view — in react-admin almost always a `<DataTable>`, and that
24
+ * nesting is kept here. All three of this layer's view children work, since
25
+ * what they select is the layout and a related panel is a list like any other.
26
+ *
27
+ * ## Two placements, and `id` is what tells them apart
28
+ *
29
+ * **Inside a `<Show>`/`<Edit>`/`<Create>`** — react-admin's own spelling — the
30
+ * panel belongs to the record on screen and takes its id from it, so `id` is
31
+ * neither needed nor accepted. This resolves to `UdsRecordConfig.related`,
32
+ * which `gp-pattern-record` renders beneath the form.
33
+ *
34
+ * **As a child of `<Resource>`** the panel *is* the resource's view, with no
35
+ * record around it to take an id from, so `id` is required and explicit. This
36
+ * resolves to `UdsListConfig.related` exactly as it did before the
37
+ * record-level member existed, and is still how a panel is written when there
38
+ * is no record pattern on the page.
39
+ *
40
+ * Neither placement guesses: an `id` where one cannot be honoured, or a
41
+ * missing `id` where there is nothing to infer it from, is an error at read
42
+ * time rather than a panel quietly listing the wrong records — or, worse, all
43
+ * of them. See ADR 0031.
44
+ */
45
+ export interface ReferenceManyFieldProps {
46
+ /** The resource holding the related records. */
47
+ reference: string;
48
+ /** The field on that resource holding this record's id. */
49
+ target: string;
50
+ /**
51
+ * This record's id. Required as a child of `<Resource>`, rejected inside a
52
+ * record view — see the module doc.
53
+ */
54
+ id?: UdsRecordId;
55
+ /** Heading rendered above the panel. Used by the record-level placement. */
56
+ label?: string;
57
+ /** The view child: `<DataTable>`, `<Board>` or `<CardGrid>`. */
58
+ children?: ReactNode;
59
+ }
60
+ /** Marker component for a related-records panel. Never rendered — see `list.tsx`'s module doc. */
61
+ export declare function ReferenceManyField(_props: ReferenceManyFieldProps): null;
62
+ /**
63
+ * Reads a `<ReferenceManyField>` element into the `UdsAdminListConfig` a
64
+ * related panel is: the view child's columns and layout, plus the `related`
65
+ * member that redirects the read to `getManyReference`.
66
+ *
67
+ * @throws when `id` is absent — there is no record context at `<Resource>`
68
+ * level to take one from, and a `related` without an id would read the whole
69
+ * resource unfiltered.
70
+ */
71
+ export declare function relatedListConfigFrom(element: ReactElement<ReferenceManyFieldProps>): UdsAdminListConfig;
72
+ /**
73
+ * Reads a `<ReferenceManyField>` written **inside a record view** into the
74
+ * `UdsRecordRelatedPanel` the pattern layer renders beneath the form.
75
+ *
76
+ * @throws when `id` is given — the panel's id is the record's, and honouring
77
+ * an explicit one would mean a panel under invoice 7 listing invoice 9's
78
+ * lines with nothing on screen saying so.
79
+ */
80
+ export declare function relatedPanelFrom(element: ReactElement<ReferenceManyFieldProps>): UdsRecordRelatedPanel;
81
+ /** Every `<ReferenceManyField>` among some children, in order. */
82
+ export declare function findReferenceManyFields(children: ReactNode): ReactElement<ReferenceManyFieldProps>[];
83
+ /** The resource a `<ReferenceManyField>` points at, which becomes the `<gp-resource name>`. */
84
+ export declare function referenceOf(element: ReactElement<ReferenceManyFieldProps>): string;
85
+ /** The first `<ReferenceManyField>` among some children, if any. */
86
+ export declare function findReferenceManyField(children: ReactNode): ReactElement<ReferenceManyFieldProps> | undefined;
@@ -0,0 +1,60 @@
1
+ import { listConfigFromChildren } from './list';
2
+ import { reactChildrenToArray } from './children';
3
+ /** Marker component for a related-records panel. Never rendered — see `list.tsx`'s module doc. */
4
+ export function ReferenceManyField(_props) {
5
+ return null;
6
+ }
7
+ /**
8
+ * Reads a `<ReferenceManyField>` element into the `UdsAdminListConfig` a
9
+ * related panel is: the view child's columns and layout, plus the `related`
10
+ * member that redirects the read to `getManyReference`.
11
+ *
12
+ * @throws when `id` is absent — there is no record context at `<Resource>`
13
+ * level to take one from, and a `related` without an id would read the whole
14
+ * resource unfiltered.
15
+ */
16
+ export function relatedListConfigFrom(element) {
17
+ const { reference, target, id, children } = element.props;
18
+ if (id === undefined) {
19
+ throw new Error(`<ReferenceManyField reference="${reference}"> is a child of <Resource>, where there is no record ` +
20
+ 'to take an id from, so `id` is required. Move it inside <Show> to have the id taken from the ' +
21
+ 'record, or pass `id` explicitly. See docs/adr/0031-the-record-pattern-gains-modes-and-related-panels.md.');
22
+ }
23
+ return { ...listConfigFromChildren(children), related: { target, id } };
24
+ }
25
+ /**
26
+ * Reads a `<ReferenceManyField>` written **inside a record view** into the
27
+ * `UdsRecordRelatedPanel` the pattern layer renders beneath the form.
28
+ *
29
+ * @throws when `id` is given — the panel's id is the record's, and honouring
30
+ * an explicit one would mean a panel under invoice 7 listing invoice 9's
31
+ * lines with nothing on screen saying so.
32
+ */
33
+ export function relatedPanelFrom(element) {
34
+ const { reference, target, id, label, children } = element.props;
35
+ if (id !== undefined) {
36
+ throw new Error(`<ReferenceManyField reference="${reference}"> is inside a record view, where its id is the ` +
37
+ "record's own, so `id` cannot be set. Remove it, or move the panel out to <Resource> level " +
38
+ 'where an explicit id is what a panel has instead of a record. ' +
39
+ 'See docs/adr/0031-the-record-pattern-gains-modes-and-related-panels.md.');
40
+ }
41
+ return { resource: reference, target, heading: label, list: listConfigFromChildren(children) };
42
+ }
43
+ /** Every `<ReferenceManyField>` among some children, in order. */
44
+ export function findReferenceManyFields(children) {
45
+ return reactChildrenToArray(children).filter(isReferenceManyField);
46
+ }
47
+ /** The resource a `<ReferenceManyField>` points at, which becomes the `<gp-resource name>`. */
48
+ export function referenceOf(element) {
49
+ return element.props.reference;
50
+ }
51
+ /** The first `<ReferenceManyField>` among some children, if any. */
52
+ export function findReferenceManyField(children) {
53
+ return reactChildrenToArray(children).find(isReferenceManyField);
54
+ }
55
+ function isReferenceManyField(node) {
56
+ return (typeof node === 'object' &&
57
+ node !== null &&
58
+ 'type' in node &&
59
+ node.type === ReferenceManyField);
60
+ }
@@ -0,0 +1,13 @@
1
+ import type { UdsReference } from '@globalpayments-private/gpds-pattern-web';
2
+ /**
3
+ * The `{ resource: 'merchants' }` shorthand every column/field that can point
4
+ * at another resource accepts: a bare resource name, defaulting the
5
+ * referenced field to `'name'`, or the full `{ resource, field }` object
6
+ * passed straight through.
7
+ *
8
+ * Shared by `list.tsx` (`DataTable.Col`'s `reference`) and `simple-form.tsx`
9
+ * (`TextInput`/`ReferenceInput`'s `reference`) so both desugar the one
10
+ * shorthand identically — `UdsListColumn.reference` and
11
+ * `UdsRecordField.reference` are both typed `UdsReference | undefined`.
12
+ */
13
+ export declare function resolveReference(reference: string | UdsReference | undefined): UdsReference | undefined;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The `{ resource: 'merchants' }` shorthand every column/field that can point
3
+ * at another resource accepts: a bare resource name, defaulting the
4
+ * referenced field to `'name'`, or the full `{ resource, field }` object
5
+ * passed straight through.
6
+ *
7
+ * Shared by `list.tsx` (`DataTable.Col`'s `reference`) and `simple-form.tsx`
8
+ * (`TextInput`/`ReferenceInput`'s `reference`) so both desugar the one
9
+ * shorthand identically — `UdsListColumn.reference` and
10
+ * `UdsRecordField.reference` are both typed `UdsReference | undefined`.
11
+ */
12
+ export function resolveReference(reference) {
13
+ if (reference === undefined) {
14
+ return undefined;
15
+ }
16
+ return typeof reference === 'string' ? { resource: reference, field: 'name' } : reference;
17
+ }