@openg2p/registry-widgets 1.1.0-dev.0 → 1.1.0-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1004,6 +1004,19 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
1004
1004
  // If not found and doesn't contain dots, try as widget-id
1005
1005
  if ((depValue === null || depValue === undefined) && !dataSource.dependsOn.includes('.')) {
1006
1006
  depValue = allValues[dataSource.dependsOn];
1007
+ // Smart resolution: If not found at top level, try to find the dependency in the same nested object
1008
+ // by looking for other keys in allValues that might contain the dependency.
1009
+ // We look for objects that contain both the current widget's path (if we can guess it) and the dependency.
1010
+ // But since we don't know the current widget's path here, we search for any object that has this dependency key.
1011
+ if (depValue === null || depValue === undefined || depValue === '') {
1012
+ for (const val of Object.values(allValues)) {
1013
+ if (val && typeof val === 'object' && !Array.isArray(val) && dataSource.dependsOn in val) {
1014
+ depValue = val[dataSource.dependsOn];
1015
+ if (depValue !== null && depValue !== undefined && depValue !== '')
1016
+ break;
1017
+ }
1018
+ }
1019
+ }
1007
1020
  }
1008
1021
  if (depValue === null || depValue === undefined || depValue === '') {
1009
1022
  // If dependency is empty, return empty array
@@ -1047,8 +1060,8 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
1047
1060
  }
1048
1061
  }
1049
1062
  else if (staticParams.level_id) {
1050
- // First level has no parent
1051
- requestParams.parent_level_value_id = null;
1063
+ // First level has no parent, send empty string as many OpenG2P APIs expect it
1064
+ requestParams.parent_level_value_id = "";
1052
1065
  }
1053
1066
  // Get service mnemonic and endpoint (required)
1054
1067
  const service = dataSource.service;
@@ -1117,10 +1130,12 @@ const transformDataSourceOptions = (data, valueKey, labelKey) => {
1117
1130
  return { value: item, label: String(item) };
1118
1131
  });
1119
1132
  }
1120
- return data.map((item) => ({
1121
- value: item[valueKey],
1122
- label: item[labelKey] || String(item[valueKey]),
1123
- }));
1133
+ return data.map((item) => {
1134
+ const value = item[valueKey];
1135
+ // Try multiple common label keys if the primary one is missing
1136
+ const label = item[labelKey] || item.name || item.label || item.mnemonic || item.level_value_mnemonic || String(value);
1137
+ return { value, label };
1138
+ });
1124
1139
  };
1125
1140
 
1126
1141
  const WidgetEventBusContext = React.createContext(null);
@@ -1898,46 +1913,71 @@ const useBaseWidget = (options) => {
1898
1913
  const userHasSetValueRef = useRef(false);
1899
1914
  // Use ref for values to avoid stale closures in handleChange
1900
1915
  const valuesRef = useRef(values);
1916
+ const loadingRef = useRef(loading);
1917
+ const dataSourceOptionsRef = useRef(dataSourceOptions);
1901
1918
  useEffect(() => {
1902
1919
  valuesRef.current = values;
1903
- }, [values]);
1920
+ loadingRef.current = loading;
1921
+ dataSourceOptionsRef.current = dataSourceOptions;
1922
+ }, [values, loading, dataSourceOptions]);
1904
1923
  // Track last dispatched value to prevent duplicate dispatches
1905
1924
  const lastDispatchedValueRef = useRef(null);
