@openg2p/registry-widgets 1.1.0-dev.9 → 1.1.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/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createSlice, configureStore } from '@reduxjs/toolkit';
2
- import React, { useContext, createContext, useMemo, useEffect, useRef, useCallback, useState } from 'react';
2
+ import React, { useContext, createContext, useMemo, useEffect, useRef, useCallback, useState, useId } from 'react';
3
3
  import { Provider, useDispatch, useSelector, useStore } from 'react-redux';
4
4
  import { z } from 'zod';
5
5
  import { createPortal } from 'react-dom';
@@ -807,6 +807,40 @@ const parseDateFromFormat = (dateString, format) => {
807
807
  }
808
808
  return parseDate(dateString);
809
809
  };
810
+ /**
811
+ * Resolve a stored date value (ISO or parseable string) to YYYY-MM-DD for comparisons.
812
+ */
813
+ const resolveDateBoundFromFieldValue = (fieldValue) => {
814
+ if (fieldValue == null || fieldValue === '') {
815
+ return undefined;
816
+ }
817
+ const iso = formatDateToISO(fieldValue);
818
+ return iso || undefined;
819
+ };
820
+ /**
821
+ * Pick the stricter (later) minimum when combining static and field-based bounds.
822
+ */
823
+ const mergeMinDateBounds = (boundA, boundB) => {
824
+ if (!boundA) {
825
+ return boundB;
826
+ }
827
+ if (!boundB) {
828
+ return boundA;
829
+ }
830
+ return boundA > boundB ? boundA : boundB;
831
+ };
832
+ /**
833
+ * Pick the stricter (earlier) maximum when combining static and field-based bounds.
834
+ */
835
+ const mergeMaxDateBounds = (boundA, boundB) => {
836
+ if (!boundA) {
837
+ return boundB;
838
+ }
839
+ if (!boundB) {
840
+ return boundA;
841
+ }
842
+ return boundA < boundB ? boundA : boundB;
843
+ };
810
844
  /**
811
845
  * Get min date based on constraint type
812
846
  */
@@ -849,10 +883,7 @@ const getMaxDate = (constraint, maxDate) => {
849
883
  }
850
884
  return undefined;
851
885
  };
