@openg2p/registry-widgets 1.1.0-dev.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1078,16 +1078,12 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
1078
1078
  console.error('[getApiDataSource] API data source missing endpoint. Use "endpoint" field to specify the operation (e.g., "get_g2p_geo_level_values")');
1079
1079
  return [];
1080
1080
  }
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
- }
1081
+ // Call handler — let any throw propagate to the outer catch so it is logged once
1082
+ // by useBaseWidget rather than double-logged here (which can cascade when
1083
+ // intercept-console-error.js converts console.error calls into thrown errors).
1084
+ const response = await dataSourceRequestHandler(service, endpoint, method, requestParams, {
1085
+ headers: dataSource.headers,
1086
+ });
1091
1087
  // Handle OpenG2P response format (response_body.response_payload)
1092
1088
  if (response && typeof response === 'object') {
1093
1089
  if (response.response_body?.response_payload && Array.isArray(response.response_body.response_payload)) {
@@ -1110,8 +1106,8 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
1110
1106
  return [];
1111
1107
  }
1112
1108
  catch (error) {
1113
- console.error('Error fetching API data source:', error);
1114
- return [];
1109
+ // Rethrow so useBaseWidget's catch can log it with full widget context
1110
+ throw error;
1115
1111
  }
1116
1112
  };
1117
1113
  /**
@@ -2305,7 +2301,7 @@ const useBaseWidget = (options) => {
2305
2301
  dispatch(setDataSource({ widgetId, data: transformed }));
2306
2302
  }
2307
2303
  catch (error) {
2308
- console.error(`[useBaseWidget] ERROR loading data source for ${widgetId}:`, error);
2304
+ console.error(`[useBaseWidget] ERROR loading data source for widget "${widgetId}" (type="${dataSource.type}"):`, error, '\nWidget config:', config, '\ndataSourceRequestHandler provided:', Boolean(dataSourceRequestHandler));
2309
2305
  dispatch(setDataSource({ widgetId, data: [] }));
2310
2306
  }
2311
2307
  finally {
@@ -3947,7 +3943,7 @@ function scopedClassSelectors(sectionClassId, classNames) {
3947
3943
  * - Panels wrap when they exceed available width
3948
3944
  * - Sections can sit side-by-side if there's space
3949
3945
  */
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, }) => {
3946
+ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequestHandler, schemaData, onValueChange, gridColumnSpan, onSectionSave, hideEditButton = false, mode = 'RegistryView', namespace, changeRequestType, showChangeRequestLabel = true, dbSectionId, sectionRegisterId, onSectionDirtyChange, sectionIndex, sectionCount, expandedSectionIndex, onExpandSection, onSectionSaveSuccess, onPreviousSection, isDraft, isAccessible = false, onEditModeChange, forceExitEdit, }) => {
3951
3947
  const { translateConfig, translate } = useWidgetTranslation();
3952
3948
  const resolvedTheme = useWidgetTheme();
3953
3949
  const portalCSSVariables = React.useMemo(() => themeToCSSVariables(resolvedTheme), [resolvedTheme]);
@@ -4013,16 +4009,25 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4013
4009
  const isExpandedFromContainer = typeof sectionIndex === 'number' && expandedSectionIndex === sectionIndex;
4014
4010
  const isExpandedStandalone = sectionIndex === undefined && standaloneExpanded;
4015
4011
  const isExpanded = mode === 'IntakeForm' && (isExpandedFromContainer || isExpandedStandalone);
4012
+ // IntakeForm only: tracks whether the user has clicked Next on this section at least once.
4013
+ // Used to unlock the accordion header so the user can navigate back to a visited section.
4014
+ const [hasBeenSavedByUser, setHasBeenSavedByUser] = React.useState(false);
4015
+ // Accordion header click behaviour in IntakeForm mode:
4016
+ // - Standalone (no sectionIndex): always toggleable.
4017
+ // - Managed by SectionsContainer: toggleable only when isAccessible is true
4018
+ // (i.e. the section has been visited OR is the immediate next one).
4019
+ // Sections beyond that remain locked.
4016
4020
  const handleAccordionToggle = React.useCallback(() => {
4017
4021
  if (mode !== 'IntakeForm')
4018
4022
  return;
4019
- if (typeof sectionIndex === 'number' && onExpandSection) {
4020
- onExpandSection(sectionIndex);
4021
- }
4022
- else if (sectionIndex === undefined) {
4023
+ if (sectionIndex === undefined) {
4023
4024
  setStandaloneExpanded(prev => !prev);
4024
4025
  }
4025
- }, [mode, sectionIndex, onExpandSection]);
4026
+ else if (isAccessible && onExpandSection) {
4027
+ onExpandSection(sectionIndex);
4028
+ }
4029
+ // Intentionally no-op for locked sections (isAccessible === false)
4030
+ }, [mode, sectionIndex, isAccessible, onExpandSection]);
4026
4031
  // Recursively count all vertical panels, especially those nested inside horizontal panels
