@openg2p/registry-widgets 1.1.0-dev.8 → 1.1.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.
- package/dist/components/SectionRenderer.d.ts +7 -1
- package/dist/components/SectionRenderer.d.ts.map +1 -1
- package/dist/components/SectionsContainer.d.ts.map +1 -1
- package/dist/hooks/useBaseWidget.d.ts.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.esm.js +105 -48
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +105 -48
- package/dist/index.js.map +1 -1
- package/dist/utils/dataSource.d.ts.map +1 -1
- package/dist/utils/sectionValidate.d.ts +9 -1
- package/dist/utils/sectionValidate.d.ts.map +1 -1
- package/dist/utils/validation.d.ts +6 -2
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/widgets/TextAreaWidget.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -246,24 +246,27 @@ const getValidationPattern = (validationType) => {
|
|
|
246
246
|
};
|
|
247
247
|
|
|
248
248
|
/**
|
|
249
|
-
* Validate value against validation rules
|
|
249
|
+
* Validate value against validation rules.
|
|
250
|
+
*
|
|
251
|
+
* @param skipRequired - When true, required-field checks are skipped (used by
|
|
252
|
+
* per-section Save/Next buttons so the user can move between sections without
|
|
253
|
+
* filling every mandatory field; only format/range checks still run).
|
|
250
254
|
*/
|
|
251
|
-
const validateWidget = (value, validation, required = false) => {
|
|
255
|
+
const validateWidget = (value, validation, required = false, skipRequired = false) => {
|
|
252
256
|
const errors = [];
|
|
253
257
|
if (!validation && !required) {
|
|
254
258
|
return errors;
|
|
255
259
|
}
|
|
256
|
-
// Check required
|
|
257
|
-
const isRequired = validation?.required ?? required;
|
|
260
|
+
// Check required (skipped when navigating between sections)
|
|
261
|
+
const isRequired = !skipRequired && (validation?.required ?? required);
|
|
258
262
|
// For boolean, false is a valid value, so only check for null/undefined/empty string
|
|
259
263
|
const isEmpty = value === null || value === undefined || value === '';
|
|
260
264
|
if (isRequired && isEmpty) {
|
|
261
265
|
errors.push('This field is required');
|
|
262
266
|
return errors; // Return early if required field is empty
|
|
263
267
|
}
|
|
264
|
-
// Skip
|
|
265
|
-
|
|
266
|
-
if (isEmpty && !isRequired) {
|
|
268
|
+
// Skip format/range validations if value is empty
|
|
269
|
+
if (isEmpty) {
|
|
267
270
|
return errors;
|
|
268
271
|
}
|
|
269
272
|
if (!validation) {
|
|
@@ -1075,16 +1078,12 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
|
|
|
1075
1078
|
console.error('[getApiDataSource] API data source missing endpoint. Use "endpoint" field to specify the operation (e.g., "get_g2p_geo_level_values")');
|
|
1076
1079
|
return [];
|
|
1077
1080
|
}
|
|
1078
|
-
let
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
}
|
|
1084
|
-
catch (error) {
|
|
1085
|
-
console.error('[getApiDataSource] Handler error:', error);
|
|
1086
|
-
throw error;
|
|
1087
|
-
}
|
|
1081
|
+
// Call handler — let any throw propagate to the outer catch so it is logged once
|
|
1082
|
+
// by useBaseWidget rather than double-logged here (which can cascade when
|
|
1083
|
+
// intercept-console-error.js converts console.error calls into thrown errors).
|
|
1084
|
+
const response = await dataSourceRequestHandler(service, endpoint, method, requestParams, {
|
|
1085
|
+
headers: dataSource.headers,
|
|
1086
|
+
});
|
|
1088
1087
|
// Handle OpenG2P response format (response_body.response_payload)
|
|
1089
1088
|
if (response && typeof response === 'object') {
|
|
1090
1089
|
if (response.response_body?.response_payload && Array.isArray(response.response_body.response_payload)) {
|
|
@@ -1107,8 +1106,8 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
|
|
|
1107
1106
|
return [];
|
|
1108
1107
|
}
|
|
1109
1108
|
catch (error) {
|
|
1110
|
-
|
|
1111
|
-
|
|
1109
|
+
// Rethrow so useBaseWidget's catch can log it with full widget context
|
|
1110
|
+
throw error;
|
|
1112
1111
|
}
|
|
1113
1112
|
};
|
|
1114
1113
|
/**
|
|
@@ -2302,7 +2301,7 @@ const useBaseWidget = (options) => {
|
|
|
2302
2301
|
dispatch(setDataSource({ widgetId, data: transformed }));
|
|
2303
2302
|
}
|
|
2304
2303
|
catch (error) {
|
|
2305
|
-
console.error(`[useBaseWidget] ERROR loading data source for ${widgetId}:`, error);
|
|
2304
|
+
console.error(`[useBaseWidget] ERROR loading data source for widget "${widgetId}" (type="${dataSource.type}"):`, error, '\nWidget config:', config, '\ndataSourceRequestHandler provided:', Boolean(dataSourceRequestHandler));
|
|
2306
2305
|
dispatch(setDataSource({ widgetId, data: [] }));
|
|
2307
2306
|
}
|
|
2308
2307
|
finally {
|
|
@@ -3807,7 +3806,15 @@ const collectWidgets = (panels) => {
|
|
|
3807
3806
|
});
|
|
3808
3807
|
return widgets;
|
|
3809
3808
|
};
|
|
3810
|
-
|
|
3809
|
+
/**
|
|
3810
|
+
* Validate all widgets in a section and dispatch errors to the store.
|
|
3811
|
+
*
|
|
3812
|
+
* @param skipRequired - When true, required-field checks (widget-required,
|
|
3813
|
+
* validation.required, document-required) are skipped. Use this for
|
|
3814
|
+
* per-section Save/Next navigation so the user can advance without filling
|
|
3815
|
+
* every mandatory field; only format/range errors are reported.
|
|
3816
|
+
*/
|
|
3817
|
+
const sectionValidate = (section, currentSchemaData, dispatch, skipRequired = false) => {
|
|
3811
3818
|
const allWidgets = collectWidgets(section.panels);
|
|
3812
3819
|
let isValid = true;
|
|
3813
3820
|
for (const widget of allWidgets) {
|
|
@@ -3816,7 +3823,7 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
|
|
|
3816
3823
|
continue;
|
|
3817
3824
|
const widgetId = widget['widget-id'];
|
|
3818
3825
|
const value = getWidgetValue(currentSchemaData, widget['widget-data-path'], widgetId);
|
|
3819
|
-
const errors = validateWidget(value, widget['widget-data-validation'], widget['widget-required']);
|
|
3826
|
+
const errors = validateWidget(value, widget['widget-data-validation'], widget['widget-required'], skipRequired);
|
|
3820
3827
|
if (errors.length > 0) {
|
|
3821
3828
|
isValid = false;
|
|
3822
3829
|
dispatch(setTouched({ widgetId, touched: true }));
|
|
@@ -3827,10 +3834,10 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
|
|
|
3827
3834
|
dispatch(setError({ widgetId, errors: [] }));
|
|
3828
3835
|
}
|
|
3829
3836
|
}
|
|
3830
|
-
// Supporting Documents
|
|
3837
|
+
// Supporting Documents — only check required when not skipping required validation
|
|
3831
3838
|
section['section-supporting-documents']?.forEach((doc, index) => {
|
|
3832
3839
|
const widgetId = `supporting-doc-${section['section-id']}-${index}`;
|
|
3833
|
-
if (doc['document-required']) {
|
|
3840
|
+
if (!skipRequired && doc['document-required']) {
|
|
3834
3841
|
const file = getValueByPath(currentSchemaData, doc['document-data-path']);
|
|
3835
3842
|
if (!file) {
|
|
3836
3843
|
isValid = false;
|
|
@@ -3936,7 +3943,7 @@ function scopedClassSelectors(sectionClassId, classNames) {
|
|
|
3936
3943
|
* - Panels wrap when they exceed available width
|
|
3937
3944
|
* - Sections can sit side-by-side if there's space
|
|
3938
3945
|
*/
|
|
3939
|
-
const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequestHandler, schemaData, onValueChange, gridColumnSpan, onSectionSave, hideEditButton = false, mode = 'RegistryView', namespace, changeRequestType, showChangeRequestLabel = true, dbSectionId, sectionRegisterId, onSectionDirtyChange, sectionIndex, sectionCount, expandedSectionIndex, onExpandSection, onSectionSaveSuccess, onPreviousSection, isDraft, onEditModeChange, forceExitEdit, }) => {
|
|
3946
|
+
const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequestHandler, schemaData, onValueChange, gridColumnSpan, onSectionSave, hideEditButton = false, mode = 'RegistryView', namespace, changeRequestType, showChangeRequestLabel = true, dbSectionId, sectionRegisterId, onSectionDirtyChange, sectionIndex, sectionCount, expandedSectionIndex, onExpandSection, onSectionSaveSuccess, onPreviousSection, isDraft, isAccessible = false, onEditModeChange, forceExitEdit, }) => {
|
|
3940
3947
|
const { translateConfig, translate } = useWidgetTranslation();
|
|
3941
3948
|
const resolvedTheme = useWidgetTheme();
|
|
3942
3949
|
const portalCSSVariables = React.useMemo(() => themeToCSSVariables(resolvedTheme), [resolvedTheme]);
|
|
@@ -4002,16 +4009,25 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4002
4009
|
const isExpandedFromContainer = typeof sectionIndex === 'number' && expandedSectionIndex === sectionIndex;
|
|
4003
4010
|
const isExpandedStandalone = sectionIndex === undefined && standaloneExpanded;
|
|
4004
4011
|
const isExpanded = mode === 'IntakeForm' && (isExpandedFromContainer || isExpandedStandalone);
|
|
4012
|
+
// IntakeForm only: tracks whether the user has clicked Next on this section at least once.
|
|
4013
|
+
// Used to unlock the accordion header so the user can navigate back to a visited section.
|
|
4014
|
+
const [hasBeenSavedByUser, setHasBeenSavedByUser] = React.useState(false);
|
|
4015
|
+
// Accordion header click behaviour in IntakeForm mode:
|
|
4016
|
+
// - Standalone (no sectionIndex): always toggleable.
|
|
4017
|
+
// - Managed by SectionsContainer: toggleable only when isAccessible is true
|
|
4018
|
+
// (i.e. the section has been visited OR is the immediate next one).
|
|
4019
|
+
// Sections beyond that remain locked.
|
|
4005
4020
|
const handleAccordionToggle = React.useCallback(() => {
|
|
4006
4021
|
if (mode !== 'IntakeForm')
|
|
4007
4022
|
return;
|
|
4008
|
-
if (
|
|
4009
|
-
onExpandSection(sectionIndex);
|
|
4010
|
-
}
|
|
4011
|
-
else if (sectionIndex === undefined) {
|
|
4023
|
+
if (sectionIndex === undefined) {
|
|
4012
4024
|
setStandaloneExpanded(prev => !prev);
|
|
4013
4025
|
}
|
|
4014
|
-
|
|
4026
|
+
else if (isAccessible && onExpandSection) {
|
|
4027
|
+
onExpandSection(sectionIndex);
|
|
4028
|
+
}
|
|
4029
|
+
// Intentionally no-op for locked sections (isAccessible === false)
|
|
4030
|
+
}, [mode, sectionIndex, isAccessible, onExpandSection]);
|
|
4015
4031
|
// Recursively count all vertical panels, especially those nested inside horizontal panels
|
|
4016
4032
|
// Typically: horizontal panels at first level contain vertical panels at second level
|
|
4017
4033
|
// Accounts for panel-column-span: a panel with column-span 3 counts as 3 columns
|
|
@@ -4346,8 +4362,6 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4346
4362
|
const baselineSnapshotRef = React.useRef(null);
|
|
4347
4363
|
// IntakeForm only: increment when baseline is updated after save - forces badge to update (refs don't trigger re-renders)
|
|
4348
4364
|
const [intakeFormBaselineTrigger, setIntakeFormBaselineTrigger] = React.useState(0);
|
|
4349
|
-
// IntakeForm only: tracks whether the user has actually saved this section (prevents "Saved" badge on initial load)
|
|
4350
|
-
const [hasBeenSavedByUser, setHasBeenSavedByUser] = React.useState(false);
|
|
4351
4365
|
// IntakeForm: treat as edit mode for dirty tracking when isDraft. RegistryView: use isEditMode.
|
|
4352
4366
|
const effectiveEditModeForDirty = mode === 'IntakeForm' ? (isDraft !== false) : isEditMode;
|
|
4353
4367
|
// Compute isDirty: compare current store state to baseline (only when in edit mode)
|
|
@@ -4465,7 +4479,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4465
4479
|
const sectionWidgets = collectWidgets(originalSection.panels);
|
|
4466
4480
|
const currentState = store.getState().widget;
|
|
4467
4481
|
const currentSchemaData = currentState.values || {};
|
|
4468
|
-
const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch);
|
|
4482
|
+
const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch, true);
|
|
4469
4483
|
if (!isSectionValid) {
|
|
4470
4484
|
return;
|
|
4471
4485
|
}
|
|
@@ -4531,7 +4545,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4531
4545
|
const sectionWidgets = collectWidgets(originalSection.panels);
|
|
4532
4546
|
const currentState = store.getState().widget;
|
|
4533
4547
|
const currentSchemaData = currentState.values || {};
|
|
4534
|
-
const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch);
|
|
4548
|
+
const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch, true);
|
|
4535
4549
|
if (!isSectionValid)
|
|
4536
4550
|
return;
|
|
4537
4551
|
const oldSchemaData = schemaData || contextSchemaData;
|
|
@@ -4580,6 +4594,11 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4580
4594
|
}
|
|
4581
4595
|
onSectionDirtyChange?.(sectionId, false);
|
|
4582
4596
|
}
|
|
4597
|
+
else if (mode === 'IntakeForm') {
|
|
4598
|
+
// No onSectionSave provided, but still mark section as visited so the
|
|
4599
|
+
// user can navigate back to it by clicking the accordion header.
|
|
4600
|
+
setHasBeenSavedByUser(true);
|
|
4601
|
+
}
|
|
4583
4602
|
// Always navigate to the next section
|
|
4584
4603
|
onSectionSaveSuccess?.(sectionIndex);
|
|
4585
4604
|
}, [store, onSectionSave, onSectionSaveSuccess, sectionIndex, originalSection, schemaData, contextSchemaData, namespace, hasSupportingDocuments, dbSectionId, sectionRegisterId, dispatch, buildSectionSnapshot, sectionId, onSectionDirtyChange, mode, isDraft]);
|
|
@@ -4798,13 +4817,17 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4798
4817
|
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header h2 {
|
|
4799
4818
|
color: var(--owt-color-primary-dark, #F07B1A);
|
|
4800
4819
|
}
|
|
4801
|
-
|
|
4820
|
+
/* Hover / focus only shown when the header is actually interactive (standalone mode) */
|
|
4821
|
+
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="true"]:hover {
|
|
4802
4822
|
opacity: 0.85;
|
|
4803
4823
|
}
|
|
4804
|
-
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header:focus-visible {
|
|
4824
|
+
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="true"]:focus-visible {
|
|
4805
4825
|
outline: 2px solid var(--owt-color-primary, #F5BB1A);
|
|
4806
4826
|
outline-offset: 2px;
|
|
4807
4827
|
}
|
|
4828
|
+
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="false"]:focus-visible {
|
|
4829
|
+
outline: none;
|
|
4830
|
+
}
|
|
4808
4831
|
.${sectionClassId}.intake-form-accordion-item .intake-form-accordion-content {
|
|
4809
4832
|
padding-top: 8px;
|
|
4810
4833
|
padding-bottom: 0px;
|
|
@@ -4847,7 +4870,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4847
4870
|
}),
|
|
4848
4871
|
}, children: mode === 'IntakeForm' ? (
|
|
4849
4872
|
/* IntakeForm: accordion layout - header always visible, content only when expanded */
|
|
4850
|
-
jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("button", { type: "button", id: `intake-form-accordion-header-${sectionId}`, className: "intake-form-accordion-header", onClick: handleAccordionToggle, "aria-expanded": isExpanded, "aria-controls": isExpanded ? `intake-form-accordion-content-${sectionId}` : undefined, style: {
|
|
4873
|
+
jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("button", { type: "button", id: `intake-form-accordion-header-${sectionId}`, className: "intake-form-accordion-header", onClick: handleAccordionToggle, "aria-expanded": isExpanded, "aria-controls": isExpanded ? `intake-form-accordion-content-${sectionId}` : undefined, "data-interactive": sectionIndex === undefined || isAccessible ? 'true' : 'false', style: {
|
|
4851
4874
|
width: '100%',
|
|
4852
4875
|
display: 'flex',
|
|
4853
4876
|
alignItems: 'flex-start',
|
|
@@ -4857,7 +4880,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
|
|
|
4857
4880
|
marginBottom: 0,
|
|
4858
4881
|
background: 'none',
|
|
4859
4882
|
border: 'none',
|
|
4860
|
-
cursor: 'pointer',
|
|
4883
|
+
cursor: sectionIndex === undefined || isAccessible ? 'pointer' : 'default',
|
|
4861
4884
|
textAlign: 'left',
|
|
4862
4885
|
fontFamily: 'Roboto, sans-serif',
|
|
4863
4886
|
}, children: [jsxRuntimeExports.jsxs("div", { style: { flex: 1, display: 'flex', alignItems: 'center', gap: '12px', minWidth: 0 }, children: [jsxRuntimeExports.jsx("h2", { className: "text-xl font-semibold", style: { margin: 0 }, children: sectionToRender['section-title']
|
|
@@ -5155,6 +5178,11 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
5155
5178
|
const dataSourceRequestHandler = propDataSourceRequestHandler || contextDataSourceRequestHandler;
|
|
5156
5179
|
// IntakeForm mode: accordion state - which section is expanded (null = none; first expanded by default)
|
|
5157
5180
|
const [expandedSectionIndex, setExpandedSectionIndex] = React.useState(0);
|
|
5181
|
+
// IntakeForm mode: high-water mark of the furthest section the user has clicked Next on.
|
|
5182
|
+
// A section at index i is accessible when i <= maxVisitedIndex + 1
|
|
5183
|
+
// (i.e. every visited section plus the one immediately after it).
|
|
5184
|
+
// Starts at -1 so only section 0 is accessible before any Next is clicked.
|
|
5185
|
+
const [maxVisitedIndex, setMaxVisitedIndex] = React.useState(-1);
|
|
5158
5186
|
// RegistryView: track which section is currently in edit mode (by section-id); null = none
|
|
5159
5187
|
const [editingSectionId, setEditingSectionId] = React.useState(null);
|
|
5160
5188
|
const handleEditModeChange = React.useCallback((sectionId, editing) => {
|
|
@@ -5165,6 +5193,10 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
5165
5193
|
// Stable ref for namespace so formHandle useMemo doesn't depend on the (possibly inline) function identity
|
|
5166
5194
|
const namespaceRef = React.useRef(namespace);
|
|
5167
5195
|
namespaceRef.current = namespace;
|
|
5196
|
+
// Stable refs so formHandle closure can access current mode and accordion setter without stale captures
|
|
5197
|
+
const modeRef = React.useRef(mode);
|
|
5198
|
+
modeRef.current = mode;
|
|
5199
|
+
const setExpandedSectionIndexRef = React.useRef(setExpandedSectionIndex);
|
|
5168
5200
|
// Track dirty (unsaved changes) per section for form handle validation
|
|
5169
5201
|
const sectionDirtyMapRef = React.useRef({});
|
|
5170
5202
|
const handleSectionDirtyChange = React.useCallback((sectionId, isDirty) => {
|
|
@@ -5175,8 +5207,9 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
5175
5207
|
const handleExpandSection = React.useCallback((index) => {
|
|
5176
5208
|
setExpandedSectionIndex(prev => (prev === index ? null : index));
|
|
5177
5209
|
}, []);
|
|
5178
|
-
// IntakeForm mode: called after section save - collapse current, expand next
|
|
5210
|
+
// IntakeForm mode: called after section save - advance high-water mark, collapse current, expand next
|
|
5179
5211
|
const handleSectionSaveSuccess = React.useCallback((index) => {
|
|
5212
|
+
setMaxVisitedIndex(prev => Math.max(prev, index));
|
|
5180
5213
|
if (index + 1 < safeSections.length) {
|
|
5181
5214
|
setExpandedSectionIndex(index + 1);
|
|
5182
5215
|
}
|
|
@@ -5226,33 +5259,52 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
5226
5259
|
};
|
|
5227
5260
|
return {
|
|
5228
5261
|
validate: async () => {
|
|
5229
|
-
|
|
5262
|
+
if (modeRef.current !== 'IntakeForm')
|
|
5263
|
+
checkNoUnsavedChanges();
|
|
5230
5264
|
const values = getValues();
|
|
5231
5265
|
let allValid = true;
|
|
5266
|
+
let firstInvalidIndex = null;
|
|
5232
5267
|
for (let i = 0; i < safeSections.length; i++) {
|
|
5233
5268
|
const section = safeSections[i];
|
|
5234
5269
|
const ns = getNamespace(section, i);
|
|
5235
5270
|
const sectionToValidate = ns ? namespaceSectionConfig(section, ns) : section;
|
|
5236
5271
|
const valid = sectionValidate(sectionToValidate, values, dispatch);
|
|
5237
|
-
if (!valid)
|
|
5272
|
+
if (!valid) {
|
|
5273
|
+
if (firstInvalidIndex === null)
|
|
5274
|
+
firstInvalidIndex = i;
|
|
5238
5275
|
allValid = false;
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
if (!allValid && modeRef.current === 'IntakeForm' && firstInvalidIndex !== null) {
|
|
5279
|
+
setExpandedSectionIndexRef.current(firstInvalidIndex);
|
|
5239
5280
|
}
|
|
5240
5281
|
return allValid;
|
|
5241
5282
|
},
|
|
5242
5283
|
getFormData: () => getValues(),
|
|
5243
5284
|
validateAndGetData: async () => {
|
|
5244
|
-
|
|
5285
|
+
if (modeRef.current !== 'IntakeForm')
|
|
5286
|
+
checkNoUnsavedChanges();
|
|
5245
5287
|
const values = getValues();
|
|
5246
5288
|
const results = [];
|
|
5289
|
+
let firstInvalidIndex = null;
|
|
5247
5290
|
for (let i = 0; i < safeSections.length; i++) {
|
|
5248
5291
|
const section = safeSections[i];
|
|
5249
5292
|
const ns = getNamespace(section, i);
|
|
5250
5293
|
const sectionToValidate = ns ? namespaceSectionConfig(section, ns) : section;
|
|
5251
5294
|
const valid = sectionValidate(sectionToValidate, values, dispatch);
|
|
5252
5295
|
if (!valid) {
|
|
5253
|
-
|
|
5296
|
+
if (firstInvalidIndex === null)
|
|
5297
|
+
firstInvalidIndex = i;
|
|
5298
|
+
}
|
|
5299
|
+
else {
|
|
5300
|
+
results.push(buildSectionChanges(section, values, ns));
|
|
5254
5301
|
}
|
|
5255
|
-
|
|
5302
|
+
}
|
|
5303
|
+
if (firstInvalidIndex !== null) {
|
|
5304
|
+
if (modeRef.current === 'IntakeForm') {
|
|
5305
|
+
setExpandedSectionIndexRef.current(firstInvalidIndex);
|
|
5306
|
+
}
|
|
5307
|
+
throw new Error('Validation failed. Please fix the errors and try again.');
|
|
5256
5308
|
}
|
|
5257
5309
|
return results;
|
|
5258
5310
|
},
|
|
@@ -5351,6 +5403,8 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
|
|
|
5351
5403
|
onSectionSaveSuccess: handleSectionSaveSuccess,
|
|
5352
5404
|
onPreviousSection: handlePreviousSection,
|
|
5353
5405
|
isDraft,
|
|
5406
|
+
// Accessible = every visited section + the one immediately after
|
|
5407
|
+
isAccessible: index <= maxVisitedIndex + 1,
|
|
5354
5408
|
}
|
|
5355
5409
|
: {};
|
|
5356
5410
|
// RegistryView: single-edit coordination props
|
|
@@ -7333,7 +7387,7 @@ const NumberInputWidget = ({ config }) => {
|
|
|
7333
7387
|
if (parsed === null) {
|
|
7334
7388
|
// Allow empty input or partial input (e.g., "-", ".")
|
|
7335
7389
|
if (inputValue === '' || inputValue === '-' || inputValue === '.') {
|
|
7336
|
-
onChange(
|
|
7390
|
+
onChange(null);
|
|
7337
7391
|
}
|
|
7338
7392
|
// Don't update if invalid - let user continue typing
|
|
7339
7393
|
return;
|
|
@@ -9697,10 +9751,13 @@ const TextAreaWidget = ({ config }) => {
|
|
|
9697
9751
|
// For readonly mode, render as preformatted text using <pre> tag
|
|
9698
9752
|
if (isReadonly) {
|
|
9699
9753
|
const displayValue = getStringValue() || '-';
|
|
9700
|
-
return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] TextAreaDisplayWidget flex flex-col sm:flex-row sm:items-start", children: [label && (jsxRuntimeExports.jsxs("div", { className: "text-base text-gray-600 font-medium md:min-w-[120px] sm:pr-4 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: label, children: [label, ":"] })), jsxRuntimeExports.jsx("div", { className: "flex-1", children: jsxRuntimeExports.jsx("
|
|
9754
|
+
return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] TextAreaDisplayWidget flex flex-col sm:flex-row sm:items-start", children: [label && (jsxRuntimeExports.jsxs("div", { className: "text-base text-gray-600 font-medium md:min-w-[120px] sm:pr-4 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: label, children: [label, ":"] })), jsxRuntimeExports.jsx("div", { className: "flex-1", children: jsxRuntimeExports.jsx("div", { title: String(displayValue), className: "text-base text-gray-900 font-medium overflow-y-auto whitespace-pre-wrap break-words", style: {
|
|
9701
9755
|
fontFamily: 'Roboto, sans-serif',
|
|
9702
|
-
|
|
9703
|
-
|
|
9756
|
+
height: '56px',
|
|
9757
|
+
minHeight: '56px',
|
|
9758
|
+
maxHeight: '56px',
|
|
9759
|
+
lineHeight: '20px',
|
|
9760
|
+
padding: '8px 0',
|
|
9704
9761
|
backgroundColor: 'transparent',
|
|
9705
9762
|
border: 'none',
|
|
9706
9763
|
}, children: displayValue }) })] }));
|