852
- /**
853
- * Validate date constraints
854
- */
855
- const validateDateConstraints = (date, minDate, maxDate, constraint) => {
886
+ const validateDateConstraints = (date, minDate, maxDate, constraint, messages) => {
856
887
  if (!date)
857
888
  return null;
858
889
  const dateObj = date instanceof Date ? date : parseDate(date);
@@ -871,15 +902,12 @@ const validateDateConstraints = (date, minDate, maxDate, constraint) => {
871
902
  return 'Date must be in the future';
872
903
  }
873
904
  }
874
- // Check minDate
875
- const effectiveMinDate = getMinDate(constraint, minDate);
876
- if (effectiveMinDate && dateISO < effectiveMinDate) {
877
- return `Date must be on or after ${effectiveMinDate}`;
905
+ // minDate / maxDate are effective bounds (static + field-based), resolved by the caller
906
+ if (minDate && dateISO < minDate) {
907
+ return messages?.minDateMessage ?? `Date must be on or after ${minDate}`;
878
908
  }
879
- // Check maxDate
880
- const effectiveMaxDate = getMaxDate(constraint, maxDate);
881
- if (effectiveMaxDate && dateISO > effectiveMaxDate) {
882
- return `Date must be on or before ${effectiveMaxDate}`;
909
+ if (maxDate && dateISO > maxDate) {
910
+ return messages?.maxDateMessage ?? `Date must be on or before ${maxDate}`;
883
911
  }
884
912
  return null;
885
913
  };
@@ -1077,16 +1105,12 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
1077
1105
  console.error('[getApiDataSource] API data source missing endpoint. Use "endpoint" field to specify the operation (e.g., "get_g2p_geo_level_values")');
1078
1106
  return [];
1079
1107
  }
1080
- let response;
1081
- try {
1082
- response = await dataSourceRequestHandler(service, endpoint, method, requestParams, {
1083
- headers: dataSource.headers,
1084
- });
1085
- }
1086
- catch (error) {
1087
- console.error('[getApiDataSource] Handler error:', error);
1088
- throw error;
1089
- }
1108
+ // Call handler — let any throw propagate to the outer catch so it is logged once
1109
+ // by useBaseWidget rather than double-logged here (which can cascade when
1110
+ // intercept-console-error.js converts console.error calls into thrown errors).
1111
+ const response = await dataSourceRequestHandler(service, endpoint, method, requestParams, {
1112
+ headers: dataSource.headers,
1113
+ });
1090
1114
  // Handle OpenG2P response format (response_body.response_payload)
1091
1115
  if (response && typeof response === 'object') {
1092
1116
  if (response.response_body?.response_payload && Array.isArray(response.response_body.response_payload)) {
@@ -1109,8 +1133,8 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
1109
1133
  return [];
1110
1134
  }
1111
1135
  catch (error) {
1112
- console.error('Error fetching API data source:', error);
1113
- return [];
1136
+ // Rethrow so useBaseWidget's catch can log it with full widget context
1137
+ throw error;
1114
1138
  }
1115
1139
  };
1116
1140
  /**
@@ -2304,7 +2328,7 @@ const useBaseWidget = (options) => {
2304
2328
  dispatch(setDataSource({ widgetId, data: transformed }));
2305
2329
  }
2306
2330
  catch (error) {
2307
- console.error(`[useBaseWidget] ERROR loading data source for ${widgetId}:`, error);
2331
+ console.error(`[useBaseWidget] ERROR loading data source for widget "${widgetId}" (type="${dataSource.type}"):`, error, '\nWidget config:', config, '\ndataSourceRequestHandler provided:', Boolean(dataSourceRequestHandler));
2308
2332
  dispatch(setDataSource({ widgetId, data: [] }));
2309
2333
  }
2310
2334
  finally {
@@ -3797,6 +3821,116 @@ const namespaceSectionConfig = (section, namespace) => {
3797
3821
  return namespaced;
3798
3822
  };
3799
3823
 
3824
+ /** Table-style widgets that bind to an array path in the store / schema. */
3825
+ function isTableLikeWidget(widget) {
3826
+ const w = widget.widget;
3827
+ /** widget-type union in types omits legacy values like simple-table still used at runtime */
3828
+ const t = widget['widget-type'];
3829
+ return (w === 'table' ||
3830
+ w === 'dialog-table' ||
3831
+ w === 'simple-table' ||
3832
+ t === 'table' ||
3833
+ t === 'simple-table');
3834
+ }
3835
+ /**
3836
+ * Resolve `records` for section save payloads.
3837
+ * - Back-compat: path ending in `.records` (e.g. `regId.records`)
3838
+ * - Else: first array snapshot at a string `widget-data-path` on a table-like widget
3839
+ * (e.g. `household.members` for dialog-table)
3840
+ */
3841
+ function extractTableRecordsFromSnapshot(snapshot, sectionWidgets) {
3842
+ const convention = Object.entries(snapshot).find(([key, value]) => key.endsWith('.records') && Array.isArray(value));
3843
+ if (convention) {
3844
+ return convention[1];
3845
+ }
3846
+ const tablePaths = [];
3847
+ sectionWidgets.forEach((widget) => {
3848
+ if (!isTableLikeWidget(widget))
3849
+ return;
3850
+ const p = widget['widget-data-path'];
3851
+ if (typeof p === 'string' && p.length > 0) {
3852
+ tablePaths.push(p);
3853
+ }
3854
+ else if (p && typeof p === 'object') {
3855
+ Object.values(p).forEach((sub) => {
3856
+ if (typeof sub === 'string' && sub.length > 0)
3857
+ tablePaths.push(sub);
3858
+ });
3859
+ }
3860
+ });
3861
+ for (const path of tablePaths) {
3862
+ const val = snapshot[path];
3863
+ if (Array.isArray(val)) {
3864
+ return val;
3865
+ }
3866
+ }
3867
+ return [];
3868
+ }
3869
+
3870
+ const isColumnRequired = (column, skipRequired) => {
3871
+ if (skipRequired)
3872
+ return false;
3873
+ const validation = column['widget-data-validation'];
3874
+ return !!(column['widget-required'] || validation?.required);
3875
+ };
3876
+ const validateTableLikeWidget = (widget, currentSchemaData, dispatch, skipRequired) => {
3877
+ const widgetId = widget['widget-id'];
3878
+ if (!widgetId)
3879
+ return true;
3880
+ const columns = (widget['widget-data-columns'] || []);
3881
+ const value = getWidgetValue(currentSchemaData, widget['widget-data-path'], widgetId);
3882
+ const rows = Array.isArray(value)
3883
+ ? value
3884
+ : [];
3885
+ const activeRows = rows.filter((row) => row?.edit_action !== 'DELETE');
3886
+ const rowErrors = [];
3887
+ let isValid = true;
3888
+ if (!skipRequired && widget['widget-required'] && activeRows.length === 0) {
3889
+ dispatch(setTouched({ widgetId, touched: true }));
3890
+ dispatch(setError({ widgetId, errors: ['At least one record is required'] }));
3891
+ return false;
3892
+ }
3893
+ const hasRequiredColumns = columns.some((col) => isColumnRequired(col, skipRequired));
3894
+ if (!skipRequired && hasRequiredColumns && activeRows.length === 0) {
3895
+ dispatch(setTouched({ widgetId, touched: true }));
3896
+ dispatch(setError({
3897
+ widgetId,
3898
+ errors: ['Add at least one record and fill all required fields'],
3899
+ }));
3900
+ return false;
3901
+ }
3902
+ activeRows.forEach((row, rowIndex) => {
3903
+ columns.forEach((col) => {
3904
+ if (col['widget-readonly'])
3905
+ return;
3906
+ const key = col['column-key'];
3907
+ if (!key)
3908
+ return;
3909
+ const required = isColumnRequired(col, skipRequired);
3910
+ const cellValue = row[key];
3911
+ const errors = validateWidget(cellValue, col['widget-data-validation'], required, skipRequired);
3912
+ if (errors.length > 0) {
3913
+ isValid = false;
3914
+ const label = col['widget-label'] || key;
3915
+ rowErrors.push(`Row ${rowIndex + 1}, ${label}: ${errors[0]}`);
3916
+ }
3917
+ });
3918
+ });
3919
+ if (!isValid) {
3920
+ dispatch(setTouched({ widgetId, touched: true }));
3921
+ dispatch(setError({
3922
+ widgetId,
3923
+ errors: rowErrors.length > 0
3924
+ ? rowErrors.slice(0, 5)
3925
+ : ['Please fix required fields in the table'],
3926
+ }));
3927
+ }
3928
+ else {
3929
+ dispatch(setTouched({ widgetId, touched: false }));
3930
+ dispatch(setError({ widgetId, errors: [] }));
3931
+ }
3932
+ return isValid;
3933
+ };
3800
3934
  const collectWidgets = (panels) => {
3801
3935
  let widgets = [];
3802
3936
  panels.forEach((panel) => {
@@ -3825,6 +3959,13 @@ const sectionValidate = (section, currentSchemaData, dispatch, skipRequired = fa
3825
3959
  if (!isVisible)
3826
3960
  continue;
3827
3961
  const widgetId = widget['widget-id'];
3962
+ if (isTableLikeWidget(widget)) {
3963
+ const tableValid = validateTableLikeWidget(widget, currentSchemaData, dispatch, skipRequired);
3964
+ if (!tableValid) {
3965
+ isValid = false;
3966
+ }
3967
+ continue;
3968
+ }
3828
3969
  const value = getWidgetValue(currentSchemaData, widget['widget-data-path'], widgetId);
3829
3970
  const errors = validateWidget(value, widget['widget-data-validation'], widget['widget-required'], skipRequired);
3830
3971
  if (errors.length > 0) {
@@ -3859,52 +4000,6 @@ const sectionValidate = (section, currentSchemaData, dispatch, skipRequired = fa
3859
4000
  return isValid;
3860
4001
  };
3861
4002
 
3862
- /** Table-style widgets that bind to an array path in the store / schema. */
3863
- function isTableLikeWidget(widget) {
3864
- const w = widget.widget;
3865
- /** widget-type union in types omits legacy values like simple-table still used at runtime */
3866
- const t = widget['widget-type'];
3867
- return (w === 'table' ||
3868
- w === 'dialog-table' ||
3869
- w === 'simple-table' ||
3870
- t === 'table' ||
3871
- t === 'simple-table');
3872
- }
3873
- /**
3874
- * Resolve `records` for section save payloads.
3875
- * - Back-compat: path ending in `.records` (e.g. `regId.records`)
3876
- * - Else: first array snapshot at a string `widget-data-path` on a table-like widget
3877
- * (e.g. `household.members` for dialog-table)
3878
- */
3879
- function extractTableRecordsFromSnapshot(snapshot, sectionWidgets) {
3880
- const convention = Object.entries(snapshot).find(([key, value]) => key.endsWith('.records') && Array.isArray(value));
3881
- if (convention) {
3882
- return convention[1];
3883
- }
3884
- const tablePaths = [];
3885
- sectionWidgets.forEach((widget) => {
3886
- if (!isTableLikeWidget(widget))
3887
- return;
3888
- const p = widget['widget-data-path'];
3889
- if (typeof p === 'string' && p.length > 0) {
3890
- tablePaths.push(p);
3891
- }
3892
- else if (p && typeof p === 'object') {
3893
- Object.values(p).forEach((sub) => {
3894
- if (typeof sub === 'string' && sub.length > 0)
3895
- tablePaths.push(sub);
3896
- });
3897
- }
3898
- });
3899
- for (const path of tablePaths) {
3900
- const val = snapshot[path];
3901
- if (Array.isArray(val)) {
3902
- return val;
3903
- }
3904
- }
3905
- return [];
3906
- }
3907
-
3908
4003
  /** Root class on readonly label/value rows; SectionRenderer scopes overflow/ellipsis rules here. */
3909
4004
  const READONLY_VALUE_ROW_ROOT_CLASSES = [
3910
4005
  'TextDisplayWidget',
@@ -3946,7 +4041,7 @@ function scopedClassSelectors(sectionClassId, classNames) {
3946
4041
  * - Panels wrap when they exceed available width
3947
4042
  * - Sections can sit side-by-side if there's space
3948
4043
  */
3949
- 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, }) => {
4044
+ 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, }) => {
3950
4045
  const { translateConfig, translate } = useWidgetTranslation();
3951
4046
  const resolvedTheme = useWidgetTheme();
3952
4047
  const portalCSSVariables = useMemo(() => themeToCSSVariables(resolvedTheme), [resolvedTheme]);
@@ -4012,16 +4107,25 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4012
4107
  const isExpandedFromContainer = typeof sectionIndex === 'number' && expandedSectionIndex === sectionIndex;
4013
4108
  const isExpandedStandalone = sectionIndex === undefined && standaloneExpanded;
4014
4109
  const isExpanded = mode === 'IntakeForm' && (isExpandedFromContainer || isExpandedStandalone);
4110
+ // IntakeForm only: tracks whether the user has clicked Next on this section at least once.
4111
+ // Used to unlock the accordion header so the user can navigate back to a visited section.
4112
+ const [hasBeenSavedByUser, setHasBeenSavedByUser] = useState(false);
4113
+ // Accordion header click behaviour in IntakeForm mode:
4114
+ // - Standalone (no sectionIndex): always toggleable.
4115
+ // - Managed by SectionsContainer: toggleable only when isAccessible is true
4116
+ // (i.e. the section has been visited OR is the immediate next one).
4117
+ // Sections beyond that remain locked.
4015
4118
  const handleAccordionToggle = useCallback(() => {
4016
4119
  if (mode !== 'IntakeForm')
4017
4120
  return;
4018
- if (typeof sectionIndex === 'number' && onExpandSection) {
4019
- onExpandSection(sectionIndex);
4020
- }
4021
- else if (sectionIndex === undefined) {
4121
+ if (sectionIndex === undefined) {
4022
4122
  setStandaloneExpanded(prev => !prev);
4023
4123
  }
4024
- }, [mode, sectionIndex, onExpandSection]);
4124
+ else if (isAccessible && onExpandSection) {
4125
+ onExpandSection(sectionIndex);
4126
+ }
4127
+ // Intentionally no-op for locked sections (isAccessible === false)
4128
+ }, [mode, sectionIndex, isAccessible, onExpandSection]);
4025
4129
  // Recursively count all vertical panels, especially those nested inside horizontal panels
4026
4130
  // Typically: horizontal panels at first level contain vertical panels at second level
4027
4131
  // Accounts for panel-column-span: a panel with column-span 3 counts as 3 columns
@@ -4356,8 +4460,6 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4356
4460
  const baselineSnapshotRef = useRef(null);
4357
4461
  // IntakeForm only: increment when baseline is updated after save - forces badge to update (refs don't trigger re-renders)
4358
4462
  const [intakeFormBaselineTrigger, setIntakeFormBaselineTrigger] = useState(0);
4359
- // IntakeForm only: tracks whether the user has actually saved this section (prevents "Saved" badge on initial load)
4360
- const [hasBeenSavedByUser, setHasBeenSavedByUser] = useState(false);
4361
4463
  // IntakeForm: treat as edit mode for dirty tracking when isDraft. RegistryView: use isEditMode.
4362
4464
  const effectiveEditModeForDirty = mode === 'IntakeForm' ? (isDraft !== false) : isEditMode;
4363
4465
  // Compute isDirty: compare current store state to baseline (only when in edit mode)
@@ -4590,6 +4692,11 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4590
4692
  }
4591
4693
  onSectionDirtyChange?.(sectionId, false);
4592
4694
  }
4695
+ else if (mode === 'IntakeForm') {
4696
+ // No onSectionSave provided, but still mark section as visited so the
4697
+ // user can navigate back to it by clicking the accordion header.
4698
+ setHasBeenSavedByUser(true);
4699
+ }
4593
4700
  // Always navigate to the next section
4594
4701
  onSectionSaveSuccess?.(sectionIndex);
4595
4702
  }, [store, onSectionSave, onSectionSaveSuccess, sectionIndex, originalSection, schemaData, contextSchemaData, namespace, hasSupportingDocuments, dbSectionId, sectionRegisterId, dispatch, buildSectionSnapshot, sectionId, onSectionDirtyChange, mode, isDraft]);
@@ -4808,13 +4915,17 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4808
4915
  .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header h2 {
4809
4916
  color: var(--owt-color-primary-dark, #F07B1A);
4810
4917
  }
4811
- .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header:hover {
4918
+ /* Hover / focus only shown when the header is actually interactive (standalone mode) */
4919
+ .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="true"]:hover {
4812
4920
  opacity: 0.85;
4813
4921
  }
4814
- .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header:focus-visible {
4922
+ .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="true"]:focus-visible {
4815
4923
  outline: 2px solid var(--owt-color-primary, #F5BB1A);
4816
4924
  outline-offset: 2px;
4817
4925
  }
4926
+ .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="false"]:focus-visible {
4927
+ outline: none;
4928
+ }
4818
4929
  .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-content {
4819
4930
  padding-top: 8px;
4820
4931
  padding-bottom: 0px;
@@ -4857,7 +4968,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4857
4968
  }),
4858
4969
  }, children: mode === 'IntakeForm' ? (
4859
4970
  /* IntakeForm: accordion layout - header always visible, content only when expanded */
4860
- 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: {
4971
+ 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: {
4861
4972
  width: '100%',
4862
4973
  display: 'flex',
4863
4974
  alignItems: 'flex-start',
@@ -4867,7 +4978,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4867
4978
  marginBottom: 0,
4868
4979
  background: 'none',
4869
4980
  border: 'none',
4870
- cursor: 'pointer',
4981
+ cursor: sectionIndex === undefined || isAccessible ? 'pointer' : 'default',
4871
4982
  textAlign: 'left',
4872
4983
  fontFamily: 'Roboto, sans-serif',
4873
4984
  }, 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']
@@ -5165,6 +5276,11 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
5165
5276
  const dataSourceRequestHandler = propDataSourceRequestHandler || contextDataSourceRequestHandler;
5166
5277
  // IntakeForm mode: accordion state - which section is expanded (null = none; first expanded by default)
5167
5278
  const [expandedSectionIndex, setExpandedSectionIndex] = useState(0);
5279
+ // IntakeForm mode: high-water mark of the furthest section the user has clicked Next on.
5280
+ // A section at index i is accessible when i <= maxVisitedIndex + 1
5281
+ // (i.e. every visited section plus the one immediately after it).
5282
+ // Starts at -1 so only section 0 is accessible before any Next is clicked.
5283
+ const [maxVisitedIndex, setMaxVisitedIndex] = useState(-1);
5168
5284
  // RegistryView: track which section is currently in edit mode (by section-id); null = none
5169
5285
  const [editingSectionId, setEditingSectionId] = useState(null);
5170
5286
  const handleEditModeChange = useCallback((sectionId, editing) => {
@@ -5189,8 +5305,9 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
5189
5305
  const handleExpandSection = useCallback((index) => {
5190
5306
  setExpandedSectionIndex(prev => (prev === index ? null : index));
5191
5307
  }, []);
5192
- // IntakeForm mode: called after section save - collapse current, expand next
5308
+ // IntakeForm mode: called after section save - advance high-water mark, collapse current, expand next
5193
5309
  const handleSectionSaveSuccess = useCallback((index) => {
5310
+ setMaxVisitedIndex(prev => Math.max(prev, index));
5194
5311
  if (index + 1 < safeSections.length) {
5195
5312
  setExpandedSectionIndex(index + 1);
5196
5313
  }
@@ -5384,6 +5501,8 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
5384
5501
  onSectionSaveSuccess: handleSectionSaveSuccess,
5385
5502
  onPreviousSection: handlePreviousSection,
5386
5503
  isDraft,
5504
+ // Accessible = every visited section + the one immediately after
5505
+ isAccessible: index <= maxVisitedIndex + 1,
5387
5506
  }
5388
5507
  : {};
5389
5508
  // RegistryView: single-edit coordination props
@@ -7366,7 +7485,7 @@ const NumberInputWidget = ({ config }) => {
7366
7485
  if (parsed === null) {
7367
7486
  // Allow empty input or partial input (e.g., "-", ".")
7368
7487
  if (inputValue === '' || inputValue === '-' || inputValue === '.') {
7369
- onChange('');
7488
+ onChange(null);
7370
7489
  }
7371
7490
  // Don't update if invalid - let user continue typing
7372
7491
  return;
@@ -7490,6 +7609,8 @@ const BooleanWidget = ({ config }) => {
7490
7609
  return labels[representation];
7491
7610
  }, [representation, formatConfig, translateConfig]);
7492
7611
  const { trueLabel, falseLabel } = getLabels();
7612
+ const unsetLabel = useMemo(() => translateConfig(formatConfig?.booleanUnsetLabel || 'Not set'), [formatConfig?.booleanUnsetLabel, translateConfig]);
7613
+ const radioGroupName = `${widgetConfig['widget-id'] ?? 'boolean'}__${useId().replace(/:/g, '')}`;
7493
7614
  // Determine current value (handle null/undefined)
7494
7615
  const currentValue = useMemo(() => {
7495
7616
  if (value === null || value === undefined) {
@@ -7521,7 +7642,7 @@ const BooleanWidget = ({ config }) => {
7521
7642
  const label = translateConfig(widgetConfig['widget-label']);
7522
7643
  let displayValue = '';
7523
7644
  if (currentValue === null) {
7524
- displayValue = '-';
7645
+ displayValue = '';
7525
7646
  }
7526
7647
  else if (currentValue === true) {
7527
7648
  displayValue = trueLabel;
@@ -7533,18 +7654,20 @@ const BooleanWidget = ({ config }) => {
7533
7654
  }
7534
7655
  // Render based on control type
7535
7656
  if (controlType === 'checkbox') {
7536
- return (jsxRuntimeExports.jsx("div", { className: "mb-[10px]", children: jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-start", children: [jsxRuntimeExports.jsxs("label", { className: "text-base font-medium text-gray-700 md:min-w-[120px] sm:pr-4 sm:pt-1 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: translateConfig(widgetConfig['widget-label']), children: [translateConfig(widgetConfig['widget-label']), widgetConfig['widget-required'] && (jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-1", children: "*" }))] }), jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntimeExports.jsxs("label", { className: "flex items-center cursor-pointer", children: [jsxRuntimeExports.jsx("input", { type: "checkbox", checked: currentValue === true, onChange: handleCheckboxChange, onBlur: onBlur, disabled: !isEnabled || widgetConfig['widget-readonly'], className: "mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" }), jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-700", children: currentValue === true ? trueLabel : (currentValue === false ? falseLabel : '-') })] }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-sm mt-1", children: error[0] }))] })] }) }));
7657
+ return (jsxRuntimeExports.jsx("div", { className: "mb-[10px]", children: jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-baseline", children: [jsxRuntimeExports.jsxs("label", { className: "text-base font-medium leading-normal text-gray-700 md:min-w-[120px] sm:pr-4 mb-1 sm:mb-0 sm:pt-0.5", style: { fontFamily: 'Roboto, sans-serif' }, title: translateConfig(widgetConfig['widget-label']), children: [translateConfig(widgetConfig['widget-label']), widgetConfig['widget-required'] && (jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-1", children: "*" }))] }), jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntimeExports.jsxs("label", { className: "inline-flex items-baseline cursor-pointer gap-2", children: [jsxRuntimeExports.jsx("input", { type: "checkbox", checked: currentValue === true, onChange: handleCheckboxChange, onBlur: onBlur, disabled: !isEnabled || widgetConfig['widget-readonly'], className: "relative top-[0.2em] h-4 w-4 shrink-0 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" }), (currentValue === true || currentValue === false) && (jsxRuntimeExports.jsx("span", { className: "text-base text-gray-700 leading-normal", children: currentValue === true ? trueLabel : falseLabel }))] }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-sm mt-1", children: error[0] }))] })] }) }));
7537
7658
  }
7538
7659
  if (controlType === 'radio') {
7539
7660
  const containerClass = orientation === 'horizontal'
7540
- ? 'flex flex-row space-x-4'
7541
- : 'flex flex-col space-y-2';
7542
- return (jsxRuntimeExports.jsx("div", { className: "mb-[10px]", children: jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-start", children: [jsxRuntimeExports.jsxs("label", { className: "text-base font-medium text-gray-700 md:min-w-[120px] sm:pr-4 sm:pt-1 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: translateConfig(widgetConfig['widget-label']), children: [translateConfig(widgetConfig['widget-label']), widgetConfig['widget-required'] && (jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-1", children: "*" }))] }), jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntimeExports.jsxs("div", { className: containerClass, onBlur: onBlur, children: [allowUnset && (jsxRuntimeExports.jsxs("label", { className: `flex items-center cursor-pointer ${!isEnabled || widgetConfig['widget-readonly'] ? 'opacity-50 cursor-not-allowed' : ''}`, children: [jsxRuntimeExports.jsx("input", { type: "radio", name: widgetConfig['widget-id'], checked: currentValue === null, onChange: () => handleRadioChange(null), disabled: !isEnabled || widgetConfig['widget-readonly'], className: "mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" }), jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-700", children: "-" })] })), jsxRuntimeExports.jsxs("label", { className: `flex items-center cursor-pointer ${!isEnabled || widgetConfig['widget-readonly'] ? 'opacity-50 cursor-not-allowed' : ''}`, children: [jsxRuntimeExports.jsx("input", { type: "radio", name: widgetConfig['widget-id'], checked: currentValue === true, onChange: () => handleRadioChange(true), disabled: !isEnabled || widgetConfig['widget-readonly'], className: "mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" }), jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-700", children: trueLabel })] }), jsxRuntimeExports.jsxs("label", { className: `flex items-center cursor-pointer ${!isEnabled || widgetConfig['widget-readonly'] ? 'opacity-50 cursor-not-allowed' : ''}`, children: [jsxRuntimeExports.jsx("input", { type: "radio", name: widgetConfig['widget-id'], checked: currentValue === false, onChange: () => handleRadioChange(false), disabled: !isEnabled || widgetConfig['widget-readonly'], className: "mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" }), jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-700", children: falseLabel })] })] }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-sm mt-1", children: error[0] }))] })] }) }));
7661
+ ? 'flex flex-row flex-wrap items-baseline gap-x-4 gap-y-2'
7662
+ : 'flex flex-col items-start gap-2';
7663
+ const radioDisabled = !isEnabled || widgetConfig['widget-readonly'];
7664
+ const optionDisabledClass = radioDisabled ? 'opacity-50 cursor-not-allowed' : '';
7665
+ return (jsxRuntimeExports.jsx("div", { className: "mb-[10px]", children: jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-baseline", children: [jsxRuntimeExports.jsxs("label", { className: "text-base font-medium leading-normal text-gray-700 md:min-w-[120px] sm:pr-4 mb-1 sm:mb-0 sm:pt-0.5", style: { fontFamily: 'Roboto, sans-serif' }, title: translateConfig(widgetConfig['widget-label']), children: [translateConfig(widgetConfig['widget-label']), widgetConfig['widget-required'] && (jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-1", children: "*" }))] }), jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntimeExports.jsxs("div", { className: containerClass, onBlur: onBlur, children: [allowUnset && (jsxRuntimeExports.jsxs("label", { className: `inline-flex items-baseline gap-2 cursor-pointer ${optionDisabledClass}`, children: [jsxRuntimeExports.jsx("input", { type: "radio", name: radioGroupName, checked: currentValue === null, onChange: () => handleRadioChange(null), disabled: radioDisabled, className: "relative top-[0.2em] h-4 w-4 shrink-0 text-blue-600 focus:ring-blue-500 border-gray-300" }), jsxRuntimeExports.jsx("span", { className: "text-base text-gray-700 leading-normal", children: unsetLabel })] })), jsxRuntimeExports.jsxs("label", { className: `inline-flex items-baseline gap-2 cursor-pointer ${optionDisabledClass}`, children: [jsxRuntimeExports.jsx("input", { type: "radio", name: radioGroupName, checked: currentValue === true, onChange: () => handleRadioChange(true), disabled: radioDisabled, className: "relative top-[0.2em] h-4 w-4 shrink-0 text-blue-600 focus:ring-blue-500 border-gray-300" }), jsxRuntimeExports.jsx("span", { className: "text-base text-gray-700 leading-normal", children: trueLabel })] }), jsxRuntimeExports.jsxs("label", { className: `inline-flex items-baseline gap-2 cursor-pointer ${optionDisabledClass}`, children: [jsxRuntimeExports.jsx("input", { type: "radio", name: radioGroupName, checked: currentValue === false, onChange: () => handleRadioChange(false), disabled: radioDisabled, className: "relative top-[0.2em] h-4 w-4 shrink-0 text-blue-600 focus:ring-blue-500 border-gray-300" }), jsxRuntimeExports.jsx("span", { className: "text-base text-gray-700 leading-normal", children: falseLabel })] })] }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-sm mt-1", children: error[0] }))] })] }) }));
7543
7666
  }
7544
7667
  // Toggle/switch control type
7545
- return (jsxRuntimeExports.jsx("div", { className: "mb-[10px]", children: jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-start", children: [jsxRuntimeExports.jsxs("label", { className: "text-base font-medium text-gray-700 sm:min-w-[150px] sm:pr-4 sm:pt-1 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: translateConfig(widgetConfig['widget-label']), children: [translateConfig(widgetConfig['widget-label']), widgetConfig['widget-required'] && (jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-1", children: "*" }))] }), jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntimeExports.jsxs("div", { className: "flex items-center space-x-3", onBlur: onBlur, children: [allowUnset && (jsxRuntimeExports.jsx("button", { type: "button", onClick: () => handleChange(null), disabled: !isEnabled || widgetConfig['widget-readonly'], className: `px-3 py-1 text-sm border ${currentValue === null
7668
+ return (jsxRuntimeExports.jsx("div", { className: "mb-[10px]", children: jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-baseline", children: [jsxRuntimeExports.jsxs("label", { className: "text-base font-medium leading-normal text-gray-700 sm:min-w-[150px] sm:pr-4 mb-1 sm:mb-0 sm:pt-0.5", style: { fontFamily: 'Roboto, sans-serif' }, title: translateConfig(widgetConfig['widget-label']), children: [translateConfig(widgetConfig['widget-label']), widgetConfig['widget-required'] && (jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-1", children: "*" }))] }), jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap items-center gap-3", onBlur: onBlur, children: [allowUnset && (jsxRuntimeExports.jsx("button", { type: "button", onClick: () => handleChange(null), disabled: !isEnabled || widgetConfig['widget-readonly'], className: `px-3 py-1 text-sm border ${currentValue === null
7546
7669
  ? 'bg-blue-600 text-white border-blue-600'
7547
- : 'bg-white text-gray-700 border-gray-300'} ${!isEnabled || widgetConfig['widget-readonly'] ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'}`, style: { borderRadius: '15px' }, children: "-" })), jsxRuntimeExports.jsx("button", { type: "button", onClick: () => handleChange(true), disabled: !isEnabled || widgetConfig['widget-readonly'], className: `px-3 py-1 text-sm border ${currentValue === true
7670
+ : 'bg-white text-gray-700 border-gray-300'} ${!isEnabled || widgetConfig['widget-readonly'] ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'}`, style: { borderRadius: '15px' }, children: unsetLabel })), jsxRuntimeExports.jsx("button", { type: "button", onClick: () => handleChange(true), disabled: !isEnabled || widgetConfig['widget-readonly'], className: `px-3 py-1 text-sm border ${currentValue === true
7548
7671
  ? 'bg-blue-600 text-white border-blue-600'
7549
7672
  : 'bg-white text-gray-700 border-gray-300'} ${!isEnabled || widgetConfig['widget-readonly'] ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'}`, style: { borderRadius: '15px' }, children: trueLabel }), jsxRuntimeExports.jsx("button", { type: "button", onClick: () => handleChange(false), disabled: !isEnabled || widgetConfig['widget-readonly'], className: `px-3 py-1 text-sm border ${currentValue === false
7550
7673
  ? 'bg-blue-600 text-white border-blue-600'
@@ -7552,42 +7675,76 @@ const BooleanWidget = ({ config }) => {
7552
7675
  };
7553
7676
 
7554
7677
  const DateInputWidget = ({ config }) => {
7555
- const { value, formattedValue, error, touched, isEnabled, onChange, onBlur, config: widgetConfig, } = useBaseWidget({ config });
7556
- const { translate, translateConfig } = useWidgetTranslation();
7678
+ const { value, error, touched, isEnabled, onChange, onBlur, setError, config: widgetConfig, } = useBaseWidget({ config });
7679
+ const formValues = useSelector((state) => state.widget.values);
7680
+ const { translateConfig } = useWidgetTranslation();
7557
7681
  const formatConfig = widgetConfig['widget-data-format'];
7558
7682
  const optionsConfig = widgetConfig['widget-data-options'];
7559
7683
  const dateFormat = formatConfig?.dateFormat || 'YYYY-MM-DD';
7560
- const inputMethod = formatConfig?.inputMethod || 'picker'; // Default to picker for better UX
7684
+ const inputMethod = formatConfig?.inputMethod || 'picker';
7561
7685
  const dateConstraint = formatConfig?.dateConstraint || 'any';
7562
7686
  const minDate = optionsConfig?.minDate;
7563
7687
  const maxDate = optionsConfig?.maxDate;
7688
+ const minDateField = optionsConfig?.minDateField;
7689
+ const maxDateField = optionsConfig?.maxDateField;
7690
+ const minDateMessage = optionsConfig?.minDateMessage
7691
+ ? translateConfig(optionsConfig.minDateMessage)
7692
+ : undefined;
7693
+ const maxDateMessage = optionsConfig?.maxDateMessage
7694
+ ? translateConfig(optionsConfig.maxDateMessage)
7695
+ : undefined;
7564
7696
  const defaultToToday = widgetConfig['widget-data-default'] === 'today';
7565
- // Track manual input value (for manual/hybrid modes)
7566
7697
  const [manualInputValue, setManualInputValue] = useState('');
7567
7698
  const [isFocused, setIsFocused] = useState(false);
7568
- // Initialize default value to today if configured
7699
+ const fieldMinDate = useMemo(() => {
7700
+ if (!minDateField) {
7701
+ return undefined;
7702
+ }
7703
+ return resolveDateBoundFromFieldValue(getValueByPath(formValues, minDateField));
7704
+ }, [formValues, minDateField]);
7705
+ const fieldMaxDate = useMemo(() => {
7706
+ if (!maxDateField) {
7707
+ return undefined;
7708
+ }
7709
+ return resolveDateBoundFromFieldValue(getValueByPath(formValues, maxDateField));
7710
+ }, [formValues, maxDateField]);
7711
+ const effectiveMinDate = useMemo(() => {
7712
+ const staticMin = getMinDate(dateConstraint, minDate);
7713
+ return mergeMinDateBounds(staticMin, fieldMinDate);
7714
+ }, [dateConstraint, minDate, fieldMinDate]);
7715
+ const effectiveMaxDate = useMemo(() => {
7716
+ const staticMax = getMaxDate(dateConstraint, maxDate);
7717
+ return mergeMaxDateBounds(staticMax, fieldMaxDate);
7718
+ }, [dateConstraint, maxDate, fieldMaxDate]);
7719
+ const constraintMessages = useMemo(() => ({ minDateMessage, maxDateMessage }), [minDateMessage, maxDateMessage]);
7720
+ const runDateConstraintValidation = useCallback((dateValue) => {
7721
+ if (!dateValue) {
7722
+ return null;
7723
+ }
7724
+ return validateDateConstraints(dateValue, effectiveMinDate, effectiveMaxDate, dateConstraint, constraintMessages);
7725
+ }, [effectiveMinDate, effectiveMaxDate, dateConstraint, constraintMessages]);
7569
7726
  useEffect(() => {
7570
7727
  if (defaultToToday && (value === null || value === undefined || value === '')) {
7571
7728
  const todayISO = formatDateToISO(new Date());
7572
7729
  onChange(todayISO);
7573
7730
  }
7574
7731
  }, [defaultToToday, value, onChange]);
7575
- // Get effective min/max dates
7576
- const effectiveMinDate = useMemo(() => {
7577
- return getMinDate(dateConstraint, minDate);
7578
- }, [dateConstraint, minDate]);
7579
- const effectiveMaxDate = useMemo(() => {
7580
- return getMaxDate(dateConstraint, maxDate);
7581
- }, [dateConstraint, maxDate]);
7582
- // Convert ISO value to display format
7732
+ // Re-validate when a relative bound field changes (e.g. start date set after end date)
7733
+ useEffect(() => {
7734
+ if (!value) {
7735
+ return;
7736
+ }
7737
+ const constraintError = runDateConstraintValidation(value);
7738
+ if (constraintError) {
7739
+ setError([constraintError]);
7740
+ }
7741
+ }, [fieldMinDate, fieldMaxDate, value, runDateConstraintValidation, setError]);
7583
7742
  const getDisplayValue = useCallback(() => {
7584
- // For picker mode, always use YYYY-MM-DD
7585
7743
  if (inputMethod === 'picker') {
7586
7744
  if (!value)
7587
7745
  return '';
7588
7746
  return formatDateToISO(value);
7589
7747
  }
7590
- // For manual/hybrid modes, use custom format
7591
7748
  if (isFocused && manualInputValue) {
7592
7749
  return manualInputValue;
7593
7750
  }
@@ -7598,7 +7755,6 @@ const DateInputWidget = ({ config }) => {
7598
7755
  }
7599
7756
  return formatDateToString(value, dateFormat);
7600
7757
  }, [value, inputMethod, dateFormat, isFocused, manualInputValue]);
7601
- // Initialize manual input value
7602
7758
  useEffect(() => {
7603
7759
  if (!isFocused && value) {
7604
7760
  if (dateFormat === 'YYYY-MM-DD') {
@@ -7609,66 +7765,81 @@ const DateInputWidget = ({ config }) => {
7609
7765
  }
7610
7766
  }
7611
7767
  }, [value, dateFormat, isFocused]);
7612
- // Handle input change
7768
+ const applyConstraintError = useCallback((dateValue) => {
7769
+ const constraintError = runDateConstraintValidation(dateValue);
7770
+ setError(constraintError ? [constraintError] : []);
7771
+ }, [runDateConstraintValidation, setError]);
7613
7772
  const handleChange = useCallback((e) => {
7614
7773
  const inputValue = e.target.value;
7615
7774
  if (inputMethod === 'picker') {
7616
- // Picker mode: input is always YYYY-MM-DD
7617
7775
  if (inputValue) {
7618
7776
  const date = parseDate(inputValue);
7619
7777
  if (date) {
7620
- onChange(formatDateToISO(date));
7778
+ const iso = formatDateToISO(date);
7779
+ onChange(iso);
7780
+ applyConstraintError(iso);
7621
7781
  }
7622
7782
  else {
7623
7783
  onChange('');
7784
+ setError([]);
7624
7785
  }
7625
7786
  }
7626
7787
  else {
7627
7788
  onChange('');
7789
+ setError([]);
7628
7790
  }
7629
7791
  }
7630
7792
  else {
7631
- // Manual/hybrid mode: parse custom format
7632
7793
  setManualInputValue(inputValue);
7633
7794
  if (inputValue) {
7634
7795
  const date = parseDateFromFormat(inputValue, dateFormat);
7635
7796
  if (date) {
7636
- // Validate constraints
7637
- const constraintError = validateDateConstraints(date, minDate, maxDate, dateConstraint);
7638
- if (!constraintError) {
7639
- onChange(formatDateToISO(date));
7640
- }
7641
- else {
7642
- // Still update the value but validation will catch it
7643
- onChange(formatDateToISO(date));
7644
- }
7797
+ const iso = formatDateToISO(date);
7798
+ onChange(iso);
7799
+ applyConstraintError(iso);
7645
7800
  }
7646
7801
  }
7647
7802
  else {
7648
7803
  onChange('');
7804
+ setError([]);
7649
7805
  }
7650
7806
  }
7651
- }, [inputMethod, dateFormat, onChange, minDate, maxDate, dateConstraint]);
7652
- // Handle blur - validate and format
7807
+ }, [inputMethod, dateFormat, onChange, applyConstraintError, setError]);
7653
7808
  const handleBlur = useCallback(() => {
7654
7809
  setIsFocused(false);
7655
7810
  if (inputMethod !== 'picker' && manualInputValue) {
7656
7811
  const date = parseDateFromFormat(manualInputValue, dateFormat);
7657
7812
  if (date) {
7658
- // Format the value according to the format
7659
7813
  const formatted = formatDateToString(date, dateFormat);
7660
7814
  setManualInputValue(formatted);
7661
- onChange(formatDateToISO(date));
7815
+ const iso = formatDateToISO(date);
7816
+ onChange(iso);
7817
+ applyConstraintError(iso);
7662
7818
  }
7663
7819
  else {
7664
- // Invalid date, clear it
7665
7820
  setManualInputValue('');
7666
7821
  onChange('');
7822
+ setError([]);
7667
7823
  }
7668
7824
  }
7669
7825
  onBlur();
7670
- }, [inputMethod, manualInputValue, dateFormat, onChange, onBlur]);
7671
- // Handle focus
7826
+ if (value) {
7827
+ const constraintError = runDateConstraintValidation(value);
7828
+ if (constraintError) {
7829
+ setError([constraintError]);
7830
+ }
7831
+ }
7832
+ }, [
7833
+ inputMethod,
7834
+ manualInputValue,
7835
+ dateFormat,
7836
+ onChange,
7837
+ onBlur,
7838
+ applyConstraintError,
7839
+ value,
7840
+ runDateConstraintValidation,
7841
+ setError,
7842
+ ]);
7672
7843
  const handleFocus = useCallback(() => {
7673
7844
  setIsFocused(true);
7674
7845
  if (value) {
@@ -7680,15 +7851,15 @@ const DateInputWidget = ({ config }) => {
7680
7851
  }
7681
7852
  }
7682
7853
  }, [value, dateFormat]);
7683
- // Determine placeholder
7684
7854
  const placeholder = useMemo(() => {
7685
- const hasValue = getDisplayValue() && getDisplayValue().trim().length > 0;
7855
+ const display = getDisplayValue();
7856
+ const hasValue = display && display.trim().length > 0;
7686
7857
  const placeholderText = translateConfig(widgetConfig['widget-data-placeholder']);
7687
- return hasValue ? undefined : (placeholderText || dateFormat);
7858
+ return hasValue ? undefined : placeholderText || dateFormat;
7688
7859
  }, [getDisplayValue, widgetConfig, translateConfig, dateFormat]);
7689
- // Determine input type
7690
7860
  const inputType = inputMethod === 'picker' ? 'date' : 'text';
7691
- // For readonly mode, render as display text
7861
+ const showRequiredError = widgetConfig['widget-required'] && (!value || value === '');
7862
+ const showValidationError = touched && error.length > 0;
7692
7863
  if (widgetConfig['widget-readonly']) {
7693
7864
  const label = translateConfig(widgetConfig['widget-label']);
7694
7865
  let displayValue = '';
@@ -7705,9 +7876,9 @@ const DateInputWidget = ({ config }) => {
7705
7876
  }
7706
7877
  return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] DateDisplayWidget 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", { className: "text-base text-gray-900 font-medium", title: String(displayValue ?? ''), children: displayValue }) })] }));
7707
7878
  }
7708
- return (jsxRuntimeExports.jsx("div", { className: "mb-[10px]", children: jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-start", children: [jsxRuntimeExports.jsxs("label", { className: "text-base font-medium text-gray-700 md:min-w-[120px] sm:pr-4 sm:pt-1 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: translateConfig(widgetConfig['widget-label']), children: [translateConfig(widgetConfig['widget-label']), widgetConfig['widget-required'] && (jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-1", children: "*" }))] }), jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntimeExports.jsx("input", { type: inputType, value: getDisplayValue(), onChange: handleChange, onBlur: handleBlur, onFocus: handleFocus, disabled: !isEnabled || widgetConfig['widget-readonly'], placeholder: placeholder, min: inputMethod === 'picker' ? effectiveMinDate : undefined, max: inputMethod === 'picker' ? effectiveMaxDate : undefined, className: `w-full sm:w-[180px] max-w-full h-[30px] px-3 border shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${(touched && error.length > 0) || (widgetConfig['widget-required'] && (!value || value === ''))
7879
+ return (jsxRuntimeExports.jsx("div", { className: "mb-[10px]", children: jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-start", children: [jsxRuntimeExports.jsxs("label", { className: "text-base font-medium text-gray-700 md:min-w-[120px] sm:pr-4 sm:pt-1 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: translateConfig(widgetConfig['widget-label']), children: [translateConfig(widgetConfig['widget-label']), widgetConfig['widget-required'] && jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-1", children: "*" })] }), jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntimeExports.jsx("input", { type: inputType, value: getDisplayValue(), onChange: handleChange, onBlur: handleBlur, onFocus: handleFocus, disabled: !isEnabled || widgetConfig['widget-readonly'], placeholder: placeholder, min: inputMethod === 'picker' ? effectiveMinDate : undefined, max: inputMethod === 'picker' ? effectiveMaxDate : undefined, className: `w-full sm:w-[180px] max-w-full h-[30px] px-3 border shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${showValidationError || showRequiredError
7709
7880
  ? 'border-red-500 focus:ring-red-500 focus:border-red-500'
7710
- : 'border-gray-300'} ${!isEnabled || widgetConfig['widget-readonly'] ? 'bg-gray-100 cursor-not-allowed' : 'bg-white'}`, style: { borderRadius: '10px' }, title: translateConfig(widgetConfig['widget-data-tooltip']) }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-sm mt-1", children: error[0] }))] })] }) }));
7881
+ : 'border-gray-300'} ${!isEnabled || widgetConfig['widget-readonly'] ? 'bg-gray-100 cursor-not-allowed' : 'bg-white'}`, style: { borderRadius: '10px' }, title: translateConfig(widgetConfig['widget-data-tooltip']) }), showValidationError && jsxRuntimeExports.jsx("p", { className: "text-red-500 text-sm mt-1", children: error[0] })] })] }) }));
7711
7882
  };