4027
4032
  // Typically: horizontal panels at first level contain vertical panels at second level
4028
4033
  // Accounts for panel-column-span: a panel with column-span 3 counts as 3 columns
@@ -4357,8 +4362,6 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4357
4362
  const baselineSnapshotRef = React.useRef(null);
4358
4363
  // IntakeForm only: increment when baseline is updated after save - forces badge to update (refs don't trigger re-renders)
4359
4364
  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
4365
  // IntakeForm: treat as edit mode for dirty tracking when isDraft. RegistryView: use isEditMode.
4363
4366
  const effectiveEditModeForDirty = mode === 'IntakeForm' ? (isDraft !== false) : isEditMode;
4364
4367
  // Compute isDirty: compare current store state to baseline (only when in edit mode)
@@ -4591,6 +4594,11 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4591
4594
  }
4592
4595
  onSectionDirtyChange?.(sectionId, false);
4593
4596
  }
4597
+ else if (mode === 'IntakeForm') {
4598
+ // No onSectionSave provided, but still mark section as visited so the
4599
+ // user can navigate back to it by clicking the accordion header.
4600
+ setHasBeenSavedByUser(true);
4601
+ }
4594
4602
  // Always navigate to the next section
4595
4603
  onSectionSaveSuccess?.(sectionIndex);
4596
4604
  }, [store, onSectionSave, onSectionSaveSuccess, sectionIndex, originalSection, schemaData, contextSchemaData, namespace, hasSupportingDocuments, dbSectionId, sectionRegisterId, dispatch, buildSectionSnapshot, sectionId, onSectionDirtyChange, mode, isDraft]);
@@ -4809,13 +4817,17 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4809
4817
  .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header h2 {
4810
4818
  color: var(--owt-color-primary-dark, #F07B1A);
4811
4819
  }
4812
- .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header:hover {
4820
+ /* Hover / focus only shown when the header is actually interactive (standalone mode) */
4821
+ .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="true"]:hover {
4813
4822
  opacity: 0.85;
4814
4823
  }
4815
- .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header:focus-visible {
4824
+ .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="true"]:focus-visible {
4816
4825
  outline: 2px solid var(--owt-color-primary, #F5BB1A);
4817
4826
  outline-offset: 2px;
4818
4827
  }
4828
+ .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="false"]:focus-visible {
4829
+ outline: none;
4830
+ }
4819
4831
  .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-content {
4820
4832
  padding-top: 8px;
4821
4833
  padding-bottom: 0px;
@@ -4858,7 +4870,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4858
4870
  }),
