@genesislcap/foundation-forms 14.487.0 → 14.488.1-FUI-2571.1
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.
- package/dist/custom-elements.json +630 -2
- package/dist/dts/form.d.ts +77 -0
- package/dist/dts/form.d.ts.map +1 -1
- package/dist/dts/form.styles.d.ts.map +1 -1
- package/dist/dts/form.template.d.ts.map +1 -1
- package/dist/dts/index.d.ts +3 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/jsonforms/renderers/CategorizationWrapperRenderer.d.ts.map +1 -1
- package/dist/dts/react.d.ts +10 -0
- package/dist/dts/review.d.ts +34 -0
- package/dist/dts/review.d.ts.map +1 -0
- package/dist/dts/review.styles.d.ts +3 -0
- package/dist/dts/review.styles.d.ts.map +1 -0
- package/dist/dts/review.template.d.ts +6 -0
- package/dist/dts/review.template.d.ts.map +1 -0
- package/dist/dts/utils/index.d.ts +1 -0
- package/dist/dts/utils/index.d.ts.map +1 -1
- package/dist/dts/utils/review-utils.d.ts +55 -0
- package/dist/dts/utils/review-utils.d.ts.map +1 -0
- package/dist/dts/utils/schema-utils.d.ts +12 -0
- package/dist/dts/utils/schema-utils.d.ts.map +1 -1
- package/dist/esm/form.js +120 -1
- package/dist/esm/form.styles.js +9 -2
- package/dist/esm/form.template.js +58 -24
- package/dist/esm/index.js +3 -0
- package/dist/esm/jsonforms/renderers/CategorizationWrapperRenderer.js +1 -45
- package/dist/esm/review.js +63 -0
- package/dist/esm/review.styles.js +93 -0
- package/dist/esm/review.template.js +38 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/esm/utils/review-utils.js +205 -0
- package/dist/esm/utils/schema-utils.js +48 -0
- package/dist/foundation-forms.api.json +1044 -13
- package/dist/foundation-forms.d.ts +181 -0
- package/dist/react.cjs +7 -0
- package/dist/react.mjs +6 -0
- package/package.json +15 -15
|
@@ -6,11 +6,12 @@ import { CategorizationWrapper } from './jsonforms/renderers/CategorizationWrapp
|
|
|
6
6
|
import { DispatchRenderer } from './jsonforms/renderers/dispatch-renderer';
|
|
7
7
|
import { StepperWrapper } from './jsonforms/renderers/StepperWrapperRenderer';
|
|
8
8
|
import { VerticalCategorizationWrapper } from './jsonforms/renderers/VerticalCategorizationWrapperRenderer';
|
|
9
|
-
|
|
9
|
+
import { Review } from './review';
|
|
10
|
+
avoidTreeShaking(JSONForms, ArrayListWrapper, CategorizationWrapper, VerticalCategorizationWrapper, DispatchRenderer, StepperWrapper, Review);
|
|
10
11
|
/** @internal */
|
|
11
12
|
export const getPrefixedForm = (prefix) => html `
|
|
12
13
|
<template>
|
|
13
|
-
${when((x) => x.bulkInsert, html `
|
|
14
|
+
${when((x) => !x.reviewMode && x.bulkInsert, html `
|
|
14
15
|
<div class="csv-upload-section" part="csv-upload">
|
|
15
16
|
<input
|
|
16
17
|
type="file"
|
|
@@ -39,43 +40,76 @@ export const getPrefixedForm = (prefix) => html `
|
|
|
39
40
|
</${prefix}-button>
|
|
40
41
|
</div>
|
|
41
42
|
`)}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
43
|
+
${when((x) => !x.reviewMode, html `
|
|
44
|
+
<json-forms
|
|
45
|
+
${ref('jsonFormsEl')}
|
|
46
|
+
@submit-button-clicked=${(x) => x._submit()}
|
|
47
|
+
@submit-part=${(x, c) => x.submitPart(c.event)}
|
|
48
|
+
@submit-single-row=${(x, c) => x.submitSingleRow(c.event.detail.index)}
|
|
49
|
+
@reset-form=${(x) => x.reset(false)}
|
|
50
|
+
?readonly=${(x) => x.readonly}
|
|
51
|
+
:submitted=${(x) => x.submitted}
|
|
52
|
+
:renderers=${(x) => x.mergedRenderers}
|
|
53
|
+
:uischema=${(x) => x.uischema}
|
|
54
|
+
:schema=${(x) => x.jsonSchema}
|
|
55
|
+
:data=${(x) => x.data}
|
|
56
|
+
:serverFieldErrors=${(x) => x.serverFieldErrors}
|
|
57
|
+
:prefix=${(x) => x.prefix}
|
|
58
|
+
:rowSubmitStatuses=${(x) => x.rowSubmitStatuses}
|
|
59
|
+
:bulkInsert=${(x) => x.bulkInsert}
|
|
60
|
+
@data-change=${(x, c) => x.onChange(c.event)}
|
|
61
|
+
></json-forms>
|
|
62
|
+
`)}
|
|
63
|
+
${when((x) => x.reviewMode, html `
|
|
64
|
+
<foundation-form-review
|
|
65
|
+
part="review-panel"
|
|
66
|
+
design-system-prefix=${(x) => x.prefix}
|
|
67
|
+
heading=${(x) => x.reviewHeading}
|
|
68
|
+
:recordId=${(x) => x.reviewRecordId}
|
|
69
|
+
:sections=${(x) => x.reviewSections}
|
|
70
|
+
></foundation-form-review>
|
|
71
|
+
`)}
|
|
72
|
+
${when((x) => x.reviewMode || x.isSubmitHidden || x.isResetVisible, html `
|
|
60
73
|
<slot name="userActions" part="userActions" class="actions">
|
|
61
|
-
${when((x) => x.isResetVisible, html `
|
|
74
|
+
${when((x) => !x.reviewMode && x.isResetVisible, html `
|
|
62
75
|
<${prefix}-button
|
|
63
76
|
@click="${(x) => x.handleResetClicked()}"
|
|
64
77
|
class="reset-button"
|
|
65
78
|
appearance="neutral"
|
|
66
79
|
data-test-id="reset-button"
|
|
67
80
|
>
|
|
68
|
-
|
|
81
|
+
${(x) => x.resetButtonText}
|
|
82
|
+
</${prefix}-button>
|
|
83
|
+
`)}
|
|
84
|
+
${when((x) => x.reviewMode, html `
|
|
85
|
+
<${prefix}-button
|
|
86
|
+
@click="${(x) => x.backFromReview()}"
|
|
87
|
+
class="reset-button"
|
|
88
|
+
appearance="neutral"
|
|
89
|
+
data-test-id="review-back-button"
|
|
90
|
+
>
|
|
91
|
+
Back
|
|
69
92
|
</${prefix}-button>
|
|
70
93
|
`)}
|
|
71
|
-
${when((x) => x.isSubmitHidden, html `
|
|
94
|
+
${when((x) => !x.reviewMode && x.isSubmitHidden, html `
|
|
72
95
|
<${prefix}-button
|
|
73
96
|
@click="${(x) => x._submit()}"
|
|
74
97
|
?disable=${(x) => !x.submitting}
|
|
75
98
|
class="submit-button"
|
|
76
99
|
appearance="accent"
|
|
77
100
|
>
|
|
78
|
-
${(x) => (x.bulkInsert ?
|
|
101
|
+
${(x) => (x.bulkInsert ? x.bulkSubmitButtonText : x.submitButtonText)}
|
|
102
|
+
</${prefix}-button>
|
|
103
|
+
`)}
|
|
104
|
+
${when((x) => x.reviewMode, html `
|
|
105
|
+
<${prefix}-button
|
|
106
|
+
@click="${(x) => x._submitFromReview()}"
|
|
107
|
+
?disabled=${(x) => x.submitting}
|
|
108
|
+
class="submit-button"
|
|
109
|
+
appearance="accent"
|
|
110
|
+
data-test-id="review-submit-button"
|
|
111
|
+
>
|
|
112
|
+
${(x) => (x.showSubmitReview ? x.reviewButtonText : x.submitButtonText)}
|
|
79
113
|
</${prefix}-button>
|
|
80
114
|
`)}
|
|
81
115
|
</slot>
|
package/dist/esm/index.js
CHANGED
|
@@ -2,6 +2,9 @@ export * from './filters';
|
|
|
2
2
|
export * from './form';
|
|
3
3
|
export * from './form.styles';
|
|
4
4
|
export * from './form.template';
|
|
5
|
+
export * from './review';
|
|
6
|
+
export * from './review.styles';
|
|
7
|
+
export * from './review.template';
|
|
5
8
|
export { MulticolumnDropdownControlRendererEntry, MulticolumnDropdownControlRendererTemplate, } from './jsonforms/renderers/MulticolumnDropdownControlRenderer';
|
|
6
9
|
export { LayoutVerticalCategorizationRendererEntry } from './jsonforms/renderers/LayoutVerticalCategorizationRenderer';
|
|
7
10
|
export { controlWrapperTemplate, controlWrapperStyles,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { __decorate } from "tslib";
|
|
2
2
|
import { html, css, observable, customElement, FASTElement, repeat, when, } from '@microsoft/fast-element';
|
|
3
3
|
import { classNames } from '@microsoft/fast-web-utilities';
|
|
4
|
+
import { extractErrorScopes, uiSchemaHasAnyError } from '../../utils/schema-utils';
|
|
4
5
|
/**
|
|
5
6
|
* Resolves the `activeCategory` option (the zero-based index of the category
|
|
6
7
|
* tab that should open on first render) to a valid tab index. Out-of-bounds or
|
|
@@ -160,48 +161,3 @@ CategorizationWrapper = __decorate([
|
|
|
160
161
|
})
|
|
161
162
|
], CategorizationWrapper);
|
|
162
163
|
export { CategorizationWrapper };
|
|
163
|
-
function uiSchemaHasAnyError(uiSchema, errorScopes) {
|
|
164
|
-
// Direct scope match
|
|
165
|
-
if (typeof uiSchema.scope === 'string' &&
|
|
166
|
-
errorScopes.includes(uiSchema.scope)) {
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
// Check composite children via options.childElements
|
|
170
|
-
const options = uiSchema.options;
|
|
171
|
-
const childElements = options === null || options === void 0 ? void 0 : options.childElements;
|
|
172
|
-
if (Array.isArray(childElements)) {
|
|
173
|
-
for (const child of childElements) {
|
|
174
|
-
if (uiSchemaHasAnyError(child, errorScopes)) {
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
// Check layout children (elements)
|
|
180
|
-
if (Array.isArray(uiSchema.elements)) {
|
|
181
|
-
for (const el of uiSchema.elements) {
|
|
182
|
-
if (uiSchemaHasAnyError(el, errorScopes)) {
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
function extractErrorScopes(errors) {
|
|
190
|
-
var _a;
|
|
191
|
-
const scopes = new Set();
|
|
192
|
-
for (const error of errors) {
|
|
193
|
-
if (error.keyword === 'required' && ((_a = error.params) === null || _a === void 0 ? void 0 : _a.missingProperty)) {
|
|
194
|
-
const basePath = error.instancePath || '';
|
|
195
|
-
const prop = error.params.missingProperty;
|
|
196
|
-
// Normalize the base path
|
|
197
|
-
const fullPath = basePath === '' ? `#/properties/${prop}` : `#${basePath}/properties/${prop}`;
|
|
198
|
-
scopes.add(fullPath);
|
|
199
|
-
}
|
|
200
|
-
else if (error.instancePath) {
|
|
201
|
-
// Remove leading slash and convert to JSON Pointer path
|
|
202
|
-
const instancePath = `#${error.instancePath.replace(/\//g, '/properties/')}`;
|
|
203
|
-
scopes.add(instancePath);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return Array.from(scopes);
|
|
207
|
-
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { attr, customElement, FASTElement, observable, volatile } from '@microsoft/fast-element';
|
|
3
|
+
import { foundationFormReviewStyles as styles } from './review.styles';
|
|
4
|
+
import { foundationFormReviewTemplate as template } from './review.template';
|
|
5
|
+
import { filterReviewSections } from './utils/review-utils';
|
|
6
|
+
/**
|
|
7
|
+
* Read-only preview panel shown by `<foundation-form>` when `show-submit-review` is enabled.
|
|
8
|
+
* Renders the reviewed data grouped into sections with a header and a filter field.
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
let Review = class Review extends FASTElement {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(...arguments);
|
|
14
|
+
this.prefix = 'rapid';
|
|
15
|
+
/**
|
|
16
|
+
* Title shown in the review header.
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
this.heading = 'Review';
|
|
20
|
+
/**
|
|
21
|
+
* Sections of label/value pairs to render, grouped by their nearest UI schema `Group`.
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
this.sections = [];
|
|
25
|
+
/**
|
|
26
|
+
* Free-text query filtering fields by label or value. Sections with no matching
|
|
27
|
+
* fields are hidden.
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
this.filterText = '';
|
|
31
|
+
}
|
|
32
|
+
/** @internal */
|
|
33
|
+
get filteredSections() {
|
|
34
|
+
return filterReviewSections(this.sections, this.filterText);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
__decorate([
|
|
38
|
+
attr({ attribute: 'design-system-prefix' })
|
|
39
|
+
], Review.prototype, "prefix", void 0);
|
|
40
|
+
__decorate([
|
|
41
|
+
attr
|
|
42
|
+
], Review.prototype, "heading", void 0);
|
|
43
|
+
__decorate([
|
|
44
|
+
attr({ attribute: 'record-id' })
|
|
45
|
+
], Review.prototype, "recordId", void 0);
|
|
46
|
+
__decorate([
|
|
47
|
+
observable
|
|
48
|
+
], Review.prototype, "sections", void 0);
|
|
49
|
+
__decorate([
|
|
50
|
+
observable
|
|
51
|
+
], Review.prototype, "filterText", void 0);
|
|
52
|
+
__decorate([
|
|
53
|
+
volatile
|
|
54
|
+
], Review.prototype, "filteredSections", null);
|
|
55
|
+
Review = __decorate([
|
|
56
|
+
customElement({
|
|
57
|
+
name: 'foundation-form-review',
|
|
58
|
+
template,
|
|
59
|
+
styles,
|
|
60
|
+
shadowOptions: null,
|
|
61
|
+
})
|
|
62
|
+
], Review);
|
|
63
|
+
export { Review };
|
|
@@ -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,38 @@
|
|
|
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
|
+
class="review-filter-input"
|
|
17
|
+
data-test-id="review-filter-input"
|
|
18
|
+
></${prefix}-text-field>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="review-body">
|
|
21
|
+
${repeat((x) => x.filteredSections, html `
|
|
22
|
+
${when((section) => section.fields.length > 0, html `
|
|
23
|
+
<div class="review-section-header">${(section) => { var _a; return (_a = section.title) !== null && _a !== void 0 ? _a : '—'; }}</div>
|
|
24
|
+
${repeat((section) => section.fields, html `
|
|
25
|
+
<div class="review-field">
|
|
26
|
+
<span class="review-field-label">${(field) => field.label}</span>
|
|
27
|
+
<span class="review-field-value">${(field) => field.value}</span>
|
|
28
|
+
</div>
|
|
29
|
+
`)}
|
|
30
|
+
`)}
|
|
31
|
+
`)}
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
`;
|
|
35
|
+
/** @internal */
|
|
36
|
+
export const foundationFormReviewTemplate = html `
|
|
37
|
+
${(x) => getPrefixedReview(x.prefix)}
|
|
38
|
+
`;
|
package/dist/esm/utils/index.js
CHANGED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { capitalCase } from 'change-case';
|
|
2
|
+
/**
|
|
3
|
+
* Flattens `buildReviewSections` output into a single ordered list.
|
|
4
|
+
* Section titles are prepended to field labels as "Title — Field".
|
|
5
|
+
*/
|
|
6
|
+
export function buildReviewFields(data, uiSchema, schema) {
|
|
7
|
+
return buildReviewSections(data, uiSchema, schema).flatMap((section) => section.fields.map((field) => ({
|
|
8
|
+
label: section.title ? `${section.title} — ${field.label}` : field.label,
|
|
9
|
+
value: field.value,
|
|
10
|
+
})));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Builds the review panel's fields grouped into sections by their nearest `Group` layout.
|
|
14
|
+
* Ungrouped fields are collected into a single section with a `null` title (rendered with
|
|
15
|
+
* a placeholder heading). Falls back to a flat, single ungrouped section when no UI schema
|
|
16
|
+
* is available.
|
|
17
|
+
*/
|
|
18
|
+
export function buildReviewSections(data, uiSchema, schema) {
|
|
19
|
+
const sections = [];
|
|
20
|
+
const sectionsByTitle = new Map();
|
|
21
|
+
const addField = (title, field) => {
|
|
22
|
+
let section = sectionsByTitle.get(title);
|
|
23
|
+
if (!section) {
|
|
24
|
+
section = { title, fields: [] };
|
|
25
|
+
sectionsByTitle.set(title, section);
|
|
26
|
+
sections.push(section);
|
|
27
|
+
}
|
|
28
|
+
section.fields.push(field);
|
|
29
|
+
};
|
|
30
|
+
const walk = (element, groupTitle) => {
|
|
31
|
+
var _a, _b, _c, _d, _e;
|
|
32
|
+
if (!element)
|
|
33
|
+
return;
|
|
34
|
+
if (element.type === 'Control' && element.scope) {
|
|
35
|
+
if ((_a = element.options) === null || _a === void 0 ? void 0 : _a.hidden)
|
|
36
|
+
return;
|
|
37
|
+
const match = element.scope.match(/^#\/properties\/(.+)$/);
|
|
38
|
+
if (!match)
|
|
39
|
+
return;
|
|
40
|
+
const fieldName = match[1];
|
|
41
|
+
const raw = getDataValueAtPath(data, fieldName);
|
|
42
|
+
if (raw === undefined ||
|
|
43
|
+
raw === null ||
|
|
44
|
+
raw === '' ||
|
|
45
|
+
(Array.isArray(raw) && raw.length === 0))
|
|
46
|
+
return;
|
|
47
|
+
const shortName = fieldName.split('/properties/').pop();
|
|
48
|
+
const fieldLabel = element.label || capitalCase(shortName);
|
|
49
|
+
const displayValue = (_c = (_b = resolveOptionLabel(raw, element.options)) !== null && _b !== void 0 ? _b : resolveEnumLabel(raw, fieldName, schema)) !== null && _c !== void 0 ? _c : raw;
|
|
50
|
+
addField(groupTitle, { label: fieldLabel, value: formatValueForReview(displayValue) });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const nextGroup = element.type === 'Group' && element.label ? element.label : groupTitle;
|
|
54
|
+
const children = (_d = element.elements) !== null && _d !== void 0 ? _d : (_e = element.options) === null || _e === void 0 ? void 0 : _e.childElements;
|
|
55
|
+
if (children) {
|
|
56
|
+
for (const child of children) {
|
|
57
|
+
walk(child, nextGroup);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
if (uiSchema) {
|
|
62
|
+
walk(uiSchema, null);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
for (const [key, raw] of Object.entries(data !== null && data !== void 0 ? data : {})) {
|
|
66
|
+
if (raw === undefined || raw === null || raw === '')
|
|
67
|
+
continue;
|
|
68
|
+
addField(null, { label: capitalCase(key), value: formatValueForReview(raw) });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return sections;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Filters review sections down to fields whose label or value contains `query`
|
|
75
|
+
* (case-insensitive). Sections left with no matching fields are dropped entirely.
|
|
76
|
+
* Returns `sections` unchanged when `query` is blank.
|
|
77
|
+
*/
|
|
78
|
+
export function filterReviewSections(sections, query) {
|
|
79
|
+
const normalized = query.trim().toLowerCase();
|
|
80
|
+
if (!normalized)
|
|
81
|
+
return sections;
|
|
82
|
+
return sections
|
|
83
|
+
.map((section) => ({
|
|
84
|
+
title: section.title,
|
|
85
|
+
fields: section.fields.filter((field) => field.label.toLowerCase().includes(normalized) ||
|
|
86
|
+
field.value.toLowerCase().includes(normalized)),
|
|
87
|
+
}))
|
|
88
|
+
.filter((section) => section.fields.length > 0);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Recursively walks a UI schema element, collecting visible Control fields into `out`.
|
|
92
|
+
* Passes the nearest Group label down so children are labelled "Group — Field".
|
|
93
|
+
*/
|
|
94
|
+
export function collectReviewFields(element, data, out, groupLabel, schema) {
|
|
95
|
+
var _a, _b, _c, _d, _e;
|
|
96
|
+
if (element.type === 'Control' && element.scope) {
|
|
97
|
+
if ((_a = element.options) === null || _a === void 0 ? void 0 : _a.hidden)
|
|
98
|
+
return;
|
|
99
|
+
const match = element.scope.match(/^#\/properties\/(.+)$/);
|
|
100
|
+
if (!match)
|
|
101
|
+
return;
|
|
102
|
+
const fieldName = match[1];
|
|
103
|
+
const raw = getDataValueAtPath(data, fieldName);
|
|
104
|
+
if (raw === undefined || raw === null || raw === '' || (Array.isArray(raw) && raw.length === 0))
|
|
105
|
+
return;
|
|
106
|
+
const shortName = fieldName.split('/properties/').pop();
|
|
107
|
+
const fieldLabel = element.label || capitalCase(shortName);
|
|
108
|
+
const displayValue = (_c = (_b = resolveOptionLabel(raw, element.options)) !== null && _b !== void 0 ? _b : resolveEnumLabel(raw, fieldName, schema)) !== null && _c !== void 0 ? _c : raw;
|
|
109
|
+
out.push({
|
|
110
|
+
label: groupLabel ? `${groupLabel} — ${fieldLabel}` : fieldLabel,
|
|
111
|
+
value: formatValueForReview(displayValue),
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const nextGroup = element.type === 'Group' && element.label ? element.label : groupLabel;
|
|
116
|
+
const children = (_d = element.elements) !== null && _d !== void 0 ? _d : (_e = element.options) === null || _e === void 0 ? void 0 : _e.childElements;
|
|
117
|
+
if (children) {
|
|
118
|
+
for (const child of children) {
|
|
119
|
+
collectReviewFields(child, data, out, nextGroup, schema);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Looks up the display label for a raw value from `options.data` (used by segmented,
|
|
125
|
+
* radio group, and enum-with-explicit-data renderers). Returns `null` when no match found.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveOptionLabel(raw, options) {
|
|
128
|
+
var _a;
|
|
129
|
+
if (!Array.isArray(options === null || options === void 0 ? void 0 : options.data))
|
|
130
|
+
return null;
|
|
131
|
+
const entry = options.data.find((item) => item.value === raw);
|
|
132
|
+
return (_a = entry === null || entry === void 0 ? void 0 : entry.label) !== null && _a !== void 0 ? _a : null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* For plain enum combobox fields (no options.data), applies the same `capitalCase`
|
|
136
|
+
* transform the renderer uses — e.g. `SEMI_ANNUAL` → `Semi Annual`.
|
|
137
|
+
* Navigates the JSON schema via the fieldName path to confirm the field is an enum
|
|
138
|
+
* before transforming. Returns `null` for non-enum fields.
|
|
139
|
+
*/
|
|
140
|
+
export function resolveEnumLabel(raw, fieldName, schema) {
|
|
141
|
+
var _a, _b, _c, _d;
|
|
142
|
+
if (typeof raw !== 'string')
|
|
143
|
+
return null;
|
|
144
|
+
if (!(schema === null || schema === void 0 ? void 0 : schema.properties))
|
|
145
|
+
return null;
|
|
146
|
+
let fieldSchema = schema;
|
|
147
|
+
for (const part of fieldName.split('/properties/')) {
|
|
148
|
+
fieldSchema = (_a = fieldSchema === null || fieldSchema === void 0 ? void 0 : fieldSchema.properties) === null || _a === void 0 ? void 0 : _a[part];
|
|
149
|
+
}
|
|
150
|
+
if (!fieldSchema)
|
|
151
|
+
return null;
|
|
152
|
+
// Matches plain `enum: [...]` or nullable `oneOf: [{ enum: [...] }, { type: 'null' }]`
|
|
153
|
+
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;
|
|
154
|
+
if (Array.isArray(enumValues) && enumValues.includes(raw)) {
|
|
155
|
+
return capitalCase(raw);
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Navigates a `/properties/`-separated path in data to support nested object scopes
|
|
161
|
+
* such as `ADDRESS/properties/STREET` produced by Group layout UI schemas.
|
|
162
|
+
*/
|
|
163
|
+
export function getDataValueAtPath(data, fieldName) {
|
|
164
|
+
return navigateSegments(data, fieldName.split('/properties/'));
|
|
165
|
+
}
|
|
166
|
+
/** Core traversal — walks `data` one property key at a time. */
|
|
167
|
+
function navigateSegments(data, segments) {
|
|
168
|
+
let value = data;
|
|
169
|
+
for (const segment of segments) {
|
|
170
|
+
if (value == null)
|
|
171
|
+
return undefined;
|
|
172
|
+
value = value[segment];
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Converts a raw field value to a display string for the review panel.
|
|
178
|
+
* Arrays of primitives are comma-joined; arrays of objects and plain objects
|
|
179
|
+
* are rendered as readable `key: value` lines.
|
|
180
|
+
*/
|
|
181
|
+
export function formatValueForReview(value) {
|
|
182
|
+
if (Array.isArray(value)) {
|
|
183
|
+
if (value.length === 0)
|
|
184
|
+
return '';
|
|
185
|
+
if (value.every((v) => v === null || typeof v !== 'object')) {
|
|
186
|
+
return value.join(', ');
|
|
187
|
+
}
|
|
188
|
+
return value
|
|
189
|
+
.map((item, i) => {
|
|
190
|
+
const pairs = Object.entries(item !== null && item !== void 0 ? item : {})
|
|
191
|
+
.filter(([, v]) => v !== null && v !== undefined && v !== '')
|
|
192
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
193
|
+
.join(', ');
|
|
194
|
+
return `[${i + 1}] ${pairs}`;
|
|
195
|
+
})
|
|
196
|
+
.join('\n');
|
|
197
|
+
}
|
|
198
|
+
if (typeof value === 'object') {
|
|
199
|
+
return Object.entries(value)
|
|
200
|
+
.filter(([, v]) => v !== null && v !== undefined && v !== '')
|
|
201
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
202
|
+
.join(', ');
|
|
203
|
+
}
|
|
204
|
+
return String(value);
|
|
205
|
+
}
|
|
@@ -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
|