@openg2p/registry-widgets 1.1.0-dev.10 → 1.1.0-dev.12

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
@@ -3947,7 +3947,7 @@ function scopedClassSelectors(sectionClassId, classNames) {
3947
3947
  * - Panels wrap when they exceed available width
3948
3948
  * - Sections can sit side-by-side if there's space
3949
3949
  */
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, }) => {
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, isAccessible = false, onEditModeChange, forceExitEdit, }) => {
3951
3951
  const { translateConfig, translate } = useWidgetTranslation();
3952
3952
  const resolvedTheme = useWidgetTheme();
3953
3953
  const portalCSSVariables = React.useMemo(() => themeToCSSVariables(resolvedTheme), [resolvedTheme]);
@@ -4013,17 +4013,25 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4013
4013
  const isExpandedFromContainer = typeof sectionIndex === 'number' && expandedSectionIndex === sectionIndex;
4014
4014
  const isExpandedStandalone = sectionIndex === undefined && standaloneExpanded;
4015
4015
  const isExpanded = mode === 'IntakeForm' && (isExpandedFromContainer || isExpandedStandalone);
4016
- // When the section is managed by SectionsContainer (sectionIndex is set), header clicks are
4017
- // disabled navigation is only allowed via the Next / Previous buttons.
4018
- // Standalone usage (sectionIndex undefined) keeps the classic toggle behaviour.
4016
+ // IntakeForm only: tracks whether the user has clicked Next on this section at least once.
4017
+ // Used to unlock the accordion header so the user can navigate back to a visited section.
4018
+ const [hasBeenSavedByUser, setHasBeenSavedByUser] = React.useState(false);
4019
+ // Accordion header click behaviour in IntakeForm mode:
4020
+ // - Standalone (no sectionIndex): always toggleable.
4021
+ // - Managed by SectionsContainer: toggleable only when isAccessible is true
4022
+ // (i.e. the section has been visited OR is the immediate next one).
4023
+ // Sections beyond that remain locked.
4019
4024
  const handleAccordionToggle = React.useCallback(() => {
4020
4025
  if (mode !== 'IntakeForm')
4021
4026
  return;
4022
4027
  if (sectionIndex === undefined) {
4023
4028
  setStandaloneExpanded(prev => !prev);
4024
4029
  }
4025
- // Intentionally no-op when sectionIndex is set (managed by SectionsContainer)
4026
- }, [mode, sectionIndex]);
4030
+ else if (isAccessible && onExpandSection) {
4031
+ onExpandSection(sectionIndex);
4032
+ }
4033
+ // Intentionally no-op for locked sections (isAccessible === false)
4034
+ }, [mode, sectionIndex, isAccessible, onExpandSection]);
4027
4035
  // Recursively count all vertical panels, especially those nested inside horizontal panels
4028
4036
  // Typically: horizontal panels at first level contain vertical panels at second level
4029
4037
  // Accounts for panel-column-span: a panel with column-span 3 counts as 3 columns
@@ -4358,8 +4366,6 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4358
4366
  const baselineSnapshotRef = React.useRef(null);
4359
4367
  // IntakeForm only: increment when baseline is updated after save - forces badge to update (refs don't trigger re-renders)
4360
4368
  const [intakeFormBaselineTrigger, setIntakeFormBaselineTrigger] = React.useState(0);
4361
- // IntakeForm only: tracks whether the user has actually saved this section (prevents "Saved" badge on initial load)
4362
- const [hasBeenSavedByUser, setHasBeenSavedByUser] = React.useState(false);
4363
4369
  // IntakeForm: treat as edit mode for dirty tracking when isDraft. RegistryView: use isEditMode.
4364
4370
  const effectiveEditModeForDirty = mode === 'IntakeForm' ? (isDraft !== false) : isEditMode;
4365
4371
  // Compute isDirty: compare current store state to baseline (only when in edit mode)
@@ -4592,6 +4598,11 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4592
4598
  }
4593
4599
  onSectionDirtyChange?.(sectionId, false);
4594
4600
  }