1906
- // Get current value
1907
- const currentValue = useMemo(() => {
1908
- if (isLayoutWidget) {
1909
- return undefined; // Layout widgets don't have values
1925
+ // Helper to extract displayable value from object (especially geo hierarchy objects)
1926
+ const extractValueFromObject = useCallback((obj) => {
1927
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
1928
+ return obj;
1910
1929
  }
1911
- // Helper to extract displayable value from object (especially geo hierarchy objects)
1912
- const extractValueFromObject = (obj) => {
1913
- if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
1914
- return obj;
1915
- }
1916
- // Check for geo hierarchy structure first
1917
- if ('geo_code_hierarchy_json' in obj || 'geo_lowest_level_value_id' in obj) {
1918
- if ('geo_lowest_level_value_id' in obj) {
1919
- return obj.geo_lowest_level_value_id;
1930
+ // Check for geo hierarchy structure first
1931
+ const geoConfig = config['widget-geo-config'];
1932
+ if (geoConfig) {
1933
+ // If we have a geo hierarchy object, extract the value for this specific level
1934
+ const hierarchy = obj.hierarchy || obj.geo_code_hierarchy_json?.hierarchy;
1935
+ if (Array.isArray(hierarchy)) {
1936
+ const levelData = hierarchy.find((l) => l.level === geoConfig.level);
1937
+ if (levelData) {
1938
+ return levelData.level_value_id;
1920
1939
  }
1921
- // If it's a geo hierarchy object but no extractable ID, return undefined to avoid rendering object
1922
- return undefined;
1923
1940
  }
1924
- // Try common value fields
1925
- if ('value' in obj) {
1926
- return obj.value;
1941
+ }
1942
+ if ('geo_code_hierarchy_json' in obj || 'geo_lowest_level_value_id' in obj || 'hierarchy' in obj) {
1943
+ if ('geo_lowest_level_value_id' in obj) {
1944
+ return obj.geo_lowest_level_value_id;
1927
1945
  }
1928
- if ('id' in obj) {
1929
- return obj.id;
1946
+ if ('lowest_level_value_id' in obj) {
1947
+ return obj.lowest_level_value_id;
1930
1948
  }
1931
- if ('label' in obj) {
1932
- return obj.label;
1949
+ // Fallback for nested geo_code_hierarchy_json
1950
+ if (obj.geo_code_hierarchy_json?.lowest_level_value_id) {
1951
+ return obj.geo_code_hierarchy_json.lowest_level_value_id;
1933
1952
  }
1934
- if ('name' in obj) {
1935
- return obj.name;
1953
+ if (obj.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
1954
+ return obj.geo_code_hierarchy_json.geo_lowest_level_value_id;
1936
1955
  }
1937
- // If no extractable value found, return undefined to avoid rendering object as React child
1938
- // This prevents "Objects are not valid as a React child" errors
1956
+ // If it's a geo hierarchy object but no extractable ID, return undefined to avoid rendering object
1939
1957
  return undefined;
1940
- };
1958
+ }
1959
+ // Try common value fields
1960
+ if ('value' in obj) {
1961
+ return obj.value;
1962
+ }
1963
+ if ('id' in obj) {
1964
+ return obj.id;
1965
+ }
1966
+ if ('label' in obj) {
1967
+ return obj.label;
1968
+ }
1969
+ if ('name' in obj) {
1970
+ return obj.name;
1971
+ }
1972
+ // If no extractable value found, return undefined to avoid rendering object as React child
1973
+ // This prevents "Objects are not valid as a React child" errors
1974
+ return undefined;
1975
+ }, [config]);
1976
+ // Get current value
1977
+ const currentValue = useMemo(() => {
1978
+ if (isLayoutWidget) {
1979
+ return undefined; // Layout widgets don't have values
1980
+ }
1941
1981
  // Try to get value from widgetId first (this should have the actual selected value)
1942
1982
  // For geo widgets with dataPath, widgetId stores the actual ID, while dataPath stores the hierarchy object
1943
1983
  let value = values[widgetId];
@@ -1978,6 +2018,33 @@ const useBaseWidget = (options) => {
1978
2018
  }
1979
2019
  return value !== undefined ? value : config['widget-data-default'];
1980
2020
  }, [values, config, widgetId, isLayoutWidget]);
2021
+ // Track the last value we attempted to mirror to prevent infinite loops
2022
+ const lastMirroredValueRef = useRef(null);
2023
+ // Mirror value from dataPath to widgetId in Redux state if it's not already there.
2024
+ // This is essential for widgets that depend on this widget via 'dependsOn' using its widgetId,
2025
+ // especially when the actual data is stored in a nested path.
2026
+ // CRITICAL: This ensures that dependencies are resolved correctly when entering Edit mode.
2027
+ useEffect(() => {
2028
+ if (isLayoutWidget || !config['widget-data-path']) {
2029
+ return;
2030
+ }
2031
+ const rawValue = getWidgetValue(values, config['widget-data-path'], widgetId);
2032
+ if (rawValue !== undefined && rawValue !== null) {
2033
+ const extractedValue = extractValueFromObject(rawValue);
2034
+ // Only mirror if:
2035
+ // 1. The top-level value is undefined (initial load or entering edit mode)
2036
+ // 2. We haven't already tried to mirror this specific value (prevents loops if dispatch is ignored or delayed)
2037
+ // 3. The extracted value is valid
2038
+ if (values[widgetId] === undefined &&
2039
+ extractedValue !== undefined &&
2040
+ extractedValue !== null &&
2041
+ lastMirroredValueRef.current !== extractedValue) {
2042
+ lastMirroredValueRef.current = extractedValue;
2043
+ dispatch(setValue({ widgetId, value: extractedValue }));
2044
+ }
2045
+ }
2046
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2047
+ }, [values, config['widget-data-path'], widgetId, isLayoutWidget]);
1981
2048
  // Initialize default value only once on mount (skip for layout widgets)
1982
2049
  useEffect(() => {
1983
2050
  if (isLayoutWidget) {
@@ -2000,6 +2067,20 @@ const useBaseWidget = (options) => {
2000
2067
  if (currentValue === newValue) {
2001
2068
  return;
2002
2069
  }
2070
+ // CRITICAL FIX: Ignore auto-clears (empty string or undefined) from UI components
2071
+ // when the widget's data source is currently loading OR if options are empty.
2072
+ // This prevents data disappearance when switching to Edit mode and components
2073
+ // incorrectly clear values before options load or if handler is temporarily missing.
2074
+ if (newValue === '' || newValue === null || newValue === undefined) {
2075
+ if (loadingRef.current) {
2076
+ console.warn(`[useBaseWidget] Ignoring empty value for ${widgetId} because data source is loading`);
2077
+ return;
2078
+ }
2079
+ if (config['widget-data-source']?.type === 'api' && dataSourceOptionsRef.current.length === 0) {
2080
+ console.warn(`[useBaseWidget] Ignoring empty value for ${widgetId} because API options are empty`);
2081
+ return;
2082
+ }
2083
+ }
2003
2084
  // Mark that user has set a value (unless this is the default initialization)
2004
2085
  if (newValue !== config['widget-data-default'] || userHasSetValueRef.current) {
2005
2086
  userHasSetValueRef.current = true;
@@ -2017,6 +2098,13 @@ const useBaseWidget = (options) => {
2017
2098
  }
2018
2099
  else {
2019
2100
  // Has dataPath: update both widgetId and dataPath
2101
+ // CRITICAL: For geo widgets, we do NOT want to overwrite the shared hierarchy dataPath
2102
+ // with a primitive value (the selected ID). The hierarchy object is managed by useGeoWidgetCascade.
2103
+ if (config['widget-geo-config']) {
2104
+ dispatch(setValue({ widgetId, value: newValue }));
2105
+ return;
2106
+ }
2107
+ // For non-geo widgets, update both widgetId and dataPath
2020
2108
  // CRITICAL: Create updated values object with newValue already set
2021
2109
  // This prevents setWidgetValue from reading stale values
2022
2110
  const currentValuesWithUpdate = {
@@ -2111,6 +2199,17 @@ const useBaseWidget = (options) => {
2111
2199
  const apiService = dataSource?.type === 'api' ? dataSource.service : '';
2112
2200
  const apiEndpoint = dataSource?.type === 'api' ? dataSource.endpoint : '';
2113
2201
  const configKey = `${widgetId}-${isReadonly}-${dataSource?.type || 'none'}-${apiService}-${apiEndpoint}`;
2202
+ // Extract dependency value using a granular selector to prevent unnecessary re-renders
2203
+ // and infinite loops when other unrelated values in the state change.
2204
+ const dependencyValue = useSelector((state) => {
2205
+ if (dataSource?.type !== 'api' || !dataSource.dependsOn) {
2206
+ return null;
2207
+ }
2208
+ if (dataSource.dependsOn.includes('.')) {
2209
+ return getWidgetValue(state.widget.values, dataSource.dependsOn, '');
2210
+ }
2211
+ return state.widget.values[dataSource.dependsOn];
2212
+ });
2114
2213
  // Handle data source loading
2115
2214
  useEffect(() => {
2116
2215
  if (!dataSource) {
@@ -2131,6 +2230,17 @@ const useBaseWidget = (options) => {
2131
2230
  }
2132
2231
  else {
2133
2232
  depValue = values[dataSource.dependsOn];
2233
+ // Smart resolution: If not found at top level, and current widget has a nested dataPath,
2234
+ // try to find the dependency in the same nested object.
2235
+ if ((depValue === undefined || depValue === null || depValue === '') &&
2236
+ typeof config['widget-data-path'] === 'string' &&
2237
+ config['widget-data-path'].includes('.')) {
2238
+ const pathParts = config['widget-data-path'].split('.');
2239
+ pathParts.pop(); // Remove current field name
2240
+ const prefix = pathParts.join('.');
2241
+ const tryPath = `${prefix}.${dataSource.dependsOn}`;
2242
+ depValue = getWidgetValue(values, tryPath, '');
2243
+ }
2134
2244
  }
2135
2245
  // If dependency is empty, don't load (will load when dependency has value)
2136
2246
  if (depValue === null || depValue === undefined || depValue === '') {
@@ -2164,7 +2274,7 @@ const useBaseWidget = (options) => {
2164
2274
  }
2165
2275
  // Extract level_id from widget-geo-config.level if available
2166
2276
  const levelId = geoConfig?.level;
2167
- data = await getApiDataSource(dataSource, values, currentHandler, levelId);
2277
+ data = await getApiDataSource(dataSource, valuesRef.current, currentHandler, levelId);
2168
2278
  }
2169
2279
  else if (dataSource.type === 'schema') {
2170
2280
  data = getSchemaDataSource(dataSource, schemaData || {});
@@ -2199,9 +2309,9 @@ const useBaseWidget = (options) => {
2199
2309
  }
2200
2310
  };
2201
2311
  loadDataSource();
2202
- // Use configKey to ensure effect runs when readonly state changes
2312
+ // Use configKey and dependencyValue to ensure effect runs only when relevant state changes
2203
2313
  // eslint-disable-next-line react-hooks/exhaustive-deps
2204
- }, [configKey, values, dataSourceRequestHandler, schemaData, widgetId, dispatch]);
2314
+ }, [configKey, dependencyValue, dataSourceRequestHandler, schemaData, widgetId, dispatch]);
2205
2315
  return {
2206
2316
  widgetId,
2207
2317
  value: currentValue,
@@ -2402,6 +2512,9 @@ const useGeoWidgetCascade = (options) => {
2402
2512
  const geoConfig = config['widget-geo-config'];
2403
2513
  const dataSource = config['widget-data-source'];
2404
2514
  const dataPath = config['widget-data-path'];
2515
+ const groupId = typeof dataPath === 'string' && dataPath.includes('.')
2516
+ ? dataPath.split('.').slice(0, -1).join('.')
2517
+ : 'default';
2405
2518
  const valuesRef = useRef(values);
2406
2519
  const handlerRef = useRef(dataSourceRequestHandler);
2407
2520
  // Keep refs updated
@@ -2411,10 +2524,36 @@ const useGeoWidgetCascade = (options) => {
2411
2524
  }, [values, dataSourceRequestHandler]);
2412
2525
  // Get current value and data source options
2413
2526
  const currentValue = useSelector((state) => {
2414
- if (!dataPath) {
2415
- return state.widget.values[widgetId];
2527
+ // Try to get value from widgetId first (most recent selection)
2528
+ let value = state.widget.values[widgetId];
2529
+ // If not found in widgetId, try dataPath
2530
+ if (value === undefined && dataPath) {
2531
+ value = getWidgetValue(state.widget.values, dataPath, widgetId);
2532
+ }
2533
+ // Extract value if it's a geo hierarchy object
2534
+ if (value && typeof value === 'object' && !Array.isArray(value) && geoConfig) {
2535
+ const hierarchy = value.hierarchy || value.geo_code_hierarchy_json?.hierarchy;
2536
+ if (Array.isArray(hierarchy)) {
2537
+ const levelData = hierarchy.find((l) => l.level === geoConfig.level);
2538
+ if (levelData) {
2539
+ return levelData.level_value_id;
2540
+ }
2541
+ }
2542
+ // Extended fallbacks (matching useBaseWidget)
2543
+ if ('geo_lowest_level_value_id' in value) {
2544
+ return value.geo_lowest_level_value_id;
2545
+ }
2546
+ if ('lowest_level_value_id' in value) {
2547
+ return value.lowest_level_value_id;
2548
+ }
2549
+ if (value.geo_code_hierarchy_json?.lowest_level_value_id) {
2550
+ return value.geo_code_hierarchy_json.lowest_level_value_id;
2551
+ }
2552
+ if (value.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
2553
+ return value.geo_code_hierarchy_json.geo_lowest_level_value_id;
2554
+ }
2416
2555
  }
2417
- return getWidgetValue(state.widget.values, dataPath, widgetId);
2556
+ return value;
2418
2557
  });
2419
2558
  // Memoize selector to avoid returning new array reference
2420
2559
  const dataSourceOptions = useSelector((state) => state.widget.dataSources[widgetId] ?? EMPTY_DATA_SOURCE);
@@ -2434,11 +2573,17 @@ const useGeoWidgetCascade = (options) => {
2434
2573
  await new Promise(resolve => setTimeout(resolve, 0));
2435
2574
  const currentValues = valuesRef.current;
2436
2575
  const currentHandler = handlerRef.current;
2437
- // CRITICAL: Get the parent value from Redux state, not from the event
2438
- // The event.value might be stale, but Redux state is always current
2439
- const parentValue = currentValues[parentWidgetId];
2576
+ // CRITICAL: Try to get parent value from event first, then from Redux
2577
+ let parentValue = event.value;
2578
+ if (parentValue === undefined || parentValue === null) {
2579
+ parentValue = currentValues[parentWidgetId];
2580
+ // If not found in top-level values, try to find it via dataPath or dependsOn
2581
+ if (parentValue === undefined && dataSource.dependsOn) {
2582
+ parentValue = getWidgetValue(currentValues, dataSource.dependsOn, '');
2583
+ }
2584
+ }
2440
2585
  // Remove this level and all below from hierarchy
2441
- geoHierarchyBuilder.removeLevelAndBelow(level);
2586
+ geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2442
2587
  // Clear this widget's value
2443
2588
  // CRITICAL: Only dispatch setValue for THIS widget, not for parent or other widgets
2444
2589
  // setWidgetValue returns the entire updated state, but we only want to update this widget
@@ -2493,7 +2638,8 @@ const useGeoWidgetCascade = (options) => {
2493
2638
  }
2494
2639
  }
2495
2640
  else {
2496
- // If parent value is cleared, clear the data source
2641
+ // If parent value is cleared, clear the data source and hierarchy
2642
+ geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2497
2643
  dispatch(setDataSource({ widgetId, data: [] }));
2498
2644
  }
2499
2645
  };
@@ -2508,25 +2654,49 @@ const useGeoWidgetCascade = (options) => {
2508
2654
  if (!geoConfig) {
2509
2655
  return;
2510
2656
  }
2511
- // Skip if value is empty/null (but allow 0 and false)
2512
- if (currentValue === null || currentValue === undefined || currentValue === '') {
2513
- // If value was cleared, remove this level and below from hierarchy
2657
+ // Skip if value is undefined (it might still be loading or rehydrating)
2658
+ // ONLY clear hierarchy if the value is explicitly null or empty string (user action)
2659
+ if (currentValue === null || currentValue === '') {
2514
2660
  const { level } = geoConfig;
2515
- geoHierarchyBuilder.removeLevelAndBelow(level);
2661
+ geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2662
+ // If we have a dataPath, we need to update Redux with the cleared hierarchy
2663
+ if (dataPath) {
2664
+ const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
2665
+ let finalUpdatedValues = valuesRef.current;
2666
+ // Use logic similar to the build section below to update the dataPath
2667
+ if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
2668
+ const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
2669
+ finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
2670
+ finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson?.geo_lowest_level_value_id);
2671
+ }
2672
+ else {
2673
+ finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
2674
+ }
2675
+ dispatch(setValues(finalUpdatedValues));
2676
+ }
2516
2677
  return;
2517
2678
  }
2679
+ if (currentValue === undefined) {
2680
+ return; // Skip if undefined (still initializing)
2681
+ }
2518
2682
  const { level, isLastLevel } = geoConfig;
2519
- // For last level, check if hierarchy is already built to prevent endless loops
2520
- if (isLastLevel && dataPath) {
2683
+ // Check if hierarchy is already built to prevent endless loops
2684
+ if (dataPath) {
2521
2685
  const currentHierarchy = getWidgetValue(valuesRef.current, dataPath, widgetId);
2522
2686
  // If hierarchy JSON is already set and matches current value, skip rebuilding
2523
- if (currentHierarchy && typeof currentHierarchy === 'object' && currentHierarchy.geo_code_hierarchy_json) {
2524
- // Check if the lowest level value matches
2525
- const currentLevelValue = typeof currentValue === 'object'
2526
- ? (currentValue.level_value_id || currentValue.id || currentValue.value)
2527
- : currentValue;
2528
- if (currentHierarchy.geo_lowest_level_value_id === currentLevelValue) {
2529
- return; // Hierarchy already built for this value, skip
2687
+ if (currentHierarchy && typeof currentHierarchy === 'object') {
2688
+ // Check if this specific level's value matches the hierarchy
2689
+ const hierarchyArray = currentHierarchy.hierarchy || currentHierarchy.geo_code_hierarchy_json?.hierarchy;
2690
+ if (Array.isArray(hierarchyArray)) {
2691
+ const currentLevelValue = typeof currentValue === 'object'
2692
+ ? (currentValue.level_value_id || currentValue.id || currentValue.value)
2693
+ : currentValue;
2694
+ const levelData = hierarchyArray.find((l) => l.level === geoConfig.level);
2695
+ // If this level is already correctly represented in the hierarchy, skip rebuilding
2696
+ // String conversion ensures comparison works for mixed types
2697
+ if (levelData && String(levelData.level_value_id) === String(currentLevelValue)) {
2698
+ return;
2699
+ }
2530
2700
  }
2531
2701
  }
2532
2702
  }
@@ -2549,23 +2719,31 @@ const useGeoWidgetCascade = (options) => {
2549
2719
  return;
2550
2720
  }
2551
2721
  // When a widget's own value changes, remove this level and all below from hierarchy first
2552
- // This ensures that when level 1 changes, we clear the hierarchy and rebuild from scratch
2553
- // The addLevel method already handles removing existing levels, but we explicitly clear to be safe
2554
- geoHierarchyBuilder.removeLevelAndBelow(level);
2722
+ geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2555
2723
  // Add level to hierarchy
2556
- geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic);
2557
- // If this is the last level, build and store hierarchy JSON
2558
- if (isLastLevel && dataPath) {
2559
- const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson();
2724
+ geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic, groupId);
2725
+ // Build and store hierarchy JSON on every change
2726
+ if (dataPath) {
2727
+ const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
2560
2728
  if (hierarchyJson) {
2561
- // Store both geo_lowest_level_value_id and geo_code_hierarchy_json
2562
- const updatedValues = setWidgetValue(valuesRef.current, dataPath, widgetId, {
2563
- geo_lowest_level_value_id: hierarchyJson.geo_lowest_level_value_id,
2564
- geo_code_hierarchy_json: hierarchyJson.geo_code_hierarchy_json,
2565
- });
2566
- Object.entries(updatedValues).forEach(([key, value]) => {
2567
- dispatch(setValue({ widgetId: key, value }));
2568
- });
2729
+ // Fix: Avoid double nesting of geo_code_hierarchy_json
2730
+ // If dataPath ends with .geo_code_hierarchy_json, we want to save the content directly to it
2731
+ // and save the lowest level ID as a sibling
2732
+ let finalUpdatedValues = valuesRef.current;
2733
+ if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
2734
+ const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
2735
+ // Save hierarchy JSON content directly to dataPath (avoiding double nesting)
2736
+ finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
2737
+ // Save lowest level ID as sibling
2738
+ finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson.geo_lowest_level_value_id);
2739
+ }
2740
+ else {
2741
+ // Fallback if path doesn't follow the naming convention
2742
+ finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
2743
+ }
2744
+ // CRITICAL: Use setValues for deep merge instead of replacing root keys with setValue
2745
+ // setWidgetValue returns the complete updated state object with all keys preserved
2746
+ dispatch(setValues(finalUpdatedValues));
2569
2747
  }
2570
2748
  }
2571
2749
  }, [geoConfig, currentValue, widgetId, dataPath, dispatch, dataSourceOptions]);
@@ -3670,6 +3848,39 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
3670
3848
  return isValid;
3671
3849
  };
3672
3850
 
3851
+ /** Root class on readonly label/value rows; SectionRenderer scopes overflow/ellipsis rules here. */
3852
+ const READONLY_VALUE_ROW_ROOT_CLASSES = [
3853
+ 'TextDisplayWidget',
3854
+ 'TextAreaDisplayWidget',
3855
+ 'SelectDisplayWidget',
3856
+ 'PhoneDisplayWidget',
3857
+ 'NumberDisplayWidget',
3858
+ 'CurrencyDisplayWidget',
3859
+ 'RadioDisplayWidget',
3860
+ 'DateDisplayWidget',
3861
+ 'DateTimeDisplayWidget',
3862
+ 'CheckboxDisplayWidget',
3863
+ 'BooleanDisplayWidget',
3864
+ 'FileDisplayWidget',
3865
+ 'DisplayFieldWidget',
3866
+ ];
3867
+ /** Rows whose value is one line in .flex-1 > .text-gray-900 (ellipsis; full string via title on the element). */
3868
+ const READONLY_SINGLE_LINE_VALUE_ROW_CLASSES = [
3869
+ 'TextDisplayWidget',
3870
+ 'SelectDisplayWidget',
3871
+ 'PhoneDisplayWidget',
3872
+ 'NumberDisplayWidget',
3873
+ 'CurrencyDisplayWidget',
3874
+ 'RadioDisplayWidget',
3875
+ 'DateDisplayWidget',
3876
+ 'DateTimeDisplayWidget',
3877
+ 'CheckboxDisplayWidget',
3878
+ 'BooleanDisplayWidget',
3879
+ 'DisplayFieldWidget',
3880
+ ];
3881
+ function scopedClassSelectors(sectionClassId, classNames) {
3882
+ return classNames.map((c) => `.${sectionClassId} .${c}`).join(',\n ');
3883
+ }
3673
3884
  /**
3674
3885
  * Renders a section with its panels
3675
3886
  *
@@ -3698,41 +3909,22 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
3698
3909
  }
3699
3910
  return section;
3700
3911
  }, [section, namespace]);
3701
- // Create namespaced schemaData if namespace is provided
3702
- // This ensures widgets can read initial values from schemaData at namespaced paths
3912
+ // Create namespaced schemaData if namespace is provided.
3913
+ // Widgets with namespaced data-paths (e.g. "rv-section-0.a1a4d25a.birth_date")
3914
+ // need a nested object at values[namespace] so getValueByPath can traverse it.
3703
3915
  const namespacedSchemaData = useMemo(() => {
3704
3916
  if (!namespace || !currentSchemaData) {
3705
3917
  return schemaData;
3706
3918
  }
3707
- // Create a namespaced version of schemaData by copying values to namespaced paths
3708
- const namespaced = { ...currentSchemaData };
3709
- // Copy all top-level keys to namespaced paths
3710
- Object.keys(currentSchemaData).forEach(key => {
3711
- const namespacedKey = `${namespace}.${key}`;
3712
- if (!(namespacedKey in namespaced)) {
3713
- namespaced[namespacedKey] = currentSchemaData[key];
3714
- }
3715
- });
3716
- // Also handle nested objects - copy nested values to namespaced paths
3717
- const copyNestedValues = (obj, prefix = '') => {
3718
- if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
3719
- Object.keys(obj).forEach(key => {
3720
- const fullPath = prefix ? `${prefix}.${key}` : key;
3721
- const namespacedPath = `${namespace}.${fullPath}`;
3722
- if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
3723
- copyNestedValues(obj[key], fullPath);
3724
- // Also set the nested object at the namespaced path
3725
- setValueByPath(namespaced, namespacedPath, obj[key]);
3726
- }
3727
- else {
3728
- setValueByPath(namespaced, namespacedPath, obj[key]);
3729
- }
3730
- });
3731
- }
3732
- };
3733
- copyNestedValues(currentSchemaData);
3734
- return namespaced;
3919
+ return { ...currentSchemaData, [namespace]: currentSchemaData };
3735
3920
  }, [namespace, schemaData, currentSchemaData]);
3921
+ // Populate the store with namespaced schema data so that namespaced widgets
3922
+ // can read their initial values via getValueByPath on the namespaced paths.
3923
+ useEffect(() => {
3924
+ if (namespace && namespacedSchemaData) {
3925
+ dispatch(setValues(namespacedSchemaData));
3926
+ }
3927
+ }, [namespace, namespacedSchemaData, dispatch]);
3736
3928
  const crViewData = useMemo(() => {
3737
3929
  if (mode !== 'CRView')
3738
3930
  return null;
@@ -3755,6 +3947,9 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
3755
3947
  const sectionId = sectionToRender['section-id'];
3756
3948
  const gridId = `section-panels-${sectionId}`;
3757
3949
  const sectionClassId = `section-${sectionId}`;
3950
+ const readonlyValueRowRootsCss = useMemo(() => scopedClassSelectors(sectionClassId, READONLY_VALUE_ROW_ROOT_CLASSES), [sectionClassId]);
3951
+ const readonlyValueRowFlex1Css = useMemo(() => READONLY_VALUE_ROW_ROOT_CLASSES.map((c) => `.${sectionClassId} .${c} > .flex-1`).join(',\n '), [sectionClassId]);
3952
+ const readonlySingleLineValueTextCss = useMemo(() => READONLY_SINGLE_LINE_VALUE_ROW_CLASSES.map((c) => `.${sectionClassId} .${c} > .flex-1 > .text-gray-900`).join(',\n '), [sectionClassId]);
3758
3953
  // IntakeForm mode: accordion expand/collapse state (supports toggle)
3759
3954
  const [standaloneExpanded, setStandaloneExpanded] = useState(true); // For sectionIndex undefined (standalone use)
3760
3955
  const isExpandedFromContainer = typeof sectionIndex === 'number' && expandedSectionIndex === sectionIndex;
@@ -4107,6 +4302,8 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4107
4302
  const baselineSnapshotRef = useRef(null);
4108
4303
  // IntakeForm only: increment when baseline is updated after save - forces badge to update (refs don't trigger re-renders)
4109
4304
  const [intakeFormBaselineTrigger, setIntakeFormBaselineTrigger] = useState(0);
4305
+ // IntakeForm only: tracks whether the user has actually saved this section (prevents "Saved" badge on initial load)
4306
+ const [hasBeenSavedByUser, setHasBeenSavedByUser] = useState(false);
4110
4307
  // IntakeForm: treat as edit mode for dirty tracking when isDraft. RegistryView: use isEditMode.
4111
4308
  const effectiveEditModeForDirty = mode === 'IntakeForm' ? (isDraft !== false) : isEditMode;
4112
4309
  // Compute isDirty: compare current store state to baseline (only when in edit mode)
@@ -4149,18 +4346,68 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4149
4346
  const intakeFormSectionStatus = useMemo(() => {
4150
4347
  if (mode !== 'IntakeForm' || isDraft === false)
4151
4348
  return null;
4152
- const hasValue = (v) => v !== undefined && v !== null && (typeof v !== 'string' || v.trim().length > 0);
4153
- const currentSnapshot = buildSectionSnapshot(storeValues, namespace);
4154
- const record = currentSnapshot.records?.[0];
4155
- const hasData = record &&
4156
- typeof record === 'object' &&
4157
- Object.values(record).some((v) => hasValue(v));
4158
4349
  if (isDirty)
4159
4350
  return 'modified';
4160
- if (hasData)
4351
+ if (hasBeenSavedByUser)
4161
4352
  return 'saved';
4162
4353
  return null;
4163
- }, [mode, isDirty, storeValues, namespace, buildSectionSnapshot]);
4354
+ }, [mode, isDirty, hasBeenSavedByUser]);
4355
+ // Revert store values to the original schemaData for this section's widgets.
4356
+ // Used by both handleSave (RegistryView raises a CR, so values should not persist)
4357
+ // and handleCancel.
4358
+ const revertToOriginalValues = useCallback(() => {
4359
+ const sectionWidgets = collectWidgets(originalSection.panels);
4360
+ const oldSchemaData = schemaData || contextSchemaData;
4361
+ const currentStoreValues = store.getState().widget.values;
4362
+ let newStoreValues = currentStoreValues;
4363
+ sectionWidgets.forEach(widget => {
4364
+ const originalWidgetId = widget['widget-id'];
4365
+ const namespacedWidgetId = namespace ? `${namespace}__${originalWidgetId}` : originalWidgetId;
4366
+ const widgetId = namespacedWidgetId;
4367
+ const originalDataPath = widget['widget-data-path'];
4368
+ const storeDataPath = namespace && originalDataPath
4369
+ ? (typeof originalDataPath === 'string'
4370
+ ? `${namespace}.${originalDataPath}`
4371
+ : Object.fromEntries(Object.entries(originalDataPath).map(([key, path]) => [key, `${namespace}.${path}`])))
4372
+ : originalDataPath;
4373
+ if (widgetId && originalDataPath) {
4374
+ let oldValue;
4375
+ if (typeof originalDataPath === 'object') {
4376
+ oldValue = {};
4377
+ Object.entries(originalDataPath).forEach(([key, path]) => {
4378
+ if (typeof path === 'string') {
4379
+ oldValue[key] = getValueByPath(oldSchemaData, path);
4380
+ }
4381
+ });
4382
+ }
4383
+ else if (typeof originalDataPath === 'string') {
4384
+ oldValue = getValueByPath(oldSchemaData, originalDataPath);
4385
+ }
4386
+ if (oldValue !== undefined) {
4387
+ newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
4388
+ // Also revert the widgetId-based entry — useBaseWidget.handleChange
4389
+ // sets values[widgetId] during editing, and useBaseWidget.currentValue
4390
+ // reads values[widgetId] first before falling through to the dataPath.
4391
+ newStoreValues = { ...newStoreValues, [widgetId]: oldValue };
4392
+ }
4393
+ }
4394
+ });
4395
+ if (hasSupportingDocuments) {
4396
+ const originalSupportingDocuments = originalSection['section-supporting-documents'] || [];
4397
+ originalSupportingDocuments.forEach((doc, index) => {
4398
+ const widgetId = `supporting-doc-${sectionId}-${index}`;
4399
+ const originalDataPath = doc['document-data-path'];
4400
+ const storeDataPath = namespace && originalDataPath
4401
+ ? `${namespace}.${originalDataPath}`
4402
+ : originalDataPath;
4403
+ const oldValue = getValueByPath(oldSchemaData, originalDataPath);
4404
+ newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
4405
+ });
4406
+ }
4407
+ if (newStoreValues !== currentStoreValues) {
4408
+ dispatch(setValues(newStoreValues));
4409
+ }
4410
+ }, [originalSection, schemaData, contextSchemaData, store, namespace, hasSupportingDocuments, sectionId, dispatch]);
4164
4411
  // Handle save button click
4165
4412
  const handleSave = async () => {
4166
4413
  if (!store || !onSectionSave) {
@@ -4197,12 +4444,24 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4197
4444
  });
4198
4445
  }
4199
4446
  if (JSON.stringify(oldSchemaData) !== JSON.stringify(newSchemaData)) {
4447
+ let profileImage = null;
4448
+ for (const record of newSchemaData) {
4449
+ if (typeof record === 'object' && record !== null) {
4450
+ for (const [key, value] of Object.entries(record)) {
4451
+ if (value instanceof File) {
4452
+ profileImage = value;
4453
+ record[key] = '';
4454
+ }
4455
+ }
4456
+ }
4457
+ }
4200
4458
  try {
4201
4459
  const sectionchanges = {
4202
4460
  section_id: dbSectionId ?? originalSection['section-id'],
4203
4461
  section_register_id: sectionRegisterId,
4204
4462
  records: [...newSchemaData],
4205
- files: [...sectionFiles]
4463
+ files: [...sectionFiles],
4464
+ ...(profileImage ? { image: profileImage } : {}),
4206
4465
  };
4207
4466
  await onSectionSave(sectionchanges);
4208
4467
  }
@@ -4210,6 +4469,12 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4210
4469
  console.error('Section Changes Save failed', error);
4211
4470
  }
4212
4471
  }
4472
+ // In RegistryView, save raises a CR — the actual data update follows a
4473
+ // separate approval workflow, so revert the displayed values to the
4474
+ // originals so the view doesn't show unapproved edits.
4475
+ if (mode === 'RegistryView') {
4476
+ revertToOriginalValues();
4477
+ }
4213
4478
  setIsEditMode(false);
4214
4479
  onEditModeChange?.(originalSectionId, false);
4215
4480
  };
@@ -4239,12 +4504,24 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4239
4504
  });
4240
4505
  }
4241
4506
  if (JSON.stringify(oldSchemaData) !== JSON.stringify(newSchemaData)) {
4507
+ let profileImage = null;
4508
+ for (const record of newSchemaData) {
4509
+ if (typeof record === 'object' && record !== null) {
4510
+ for (const [key, value] of Object.entries(record)) {
4511
+ if (value instanceof File) {
4512
+ profileImage = value;
4513
+ record[key] = '';
4514
+ }
4515
+ }
4516
+ }
4517
+ }
4242
4518
  try {
4243
4519
  await onSectionSave({
4244
4520
  section_id: dbSectionId ?? originalSection['section-id'],
4245
4521
  section_register_id: sectionRegisterId,
4246
4522
  records: [...newSchemaData],
4247
4523
  files: [...sectionFiles],
4524
+ ...(profileImage ? { image: profileImage } : {}),
4248
4525
  });
4249
4526
  }
4250
4527
  catch (error) {
@@ -4255,6 +4532,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4255
4532
  if (mode === 'IntakeForm') {
4256
4533
  baselineSnapshotRef.current = buildSectionSnapshot(currentSchemaData, namespace);
4257
4534
  setIntakeFormBaselineTrigger((prev) => prev + 1);
4535
+ setHasBeenSavedByUser(true);
4258
4536
  }
4259
4537
  onSectionDirtyChange?.(sectionId, false);
4260
4538
  }
@@ -4263,64 +4541,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4263
4541
  }, [store, onSectionSave, onSectionSaveSuccess, sectionIndex, originalSection, schemaData, contextSchemaData, namespace, hasSupportingDocuments, dbSectionId, sectionRegisterId, dispatch, buildSectionSnapshot, sectionId, onSectionDirtyChange, mode, isDraft]);
4264
4542
  // Handle cancel button click
4265
4543
  const handleCancel = () => {
4266
- // Revert values in store to original schema data
4267
- // Use original section (without namespace) for collecting widgets
4268
- const sectionWidgets = collectWidgets(originalSection.panels);
4269
- const oldSchemaData = schemaData || contextSchemaData;
4270
- const currentStoreValues = store.getState().widget.values;
4271
- let newStoreValues = currentStoreValues;
4272
- sectionWidgets.forEach(widget => {
4273
- const originalWidgetId = widget['widget-id'];
4274
- // If namespace was used, we need to use namespaced widget ID and data path
4275
- const namespacedWidgetId = namespace ? `${namespace}__${originalWidgetId}` : originalWidgetId;
4276
- const widgetId = namespacedWidgetId;
4277
- const originalDataPath = widget['widget-data-path'];
4278
- // If namespace was used, data path in store is namespaced, but we read from original schema using original path
4279
- const storeDataPath = namespace && originalDataPath
4280
- ? (typeof originalDataPath === 'string'
4281
- ? `${namespace}.${originalDataPath}`
4282
- : Object.fromEntries(Object.entries(originalDataPath).map(([key, path]) => [key, `${namespace}.${path}`])))
4283
- : originalDataPath;
4284
- if (widgetId && originalDataPath) {
4285
- // Handle multi-path (object) or single path (string)
4286
- // Read from original schema data using original paths
4287
- let oldValue;
4288
- if (typeof originalDataPath === 'object') {
4289
- // Multi-path: get values for each path
4290
- oldValue = {};
4291
- Object.entries(originalDataPath).forEach(([key, path]) => {
4292
- if (typeof path === 'string') {
4293
- oldValue[key] = getValueByPath(oldSchemaData, path);
4294
- }
4295
- });
4296
- }
4297
- else if (typeof originalDataPath === 'string') {
4298
- oldValue = getValueByPath(oldSchemaData, originalDataPath);
4299
- }
4300
- // Set in store using namespaced data path (if namespace was used)
4301
- if (oldValue !== undefined) {
4302
- newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
4303
- }
4304
- }
4305
- });
4306
- // Also revert supporting documents if any
4307
- if (hasSupportingDocuments) {
4308
- // Use original section's supporting documents to get original data paths
4309
- const originalSupportingDocuments = originalSection['section-supporting-documents'] || [];
4310
- originalSupportingDocuments.forEach((doc, index) => {
4311
- const widgetId = `supporting-doc-${sectionId}-${index}`;
4312
- const originalDataPath = doc['document-data-path'];
4313
- // If namespace was used, data path in store is namespaced
4314
- const storeDataPath = namespace && originalDataPath
4315
- ? `${namespace}.${originalDataPath}`
4316
- : originalDataPath;
4317
- const oldValue = getValueByPath(oldSchemaData, originalDataPath);
4318
- newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
4319
- });
4320
- }
4321
- if (newStoreValues !== currentStoreValues) {
4322
- dispatch(setValues(newStoreValues));
4323
- }
4544
+ revertToOriginalValues();
4324
4545
  setIsEditMode(false);
4325
4546
  onEditModeChange?.(originalSectionId, false);
4326
4547
  };
@@ -4371,20 +4592,27 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4371
4592
  white-space: nowrap !important;
4372
4593
  }
4373
4594
  /* Readonly: prevent flex row from overflowing panel */
4374
- .${sectionClassId} .TextDisplayWidget {
4595
+ ${readonlyValueRowRootsCss} {
4375
4596
  min-width: 0 !important;
4376
4597
  overflow: hidden !important;
4377
4598
  }
4378
- .${sectionClassId} .TextDisplayWidget > .flex-1 {
4599
+ ${readonlyValueRowFlex1Css} {
4379
4600
  min-width: 0 !important;
4380
4601
  overflow: hidden !important;
4381
4602
  }
4382
- /* Readonly value text truncation */
4383
- .${sectionClassId} .TextDisplayWidget > .flex-1 > .text-gray-900 {
4603
+ /* Readonly value: single-line ellipsis; full value via title on the value node */
4604
+ ${readonlySingleLineValueTextCss} {
4384
4605
  overflow: hidden;
4385
4606
  text-overflow: ellipsis;
4386
4607
  white-space: nowrap;
4387
4608
  }
4609
+ /* Readonly textarea: break unbroken long tokens; title on pre keeps full text on hover */
4610
+ .${sectionClassId} .TextAreaDisplayWidget > .flex-1 > pre {
4611
+ min-width: 0;
4612
+ max-width: 100%;
4613
+ overflow-wrap: anywhere;
4614
+ word-break: break-word;
4615
+ }
4388
4616
 
4389
4617
  /* Only apply fixed height when in edit mode */
4390
4618
  .${sectionClassId}[data-edit-mode="true"] {
@@ -4508,6 +4736,10 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4508
4736
  gap: 0.5rem;
4509
4737
  }
4510
4738
 
4739
+
4740
+
4741
+
4742
+
4511
4743
  /* IntakeForm accordion */
4512
4744
  .${sectionClassId}.intake-form-accordion-item {
4513
4745
  border-color: var(--owt-color-border-light, #E4E4E4);
@@ -4712,7 +4944,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4712
4944
  }, children: crViewData.approvedDate }))] })] })] })), mode === 'RegistryView' && !hideEditButton && (jsxRuntimeExports.jsx("hr", { className: "w-full", style: { height: '1px', marginTop: !isEditMode ? '10px' : 0, marginBottom: '14px', border: 'none', backgroundColor: 'var(--owt-color-border, #C4C4C4)' } })), mode === 'RegistryView' && !isEditMode && !hideEditButton && (jsxRuntimeExports.jsx("div", { className: "flex justify-center items-center", style: { marginBottom: '20px' }, children: jsxRuntimeExports.jsxs("button", { onClick: handleEdit, className: "font-normal inline-flex items-center gap-2 bg-transparent border-0 p-0 cursor-pointer hover:opacity-80", style: {
4713
4945
  fontFamily: 'Roboto, sans-serif',
4714
4946
  fontSize: '16px',
4715
- color: 'var(--owt-color-text-muted, #727474)'
4947
+ color: 'var(--owt-color-text-muted, #727474)',
4716
4948
  }, children: [translate('common.editDetails') || 'Edit Details', jsxRuntimeExports.jsx("img", { src: img$8, alt: "right-arrow", className: "w-3.5 h-3.5 brightness-0 opacity-50" })] }) }))] })] })) })] }));
