@genesislcap/foundation-forms 14.496.2-alpha-da9ccbd.0 → 14.497.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 (39) hide show
  1. package/dist/custom-elements.json +341 -1
  2. package/dist/dts/filters/filters.d.ts +0 -1
  3. package/dist/dts/filters/filters.d.ts.map +1 -1
  4. package/dist/dts/form.d.ts +94 -1
  5. package/dist/dts/form.d.ts.map +1 -1
  6. package/dist/dts/form.styles.d.ts.map +1 -1
  7. package/dist/dts/form.template.d.ts.map +1 -1
  8. package/dist/dts/index.d.ts +3 -0
  9. package/dist/dts/index.d.ts.map +1 -1
  10. package/dist/dts/jsonforms/renderers/CategorizationWrapperRenderer.d.ts.map +1 -1
  11. package/dist/dts/react.d.ts +10 -0
  12. package/dist/dts/review.d.ts +34 -0
  13. package/dist/dts/review.d.ts.map +1 -0
  14. package/dist/dts/review.styles.d.ts +3 -0
  15. package/dist/dts/review.styles.d.ts.map +1 -0
  16. package/dist/dts/review.template.d.ts +6 -0
  17. package/dist/dts/review.template.d.ts.map +1 -0
  18. package/dist/dts/utils/index.d.ts +1 -0
  19. package/dist/dts/utils/index.d.ts.map +1 -1
  20. package/dist/dts/utils/review-utils.d.ts +82 -0
  21. package/dist/dts/utils/review-utils.d.ts.map +1 -0
  22. package/dist/dts/utils/schema-utils.d.ts +12 -0
  23. package/dist/dts/utils/schema-utils.d.ts.map +1 -1
  24. package/dist/esm/form.js +132 -3
  25. package/dist/esm/form.styles.js +9 -2
  26. package/dist/esm/form.template.js +63 -25
  27. package/dist/esm/index.js +3 -0
  28. package/dist/esm/jsonforms/renderers/CategorizationWrapperRenderer.js +1 -45
  29. package/dist/esm/review.js +63 -0
  30. package/dist/esm/review.styles.js +93 -0
  31. package/dist/esm/review.template.js +39 -0
  32. package/dist/esm/utils/index.js +1 -0
  33. package/dist/esm/utils/review-utils.js +241 -0
  34. package/dist/esm/utils/schema-utils.js +48 -0
  35. package/dist/foundation-forms.api.json +428 -0
  36. package/dist/foundation-forms.d.ts +224 -2
  37. package/dist/react.cjs +7 -0
  38. package/dist/react.mjs +6 -0
  39. package/package.json +15 -15