4601
+ else if (mode === 'IntakeForm') {
4602
+ // No onSectionSave provided, but still mark section as visited so the
4603
+ // user can navigate back to it by clicking the accordion header.
4604
+ setHasBeenSavedByUser(true);
4605
+ }
4595
4606
  // Always navigate to the next section
4596
4607
  onSectionSaveSuccess?.(sectionIndex);
4597
4608
  }, [store, onSectionSave, onSectionSaveSuccess, sectionIndex, originalSection, schemaData, contextSchemaData, namespace, hasSupportingDocuments, dbSectionId, sectionRegisterId, dispatch, buildSectionSnapshot, sectionId, onSectionDirtyChange, mode, isDraft]);
@@ -4863,7 +4874,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4863
4874
  }),
4864
4875
  }, children: mode === 'IntakeForm' ? (
4865
4876
  /* IntakeForm: accordion layout - header always visible, content only when expanded */
4866
- 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 ? 'true' : 'false', style: {
4877
+ 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: {
4867
4878
  width: '100%',
4868
4879
  display: 'flex',
4869
4880
  alignItems: 'flex-start',
@@ -4873,7 +4884,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4873
4884
  marginBottom: 0,
4874
4885
  background: 'none',
4875
4886
  border: 'none',
4876
- cursor: sectionIndex === undefined ? 'pointer' : 'default',
4887
+ cursor: sectionIndex === undefined || isAccessible ? 'pointer' : 'default',
4877
4888
  textAlign: 'left',
4878
4889
  fontFamily: 'Roboto, sans-serif',
4879
4890
  }, 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']
@@ -5171,6 +5182,11 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
5171
5182
  const dataSourceRequestHandler = propDataSourceRequestHandler || contextDataSourceRequestHandler;
5172
5183
  // IntakeForm mode: accordion state - which section is expanded (null = none; first expanded by default)
5173
5184
  const [expandedSectionIndex, setExpandedSectionIndex] = React.useState(0);
5185
+ // IntakeForm mode: high-water mark of the furthest section the user has clicked Next on.
5186
+ // A section at index i is accessible when i <= maxVisitedIndex + 1
5187
+ // (i.e. every visited section plus the one immediately after it).
5188
+ // Starts at -1 so only section 0 is accessible before any Next is clicked.
5189
+ const [maxVisitedIndex, setMaxVisitedIndex] = React.useState(-1);
5174
5190
  // RegistryView: track which section is currently in edit mode (by section-id); null = none
5175
5191
  const [editingSectionId, setEditingSectionId] = React.useState(null);
5176
5192
  const handleEditModeChange = React.useCallback((sectionId, editing) => {
@@ -5181,6 +5197,11 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
5181
5197
  // Stable ref for namespace so formHandle useMemo doesn't depend on the (possibly inline) function identity
5182
5198
  const namespaceRef = React.useRef(namespace);
5183
5199
  namespaceRef.current = namespace;
5200
+ // Stable ref for sections so formHandle useMemo doesn't depend on the (possibly unstable) array reference.
5201
+ // Without this, an inline sections={[...]} prop would recreate formHandle every render, causing
5202
+ // onFormReady to fire every render → host setState → infinite re-render loop.
5203
+ const safeSectionsRef = React.useRef(safeSections);
5204
+ safeSectionsRef.current = safeSections;
5184
5205
  // Stable refs so formHandle closure can access current mode and accordion setter without stale captures
5185
5206
  const modeRef = React.useRef(mode);
5186
5207
  modeRef.current = mode;
@@ -5195,8 +5216,9 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
5195
5216
  const handleExpandSection = React.useCallback((index) => {
5196
5217
  setExpandedSectionIndex(prev => (prev === index ? null : index));
5197
5218
  }, []);
5198
- // IntakeForm mode: called after section save - collapse current, expand next
5219
+ // IntakeForm mode: called after section save - advance high-water mark, collapse current, expand next
5199
5220
  const handleSectionSaveSuccess = React.useCallback((index) => {
5221
+ setMaxVisitedIndex(prev => Math.max(prev, index));
5200
5222
  if (index + 1 < safeSections.length) {
5201
5223
  setExpandedSectionIndex(index + 1);
5202
5224
  }
@@ -5228,6 +5250,9 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
5228
5250
  }, [mode, safeSections.length, expandedSectionIndex]);
5229
5251
  const UNSAVED_CHANGES_ERROR = 'Unsaved changes detected. Please save all sections before submitting.';
5230
5252
  // Form handle for onFormReady - allows host to validate and get all section data from its own Submit button
5253
+ // IMPORTANT: safeSections is intentionally accessed via safeSectionsRef.current (not closed over directly)
5254
+ // so that this memo never depends on the sections array reference. This prevents an infinite re-render
5255
+ // loop when the host passes an unstable sections prop (e.g. inline array or derived state).
5231
5256
  const formHandle = React.useMemo(() => {
5232
5257
  const getValues = () => store.getState().widget?.values || {};
5233
5258
  const getNamespace = (section, index) => {
@@ -5249,10 +5274,11 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
5249
5274
  if (modeRef.current !== 'IntakeForm')
5250
5275
  checkNoUnsavedChanges();
5251
5276
  const values = getValues();
5277
+ const sections = safeSectionsRef.current;
5252
5278
  let allValid = true;
5253
5279
  let firstInvalidIndex = null;
5254
- for (let i = 0; i < safeSections.length; i++) {
5255
- const section = safeSections[i];
5280
+ for (let i = 0; i < sections.length; i++) {
5281
+ const section = sections[i];
5256
5282
  const ns = getNamespace(section, i);
5257
5283
  const sectionToValidate = ns ? namespaceSectionConfig(section, ns) : section;
5258
5284
  const valid = sectionValidate(sectionToValidate, values, dispatch);
@@ -5272,10 +5298,11 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
5272
5298
  if (modeRef.current !== 'IntakeForm')
5273
5299
  checkNoUnsavedChanges();
5274
5300
  const values = getValues();
5301
+ const sections = safeSectionsRef.current;
5275
5302
  const results = [];
5276
5303
  let firstInvalidIndex = null;
5277
- for (let i = 0; i < safeSections.length; i++) {
5278
- const section = safeSections[i];
5304
+ for (let i = 0; i < sections.length; i++) {
5305
+ const section = sections[i];
5279
5306
  const ns = getNamespace(section, i);
5280
5307
  const sectionToValidate = ns ? namespaceSectionConfig(section, ns) : section;
5281
5308
  const valid = sectionValidate(sectionToValidate, values, dispatch);
@@ -5297,16 +5324,18 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
5297
5324
  },
5298
5325
  getStructuredData: () => {
5299
5326
  const values = getValues();
5327
+ const sections = safeSectionsRef.current;
5300
5328
  const results = [];
5301
- for (let i = 0; i < safeSections.length; i++) {
5302
- const section = safeSections[i];
5329
+ for (let i = 0; i < sections.length; i++) {
5330
+ const section = sections[i];
5303
5331
  const ns = getNamespace(section, i);
5304
5332
  results.push(buildSectionChanges(section, values, ns));
5305
5333
  }
5306
5334
  return results;
5307
5335
  },
5308
5336
  };
5309
- }, [store, dispatch, safeSections]);
5337
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5338
+ }, [store, dispatch]); // safeSections intentionally omitted — accessed via safeSectionsRef
5310
5339
  // Call onFormReady when form is ready (sections loaded)
5311
5340
  React.useEffect(() => {
5312
5341
  if (onFormReady && safeSections.length > 0) {
@@ -5390,6 +5419,8 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
5390
5419
  onSectionSaveSuccess: handleSectionSaveSuccess,
5391
5420
  onPreviousSection: handlePreviousSection,
5392
5421
  isDraft,
5422
+ // Accessible = every visited section + the one immediately after
5423
+ isAccessible: index <= maxVisitedIndex + 1,
5393
5424
  }
5394
5425
  : {};
5395
5426
  // RegistryView: single-edit coordination props