4717
4949
  };
4718
4950
 
@@ -4886,6 +5118,9 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
4886
5118
  }, []);
4887
5119
  const safeSections = sections ?? [];
4888
5120
  const prevSectionsLengthRef = useRef(safeSections.length);
5121
+ // Stable ref for namespace so formHandle useMemo doesn't depend on the (possibly inline) function identity
5122
+ const namespaceRef = useRef(namespace);
5123
+ namespaceRef.current = namespace;
4889
5124
  // Track dirty (unsaved changes) per section for form handle validation
4890
5125
  const sectionDirtyMapRef = useRef({});
4891
5126
  const handleSectionDirtyChange = useCallback((sectionId, isDirty) => {
@@ -4931,11 +5166,14 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
4931
5166
  // Form handle for onFormReady - allows host to validate and get all section data from its own Submit button
4932
5167
  const formHandle = useMemo(() => {
4933
5168
  const getValues = () => store.getState().widget?.values || {};
4934
- const getNamespace = (section, index) => namespace
4935
- ? typeof namespace === 'string'
4936
- ? namespace
4937
- : namespace(section['section-id'], index)
4938
- : undefined;
5169
+ const getNamespace = (section, index) => {
5170
+ const ns = namespaceRef.current;
5171
+ return ns
5172
+ ? typeof ns === 'string'
5173
+ ? ns
5174
+ : ns(section['section-id'], index)
5175
+ : undefined;
5176
+ };
4939
5177
  const checkNoUnsavedChanges = () => {
4940
5178
  const hasDirty = Object.values(sectionDirtyMapRef.current).some(Boolean);
4941
5179
  if (hasDirty) {
@@ -4985,7 +5223,7 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
4985
5223
  return results;
4986
5224
  },
4987
5225
  };
4988
- }, [store, dispatch, safeSections, namespace]);
5226
+ }, [store, dispatch, safeSections]);
4989
5227
  // Call onFormReady when form is ready (sections loaded)