@@ -0,0 +1,93 @@
1
+ import { designUnit, strokeWidth } from '@microsoft/fast-components';
2
+ import { css } from '@microsoft/fast-element';
3
+ import { forcedColorsStylesheetBehavior } from '@microsoft/fast-foundation';
4
+ import { SystemColors } from '@microsoft/fast-web-utilities';
5
+ /** @internal */
6
+ export const foundationFormReviewStyles = css `
7
+ foundation-form-review {
8
+ display: flex;
9
+ flex-direction: column;
10
+ flex: 1;
11
+ min-height: 0;
12
+ overflow: hidden;
13
+ }
14
+
15
+ foundation-form-review .review-header {
16
+ display: flex;
17
+ align-items: baseline;
18
+ gap: calc(${designUnit} * 2px);
19
+ padding: calc(${designUnit} * 2px) calc(${designUnit} * 2px) calc(${designUnit} * 1px);
20
+ border-bottom: calc(${strokeWidth} * 1px) solid var(--neutral-stroke-rest, rgb(0 0 0 / 12%));
21
+ }
22
+
23
+ foundation-form-review .review-title {
24
+ font-size: var(--type-ramp-plus-1-font-size, 1rem);
25
+ line-height: var(--type-ramp-plus-1-line-height, 1.5rem);
26
+ font-weight: 600;
27
+ color: var(--neutral-foreground-rest);
28
+ }
29
+
30
+ foundation-form-review .review-id {
31
+ font-family: var(--mono-font-family, 'Courier New', Courier, monospace);
32
+ font-size: var(--type-ramp-base-font-size, 0.875rem);
33
+ color: var(--neutral-foreground-hint, rgb(0 0 0 / 60%));
34
+ }
35
+
36
+ foundation-form-review .review-filter {
37
+ padding: calc(${designUnit} * 2px);
38
+ }
39
+
40
+ foundation-form-review .review-filter-input {
41
+ width: 100%;
42
+ }
43
+
44
+ foundation-form-review .review-body {
45
+ flex: 1;
46
+ overflow-y: auto;
47
+ min-height: 0;
48
+ scrollbar-color: var(--neutral-stroke-rest) transparent;
49
+ }
50
+
51
+ foundation-form-review .review-section-header {
52
+ font-size: var(--type-ramp-minus-1-font-size, 0.75rem);
53
+ line-height: var(--type-ramp-minus-1-line-height, 1rem);
54
+ font-weight: 600;
55
+ letter-spacing: 0.04em;
56
+ text-transform: uppercase;
57
+ color: var(--neutral-foreground-hint, rgb(0 0 0 / 60%));
58
+ background-color: var(--neutral-layer-2, rgb(0 0 0 / 4%));
59
+ padding: calc(${designUnit} * 1px) calc(${designUnit} * 2px);
60
+ margin-top: calc(${designUnit} * 3px);
61
+ }
62
+
63
+ foundation-form-review .review-field {
64
+ display: flex;
65
+ align-items: baseline;
66
+ justify-content: space-between;
67
+ gap: calc(${designUnit} * 2px);
68
+ padding: calc(${designUnit} * 1.5px) calc(${designUnit} * 2px);
69
+ border-bottom: calc(${strokeWidth} * 1px) solid var(--neutral-stroke-rest, rgb(0 0 0 / 12%));
70
+ }
71
+
72
+ foundation-form-review .review-field:last-child {
73
+ border-bottom: none;
74
+ }
75
+
76
+ foundation-form-review .review-field-label {
77
+ color: var(--neutral-foreground-hint, rgb(0 0 0 / 60%));
78
+ font-size: var(--type-ramp-base-font-size, 0.875rem);
79
+ flex-shrink: 0;
80
+ }
81
+
82
+ foundation-form-review .review-field-value {
83
+ color: var(--neutral-foreground-rest);
84
+ font-size: var(--type-ramp-base-font-size, 0.875rem);
85
+ text-align: right;
86
+ word-break: break-word;
87
+ white-space: pre-wrap;
88
+ }
89
+ `.withBehaviors(forcedColorsStylesheetBehavior(css `
90
+ foundation-form-review {
91
+ background: ${SystemColors.Canvas};
92
+ }
93
+ `));
@@ -0,0 +1,39 @@
1
+ import { sync } from '@genesislcap/foundation-utils';
2
+ import { html, repeat, when } from '@microsoft/fast-element';
3
+ /** @internal */
4
+ export const getPrefixedReview = (prefix) => html `
5
+ <template>
6
+ <div class="review-header">
7
+ <span class="review-title">${(x) => x.heading}</span>
8
+ ${when((x) => x.recordId, html `
9
+ <span class="review-id">${(x) => x.recordId}</span>
10
+ `)}
11
+ </div>
12
+ <div class="review-filter">
13
+ <${prefix}-text-field
14
+ value=${sync((x) => x.filterText, 'string', 'input')}
15
+ placeholder="Filter fields..."
16
+ aria-label="Filter fields"
17
+ class="review-filter-input"
18
+ data-test-id="review-filter-input"
19
+ ></${prefix}-text-field>
20
+ </div>
21
+ <div class="review-body">
22
+ ${repeat((x) => x.filteredSections, html `
23
+ ${when((section) => section.fields.length > 0, html `
24
+ <div class="review-section-header">${(section) => { var _a; return (_a = section.title) !== null && _a !== void 0 ? _a : '—'; }}</div>
25
+ ${repeat((section) => section.fields, html `
26
+ <div class="review-field">
27
+ <span class="review-field-label">${(field) => field.label}</span>
28
+ <span class="review-field-value">${(field) => field.value}</span>
29
+ </div>
30
+ `)}
31
+ `)}
32
+ `)}
33
+ </div>
34
+ </template>
35
+ `;
36
+ /** @internal */
37
+ export const foundationFormReviewTemplate = html `
38
+ ${(x) => getPrefixedReview(x.prefix)}
39
+ `;
@@ -1,4 +1,5 @@
1
1
  export * from './csv-parser';