7712
7883
 
7713
7884
  /**
@@ -8255,7 +8426,7 @@ const CheckboxWidget = ({ config }) => {
8255
8426
  const displayValue = isChecked ? 'Yes' : 'No';
8256
8427
  return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] CheckboxDisplayWidget 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", { className: "text-base text-gray-900 font-medium", title: String(displayValue ?? ''), children: displayValue }) })] }));
8257
8428
  }
8258
- return (jsxRuntimeExports.jsx("div", { className: "mb-[10px]", children: jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-start", children: [jsxRuntimeExports.jsxs("label", { className: "text-base font-medium text-gray-700 md:min-w-[120px] sm:pr-4 sm:pt-1 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: translateConfig(widgetConfig['widget-label']), children: [translateConfig(widgetConfig['widget-label']), widgetConfig['widget-required'] && (jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-1", children: "*" }))] }), jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntimeExports.jsxs("label", { className: "flex items-center cursor-pointer", children: [jsxRuntimeExports.jsx("input", { type: "checkbox", checked: isChecked, onChange: (e) => onChange(e.target.checked), onBlur: onBlur, disabled: !isEnabled || widgetConfig['widget-readonly'], className: "mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" }), jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-700", children: isChecked ? 'Yes' : 'No' })] }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-sm mt-1", children: error[0] }))] })] }) }));
8429
+ return (jsxRuntimeExports.jsx("div", { className: "mb-[10px]", children: jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-baseline", children: [jsxRuntimeExports.jsxs("label", { className: "text-base font-medium leading-normal text-gray-700 md:min-w-[120px] sm:pr-4 mb-1 sm:mb-0 sm:pt-0.5", style: { fontFamily: 'Roboto, sans-serif' }, title: translateConfig(widgetConfig['widget-label']), children: [translateConfig(widgetConfig['widget-label']), widgetConfig['widget-required'] && (jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-1", children: "*" }))] }), jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntimeExports.jsxs("label", { className: "inline-flex cursor-pointer items-baseline gap-2", children: [jsxRuntimeExports.jsx("input", { type: "checkbox", checked: isChecked, onChange: (e) => onChange(e.target.checked), onBlur: onBlur, disabled: !isEnabled || widgetConfig['widget-readonly'], className: "relative top-[0.2em] h-4 w-4 shrink-0 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" }), jsxRuntimeExports.jsx("span", { className: "text-base leading-normal text-gray-700", children: isChecked ? 'Yes' : 'No' })] }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-sm mt-1", children: error[0] }))] })] }) }));
8259
8430
  }
8260
8431
  // Multiple checkboxes (with data source) - for array values
8261
8432
  // Process and sort options if needed
@@ -8298,7 +8469,7 @@ const CheckboxWidget = ({ config }) => {
8298
8469
  switch (layout) {
8299
8470
  case 'horizontal':
8300
8471
  return {
8301
- className: 'flex flex-row flex-wrap gap-4',
8472
+ className: 'flex flex-row flex-wrap items-baseline gap-4',
8302
8473
  style: undefined,
8303
8474
  };
8304
8475
  case 'grid':
@@ -8311,7 +8482,7 @@ const CheckboxWidget = ({ config }) => {
8311
8482
  case 'vertical':
8312
8483
  default:
8313
8484
  return {
8314
- className: 'flex flex-col space-y-2',
8485
+ className: 'flex flex-col gap-2',
8315
8486
  style: undefined,
8316
8487
  };
8317
8488
  }
@@ -8325,7 +8496,7 @@ const CheckboxWidget = ({ config }) => {
8325
8496
  : '-';
8326
8497
  return (jsxRuntimeExports.jsxs("div", { className: "mb-3 CheckboxDisplayWidget flex flex-col sm:flex-row sm:items-start", children: [label && (jsxRuntimeExports.jsxs("div", { className: "text-sm text-gray-600 font-medium md:min-w-[120px] sm:pr-4 mb-1 sm:mb-0", title: label, children: [label, ":"] })), jsxRuntimeExports.jsx("div", { className: "flex-1", children: jsxRuntimeExports.jsx("div", { className: "text-base text-gray-900 font-medium", title: String(displayValue ?? ''), children: displayValue }) })] }));
8327
8498
  }
8328
- return (jsxRuntimeExports.jsx("div", { className: "mb-[10px]", children: jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-start", children: [jsxRuntimeExports.jsxs("label", { className: "text-base font-medium text-gray-700 md:min-w-[120px] sm:pr-4 sm:pt-1 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: translateConfig(widgetConfig['widget-label']), children: [translateConfig(widgetConfig['widget-label']), widgetConfig['widget-required'] && (jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-1", children: "*" }))] }), jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntimeExports.jsx("div", { className: layoutConfig.className, style: layoutConfig.style, onBlur: onBlur, children: loading ? (jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500", children: translate('common.loading') })) : (processedOptions.map((option) => (jsxRuntimeExports.jsxs("label", { className: `flex items-center cursor-pointer ${!isEnabled || widgetConfig['widget-readonly'] ? 'opacity-50 cursor-not-allowed' : ''}`, children: [jsxRuntimeExports.jsx("input", { type: "checkbox", value: option.value, checked: selectedValues.includes(option.value), onChange: (e) => handleCheckboxChange(option.value, e.target.checked), disabled: !isEnabled || widgetConfig['widget-readonly'], className: "mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" }), jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-700", children: translateConfig(option.label) })] }, option.value)))) }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-sm mt-1", children: error[0] }))] })] }) }));
8499
+ return (jsxRuntimeExports.jsx("div", { className: "mb-[10px]", children: jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-baseline", children: [jsxRuntimeExports.jsxs("label", { className: "text-base font-medium leading-normal text-gray-700 md:min-w-[120px] sm:pr-4 mb-1 sm:mb-0 sm:pt-0.5", style: { fontFamily: 'Roboto, sans-serif' }, title: translateConfig(widgetConfig['widget-label']), children: [translateConfig(widgetConfig['widget-label']), widgetConfig['widget-required'] && (jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-1", children: "*" }))] }), jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntimeExports.jsx("div", { className: layoutConfig.className, style: layoutConfig.style, onBlur: onBlur, children: loading ? (jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500", children: translate('common.loading') })) : (processedOptions.map((option) => (jsxRuntimeExports.jsxs("label", { className: `inline-flex cursor-pointer items-baseline gap-2 ${!isEnabled || widgetConfig['widget-readonly'] ? 'opacity-50 cursor-not-allowed' : ''}`, children: [jsxRuntimeExports.jsx("input", { type: "checkbox", value: option.value, checked: selectedValues.includes(option.value), onChange: (e) => handleCheckboxChange(option.value, e.target.checked), disabled: !isEnabled || widgetConfig['widget-readonly'], className: "relative top-[0.2em] h-4 w-4 shrink-0 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" }), jsxRuntimeExports.jsx("span", { className: "text-base leading-normal text-gray-700", children: translateConfig(option.label) })] }, option.value)))) }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-sm mt-1", children: error[0] }))] })] }) }));
8329
8500
  };
8330
8501
 
8331
8502
  const SimpleTableWidget = ({ config }) => {
@@ -8651,16 +8822,70 @@ const TableCellNumber = ({ config, value, onValueChange }) => {
8651
8822
  backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8652
8823
  } }));
8653
8824
  };
8654
- const TableCellDate = ({ config, value, onValueChange }) => {
8825
+ const TableCellDate = ({ config, value, rowValues, onValueChange }) => {
8826
+ const { translateConfig } = useWidgetTranslation();
8655
8827
  const isReadonly = config['widget-readonly'] || false;
8656
8828
  const placeholder = config['widget-data-placeholder'] || '';
8829
+ const optionsConfig = config['widget-data-options'];
8830
+ const formatConfig = config['widget-data-format'];
8831
+ const dateConstraint = formatConfig?.dateConstraint || 'any';
8832
+ const minDate = optionsConfig?.minDate;
8833
+ const maxDate = optionsConfig?.maxDate;
8834
+ const minDateField = optionsConfig?.minDateField;
8835
+ const maxDateField = optionsConfig?.maxDateField;
8836
+ const minDateMessage = optionsConfig?.minDateMessage
8837
+ ? translateConfig(optionsConfig.minDateMessage)
8838
+ : undefined;
8839
+ const maxDateMessage = optionsConfig?.maxDateMessage
8840
+ ? translateConfig(optionsConfig.maxDateMessage)
8841
+ : undefined;
8842
+ const [constraintError, setConstraintError] = useState(null);
8843
+ const resolveSiblingDate = (fieldRef) => {
8844
+ if (!fieldRef || !rowValues) {
8845
+ return undefined;
8846
+ }
8847
+ const raw = getValueByPath(rowValues, fieldRef) ?? rowValues[fieldRef];
8848
+ return resolveDateBoundFromFieldValue(raw);
8849
+ };
8850
+ const fieldMinDate = useMemo(() => resolveSiblingDate(minDateField), [minDateField, rowValues]);
8851
+ const fieldMaxDate = useMemo(() => resolveSiblingDate(maxDateField), [maxDateField, rowValues]);
8852
+ const effectiveMinDate = useMemo(() => {
8853
+ const staticMin = getMinDate(dateConstraint, minDate);
8854
+ return mergeMinDateBounds(staticMin, fieldMinDate);
8855
+ }, [dateConstraint, minDate, fieldMinDate]);
8856
+ const effectiveMaxDate = useMemo(() => {
8857
+ const staticMax = getMaxDate(dateConstraint, maxDate);
8858
+ return mergeMaxDateBounds(staticMax, fieldMaxDate);
8859
+ }, [dateConstraint, maxDate, fieldMaxDate]);
8860
+ const constraintMessages = useMemo(() => ({ minDateMessage, maxDateMessage }), [minDateMessage, maxDateMessage]);
8657
8861
  // input type="date" requires YYYY-MM-DD format
8658
8862
  const displayValue = value && typeof value === 'string' ? value.split('T')[0] : '';
8659
- return (jsxRuntimeExports.jsx("input", { type: "date", value: displayValue, onChange: (e) => onValueChange(e.target.value), disabled: isReadonly, placeholder: placeholder, className: `w-full h-[28px] px-2 text-sm border focus:outline-none ${isReadonly ? 'cursor-not-allowed' : ''} table-cell-input`, style: {
8660
- borderRadius: '10px',
8661
- borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
8662
- backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8663
- } }));
8863
+ useEffect(() => {
8864
+ if (!displayValue) {
8865
+ setConstraintError(null);
8866
+ return;
8867
+ }
8868
+ const error = validateDateConstraints(displayValue, effectiveMinDate, effectiveMaxDate, dateConstraint, constraintMessages);
8869
+ setConstraintError(error);
8870
+ }, [displayValue, effectiveMinDate, effectiveMaxDate, dateConstraint, constraintMessages]);
8871
+ const handleChange = (e) => {
8872
+ const nextValue = e.target.value;
8873
+ onValueChange(nextValue);
8874
+ if (!nextValue) {
8875
+ setConstraintError(null);
8876
+ return;
8877
+ }
8878
+ const error = validateDateConstraints(nextValue, effectiveMinDate, effectiveMaxDate, dateConstraint, constraintMessages);
8879
+ setConstraintError(error);
8880
+ };
8881
+ const hasError = Boolean(constraintError);
8882
+ return (jsxRuntimeExports.jsxs("div", { className: "w-full", children: [jsxRuntimeExports.jsx("input", { type: "date", value: displayValue, onChange: handleChange, disabled: isReadonly, placeholder: placeholder, min: effectiveMinDate, max: effectiveMaxDate, title: constraintError || translateConfig(config['widget-data-tooltip']), className: `w-full h-[28px] px-2 text-sm border focus:outline-none ${isReadonly ? 'cursor-not-allowed' : ''} ${hasError ? 'border-red-500' : ''} table-cell-input`, style: {
8883
+ borderRadius: '10px',
8884
+ borderColor: hasError
8885
+ ? 'var(--owt-color-error, #B91C1C)'
8886
+ : 'var(--owt-widget-input-border, #C4C4C4)',
8887
+ backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8888
+ } }), hasError && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-xs mt-0.5 leading-tight", children: constraintError }))] }));
8664
8889
  };
8665
8890
  const TableWidget = ({ config }) => {
8666
8891
  const { value, error, touched, isEnabled, onChange, config: widgetConfig, } = useBaseWidget({ config });
@@ -9019,11 +9244,21 @@ const TableWidget = ({ config }) => {
9019
9244
  });
9020
9245
  }
9021
9246
  }, [isAdding, newRowData, columns, widgetConfig, rows.length, dispatch]);
9247
+ const getRowValuesForEdit = useCallback((rowIndex) => {
9248
+ if (editingState && editingState.rowIndex === rowIndex) {
9249
+ return editingState.currentValue ?? {};
9250
+ }
9251
+ if (isAdding && rowIndex === rows.length && newRowData) {
9252
+ return newRowData;
9253
+ }
9254
+ return rows[rowIndex] ?? {};
9255
+ }, [editingState, isAdding, rows, newRowData]);
9022
9256
  // Lightweight cell renderer for table cells (no labels, compact)
9023
9257
  const renderTableCell = useCallback((rowIndex, column, cellValue, isReadonly) => {
9024
9258
  const columnKey = column['column-key'];
9025
9259
  const widgetType = column.widget || 'text';
9026
9260
  const cellWidgetId = `${widgetConfig['widget-id']}-row-${rowIndex}-col-${columnKey}`;
9261
+ const rowValues = getRowValuesForEdit(rowIndex);
9027
9262
  // Use lightweight cell config (no label, minimal styling)
9028
9263
  const cellConfig = {
9029
9264
  ...column,
@@ -9046,7 +9281,7 @@ const TableWidget = ({ config }) => {
9046
9281
  return jsxRuntimeExports.jsx(TableCellNumber, { config: cellConfig, value: cellValue, onValueChange: (newValue) => updateCellValue(columnKey, newValue, rowIndex) });
9047
9282
  }
9048
9283
  else if (widgetType === 'date') {
9049
- return jsxRuntimeExports.jsx(TableCellDate, { config: cellConfig, value: cellValue, onValueChange: (newValue) => updateCellValue(columnKey, newValue, rowIndex) });
9284
+ return jsxRuntimeExports.jsx(TableCellDate, { config: cellConfig, value: cellValue, rowValues: rowValues, onValueChange: (newValue) => updateCellValue(columnKey, newValue, rowIndex) });
9050
9285
  }
9051
9286
  // For other widget types, use WidgetRenderer but with compact styling
9052
9287
  return (jsxRuntimeExports.jsx("div", { className: "table-cell-widget", style: { margin: 0, padding: 0 }, children: jsxRuntimeExports.jsx(WidgetRenderer, { config: cellConfig, schemaData: {
@@ -9054,7 +9289,7 @@ const TableWidget = ({ config }) => {
9054
9289
  }, onValueChange: (widgetId, newValue) => {
9055
9290
  updateCellValue(columnKey, newValue, rowIndex);
9056
9291
  } }) }));
9057
- }, [widgetConfig, updateCellValue]);
9292
+ }, [widgetConfig, updateCellValue, getRowValuesForEdit]);
9058
9293
  // Render cell content (widget in edit mode, formatted value in view mode)
9059
9294
  const renderCell = useCallback((rowIndex, column, row) => {
9060
9295
  const columnKey = column['column-key'];
@@ -9364,6 +9599,27 @@ const DialogTableWidget = ({ config }) => {
9364
9599
  }, [formData, columns, storeValues, dialogFieldWidgetId]);
9365
9600
  const saveDialog = useCallback(() => {
9366
9601
  const payload = collectMergedRowPayload();
9602
+ let hasErrors = false;
9603
+ columns.forEach((col) => {
9604
+ const key = col['column-key'];
9605
+ const cellWidgetId = dialogFieldWidgetId(key);
9606
+ const isColReadonly = isReadonly || col['widget-readonly'] === true;
9607
+ if (isColReadonly)
9608
+ return;
9609
+ const cellValue = payload[key];
9610
+ const validationErrors = validateWidget(cellValue, col['widget-data-validation'], col['widget-required']);
9611
+ if (validationErrors && validationErrors.length > 0) {
9612
+ hasErrors = true;
9613
+ dispatch(setError({ widgetId: cellWidgetId, errors: validationErrors }));
9614
+ dispatch(setTouched({ widgetId: cellWidgetId, touched: true }));
9615
+ }
9616
+ else {
9617
+ dispatch(setError({ widgetId: cellWidgetId, errors: [] }));
9618
+ }
9619
+ });
9620
+ if (hasErrors) {
9621
+ return;
9622
+ }
9367
9623
  if (dialogMode === 'add') {
9368
9624
  const savedRow = { ...payload, edit_action: 'ADD' };
9369
9625
  onChange([...rows, savedRow]);
@@ -9379,7 +9635,7 @@ const DialogTableWidget = ({ config }) => {
9379
9635
  onChange(newRows);
9380
9636
  closeDialog();
9381
9637
  }
9382
- }, [collectMergedRowPayload, dialogMode, onChange, rows, closeDialog, activeRowIndex]);
9638
+ }, [collectMergedRowPayload, dialogMode, onChange, rows, closeDialog, activeRowIndex, columns, dialogFieldWidgetId, isReadonly, dispatch]);
9383
9639
  const deleteRow = useCallback((rowIndex) => {
9384
9640
  const newRows = rows.filter((_, i) => i !== rowIndex);
9385
9641
  onChange(newRows);
@@ -9730,10 +9986,13 @@ const TextAreaWidget = ({ config }) => {
9730
9986
  // For readonly mode, render as preformatted text using <pre> tag
9731
9987
  if (isReadonly) {
9732
9988
  const displayValue = getStringValue() || '-';
9733
- 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("pre", { className: "text-base text-gray-900 font-medium whitespace-pre-wrap", title: String(displayValue), style: {
9989
+ 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: {
9734
9990
  fontFamily: 'Roboto, sans-serif',
9735
- margin: 0,
9736
- padding: 0,
9991
+ height: '56px',
9992
+ minHeight: '56px',
9993
+ maxHeight: '56px',
9994
+ lineHeight: '20px',
9995
+ padding: '8px 0',
9737
9996
  backgroundColor: 'transparent',
9738
9997
  border: 'none',
9739
9998
  }, children: displayValue }) })] }));
@@ -10202,20 +10461,29 @@ const HeaderSectionWidget = ({ config }) => {
10202
10461
  .${cls} .hdr-field-row {
10203
10462
  display: flex;
10204
10463
  align-items: flex-start;
10205
- gap: 0.5rem;
10206
10464
  font-size: 1rem;
10207
10465
  line-height: 1.6;
10208
10466
  }
10209
10467
 
10210
10468
  .${cls} .hdr-field-label {
10469
+ width: 50%;
10470
+ flex: 0 0 50%;
10211
10471
  color: rgba(0, 0, 0, 0.5);
10212
10472
  font-weight: 400;
10213
10473
  white-space: nowrap;
10474
+ overflow: hidden;
10475
+ text-overflow: ellipsis;
10476
+ padding-right: 4px;
10214
10477
  }
10215
10478
 
10216
10479
  .${cls} .hdr-field-value {
10480
+ width: 50%;
10481
+ flex: 0 0 50%;
10217
10482
  color: var(--owt-color-text, #111827);
10218
10483
  font-weight: 500;
10484
+ white-space: nowrap;
10485
+ overflow: hidden;
10486
+ text-overflow: ellipsis;
10219
10487
  }
10220
10488
 
10221
10489
  .${cls} .hdr-status-badge {
@@ -10225,24 +10493,38 @@ const HeaderSectionWidget = ({ config }) => {
10225
10493
  font-size: 0.75rem;
10226
10494
  font-weight: 600;
10227
10495
  color: #fff;
10496
+ max-width: 100%;
10497
+ overflow: hidden;
10498
+ text-overflow: ellipsis;
10499
+ white-space: nowrap;
10228
10500
  }
10229
10501
 
10230
10502
  .${cls} .hdr-meta-row {
10231
10503
  display: flex;
10232
10504
  align-items: baseline;
10233
- gap: 0.35rem;
10234
10505
  font-size: 1rem;
10235
10506
  line-height: 1.6;
10236
10507
  }
10237
10508
 
10238
10509
  .${cls} .hdr-meta-label {
10510
+ width: 50%;
10511
+ flex: 0 0 50%;
10239
10512
  color: rgba(0, 0, 0, 0.5);
10240
10513
  font-weight: 400;
10514
+ white-space: nowrap;
10515
+ overflow: hidden;
10516
+ text-overflow: ellipsis;
10517
+ padding-right: 4px;
10241
10518
  }
10242
10519
 
10243
10520
  .${cls} .hdr-meta-value {
10521
+ width: 50%;
10522
+ flex: 0 0 50%;
10244
10523
  color: var(--owt-color-text, #111827);
10245
10524
  font-weight: 500;
10525
+ white-space: nowrap;
10526
+ overflow: hidden;
10527
+ text-overflow: ellipsis;
10246
10528
  }
10247
10529
 
10248
10530
  .${cls} .hdr-select {
@@ -10306,7 +10588,7 @@ const HeaderSectionWidget = ({ config }) => {
10306
10588
  .parentElement?.querySelector('.hdr-avatar-placeholder');
10307
10589
  if (placeholder)
10308
10590
  placeholder.style.display = 'flex';
10309
- } })) : null, jsxRuntimeExports.jsx("div", { className: "hdr-avatar-placeholder", style: { display: displayImageUrl ? 'none' : 'flex' }, children: jsxRuntimeExports.jsx("img", { src: img$f, alt: "Profile Placeholder" }) }), !isReadonly && (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("div", { className: "hdr-avatar-overlay", children: [jsxRuntimeExports.jsxs("button", { type: "button", className: "hdr-avatar-action", onClick: () => fileInputRef.current?.click(), children: [jsxRuntimeExports.jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntimeExports.jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }), jsxRuntimeExports.jsx("polyline", { points: "17 8 12 3 7 8" }), jsxRuntimeExports.jsx("line", { x1: "12", y1: "3", x2: "12", y2: "15" })] }), "Upload"] }), jsxRuntimeExports.jsxs("button", { type: "button", className: "hdr-avatar-action hdr-avatar-action--delete", onClick: handleImageDelete, children: [jsxRuntimeExports.jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntimeExports.jsx("polyline", { points: "3 6 5 6 21 6" }), jsxRuntimeExports.jsx("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })] }), "Delete"] })] }), jsxRuntimeExports.jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", style: { display: 'none' }, onChange: handleImageUpload })] }))] }), jsxRuntimeExports.jsxs("div", { className: "hdr-info", children: [displayName && jsxRuntimeExports.jsx("div", { className: "hdr-name", children: displayName }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-field-label", children: [getLabel('functionalId'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-field-value", children: functionalId || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsx("span", { className: "hdr-field-label", children: getLabel('status') }), isReadonly ? (statusLabel ? (jsxRuntimeExports.jsx("span", { className: "hdr-status-badge", style: { backgroundColor: statusColor }, children: statusLabel })) : (jsxRuntimeExports.jsx("span", { className: "hdr-field-value", children: "-" }))) : (jsxRuntimeExports.jsxs("select", { className: "hdr-select", value: statusValue, onChange: (e) => updateFieldValue('status', e.target.value), children: [jsxRuntimeExports.jsx("option", { value: "", children: getLabel('select') }), statusOptions.map((opt) => (jsxRuntimeExports.jsx("option", { value: opt.value, children: opt.label }, opt.value)))] }))] }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-field-label", children: [getLabel('statusReason'), " :"] }), isReadonly ? (jsxRuntimeExports.jsx("span", { className: "hdr-field-value", children: statusReason || '-' })) : (jsxRuntimeExports.jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [jsxRuntimeExports.jsx("input", { type: "text", className: `hdr-input ${(!isReadonly && (showReasonRequired || isReasonMissing) && isReasonMissing) ? 'hdr-input--error' : ''}`, value: statusReason, placeholder: getLabel('enterReason'), required: isStatusChanged, "aria-required": isStatusChanged, "aria-invalid": !isReadonly && (showReasonRequired || isReasonMissing) && isReasonMissing, onBlur: () => {
10591
+ } })) : null, jsxRuntimeExports.jsx("div", { className: "hdr-avatar-placeholder", style: { display: displayImageUrl ? 'none' : 'flex' }, children: jsxRuntimeExports.jsx("img", { src: img$f, alt: "Profile Placeholder" }) }), !isReadonly && (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("div", { className: "hdr-avatar-overlay", children: [jsxRuntimeExports.jsxs("button", { type: "button", className: "hdr-avatar-action", onClick: () => fileInputRef.current?.click(), children: [jsxRuntimeExports.jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntimeExports.jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }), jsxRuntimeExports.jsx("polyline", { points: "17 8 12 3 7 8" }), jsxRuntimeExports.jsx("line", { x1: "12", y1: "3", x2: "12", y2: "15" })] }), "Upload"] }), jsxRuntimeExports.jsxs("button", { type: "button", className: "hdr-avatar-action hdr-avatar-action--delete", onClick: handleImageDelete, children: [jsxRuntimeExports.jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntimeExports.jsx("polyline", { points: "3 6 5 6 21 6" }), jsxRuntimeExports.jsx("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })] }), "Delete"] })] }), jsxRuntimeExports.jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", style: { display: 'none' }, onChange: handleImageUpload })] }))] }), jsxRuntimeExports.jsxs("div", { className: "hdr-info", children: [displayName && jsxRuntimeExports.jsx("div", { className: "hdr-name", children: displayName }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-field-label", title: `${getLabel('functionalId')} :`, children: [getLabel('functionalId'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-field-value", title: functionalId || '-', children: functionalId || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsx("span", { className: "hdr-field-label", title: getLabel('status'), children: getLabel('status') }), isReadonly ? (statusLabel ? (jsxRuntimeExports.jsx("span", { className: "hdr-status-badge", style: { backgroundColor: statusColor }, title: statusLabel, children: statusLabel })) : (jsxRuntimeExports.jsx("span", { className: "hdr-field-value", title: "-", children: "-" }))) : (jsxRuntimeExports.jsxs("select", { className: "hdr-select", value: statusValue, onChange: (e) => updateFieldValue('status', e.target.value), children: [jsxRuntimeExports.jsx("option", { value: "", children: getLabel('select') }), statusOptions.map((opt) => (jsxRuntimeExports.jsx("option", { value: opt.value, children: opt.label }, opt.value)))] }))] }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-field-label", title: `${getLabel('statusReason')} :`, children: [getLabel('statusReason'), " :"] }), isReadonly ? (jsxRuntimeExports.jsx("span", { className: "hdr-field-value", title: statusReason || '-', children: statusReason || '-' })) : (jsxRuntimeExports.jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [jsxRuntimeExports.jsx("input", { type: "text", className: `hdr-input ${(!isReadonly && (showReasonRequired || isReasonMissing) && isReasonMissing) ? 'hdr-input--error' : ''}`, value: statusReason, placeholder: getLabel('enterReason'), required: isStatusChanged, "aria-required": isStatusChanged, "aria-invalid": !isReadonly && (showReasonRequired || isReasonMissing) && isReasonMissing, onBlur: () => {
10310
10592
  if (isReasonMissing)
10311
10593
  setShowReasonRequired(true);
10312
10594
  }, onChange: (e) => {
@@ -10314,7 +10596,7 @@ const HeaderSectionWidget = ({ config }) => {
10314
10596
  if (showReasonRequired && String(e.target.value || '').trim().length > 0) {
10315
10597
  setShowReasonRequired(false);
10316
10598
  }
10317
- } }), !isReadonly && (showReasonRequired || isReasonMissing) && isReasonMissing ? (jsxRuntimeExports.jsx("div", { className: "hdr-error-text", children: getLabel('enterReason') })) : null] }))] })] })] }), jsxRuntimeExports.jsx("div", { className: "hdr-right", children: jsxRuntimeExports.jsxs("div", { className: "hdr-right-top", children: [jsxRuntimeExports.jsxs("div", { className: "hdr-meta-col", children: [jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('createdBy'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: createdBy || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('createdAt'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: createdAt || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('lastApprovedBy'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: lastApprovedBy || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('lastApprovedAt'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: lastApprovedAt || '-' })] })] }), score ? (jsxRuntimeExports.jsx("div", { className: "hdr-score-ring", style: { ['--pct']: score.percent }, "aria-label": `Completion score ${score.completionDisplay} of ${score.idealDisplay} (${score.percent}%)`, title: `${score.completionDisplay} / ${score.idealDisplay} (${score.percent}%)`, children: jsxRuntimeExports.jsx("div", { className: "hdr-score-value", children: String(score.completionDisplay) }) })) : null] }) })] })] }));
10599
+ } }), !isReadonly && (showReasonRequired || isReasonMissing) && isReasonMissing ? (jsxRuntimeExports.jsx("div", { className: "hdr-error-text", children: getLabel('enterReason') })) : null] }))] })] })] }), jsxRuntimeExports.jsx("div", { className: "hdr-right", children: jsxRuntimeExports.jsxs("div", { className: "hdr-right-top", children: [jsxRuntimeExports.jsxs("div", { className: "hdr-meta-col", children: [jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", title: `${getLabel('createdBy')} :`, children: [getLabel('createdBy'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", title: createdBy || '-', children: createdBy || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", title: `${getLabel('createdAt')} :`, children: [getLabel('createdAt'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", title: createdAt || '-', children: createdAt || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", title: `${getLabel('lastApprovedBy')} :`, children: [getLabel('lastApprovedBy'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", title: lastApprovedBy || '-', children: lastApprovedBy || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", title: `${getLabel('lastApprovedAt')} :`, children: [getLabel('lastApprovedAt'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", title: lastApprovedAt || '-', children: lastApprovedAt || '-' })] })] }), score ? (jsxRuntimeExports.jsx("div", { className: "hdr-score-ring", style: { ['--pct']: score.percent }, "aria-label": `Completion score ${score.completionDisplay} of ${score.idealDisplay} (${score.percent}%)`, title: `${score.completionDisplay} / ${score.idealDisplay} (${score.percent}%)`, children: jsxRuntimeExports.jsx("div", { className: "hdr-score-value", children: String(score.completionDisplay) }) })) : null] }) })] })] }));
10318
10600
  };
10319
10601
 
10320
10602
  function getValueByPathOrKey(obj, path) {