4990
5228
  useEffect(() => {
4991
5229
  if (onFormReady && safeSections.length > 0) {
@@ -8269,10 +8507,10 @@ const DisplayWidget = ({ config }) => {
8269
8507
  const label = translateConfig(widgetConfig['widget-label']);
8270
8508
  // If no label, render as paragraph text
8271
8509
  if (!label || label.trim() === '') {
8272
- return (jsxRuntimeExports.jsx("div", { className: "mb-3 text-base text-gray-700", title: String(displayValue ?? ''), children: displayValue }));
8510
+ return (jsxRuntimeExports.jsx("div", { className: "DisplayFieldWidget mb-3 min-w-0 w-full overflow-hidden text-ellipsis whitespace-nowrap text-base text-gray-700", title: String(displayValue ?? ''), children: displayValue }));
8273
8511
  }
8274
- // With label, render as key-value pair
8275
- return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] flex flex-col sm:flex-row sm:items-start", children: [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 text-base text-gray-900 font-medium", title: String(displayValue ?? ''), children: displayValue })] }));
8512
+ // With label, render as key-value pair (structure matches other readonly widgets for SectionRenderer ellipsis)
8513
+ return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] DisplayFieldWidget flex flex-col sm:flex-row sm:items-start", children: [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 min-w-0", children: jsxRuntimeExports.jsx("div", { className: "text-base text-gray-900 font-medium", title: String(displayValue ?? ''), children: displayValue }) })] }));
8276
8514
  };
8277
8515
 
8278
8516
  const TableCellSelect = ({ config, value, onValueChange }) => {
@@ -9310,15 +9548,43 @@ const HeaderSectionWidget = ({ config }) => {
9310
9548
  result = searchIn(schemaData);
9311
9549
  return result;
9312
9550
  }, [paths, values, schemaData]);
9313
- const imageUrl = findValue('image') || null;
9551
+ const imageVal = findValue('image');
9552
+ const imageUrlVal = findValue('imageUrl');
9553
+ const [previewUrl, setPreviewUrl] = useState(null);
9554
+ useEffect(() => {
9555
+ if (imageVal instanceof File) {
9556
+ const url = URL.createObjectURL(imageVal);
9557
+ setPreviewUrl(url);
9558
+ return () => URL.revokeObjectURL(url);
9559
+ }
9560
+ setPreviewUrl(null);
9561
+ }, [imageVal]);
9562
+ const displayImageUrl = previewUrl || (typeof imageUrlVal === 'string' && imageUrlVal ? imageUrlVal : null);
9314
9563
  const displayName = findValue('name') || '';
9315
9564
  const functionalId = findValue('functionalId') || '';
9316
9565
  const statusValue = findValue('status') || '';
9317
9566
  const statusReason = findValue('statusReason') || '';
9567
+ const completionScoreRaw = findValue('completionScore');
9568
+ const idealScoreRaw = findValue('idealScore');
9318
9569
  const createdBy = findValue('createdBy') || '';
9319
9570
  const createdAt = findValue('createdAt') || '';
9320
9571
  const lastApprovedBy = findValue('lastApprovedBy') || '';
9321
9572
  const lastApprovedAt = findValue('lastApprovedAt') || '';
9573
+ const score = useMemo(() => {
9574
+ const toNum = (v) => {
9575
+ if (v === null || v === undefined || String(v).trim() === '')
9576
+ return null;
9577
+ const n = typeof v === 'number' ? v : Number(String(v));
9578
+ return Number.isFinite(n) ? n : null;
9579
+ };
9580
+ const completion = toNum(completionScoreRaw);
9581
+ const ideal = toNum(idealScoreRaw);
9582
+ if (completion === null || ideal === null || ideal <= 0)
9583
+ return null;
9584
+ const ratio = completion / ideal;
9585
+ const percent = Math.max(0, Math.min(100, Math.round(ratio * 100)));
9586
+ return { completion, ideal, percent };
9587
+ }, [completionScoreRaw, idealScoreRaw]);
9322
9588
  // ── Format options ────────────────────────────────────────────
9323
9589
  const format = (widgetConfig['widget-data-format'] || {});
9324
9590
  const imageSize = format.imageSize || 120;
@@ -9343,18 +9609,20 @@ const HeaderSectionWidget = ({ config }) => {
9343
9609
  return opt ? opt.label : String(statusValue);
9344
9610
  }, [statusValue, statusOptions]);
9345
9611
  const statusColor = statusColors[String(statusValue).toLowerCase()] || 'var(--owt-color-text-muted, #6B7280)';
9612
+ // ── Image edit helpers ───────────────────────────────────────
9613
+ const fileInputRef = useRef(null);
9614
+ const handleImageUpload = useCallback((e) => {
9615
+ const file = e.target.files?.[0];
9616
+ if (!file)
9617
+ return;
9618
+ updateFieldValue('image', file);
9619
+ e.target.value = '';
9620
+ }, [updateFieldValue]);
9621
+ const handleImageDelete = useCallback(() => {
9622
+ updateFieldValue('image', '');
9623
+ }, [updateFieldValue]);
9346
9624
  // ── Scoped class for CSS isolation ────────────────────────────