4859
4871
  }, children: mode === 'IntakeForm' ? (
4860
4872
  /* 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: {
4873
+ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("button", { type: "button", id: `intake-form-accordion-header-${sectionId}`, className: "intake-form-accordion-header", onClick: handleAccordionToggle, "aria-expanded": isExpanded, "aria-controls": isExpanded ? `intake-form-accordion-content-${sectionId}` : undefined, "data-interactive": sectionIndex === undefined || isAccessible ? 'true' : 'false', style: {
4862
4874
  width: '100%',
4863
4875
  display: 'flex',
4864
4876
  alignItems: 'flex-start',
@@ -4868,7 +4880,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4868
4880
  marginBottom: 0,
4869
4881
  background: 'none',
4870
4882
  border: 'none',
4871
- cursor: 'pointer',
4883
+ cursor: sectionIndex === undefined || isAccessible ? 'pointer' : 'default',
4872
4884
  textAlign: 'left',
4873
4885
  fontFamily: 'Roboto, sans-serif',
4874
4886
  }, children: [jsxRuntimeExports.jsxs("div", { style: { flex: 1, display: 'flex', alignItems: 'center', gap: '12px', minWidth: 0 }, children: [jsxRuntimeExports.jsx("h2", { className: "text-xl font-semibold", style: { margin: 0 }, children: sectionToRender['section-title']
@@ -5166,6 +5178,11 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
5166
5178
  const dataSourceRequestHandler = propDataSourceRequestHandler || contextDataSourceRequestHandler;
5167
5179
  // IntakeForm mode: accordion state - which section is expanded (null = none; first expanded by default)
5168
5180
  const [expandedSectionIndex, setExpandedSectionIndex] = React.useState(0);
5181
+ // IntakeForm mode: high-water mark of the furthest section the user has clicked Next on.
5182
+ // A section at index i is accessible when i <= maxVisitedIndex + 1
5183
+ // (i.e. every visited section plus the one immediately after it).
5184
+ // Starts at -1 so only section 0 is accessible before any Next is clicked.
5185
+ const [maxVisitedIndex, setMaxVisitedIndex] = React.useState(-1);
5169
5186
  // RegistryView: track which section is currently in edit mode (by section-id); null = none
5170
5187
  const [editingSectionId, setEditingSectionId] = React.useState(null);
5171
5188
  const handleEditModeChange = React.useCallback((sectionId, editing) => {
@@ -5190,8 +5207,9 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
5190
5207
  const handleExpandSection = React.useCallback((index) => {
5191
5208
  setExpandedSectionIndex(prev => (prev === index ? null : index));
5192
5209
  }, []);
5193
- // IntakeForm mode: called after section save - collapse current, expand next
5210
+ // IntakeForm mode: called after section save - advance high-water mark, collapse current, expand next
5194
5211
  const handleSectionSaveSuccess = React.useCallback((index) => {
5212
+ setMaxVisitedIndex(prev => Math.max(prev, index));
5195
5213
  if (index + 1 < safeSections.length) {
5196
5214
  setExpandedSectionIndex(index + 1);
5197
5215
  }
@@ -5385,6 +5403,8 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
5385
5403
  onSectionSaveSuccess: handleSectionSaveSuccess,
5386
5404
  onPreviousSection: handlePreviousSection,
5387
5405
  isDraft,
5406
+ // Accessible = every visited section + the one immediately after
5407
+ isAccessible: index <= maxVisitedIndex + 1,
5388
5408
  }
5389
5409
  : {};
5390
5410
  // RegistryView: single-edit coordination props
@@ -7367,7 +7387,7 @@ const NumberInputWidget = ({ config }) => {
7367
7387
  if (parsed === null) {
7368
7388
  // Allow empty input or partial input (e.g., "-", ".")
7369
7389
  if (inputValue === '' || inputValue === '-' || inputValue === '.') {
7370
- onChange('');
7390
+ onChange(null);
7371
7391
  }
7372
7392
  // Don't update if invalid - let user continue typing
7373
7393
  return;
@@ -9731,10 +9751,13 @@ const TextAreaWidget = ({ config }) => {
9731
9751
  // For readonly mode, render as preformatted text using <pre> tag
9732
9752
  if (isReadonly) {
9733
9753
  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: {
9754
+ return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] TextAreaDisplayWidget flex flex-col sm:flex-row sm:items-start", children: [label && (jsxRuntimeExports.jsxs("div", { className: "text-base text-gray-600 font-medium md:min-w-[120px] sm:pr-4 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: label, children: [label, ":"] })), jsxRuntimeExports.jsx("div", { className: "flex-1", children: jsxRuntimeExports.jsx("div", { title: String(displayValue), className: "text-base text-gray-900 font-medium overflow-y-auto whitespace-pre-wrap break-words", style: {
9735
9755
  fontFamily: 'Roboto, sans-serif',
9736
- margin: 0,
9737
- padding: 0,
9756
+ height: '56px',
9757
+ minHeight: '56px',
9758
+ maxHeight: '56px',
9759
+ lineHeight: '20px',
9760
+ padding: '8px 0',
9738
9761
  backgroundColor: 'transparent',
9739
9762
  border: 'none',
9740
9763
  }, children: displayValue }) })] }));