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