9347
9625
  const cls = `header-section-widget-${widgetConfig['widget-id']}`;
9348
- // ── Indicator dot component ───────────────────────────────────
9349
- const Dot = ({ color }) => (jsxRuntimeExports.jsx("span", { style: {
9350
- display: 'inline-block',
9351
- width: 8,
9352
- height: 8,
9353
- borderRadius: '50%',
9354
- backgroundColor: color,
9355
- flexShrink: 0,
9356
- marginTop: 6,
9357
- } }));
9358
9626
  // ── RENDER ────────────────────────────────────────────────────
9359
9627
  return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
9360
9628
  .${cls} {
@@ -9383,6 +9651,58 @@ const HeaderSectionWidget = ({ config }) => {
9383
9651
  min-width: 220px;
9384
9652
  }
9385
9653
 
9654
+ .${cls} .hdr-right-top {
9655
+ display: flex;
9656
+ align-items: flex-start;
9657
+ justify-content: space-between;
9658
+ gap: 14px;
9659
+ width: 100%;
9660
+ }
9661
+
9662
+ .${cls} .hdr-meta-col {
9663
+ display: flex;
9664
+ flex-direction: column;
9665
+ gap: 0.5rem;
9666
+ flex: 1 1 auto;
9667
+ min-width: 0;
9668
+ }
9669
+
9670
+ .${cls} .hdr-score-ring {
9671
+ --ring-size: 54px;
9672
+ --ring-thickness: 7px;
9673
+ --ring-color: var(--owt-color-primary-dark, #F07B1A);
9674
+ --ring-track: rgba(2, 6, 23, 0.10);
9675
+ width: var(--ring-size);
9676
+ height: var(--ring-size);
9677
+ border-radius: 50%;
9678
+ background: conic-gradient(
9679
+ var(--ring-color) calc(var(--pct) * 1%),
9680
+ var(--ring-track) 0
9681
+ );
9682
+ position: relative;
9683
+ flex: 0 0 auto;
9684
+ }
9685
+
9686
+ .${cls} .hdr-score-ring::before {
9687
+ content: "";
9688
+ position: absolute;
9689
+ inset: var(--ring-thickness);
9690
+ border-radius: 50%;
9691
+ background: var(--owt-color-bg, #FFFFFF);
9692
+ }
9693
+
9694
+ .${cls} .hdr-score-value {
9695
+ position: absolute;
9696
+ inset: 0;
9697
+ display: flex;
9698
+ align-items: center;
9699
+ justify-content: center;
9700
+ font-size: 20px;
9701
+ font-weight: 700;
9702
+ color: var(--owt-color-text, #011627);
9703
+ font-family: Roboto, sans-serif;
9704
+ }
9705
+
9386
9706
  .${cls} .hdr-avatar {
9387
9707
  width: ${imageSize}px;
9388
9708
  height: ${imageSize}px;
@@ -9413,6 +9733,56 @@ const HeaderSectionWidget = ({ config }) => {
9413
9733
  border-radius: 8px;
9414
9734
  }
9415
9735
 
9736
+ .${cls} .hdr-avatar-wrapper {
9737
+ position: relative;
9738
+ width: ${imageSize}px;
9739
+ height: ${imageSize}px;
9740
+ flex-shrink: 0;
9741
+ }
9742
+
9743
+ .${cls} .hdr-avatar-overlay {
9744
+ position: absolute;
9745
+ inset: 0;
9746
+ border-radius: 8px;
9747
+ background: rgba(0, 0, 0, 0.55);
9748
+ display: flex;
9749
+ flex-direction: column;
9750
+ align-items: center;
9751
+ justify-content: center;
9752
+ gap: 6px;
9753
+ opacity: 0;
9754
+ transition: opacity 0.2s;
9755
+ }
9756
+
9757
+ .${cls} .hdr-avatar-wrapper:hover .hdr-avatar-overlay {
9758
+ opacity: 1;
9759
+ }
9760
+
9761
+ .${cls} .hdr-avatar-action {
9762
+ display: flex;
9763
+ align-items: center;
9764
+ gap: 5px;
9765
+ padding: 5px 14px;
9766
+ border: none;
9767
+ border-radius: 4px;
9768
+ background: rgba(255, 255, 255, 0.92);
9769
+ color: #374151;
9770
+ font-size: 0.7rem;
9771
+ font-weight: 500;
9772
+ cursor: pointer;
9773
+ font-family: Roboto, sans-serif;
9774
+ transition: background 0.15s;
9775
+ white-space: nowrap;
9776
+ }
9777
+
9778
+ .${cls} .hdr-avatar-action:hover {
9779
+ background: #fff;
9780
+ }
9781
+
9782
+ .${cls} .hdr-avatar-action--delete {
9783
+ color: #DC2626;
9784
+ }
9785
+
9416
9786
  .${cls} .hdr-info {
9417
9787
  display: flex;
9418
9788
  flex-direction: column;
@@ -9517,17 +9887,24 @@ const HeaderSectionWidget = ({ config }) => {
9517
9887
  min-width: 0;
9518
9888
  }
9519
9889
  }
9520
- ` }), jsxRuntimeExports.jsxs("div", { className: cls, children: [jsxRuntimeExports.jsxs("div", { className: "hdr-left", children: [jsxRuntimeExports.jsxs("div", { children: [imageUrl ? (jsxRuntimeExports.jsx("img", { src: imageUrl, alt: displayName || 'Profile', className: "hdr-avatar", onError: (e) => {
9890
+ ` }), jsxRuntimeExports.jsxs("div", { className: cls, children: [jsxRuntimeExports.jsxs("div", { className: "hdr-left", children: [jsxRuntimeExports.jsxs("div", { className: "hdr-avatar-wrapper", children: [displayImageUrl ? (jsxRuntimeExports.jsx("img", { src: displayImageUrl, alt: displayName || 'Profile', className: "hdr-avatar", onError: (e) => {
9521
9891
  e.target.style.display = 'none';
9522
9892
  const placeholder = e.target
9523
9893
  .parentElement?.querySelector('.hdr-avatar-placeholder');
9524
9894
  if (placeholder)
9525
9895
  placeholder.style.display = 'flex';
9526
- } })) : null, jsxRuntimeExports.jsx("div", { className: "hdr-avatar-placeholder", style: { display: imageUrl ? 'none' : 'flex' }, children: jsxRuntimeExports.jsx("img", { src: img$f, alt: "Profile Placeholder" }) })] }), 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.jsx(Dot, { color: "#9CA3AF" }), 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(Dot, { color: isReadonly ? statusColor : '#F59E0B' }), 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.jsx(Dot, { color: isReadonly ? '#9CA3AF' : '#F59E0B' }), jsxRuntimeExports.jsxs("span", { className: "hdr-field-label", children: [getLabel('statusReason'), " :"] }), isReadonly ? (jsxRuntimeExports.jsx("span", { className: "hdr-field-value", children: statusReason || '-' })) : (jsxRuntimeExports.jsx("input", { type: "text", className: "hdr-input", value: statusReason, placeholder: getLabel('enterReason'), onChange: (e) => updateFieldValue('statusReason', e.target.value) }))] })] })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-right", 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 || '-' })] })] })] })] }));
9896
+ } })) : 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.jsx("input", { type: "text", className: "hdr-input", value: statusReason, placeholder: getLabel('enterReason'), onChange: (e) => updateFieldValue('statusReason', e.target.value) }))] })] })] }), 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.completion} of ${score.ideal} (${score.percent}%)`, title: `${score.completion} / ${score.ideal} (${score.percent}%)`, children: jsxRuntimeExports.jsx("div", { className: "hdr-score-value", children: String(score.completion) }) })) : null] }) })] })] }));
9527
9897
  };
9528
9898
 
9529
- function tryFormatDateTime(value) {
9530
- if (typeof value !== 'string' || !value)
9899
+ function getValueByPathOrKey(obj, path) {
9900
+ if (!obj || !path)
9901
+ return undefined;
9902
+ if (Object.prototype.hasOwnProperty.call(obj, path))
9903
+ return obj[path];
9904
+ return getValueByPath(obj, path);
9905
+ }
9906
+ function tryFormatDateTime$1(value) {
9907
+ if (typeof value !== 'string' || !value)
9531
9908
  return value ? String(value) : '-';
9532
9909
  const d = new Date(value);
9533
9910
  if (Number.isNaN(d.getTime()))
@@ -9545,233 +9922,677 @@ function tryFormatDateTime(value) {
9545
9922
  return value;
9546
9923
  }
9547
9924
  }
9548
- function pickLatestScore(scores) {
9549
- if (!scores || scores.length === 0)
9550
- return null;
9925
+ function sortScores(scores) {
9551
9926
  const withTime = scores
9552
- .map((s) => {
9927
+ .map((s, idx) => {
9553
9928
  const t = typeof s?.computed_at === 'string' ? new Date(s.computed_at).getTime() : NaN;
9554
- return { s, t };
9929
+ return { s, t, idx };
9555
9930
  })
9556
- .filter((x) => !Number.isNaN(x.t));
9557
- if (withTime.length === 0)
9558
- return scores[0] || null;
9559
- withTime.sort((a, b) => b.t - a.t);
9560
- return withTime[0]?.s || null;
9931
+ .sort((a, b) => {
9932
+ const aHas = !Number.isNaN(a.t);
9933
+ const bHas = !Number.isNaN(b.t);
9934
+ if (aHas && bHas)
9935
+ return b.t - a.t;
9936
+ if (aHas)
9937
+ return -1;
9938
+ if (bHas)
9939
+ return 1;
9940
+ return a.idx - b.idx;
9941
+ });
9942
+ return withTime.map((x) => x.s);
9561
9943
  }
9562
9944
  /**
9563
- * Scores Display Widget - full-width, view-only widget
9945
+ * Scores Display Widget - full-width, view-only widget (list)
9564
9946
  *
9565
9947
  * Expected config (reference):
9566
9948
  * {
9567
9949
  * "widget": "scores-display",
9568
9950
  * "widget-type": "group",
9569
9951
  * "widget-id": "record-scores",
9570
- * "widget-data-source": {
9571
- * "type": "api",
9572
- * "service": "staff-portal-api",
9573
- * "endpoint": "get_scores",
9574
- * "method": "POST",
9575
- * "params": { "internal_record_id_path": "internal_record_id" }
9576
- * }
9952
+ * "widget-data-path": "scores"
9577
9953
  * }
9578
- *
9579
- * The host's `dataSourceRequestHandler` is invoked with:
9580
- * - service: config.widget-data-source.service
9581
- * - endpoint: config.widget-data-source.endpoint
9582
- * - method: config.widget-data-source.method (default POST)
9583
- * - params: { internal_record_id: <resolved from internal_record_id_path> }
9584
9954
  */
9585
- const ScoresDisplayWidget = ({ config, dataSourceRequestHandler: propHandler, schemaData: propSchemaData, }) => {
9586
- const { dataSourceRequestHandler: ctxHandler, schemaData: ctxSchemaData } = useWidgetContext();
9587
- const handler = propHandler || ctxHandler;
9588
- const schemaData = propSchemaData || ctxSchemaData || {};
9955
+ const ScoresDisplayWidget = ({ config, schemaData: propSchemaData, }) => {
9956
+ const { schemaData: ctxSchemaData } = useWidgetContext();
9957
+ const schemaData = (propSchemaData || ctxSchemaData || {});
9589
9958
  const values = useSelector((state) => state.widget.values);
9590
- const api = config['widget-data-source'];
9591
- const isApi = api?.type === 'api';
9592
- const apiDs = isApi ? api : null;
9593
- const internalIdPath = useMemo(() => {
9594
- if (!apiDs)
9595
- return undefined;
9596
- const p = apiDs.params || {};
9597
- const fromParams = p.internal_record_id_path || p.internalRecordIdPath;
9598
- const fromConfig = typeof config.internal_record_id_path === 'string'
9599
- ? config.internal_record_id_path
9600
- : undefined;
9601
- return fromParams || fromConfig;
9602
- }, [apiDs, config]);
9603
- const internalRecordId = useMemo(() => {
9604
- if (!internalIdPath)
9959
+ const dataPath = config['widget-data-path'];
9960
+ const rawScores = useMemo(() => {
9961
+ if (!dataPath || typeof dataPath !== 'string')
9605
9962
  return undefined;
9606
- const fromValues = getValueByPath(values || {}, internalIdPath);
9607
- if (fromValues !== undefined && fromValues !== null && String(fromValues).trim() !== '') {
9608
- return String(fromValues);
9963
+ const valuesObj = values;
9964
+ const tryResolve = (path) => {
9965
+ const fromValues = getValueByPathOrKey(valuesObj, path);
9966
+ if (fromValues !== undefined)
9967
+ return fromValues;
9968
+ return getValueByPathOrKey(schemaData, path);
9969
+ };
9970
+ // 1) Try exact path (works when schema/store is already namespaced)
9971
+ const direct = tryResolve(dataPath);
9972
+ if (direct !== undefined)
9973
+ return direct;
9974
+ // 2) If section/widget config has been namespaced (e.g. "rv-section-0.scores"),
9975
+ // fall back to the original path ("scores") so examples still work even when
9976
+ // schemaData/store are not namespaced.
9977
+ if (dataPath.includes('.')) {
9978
+ const unNamespaced = dataPath.split('.').slice(1).join('.');
9979
+ const fallback = tryResolve(unNamespaced);
9980
+ if (fallback !== undefined)
9981
+ return fallback;
9982
+ }
9983
+ return undefined;
9984
+ }, [dataPath, values, schemaData]);
9985
+ const scores = useMemo(() => {
9986
+ if (!rawScores)
9987
+ return [];
9988
+ if (Array.isArray(rawScores))
9989
+ return rawScores;
9990
+ if (typeof rawScores === 'object') {
9991
+ const maybe = rawScores.scores;
9992
+ if (Array.isArray(maybe))
9993
+ return maybe;
9994
+ }
9995
+ return [];
9996
+ }, [rawScores]);
9997
+ const sortedScores = useMemo(() => sortScores(scores), [scores]);
9998
+ const cls = `scores-display-widget-${config['widget-id']}`;
9999
+ return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
10000
+ .${cls} {
10001
+ width: 100%;
10002
+ font-family: Roboto, sans-serif;
10003
+ padding: 0;
10004
+ display: flex;
10005
+ flex-direction: column;
10006
+ gap: 8px;
10007
+ }
10008
+
10009
+ .${cls} .scores-subtle {
10010
+ font-size: 13px;
10011
+ color: var(--owt-color-text-muted, #727474);
10012
+ font-weight: 400;
10013
+ }
10014
+
10015
+ .${cls} .scores-grid {
10016
+ width: 100%;
10017
+ display: grid;
10018
+ grid-template-columns: repeat(3, minmax(220px, 1fr));
10019
+ gap: 16px;
10020
+ }
10021
+
10022
+ .${cls} .scores-card {
10023
+ border: 1px solid var(--owt-color-border-light, #E4E4E4);
10024
+ border-radius: 10px;
10025
+ background: var(--owt-color-bg, #FFFFFF);
10026
+ padding: 14px 14px;
10027
+ display: flex;
10028
+ flex-direction: column;
10029
+ gap: 10px;
10030
+ min-width: 0;
10031
+ box-shadow: 0 1px 2px rgba(1, 22, 39, 0.06), 0 6px 16px rgba(1, 22, 39, 0.06);
10032
+ }
10033
+
10034
+ .${cls} .scores-type {
10035
+ font-size: 16px;
10036
+ font-weight: 800;
10037
+ color: var(--owt-color-primary-dark, #F07B1A);
10038
+ line-height: 1.2;
10039
+ word-break: break-word;
10040
+ }
10041
+
10042
+ .${cls} .scores-value {
10043
+ font-size: 34px;
10044
+ font-weight: 800;
10045
+ color: var(--owt-color-text, #011627);
10046
+ line-height: 1.05;
10047
+ letter-spacing: -0.25px;
10048
+ }
10049
+
10050
+ .${cls} .scores-separator {
10051
+ height: 1px;
10052
+ width: 100%;
10053
+ background-color: var(--owt-color-border-light, #E4E4E4);
10054
+ border: none;
10055
+ margin: 2px 0;
10056
+ }
10057
+
10058
+ .${cls} .scores-value .scores-muted {
10059
+ font-size: 18px;
10060
+ font-weight: 600;
10061
+ color: var(--owt-color-text-muted, #727474);
10062
+ margin-left: 6px;
10063
+ }
10064
+
10065
+ .${cls} .scores-meta {
10066
+ display: flex;
10067
+ flex-direction: column;
10068
+ gap: 4px;
10069
+ }
10070
+
10071
+ .${cls} .scores-meta-line {
10072
+ font-size: 13px;
10073
+ color: var(--owt-color-text-muted, #727474);
10074
+ font-weight: 500;
9609
10075
  }
9610
- const fromSchema = getValueByPath(schemaData || {}, internalIdPath);
9611
- if (fromSchema !== undefined && fromSchema !== null && String(fromSchema).trim() !== '') {
9612
- return String(fromSchema);
10076
+
10077
+ .${cls} .scores-meta-line strong {
10078
+ color: var(--owt-color-text, #011627);
10079
+ font-weight: 700;
10080
+ }
10081
+
10082
+ @media (max-width: 1024px) {
10083
+ .${cls} .scores-grid {
10084
+ grid-template-columns: repeat(2, minmax(220px, 1fr));
10085
+ }
10086
+ }
10087
+
10088
+ @media (max-width: 640px) {
10089
+ .${cls} .scores-grid {
10090
+ grid-template-columns: 1fr;
10091
+ }
9613
10092
  }
10093
+ ` }), jsxRuntimeExports.jsx("div", { className: cls, children: sortedScores.length === 0 ? (jsxRuntimeExports.jsx("div", { className: "scores-subtle", children: "No scores available." })) : (jsxRuntimeExports.jsx("div", { className: "scores-grid", children: sortedScores.map((s, idx) => {
10094
+ const scoreType = s?.score_type ? String(s.score_type) : '-';
10095
+ const scoreValue = s?.computed_score !== undefined &&
10096
+ s?.computed_score !== null &&
10097
+ String(s.computed_score) !== ''
10098
+ ? String(s.computed_score)
10099
+ : '-';
10100
+ const computedAt = tryFormatDateTime$1(s?.computed_at);
10101
+ const key = `${scoreType}-${String(s?.computed_at || '')}-${idx}`;
10102
+ return (jsxRuntimeExports.jsxs("div", { className: "scores-card", "aria-live": idx === 0 ? 'polite' : undefined, children: [jsxRuntimeExports.jsx("div", { className: "scores-type", children: scoreType }), jsxRuntimeExports.jsx("div", { className: "scores-value", children: scoreValue }), jsxRuntimeExports.jsx("hr", { className: "scores-separator" }), jsxRuntimeExports.jsx("div", { className: "scores-meta", children: jsxRuntimeExports.jsxs("div", { className: "scores-meta-line", children: ["Computed at: ", jsxRuntimeExports.jsx("strong", { children: computedAt })] }) })] }, key));
10103
+ }) })) })] }));
10104
+ };
10105
+
10106
+ function tryFormatDateTime(value) {
10107
+ if (typeof value !== 'string' || !value)
10108
+ return value ? String(value) : '-';
10109
+ const d = new Date(value);
10110
+ if (Number.isNaN(d.getTime()))
10111
+ return value;
10112
+ try {
10113
+ return d.toLocaleString(undefined, {
10114
+ year: 'numeric',
10115
+ month: 'short',
10116
+ day: '2-digit',
10117
+ hour: '2-digit',
10118
+ minute: '2-digit',
10119
+ });
10120
+ }
10121
+ catch {
10122
+ return value;
10123
+ }
10124
+ }
10125
+ function tryFormatDate(value) {
10126
+ if (typeof value !== 'string' || !value)
10127
+ return value ? String(value) : '-';
10128
+ const d = new Date(value);
10129
+ if (Number.isNaN(d.getTime()))
10130
+ return value;
10131
+ try {
10132
+ return d.toLocaleDateString(undefined, {
10133
+ year: 'numeric',
10134
+ month: 'short',
10135
+ day: '2-digit',
10136
+ });
10137
+ }
10138
+ catch {
10139
+ return value;
10140
+ }
10141
+ }
10142
+ function displayText(value) {
10143
+ if (value === null || value === undefined || String(value).trim() === '')
10144
+ return '-';
10145
+ return String(value);
10146
+ }
10147
+ function normalizeStatus(raw) {
10148
+ if (raw === null || raw === undefined || String(raw).trim() === '')
10149
+ return 'unknown';
10150
+ const v = String(raw).trim().toLowerCase();
10151
+ if (v === 'success' || v === 'succeeded' || v === 'ok')
10152
+ return 'success';
10153
+ if (v === 'failure' || v === 'failed' || v === 'error')
10154
+ return 'failure';
10155
+ if (v === 'not done' || v === 'not_done' || v === 'not-done' || v === 'pending')
10156
+ return 'not_done';
10157
+ return 'unknown';
10158
+ }
10159
+ /** Large enough for eSignet / OIDC login; clamped so it always fits the current screen. */
10160
+ function getCenteredPopupFeatures(width, height) {
10161
+ const dualScreenLeft = window.screenLeft ?? window.screenX ?? 0;
10162
+ const dualScreenTop = window.screenTop ?? window.screenY ?? 0;
10163
+ const viewportWidth = window.innerWidth || document.documentElement.clientWidth || (typeof screen !== 'undefined' ? screen.width : width);
10164
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight || (typeof screen !== 'undefined' ? screen.height : height);
10165
+ const maxW = Math.max(320, Math.floor(viewportWidth * 0.92));
10166
+ const maxH = Math.max(400, Math.floor(viewportHeight * 0.92));
10167
+ const w = Math.max(320, Math.min(width, maxW));
10168
+ const h = Math.max(400, Math.min(height, maxH));
10169
+ const left = Math.max(0, Math.floor(viewportWidth / 2 - w / 2 + dualScreenLeft));
10170
+ const top = Math.max(0, Math.floor(viewportHeight / 2 - h / 2 + dualScreenTop));
10171
+ return [
10172
+ 'popup=yes',
10173
+ 'noopener=yes',
10174
+ 'noreferrer=yes',
10175
+ `width=${w}`,
10176
+ `height=${h}`,
10177
+ `left=${left}`,
10178
+ `top=${top}`,
10179
+ 'scrollbars=yes',
10180
+ 'resizable=yes',
10181
+ ].join(',');
10182
+ }
10183
+ function pickAuthorizationUrl(resp, explicitKey) {
10184
+ if (!resp)
10185
+ return null;
10186
+ const tryKey = (k) => {
10187
+ const v = resp?.[k];
10188
+ if (typeof v === 'string' && v)
10189
+ return v;
10190
+ return null;
10191
+ };
10192
+ if (explicitKey) {
10193
+ const v = tryKey(explicitKey);
10194
+ if (v)
10195
+ return v;
10196
+ }
10197
+ return (tryKey('authorization_url') ||
10198
+ tryKey('authorizationUrl') ||
10199
+ tryKey('auth_url') ||
10200
+ tryKey('authUrl') ||
10201
+ tryKey('url') ||
10202
+ null);
10203
+ }
10204
+ function resolveValueFromSources(path, values, schemaData) {
10205
+ if (!path)
9614
10206
  return undefined;
9615
- }, [internalIdPath, values, schemaData]);
9616
- const [loading, setLoading] = useState(false);
9617
- const [error, setError] = useState(null);
9618
- const [response, setResponse] = useState(null);
10207
+ const fromValues = getValueByPath(values, path);
10208
+ if (fromValues !== undefined)
10209
+ return fromValues;
10210
+ return getValueByPath(schemaData, path);
10211
+ }
10212
+ async function fetchProviderAuthorizationUrl(authConfig, dataSourceRequestHandler, _values, _schemaData) {
10213
+ const response = await dataSourceRequestHandler(authConfig.service, authConfig.endpoint, authConfig.method || 'GET', {});
10214
+ const payload = response?.response_body?.response_payload && typeof response.response_body.response_payload === 'object'
10215
+ ? response.response_body.response_payload
10216
+ : response;
10217
+ return pickAuthorizationUrl(payload, authConfig.authorizationUrlKey);
10218
+ }
10219
+ const IdAuthenticationWidget = ({ config, schemaData: propSchemaData }) => {
10220
+ const { dataSourceRequestHandler, schemaData: ctxSchemaData } = useWidgetContext();
10221
+ const values = useSelector((state) => state.widget.values);
10222
+ const schemaData = (propSchemaData || ctxSchemaData || {});
10223
+ const widgetId = config['widget-id'];
10224
+ const dataPath = config['widget-data-path'];
10225
+ const paths = useMemo(() => {
10226
+ if (!dataPath || typeof dataPath !== 'object')
10227
+ return {};
10228
+ return dataPath;
10229
+ }, [dataPath]);
10230
+ const authConfig = config['widget-auth-config'];
10231
+ const foundationalId = resolveValueFromSources(paths.foundationalId, values, schemaData);
10232
+ const lastAuthenticatedOn = resolveValueFromSources(paths.lastAuthenticatedOn, values, schemaData);
10233
+ const lastAuthStatusRaw = resolveValueFromSources(paths.lastAuthenticationStatus, values, schemaData);
10234
+ const expiryDate = resolveValueFromSources(paths.expiryDate, values, schemaData);
10235
+ const psut = resolveValueFromSources(paths.authenticationToken, values, schemaData);
10236
+ const status = useMemo(() => normalizeStatus(lastAuthStatusRaw), [lastAuthStatusRaw]);
10237
+ /** URL from prefetch (or default); used when opening the OIDC / eSignet popup */
10238
+ const [resolvedAuthUrl, setResolvedAuthUrl] = useState(null);
10239
+ const [providerLoading, setProviderLoading] = useState(false);
10240
+ const [authActionLoading, setAuthActionLoading] = useState(false);
10241
+ const [authError, setAuthError] = useState(null);
10242
+ const popupRef = useRef(null);
10243
+ const pollTimerRef = useRef(null);
10244
+ const emitHostEvent = useCallback((detail) => {
10245
+ if (typeof window === 'undefined')
10246
+ return;
10247
+ window.dispatchEvent(new CustomEvent('openg2p:id-authentication', {
10248
+ detail: {
10249
+ widgetId,
10250
+ ...detail,
10251
+ },
10252
+ }));
10253
+ }, [widgetId]);
10254
+ const cleanupPopup = useCallback(() => {
10255
+ if (pollTimerRef.current) {
10256
+ window.clearInterval(pollTimerRef.current);
10257
+ pollTimerRef.current = null;
10258
+ }
10259
+ popupRef.current = null;
10260
+ }, []);
9619
10261
  useEffect(() => {
9620
- let cancelled = false;
9621
- const load = async () => {
9622
- if (!apiDs) {
9623
- setError('Scores widget requires an API data source.');
9624
- setResponse(null);
9625
- return;
9626
- }
9627
- if (!handler) {
9628
- setError(null);
9629
- setResponse(null);
9630
- return;
10262
+ return () => {
10263
+ cleanupPopup();
10264
+ try {
10265
+ popupRef.current?.close?.();
9631
10266
  }
9632
- const service = apiDs.service;
9633
- const endpoint = apiDs.endpoint;
9634
- const method = apiDs.method || 'POST';
9635
- if (!service || !endpoint) {
9636
- setError('Scores widget API data source is missing service/endpoint.');
9637
- setResponse(null);
9638
- return;
10267
+ catch {
10268
+ // ignore
9639
10269
  }
9640
- if (!internalRecordId) {
9641
- setError(null);
9642
- setResponse(null);
9643
- return;
10270
+ };
10271
+ }, [cleanupPopup]);
10272
+ const prefetchKey = useMemo(() => {
10273
+ if (!authConfig)
10274
+ return 'no-config';
10275
+ return JSON.stringify({
10276
+ s: authConfig.service,
10277
+ e: authConfig.endpoint,
10278
+ m: authConfig.method,
10279
+ def: authConfig.defaultAuthorizationUrl,
10280
+ prefetch: authConfig.prefetchOnMount,
10281
+ });
10282
+ }, [authConfig]);
10283
+ // Prefetch provider login URL on mount (and when params / config change)
10284
+ useEffect(() => {
10285
+ if (!authConfig) {
10286
+ setResolvedAuthUrl(null);
10287
+ setProviderLoading(false);
10288
+ return;
10289
+ }
10290
+ if (authConfig.prefetchOnMount === false) {
10291
+ setResolvedAuthUrl(authConfig.defaultAuthorizationUrl || null);
10292
+ setProviderLoading(false);
10293
+ return;
10294
+ }
10295
+ const def = authConfig.defaultAuthorizationUrl;
10296
+ if (def) {
10297
+ setResolvedAuthUrl(def);
10298
+ }
10299
+ const canCallApi = Boolean(dataSourceRequestHandler && authConfig.service && authConfig.endpoint);
10300
+ if (!canCallApi) {
10301
+ setProviderLoading(false);
10302
+ if (!def) {
10303
+ setResolvedAuthUrl(null);
9644
10304
  }
10305
+ return;
10306
+ }
10307
+ let cancelled = false;
10308
+ setProviderLoading(true);
10309
+ (async () => {
9645
10310
  try {
9646
- setLoading(true);
9647
- setError(null);
9648
- const rawParams = apiDs.params || {};
9649
- // Never pass the path helper through to the API.
9650
- const { internal_record_id_path, internalRecordIdPath, ...rest } = rawParams;
9651
- void internal_record_id_path;
9652
- void internalRecordIdPath;
9653
- const params = {
9654
- ...rest,
9655
- internal_record_id: internalRecordId,
9656
- };
9657
- const res = await handler(service, endpoint, method, params, {
9658
- headers: apiDs.headers,
9659
- });
10311
+ const url = await fetchProviderAuthorizationUrl(authConfig, dataSourceRequestHandler, values, schemaData);
9660
10312
  if (cancelled)
9661
10313
  return;
9662
- // Accept either direct payload or OpenG2P wrapper objects.
9663
- const envelope = res && typeof res === 'object' ? res : null;
9664
- const payload = envelope?.response_body?.response_payload ?? envelope?.data ?? res;
9665
- if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
9666
- setResponse(payload);
10314
+ if (url) {
10315
+ setResolvedAuthUrl(url);
9667
10316
  }
9668
- else {
9669
- setResponse({ scores: Array.isArray(payload) ? payload : [] });
10317
+ else if (!def) {
10318
+ setResolvedAuthUrl(null);
9670
10319
  }
9671
10320
  }
9672
- catch (e) {
10321
+ catch {
9673
10322
  if (cancelled)
9674
10323
  return;
9675
- const maybeErr = e;
9676
- const msg = maybeErr && typeof maybeErr === 'object' && typeof maybeErr.message === 'string'
9677
- ? maybeErr.message
9678
- : 'Failed to load scores.';
9679
- setError(msg);
9680
- setResponse(null);
10324
+ if (!def) {
10325
+ setResolvedAuthUrl(null);
10326
+ }
9681
10327
  }
9682
10328
  finally {
9683
10329
  if (!cancelled)
9684
- setLoading(false);
10330
+ setProviderLoading(false);
9685
10331
  }
9686
- };
9687
- load();
10332
+ })();
9688
10333
  return () => {
9689
10334
  cancelled = true;
9690
10335
  };
9691
- }, [apiDs, handler, internalRecordId]);
9692
- const latest = useMemo(() => pickLatestScore(response?.scores), [response]);
9693
- const cls = `scores-display-widget-${config['widget-id']}`;
9694
- const scoreType = latest?.score_type ? String(latest.score_type) : '-';
9695
- const scoreValue = latest?.computed_score !== undefined && latest?.computed_score !== null && String(latest.computed_score) !== ''
9696
- ? String(latest.computed_score)
9697
- : '-';
9698
- const computedAt = tryFormatDateTime(latest?.computed_at);
10336
+ }, [authConfig, dataSourceRequestHandler, prefetchKey, values, schemaData]);
10337
+ const openAuthPopup = useCallback((authUrl) => {
10338
+ const pw = authConfig?.popupWidth ?? 1024;
10339
+ const ph = authConfig?.popupHeight ?? 800;
10340
+ const features = getCenteredPopupFeatures(pw, ph);
10341
+ const popup = window.open(authUrl, `${widgetId}-oidc`, features);
10342
+ if (!popup) {
10343
+ setAuthError('Popup blocked. Please allow popups and try again.');
10344
+ return;
10345
+ }
10346
+ popupRef.current = popup;
10347
+ popup.focus?.();
10348
+ setAuthError(null);
10349
+ emitHostEvent({ type: 'popup_opened' });
10350
+ if (pollTimerRef.current) {
10351
+ window.clearInterval(pollTimerRef.current);
10352
+ pollTimerRef.current = null;
10353
+ }
10354
+ pollTimerRef.current = window.setInterval(() => {
10355
+ try {
10356
+ const closed = !popupRef.current || popupRef.current.closed;
10357
+ if (closed) {
10358
+ cleanupPopup();
10359
+ emitHostEvent({ type: 'popup_closed' });
10360
+ }
10361
+ }
10362
+ catch {
10363
+ // ignore
10364
+ }
10365
+ }, 500);
10366
+ }, [authConfig, cleanupPopup, emitHostEvent, widgetId]);
10367
+ const onAuthenticate = useCallback(async () => {
10368
+ setAuthError(null);
10369
+ if (!authConfig) {
10370
+ setAuthError('Missing widget-auth-config.');
10371
+ return;
10372
+ }
10373
+ const def = authConfig.defaultAuthorizationUrl;
10374
+ const canCallApi = Boolean(dataSourceRequestHandler && authConfig.service && authConfig.endpoint);
10375
+ let url = resolvedAuthUrl;
10376
+ if (!url && canCallApi) {
10377
+ setAuthActionLoading(true);
10378
+ try {
10379
+ const fetched = await fetchProviderAuthorizationUrl(authConfig, dataSourceRequestHandler, values, schemaData);
10380
+ url = fetched || def || null;
10381
+ if (fetched) {
10382
+ setResolvedAuthUrl(fetched);
10383
+ }
10384
+ }
10385
+ catch (e) {
10386
+ url = def || null;
10387
+ if (!url) {
10388
+ setAuthError(e?.message || 'Could not load provider URL.');
10389
+ return;
10390
+ }
10391
+ }
10392
+ finally {
10393
+ setAuthActionLoading(false);
10394
+ }
10395
+ }
10396
+ else if (!url) {
10397
+ url = def || null;
10398
+ }
10399
+ if (!url) {
10400
+ setAuthError('No authorization URL. Set widget-auth-config (service + endpoint, or defaultAuthorizationUrl) and dataSourceRequestHandler on WidgetProvider if using the API.');
10401
+ return;
10402
+ }
10403
+ openAuthPopup(url);
10404
+ }, [authConfig, dataSourceRequestHandler, openAuthPopup, resolvedAuthUrl, values, schemaData]);
10405
+ useEffect(() => {
10406
+ const successType = authConfig?.successMessageType || 'openg2p:oidc:success';
10407
+ const handler = (event) => {
10408
+ const data = event?.data;
10409
+ if (!data || typeof data !== 'object')
10410
+ return;
10411
+ if (data.type !== successType)
10412
+ return;
10413
+ if (data.widgetId && data.widgetId !== widgetId)
10414
+ return;
10415
+ emitHostEvent({ type: 'authenticated', payload: data });
10416
+ try {
10417
+ popupRef.current?.close?.();
10418
+ }
10419
+ catch {
10420
+ // ignore
10421
+ }
10422
+ cleanupPopup();
10423
+ if (authConfig?.reloadOnSuccess) {
10424
+ window.location.reload();
10425
+ }
10426
+ };
10427
+ window.addEventListener('message', handler);
10428
+ return () => window.removeEventListener('message', handler);
10429
+ }, [authConfig?.reloadOnSuccess, authConfig?.successMessageType, cleanupPopup, emitHostEvent, widgetId]);
10430
+ const cls = `id-auth-widget-${widgetId}`;
10431
+ const statusLabel = useMemo(() => {
10432
+ if (status === 'success')
10433
+ return 'Success';
10434
+ if (status === 'failure')
10435
+ return 'Failure';
10436
+ if (status === 'not_done')
10437
+ return 'Not done';
10438
+ return 'Unknown';
10439
+ }, [status]);
10440
+ const statusColor = useMemo(() => {
10441
+ if (status === 'success')
10442
+ return 'var(--owt-color-success, #16A34A)';
10443
+ if (status === 'failure')
10444
+ return 'var(--owt-color-danger, #DC2626)';
10445
+ if (status === 'not_done')
10446
+ return 'var(--owt-color-warning, #D97706)';
10447
+ return 'var(--owt-color-text-muted, #6B7280)';
10448
+ }, [status]);
10449
+ const buttonBusy = authActionLoading ||
10450
+ (providerLoading && !resolvedAuthUrl && !authConfig?.defaultAuthorizationUrl);
10451
+ const buttonDisabled = !authConfig || buttonBusy;
9699
10452
  return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
9700
10453
  .${cls} {
9701
10454
  width: 100%;
9702
10455
  font-family: Roboto, sans-serif;
9703
- padding: 0;
9704
- display: flex;
9705
- flex-direction: column;
9706
- gap: 8px;
9707
10456
  }
9708
10457
 
9709
- .${cls} .scores-subtle {
9710
- font-size: 13px;
9711
- color: var(--owt-color-text-muted, #727474);
9712
- font-weight: 400;
10458
+ /* Two-column field grid; primary action in a bottom band (matches section save/edit pattern). */
10459
+ .${cls} .auth-content {
10460
+ display: flex;
10461
+ flex-direction: column;
10462
+ gap: 0;
10463
+ min-width: 0;
9713
10464
  }
9714
10465
 
9715
- .${cls} .scores-card {
9716
- width: 100%;
9717
- border: none;
9718
- border-radius: 0;
9719
- background: transparent;
9720
- padding: 0;
10466
+ .${cls} .auth-grid {
9721
10467
  display: grid;
9722
- grid-template-columns: 1fr 1fr 1fr;
9723
- gap: 16px;
9724
- align-items: center;
10468
+ grid-template-columns: repeat(2, minmax(0, 1fr));
10469
+ gap: 16px 24px;
10470
+ min-width: 0;
9725
10471
  }
9726
10472
 
9727
- .${cls} .scores-col {
9728
- min-width: 0;
10473
+ /* Each field: label (left) + value (right), same as DisplayWidget readonly */
10474
+ .${cls} .auth-cell {
9729
10475
  display: flex;
9730
- flex-direction: column;
9731
- gap: 6px;
10476
+ flex-direction: row;
10477
+ align-items: flex-start;
10478
+ gap: 12px 16px;
10479
+ min-width: 0;
9732
10480
  }
9733
10481
 
9734
- .${cls} .scores-label {
9735
- font-size: 12px;
9736
- color: var(--owt-color-text-muted, #727474);
9737
- font-weight: 600;
9738
- letter-spacing: 0.25px;
9739
- text-transform: uppercase;
10482
+ .${cls} .auth-cell.auth-cell--full {
10483
+ grid-column: 1 / -1;
9740
10484
  }
9741
10485
 
9742
- .${cls} .scores-value {
10486
+ .${cls} .auth-label {
10487
+ flex: 0 0 auto;
10488
+ min-width: 200px;
10489
+ max-width: 40%;
9743
10490
  font-size: 16px;
9744
- font-weight: 600;
9745
- color: var(--owt-color-text, #011627);
9746
- line-height: 1.25;
10491
+ color: rgba(0, 0, 0, 0.6);
10492
+ font-weight: 500;
10493
+ line-height: 1.45;
10494
+ margin: 0;
9747
10495
  word-break: break-word;
9748
10496
  }
9749
10497
 
9750
- .${cls} .scores-value--highlight {
9751
- font-weight: 800;
9752
- color: var(--owt-color-primary-dark, #F07B1A);
10498
+ .${cls} .auth-value {
10499
+ flex: 1 1 auto;
10500
+ min-width: 0;
10501
+ font-size: 16px;
10502
+ color: var(--owt-color-text, #111827);
10503
+ font-weight: 500;
10504
+ line-height: 1.45;
10505
+ word-break: break-word;
9753
10506
  }
9754
10507
 
9755
- .${cls} .scores-value-wrap {
10508
+ .${cls} .auth-bottom-actions {
10509
+ display: flex;
10510
+ flex-direction: column;
10511
+ align-items: flex-start;
10512
+ justify-content: flex-start;
10513
+ gap: 8px;
10514
+ width: 100%;
10515
+ margin-top: 20px;
10516
+ margin-bottom: 0;
10517
+ }
10518
+
10519
+ .${cls} .auth-status {
9756
10520
  display: inline-flex;
9757
- align-items: baseline;
9758
- gap: 10px;
9759
- flex-wrap: wrap;
10521
+ align-items: center;
10522
+ gap: 8px;
10523
+ width: fit-content;
10524
+ padding: 4px 10px;
10525
+ border-radius: 999px;
10526
+ background: rgba(2, 6, 23, 0.04);
10527
+ border: 1px solid rgba(2, 6, 23, 0.08);
10528
+ font-size: 13px;
10529
+ font-weight: 700;
10530
+ color: var(--owt-color-text, #011627);
9760
10531
  }
9761
10532
 
9762
- .${cls} .scores-value-badge { display: inline; }
10533
+ .${cls} .auth-dot {
10534
+ width: 8px;
10535
+ height: 8px;
10536
+ border-radius: 50%;
10537
+ background: ${statusColor};
10538
+ }
9763
10539
 
9764
- .${cls} .scores-statusline {
9765
- grid-column: 1 / -1;
9766
- margin-top: 2px;
10540
+ .${cls} .auth-token {
10541
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
10542
+ font-size: 14px;
10543
+ font-weight: 500;
10544
+ color: var(--owt-color-text, #011627);
10545
+ background: transparent;
10546
+ border: none;
10547
+ border-radius: 0;
10548
+ padding: 0;
10549
+ word-break: break-all;
9767
10550
  }
9768
10551
 
9769
- @media (max-width: 768px) {
9770
- .${cls} .scores-card {
10552
+ .${cls} .auth-button {
10553
+ /* Match SectionRegistryView Save CTA (SectionRenderer) */
10554
+ font-size: 14px;
10555
+ font-weight: 500;
10556
+ padding: 8px 24px;
10557
+ line-height: 1.5;
10558
+ border-radius: var(--owt-btn-border-radius, 10px);
10559
+ border: 1px solid var(--owt-btn-primary-border, #F07B1A);
10560
+ background-color: var(--owt-color-primary, #F5BB1A);
10561
+ color: var(--owt-color-bg, #FFFFFF);
10562
+ font-family: Roboto, sans-serif;
10563
+ cursor: pointer;
10564
+ transition: opacity 0.15s ease;
10565
+ }
10566
+
10567
+ .${cls} .auth-button:disabled {
10568
+ opacity: 0.5;
10569
+ cursor: not-allowed;
10570
+ }
10571
+
10572
+ .${cls} .auth-error {
10573
+ font-size: 12px;
10574
+ color: var(--owt-color-danger, #DC2626);
10575
+ font-weight: 700;
10576
+ line-height: 1.3;
10577
+ text-align: left;
10578
+ max-width: 100%;
10579
+ }
10580
+
10581
+ @media (max-width: 640px) {
10582
+ .${cls} .auth-grid {
9771
10583
  grid-template-columns: 1fr;
9772
10584
  }
10585
+ .${cls} .auth-cell {
10586
+ flex-direction: column;
10587
+ align-items: stretch;
10588
+ gap: 4px 0;
10589
+ }
10590
+ .${cls} .auth-label {
10591
+ min-width: 0;
10592
+ max-width: none;
10593
+ }
9773
10594
  }
9774
- ` }), jsxRuntimeExports.jsx("div", { className: cls, children: jsxRuntimeExports.jsxs("div", { className: "scores-card", children: [jsxRuntimeExports.jsxs("div", { className: "scores-col", "aria-live": "polite", children: [jsxRuntimeExports.jsx("div", { className: "scores-label", children: "Score Type" }), jsxRuntimeExports.jsx("div", { className: "scores-value-wrap", children: jsxRuntimeExports.jsx("span", { className: "scores-value-badge", children: jsxRuntimeExports.jsx("span", { className: "scores-value scores-value--highlight", children: scoreType }) }) })] }), jsxRuntimeExports.jsxs("div", { className: "scores-col", children: [jsxRuntimeExports.jsx("div", { className: "scores-label", children: "Score" }), jsxRuntimeExports.jsx("div", { className: "scores-value-wrap", children: jsxRuntimeExports.jsx("span", { className: "scores-value-badge", children: jsxRuntimeExports.jsx("span", { className: "scores-value scores-value--highlight", children: scoreValue }) }) })] }), jsxRuntimeExports.jsxs("div", { className: "scores-col", children: [jsxRuntimeExports.jsx("div", { className: "scores-label", children: "Computed at" }), jsxRuntimeExports.jsx("div", { className: "scores-value", children: computedAt })] }), jsxRuntimeExports.jsx("div", { className: "scores-statusline", children: loading ? (jsxRuntimeExports.jsx("div", { className: "scores-subtle", children: "Loading scores\u2026" })) : error ? (jsxRuntimeExports.jsx("div", { className: "scores-subtle", style: { color: 'var(--owt-color-error, #B91C1C)' }, children: error })) : !latest ? (jsxRuntimeExports.jsx("div", { className: "scores-subtle", children: "No scores available." })) : null })] }) })] }));
10595
+ ` }), jsxRuntimeExports.jsx("div", { className: cls, children: jsxRuntimeExports.jsxs("div", { className: "auth-content", children: [jsxRuntimeExports.jsxs("div", { className: "auth-grid", children: [jsxRuntimeExports.jsxs("div", { className: "auth-cell", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Foundational ID:" }), jsxRuntimeExports.jsx("div", { className: "auth-value", children: displayText(foundationalId) })] }), jsxRuntimeExports.jsxs("div", { className: "auth-cell", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Last authenticated on:" }), jsxRuntimeExports.jsx("div", { className: "auth-value", children: tryFormatDateTime(lastAuthenticatedOn) })] }), jsxRuntimeExports.jsxs("div", { className: "auth-cell", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Last authentication status:" }), jsxRuntimeExports.jsx("div", { className: "auth-value", children: jsxRuntimeExports.jsxs("div", { className: "auth-status", "aria-label": `Authentication status: ${statusLabel}`, children: [jsxRuntimeExports.jsx("span", { className: "auth-dot" }), jsxRuntimeExports.jsx("span", { children: statusLabel })] }) })] }), jsxRuntimeExports.jsxs("div", { className: "auth-cell", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Expiry date:" }), jsxRuntimeExports.jsx("div", { className: "auth-value", children: tryFormatDate(expiryDate) })] }), jsxRuntimeExports.jsxs("div", { className: "auth-cell auth-cell--full", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Authentication token (PSUT):" }), jsxRuntimeExports.jsx("div", { className: "auth-value", children: jsxRuntimeExports.jsx("div", { className: "auth-token", children: psut ? String(psut) : '-' }) })] })] }), jsxRuntimeExports.jsxs("div", { className: "auth-bottom-actions", children: [jsxRuntimeExports.jsx("button", { type: "button", className: "auth-button", onClick: onAuthenticate, disabled: buttonDisabled, children: buttonBusy ? 'Loading…' : 'Authenticate' }), authError ? jsxRuntimeExports.jsx("div", { className: "auth-error", children: authError }) : null] })] }) })] }));
9775
10596
  };
9776
10597
 
9777
10598
  /**
@@ -9817,6 +10638,8 @@ const registerDefaultWidgets = () => {
9817
10638
  widgetRegistry.register({ widget: 'header-section', component: HeaderSectionWidget });
9818
10639
  // Scores display widget for full-width computed scores display (view-only)
9819
10640
  widgetRegistry.register({ widget: 'scores-display', component: ScoresDisplayWidget });
10641
+ // ID Authentication widget for OIDC-based foundational ID authentication (view-only + action)
10642
+ widgetRegistry.register({ widget: 'id-authentication', component: IdAuthenticationWidget });
9820
10643
  };
9821
10644
  // Auto-register on import
9822
10645
  registerDefaultWidgets();
@@ -10178,5 +11001,5 @@ const translateUISchema = (schema, translate) => {
10178
11001
  };
10179
11002
  };
10180
11003
 
10181
- export { ArrayWidget, BooleanWidget, CheckboxWidget, CurrencyInputWidget, DateInputWidget, DateTimeInputWidget, DisplayWidget, FileInputWidget, HeaderSectionWidget, IterableAccordionWidget, JSONEditorPanel, NumberInputWidget, PanelRenderer, PhoneInputWidget, ProfileWidget, PropertyEditor, RadioWidget, ScoresDisplayWidget, SectionBuilder, SectionRenderer, SectionTree, SectionsContainer, SelectWidget, SimpleTableWidget, TableWidget, TextAreaWidget, TextInputWidget, VisualBuilderPanel, WidgetEventBus, WidgetProvider, WidgetRenderer, applyCaseControl, applyDecimalPrecision, applyMask, createWidgetStore, createZodSchema, defaultTheme, evaluateCondition, filterByCharacterType, formatCurrency, formatDate, formatNumber, formatPhone, formatValue, geoHierarchyBuilder, getApiDataSource, getFormattedNumberLength, getSchemaDataSource, getStaticDataSource, getValueByPath, getWidgetValue, initI18n, isAllowedKey, parseDataPath, parseNumber, registerDefaultWidgets, removeMask, resetAll, resetWidget, resolveTheme, setDataSource, setError, setLoading, setTouched, setValue, setValueByPath, setValues, setWidgetValue, shouldEnableWidget, shouldShowWidget, transformDataSourceOptions, translatePanelConfig, translateUISchema, translateWidgetConfig, useBaseWidget, useGeoWidgetCascade, useWidgetCascade, useWidgetContext, useWidgetEventBus, useWidgetTheme, useWidgetTranslation, validateNumericValue, validateWidget, widgetRegistry };
11004
+ export { ArrayWidget, BooleanWidget, CheckboxWidget, CurrencyInputWidget, DateInputWidget, DateTimeInputWidget, DisplayWidget, FileInputWidget, HeaderSectionWidget, IdAuthenticationWidget, IterableAccordionWidget, JSONEditorPanel, NumberInputWidget, PanelRenderer, PhoneInputWidget, ProfileWidget, PropertyEditor, RadioWidget, ScoresDisplayWidget, SectionBuilder, SectionRenderer, SectionTree, SectionsContainer, SelectWidget, SimpleTableWidget, TableWidget, TextAreaWidget, TextInputWidget, VisualBuilderPanel, WidgetEventBus, WidgetProvider, WidgetRenderer, applyCaseControl, applyDecimalPrecision, applyMask, createWidgetStore, createZodSchema, defaultTheme, evaluateCondition, filterByCharacterType, formatCurrency, formatDate, formatNumber, formatPhone, formatValue, geoHierarchyBuilder, getApiDataSource, getFormattedNumberLength, getSchemaDataSource, getStaticDataSource, getValueByPath, getWidgetValue, initI18n, isAllowedKey, parseDataPath, parseNumber, registerDefaultWidgets, removeMask, resetAll, resetWidget, resolveTheme, setDataSource, setError, setLoading, setTouched, setValue, setValueByPath, setValues, setWidgetValue, shouldEnableWidget, shouldShowWidget, transformDataSourceOptions, translatePanelConfig, translateUISchema, translateWidgetConfig, useBaseWidget, useGeoWidgetCascade, useWidgetCascade, useWidgetContext, useWidgetEventBus, useWidgetTheme, useWidgetTranslation, validateNumericValue, validateWidget, widgetRegistry };
10182
11005
  //# sourceMappingURL=index.esm.js.map