2
+ export * from './review-utils';
2
3
  export * from './filters';
3
4
  export * from './logger';
4
5
  export * from './translation';
@@ -0,0 +1,241 @@
1
+ import { __awaiter } from "tslib";
2
+ import { getDatasource } from '@genesislcap/foundation-comms';
3
+ import { CriteriaBuilder, ExpressionBuilder } from '@genesislcap/foundation-criteria';
4
+ import { capitalCase } from 'change-case';
5
+ /**
6
+ * Builds the review panel's fields grouped into sections by their nearest `Group` layout.
7
+ * Sections are formed from consecutive runs of the same group title as fields are
8
+ * encountered in the UI schema — not a global title lookup — so two fields that happen to
9
+ * share a title (including `null`, for ungrouped fields) don't get merged into one section
10
+ * unless they're actually adjacent. Otherwise an ungrouped field appearing after a Group
11
+ * would jump back up next to earlier ungrouped fields instead of staying where it sits in
12
+ * the form, and two distinct Groups with the same label would collapse into one. Falls back
13
+ * to a flat, single ungrouped section when no UI schema is available.
14
+ *
15
+ * Async because connected-select fields backed by `options.allOptionsResourceName` (rather
16
+ * than a static `options.data`) resolve their display label with a criteria-filtered fetch
17
+ * — see `resolveResourceOptionLabel`. The UI schema is walked synchronously to collect one
18
+ * value-resolution promise per field (preserving encounter order), then all promises settle
19
+ * in parallel via `Promise.all` before fields are added to their sections — so a slow fetch
20
+ * for one field can't reorder fields that resolved faster.
21
+ *
22
+ * This is the single walker of the UI schema for review purposes — `Form` calls it directly
23
+ * to populate `Review.sections`. Don't reimplement this traversal elsewhere; the two
24
+ * previously did drift (a since-removed `collectReviewFields` resolved labels via
25
+ * `resolveOptionLabel` / `resolveEnumLabel` only, silently skipping `resolveResourceOptionLabel`
26
+ * and showing raw ids for connected-select fields).
27
+ * @internal
28
+ */
29
+ export function buildReviewSections(data, uiSchema, schema) {
30
+ return __awaiter(this, void 0, void 0, function* () {
31
+ const sections = [];
32
+ const addField = (title, field) => {
33
+ const last = sections[sections.length - 1];
34
+ if (last && last.title === title) {
35
+ last.fields.push(field);
36
+ }
37
+ else {
38
+ sections.push({ title, fields: [field] });
39
+ }
40
+ };
41
+ const resolveDisplayValue = (raw, fieldName, options) => __awaiter(this, void 0, void 0, function* () {
42
+ var _a, _b, _c;
43
+ const displayValue = (_c = (_b = (_a = resolveOptionLabel(raw, options)) !== null && _a !== void 0 ? _a : (yield resolveResourceOptionLabel(raw, options))) !== null && _b !== void 0 ? _b : resolveEnumLabel(raw, fieldName, schema)) !== null && _c !== void 0 ? _c : raw;
44
+ return formatValueForReview(displayValue);
45
+ });
46
+ const collect = (element, groupTitle) => {
47
+ var _a, _b, _c;
48
+ if (!element)
49
+ return [];
50
+ if (element.type === 'Control' && element.scope) {
51
+ if ((_a = element.options) === null || _a === void 0 ? void 0 : _a.hidden)
52
+ return [];
53
+ const match = element.scope.match(/^#\/properties\/(.+)$/);
54
+ if (!match)
55
+ return [];
56
+ const fieldName = match[1];
57
+ const raw = getDataValueAtPath(data, fieldName);
58
+ if (raw === undefined ||
59
+ raw === null ||
60
+ raw === '' ||
61
+ (Array.isArray(raw) && raw.length === 0))
62
+ return [];
63
+ const shortName = fieldName.split('/properties/').pop();
64
+ const fieldLabel = element.label || capitalCase(shortName);
65
+ return [
66
+ {
67
+ groupTitle,
68
+ label: fieldLabel,
69
+ valuePromise: resolveDisplayValue(raw, fieldName, element.options),
70
+ },
71
+ ];
72
+ }
73
+ const nextGroup = element.type === 'Group' && element.label ? element.label : groupTitle;
74
+ const children = (_b = element.elements) !== null && _b !== void 0 ? _b : (_c = element.options) === null || _c === void 0 ? void 0 : _c.childElements;
75
+ return children ? children.flatMap((child) => collect(child, nextGroup)) : [];
76
+ };
77
+ if (uiSchema) {
78
+ const pending = collect(uiSchema, null);
79
+ const values = yield Promise.all(pending.map((field) => field.valuePromise));
80
+ pending.forEach((field, index) => {
81
+ addField(field.groupTitle, { label: field.label, value: values[index] });
82
+ });
83
+ }
84
+ else {
85
+ for (const [key, raw] of Object.entries(data !== null && data !== void 0 ? data : {})) {
86
+ if (raw === undefined || raw === null || raw === '')
87
+ continue;
88
+ addField(null, { label: capitalCase(key), value: formatValueForReview(raw) });
89
+ }
90
+ }
91
+ return sections;
92
+ });
93
+ }
94
+ /**
95
+ * Filters review sections down to fields whose label or value contains `query`
96
+ * (case-insensitive). Sections left with no matching fields are dropped entirely.
97
+ * Returns `sections` unchanged when `query` is blank.
98
+ * @internal
99
+ */
100
+ export function filterReviewSections(sections, query) {
101
+ const normalized = query.trim().toLowerCase();
102
+ if (!normalized)
103
+ return sections;
104
+ return sections
105
+ .map((section) => ({
106
+ title: section.title,
107
+ fields: section.fields.filter((field) => field.label.toLowerCase().includes(normalized) ||
108
+ field.value.toLowerCase().includes(normalized)),
109
+ }))
110
+ .filter((section) => section.fields.length > 0);
111
+ }
112
+ /**
113
+ * Looks up the display label for a raw value from `options.data` (used by segmented,
114
+ * radio group, connected-select, and enum-with-explicit-data renderers). Honors
115
+ * `options.valueField`/`options.labelField` when set, defaulting to `value`/`label`.
116
+ * Returns `null` when no match found.
117
+ * @internal
118
+ */
119
+ export function resolveOptionLabel(raw, options) {
120
+ var _a, _b, _c;
121
+ if (!Array.isArray(options === null || options === void 0 ? void 0 : options.data))
122
+ return null;
123
+ const valueKey = (_a = options.valueField) !== null && _a !== void 0 ? _a : 'value';
124
+ const labelKey = (_b = options.labelField) !== null && _b !== void 0 ? _b : 'label';
125
+ const entry = options.data.find((item) => item[valueKey] === raw);
126
+ return (_c = entry === null || entry === void 0 ? void 0 : entry[labelKey]) !== null && _c !== void 0 ? _c : null;
127
+ }
128
+ /**
129
+ * Resolves the display label for a connected-select field backed by
130
+ * `options.allOptionsResourceName` (a Data Server/Request Server resource) rather than a
131
+ * static `options.data` array — e.g. `ConnectedSelectControlRenderer` in async/remote mode.
132
+ * Fetches a single row via a criteria-filtered snapshot matching `valueField === raw`,
133
+ * then reads `labelField` off the returned row. Returns `null` when there's no resource
134
+ * to query, the resource can't be reached, or no row matches.
135
+ * @internal
136
+ */
137
+ export function resolveResourceOptionLabel(raw, options) {
138
+ return __awaiter(this, void 0, void 0, function* () {
139
+ var _a, _b, _c, _d;
140
+ const resourceName = options === null || options === void 0 ? void 0 : options.allOptionsResourceName;
141
+ if (!resourceName || raw === undefined || raw === null || raw === '')
142
+ return null;
143
+ const valueField = (_a = options.valueField) !== null && _a !== void 0 ? _a : 'value';
144
+ const labelField = (_b = options.labelField) !== null && _b !== void 0 ? _b : 'label';
145
+ const datasource = getDatasource();
146
+ try {
147
+ const criteria = new CriteriaBuilder()
148
+ .withExpression(new ExpressionBuilder().withField(valueField).withValue(raw).build())
149
+ .build();
150
+ const initialized = yield datasource.init(Object.assign(Object.assign({}, options.datasourceConfig), { resourceName,
151
+ criteria, isSnapshot: true, maxRows: 1, fields: valueField === labelField ? valueField : `${valueField} ${labelField}` }));
152
+ if (!initialized)
153
+ return null;
154
+ const rows = yield datasource.snapshotFiltered();
155
+ return (_d = (_c = rows === null || rows === void 0 ? void 0 : rows[0]) === null || _c === void 0 ? void 0 : _c[labelField]) !== null && _d !== void 0 ? _d : null;
156
+ }
157
+ catch (_e) {
158
+ return null;
159
+ }
160
+ finally {
161
+ datasource.destroy();
162
+ }
163
+ });
164
+ }
165
+ /**
166
+ * For plain enum combobox fields (no options.data), applies the same `capitalCase`
167
+ * transform the renderer uses — e.g. `SEMI_ANNUAL` → `Semi Annual`.
168
+ * Navigates the JSON schema via the fieldName path to confirm the field is an enum
169
+ * before transforming. Returns `null` for non-enum fields.
170
+ * @internal
171
+ */
172
+ export function resolveEnumLabel(raw, fieldName, schema) {
173
+ var _a, _b, _c, _d;
174
+ if (typeof raw !== 'string')
175
+ return null;
176
+ if (!(schema === null || schema === void 0 ? void 0 : schema.properties))
177
+ return null;
178
+ let fieldSchema = schema;
179
+ for (const part of fieldName.split('/properties/')) {
180
+ fieldSchema = (_a = fieldSchema === null || fieldSchema === void 0 ? void 0 : fieldSchema.properties) === null || _a === void 0 ? void 0 : _a[part];
181
+ }
182
+ if (!fieldSchema)
183
+ return null;
184
+ // Matches plain `enum: [...]` or nullable `oneOf: [{ enum: [...] }, { type: 'null' }]`
185
+ const enumValues = (_b = fieldSchema.enum) !== null && _b !== void 0 ? _b : (_d = (_c = fieldSchema.oneOf) === null || _c === void 0 ? void 0 : _c.find((x) => x.enum)) === null || _d === void 0 ? void 0 : _d.enum;
186
+ if (Array.isArray(enumValues) && enumValues.includes(raw)) {
187
+ return capitalCase(raw);
188
+ }
189
+ return null;
190
+ }
191
+ /**
192
+ * Navigates a `/properties/`-separated path in data to support nested object scopes
193
+ * such as `ADDRESS/properties/STREET` produced by Group layout UI schemas.
194
+ * @internal
195
+ */
196
+ export function getDataValueAtPath(data, fieldName) {
197
+ return navigateSegments(data, fieldName.split('/properties/'));
198
+ }
199
+ /** Core traversal — walks `data` one property key at a time. */
200
+ function navigateSegments(data, segments) {
201
+ let value = data;
202
+ for (const segment of segments) {
203
+ if (value == null)
204
+ return undefined;
205
+ value = value[segment];
206
+ }
207
+ return value;
208
+ }
209
+ /**
210
+ * Converts a raw field value to a display string for the review panel.
211
+ * Arrays of primitives are comma-joined; arrays of objects and plain objects
212
+ * are rendered as readable `key: value` lines.
213
+ * @internal
214
+ */
215
+ export function formatValueForReview(value) {
216
+ if (Array.isArray(value)) {
217
+ if (value.length === 0)
218
+ return '';
219
+ if (value.every((v) => v === null || typeof v !== 'object')) {
220
+ return value.join(', ');
221
+ }
222
+ return value
223
+ .map((item, i) => {
224
+ const pairs = Object.entries(item !== null && item !== void 0 ? item : {})
225
+ .filter(([, v]) => v !== null && v !== undefined && v !== '')
226
+ .map(([k, v]) => `${k}: ${v}`)
227
+ .join(', ');
228
+ return `[${i + 1}] ${pairs}`;
229
+ })
230
+ .join('\n');
231
+ }
232
+ if (value === null || value === undefined)
233
+ return '';
234
+ if (typeof value === 'object') {
235
+ return Object.entries(value)
236
+ .filter(([, v]) => v !== null && v !== undefined && v !== '')
237
+ .map(([k, v]) => `${k}: ${v}`)
238
+ .join(', ');
239
+ }
240
+ return String(value);
241
+ }
@@ -93,6 +93,54 @@ export function isBulkUiSchema(uischema) {
93
93
  var _a;
94
94
  return (_a = uischema === null || uischema === void 0 ? void 0 : uischema.elements) === null || _a === void 0 ? void 0 : _a.some((element) => element.scope === '#/properties/items' && element.type === 'Control');
95
95
  }
96
+ /**
97
+ * Converts AJV validation errors to the JSON Schema scope strings they correspond to
98
+ * (e.g. `#/properties/ADDRESS/properties/STREET`). Used to match errors back to UI
99
+ * schema controls for features like categorization-tab error indicators.
100
+ */
101
+ export function extractErrorScopes(errors) {
102
+ var _a;
103
+ const scopes = new Set();
104
+ for (const error of errors) {
105
+ if (error.keyword === 'required' && ((_a = error.params) === null || _a === void 0 ? void 0 : _a.missingProperty)) {
106
+ const basePath = error.instancePath || '';
107
+ const prop = error.params.missingProperty;
108
+ const fullPath = basePath === '' ? `#/properties/${prop}` : `#${basePath}/properties/${prop}`;
109
+ scopes.add(fullPath);
110
+ }
111
+ else if (error.instancePath) {
112
+ // Convert JSON Pointer /ADDRESS/STREET → #/properties/ADDRESS/properties/STREET
113
+ const instancePath = `#${error.instancePath.replace(/\//g, '/properties/')}`;
114
+ scopes.add(instancePath);
115
+ }
116
+ }
117
+ return Array.from(scopes);
118
+ }
119
+ /**
120
+ * Returns true when `uiSchema` (or any of its descendants) has a scope that appears
121
+ * in `errorScopes`. Walks both `elements` and `options.childElements`.
122
+ */
123
+ export function uiSchemaHasAnyError(uiSchema, errorScopes) {
124
+ var _a;
125
+ if (typeof uiSchema.scope === 'string' &&
126
+ errorScopes.includes(uiSchema.scope)) {
127
+ return true;
128
+ }
129
+ const childElements = (_a = uiSchema.options) === null || _a === void 0 ? void 0 : _a.childElements;
130
+ if (Array.isArray(childElements)) {
131
+ for (const child of childElements) {
132
+ if (uiSchemaHasAnyError(child, errorScopes))
133
+ return true;
134
+ }
135
+ }
136
+ if (Array.isArray(uiSchema.elements)) {
137
+ for (const el of uiSchema.elements) {
138
+ if (uiSchemaHasAnyError(el, errorScopes))
139
+ return true;
140
+ }
141
+ }
142
+ return false;
143
+ }
96
144
  /**
97
145
  * Generates a UI schema suitable for bulk insert mode with array control.
98
146
  * @param userProvidedUiSchema - Optional user-provided UI schema to use as childUiSchema