@openg2p/registry-widgets 1.1.1 → 1.1.2-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -174,15 +174,33 @@ const parseDataPath = (dataPath) => {
174
174
  /**
175
175
  * Get value from widget state using data path
176
176
  */
177
+ /**
178
+ * Resolve a widget-id reference in Redux values.
179
+ * Supports namespaced ids (e.g. "rv-section-0__region_code" when ref is "region_code").
180
+ */
181
+ const resolveWidgetIdValue = (values, ref) => {
182
+ if (!ref) {
183
+ return undefined;
184
+ }
185
+ if (ref.includes('.')) {
186
+ return getValueByPath(values, ref);
187
+ }
188
+ if (Object.prototype.hasOwnProperty.call(values, ref)) {
189
+ return values[ref];
190
+ }
191
+ const suffix = `__${ref}`;
192
+ for (const [key, val] of Object.entries(values)) {
193
+ if (key.endsWith(suffix)) {
194
+ return val;
195
+ }
196
+ }
197
+ return undefined;
198
+ };
177
199
  const getWidgetValue = (values, dataPath, widgetId) => {
178
200
  if (!dataPath) {
179
201
  // Fallback to widget-id if no data path
180
202
  return values[widgetId];
181
203
  }
182
- if (widgetId == "user-profile") {
183
- console.log('values', values);
184
- console.log('dataPath', dataPath);
185
- }
186
204
  if (typeof dataPath === 'string') {
187
205
  return getValueByPath(values, dataPath);
188
206
  }
@@ -673,6 +691,21 @@ const getFormattedNumberLength = (value, format) => {
673
691
  const formatted = formatNumber(typeof value === 'string' ? parseFloat(value) : value, format);
674
692
  return formatted.length;
675
693
  };
694
+ const normalizeNumericDefault = (defaultValue, format) => {
695
+ if (defaultValue === undefined) {
696
+ return undefined;
697
+ }
698
+ if (defaultValue === null || defaultValue === '') {
699
+ return null;
700
+ }
701
+ const numValue = typeof defaultValue === 'number'
702
+ ? defaultValue
703
+ : parseNumber(String(defaultValue), format);
704
+ if (numValue === null || isNaN(numValue)) {
705
+ return undefined;
706
+ }
707
+ return applyDecimalPrecision(numValue, format);
708
+ };
676
709
 
677
710
  /**
678
711
  * Date input utilities for parsing, formatting, and validation
@@ -1031,24 +1064,11 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
1031
1064
  // dependsOn can be either a data path (e.g., "person.address") or a widget-id
1032
1065
  let depValue = null;
1033
1066
  if (dataSource.dependsOn) {
1034
- // First try as data path
1035
- depValue = getValueByPath(allValues, dataSource.dependsOn);
1036
- // If not found and doesn't contain dots, try as widget-id
1037
- if ((depValue === null || depValue === undefined) && !dataSource.dependsOn.includes('.')) {
1038
- depValue = allValues[dataSource.dependsOn];
1039
- // Smart resolution: If not found at top level, try to find the dependency in the same nested object
1040
- // by looking for other keys in allValues that might contain the dependency.
1041
- // We look for objects that contain both the current widget's path (if we can guess it) and the dependency.
1042
- // But since we don't know the current widget's path here, we search for any object that has this dependency key.
1043
- if (depValue === null || depValue === undefined || depValue === '') {
1044
- for (const val of Object.values(allValues)) {
1045
- if (val && typeof val === 'object' && !Array.isArray(val) && dataSource.dependsOn in val) {
1046
- depValue = val[dataSource.dependsOn];
1047
- if (depValue !== null && depValue !== undefined && depValue !== '')
1048
- break;
1049
- }
1050
- }
1051
- }
1067
+ if (dataSource.dependsOn.includes('.')) {
1068
+ depValue = getValueByPath(allValues, dataSource.dependsOn);
1069
+ }
1070
+ else {
1071
+ depValue = resolveWidgetIdValue(allValues, dataSource.dependsOn);
1052
1072
  }
1053
1073
  if (depValue === null || depValue === undefined || depValue === '') {
1054
1074
  // If dependency is empty, return empty array
@@ -1917,6 +1937,247 @@ const WidgetProvider = ({ store, dataSourceRequestHandler, schemaData, translate
1917
1937
  return content;
1918
1938
  };
1919
1939
 
1940
+ /**
1941
+ * Geo Hierarchy Builder
1942
+ * Manages geo hierarchy state and builds hierarchy JSON structure
1943
+ */
1944
+ function extractLevelValueFromStored(value, geoConfig) {
1945
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1946
+ return value;
1947
+ }
1948
+ const hierarchy = value.hierarchy || value.geo_code_hierarchy_json?.hierarchy;
1949
+ if (Array.isArray(hierarchy)) {
1950
+ const levelData = hierarchy.find((l) => l.level === geoConfig.level);
1951
+ if (levelData) {
1952
+ return levelData.level_value_id;
1953
+ }
1954
+ // Level absent from hierarchy (e.g. cleared by upstream cascade) — do not use lowest-level fallback
1955
+ return undefined;
1956
+ }
1957
+ if ('geo_lowest_level_value_id' in value) {
1958
+ return value.geo_lowest_level_value_id;
1959
+ }
1960
+ if ('lowest_level_value_id' in value) {
1961
+ return value.lowest_level_value_id;
1962
+ }
1963
+ if (value.geo_code_hierarchy_json?.lowest_level_value_id) {
1964
+ return value.geo_code_hierarchy_json.lowest_level_value_id;
1965
+ }
1966
+ if (value.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
1967
+ return value.geo_code_hierarchy_json.geo_lowest_level_value_id;
1968
+ }
1969
+ return undefined;
1970
+ }
1971
+ /**
1972
+ * Resolve the display value for a geo level widget.
1973
+ * When widgetId is explicitly set in Redux (including cleared undefined/null), do not
1974
+ * fall back to shared hierarchy dataPath — that stale path was keeping grandchildren visible.
1975
+ */
1976
+ function resolveGeoWidgetLevelValue(values, widgetId, dataPath, geoConfig) {
1977
+ if (Object.prototype.hasOwnProperty.call(values, widgetId)) {
1978
+ let value = values[widgetId];
1979
+ if (value === undefined || value === null || value === '') {
1980
+ return value;
1981
+ }
1982
+ if (typeof value === 'object' && !Array.isArray(value)) {
1983
+ return extractLevelValueFromStored(value, geoConfig);
1984
+ }
1985
+ return value;
1986
+ }
1987
+ if (!dataPath) {
1988
+ return undefined;
1989
+ }
1990
+ let value = getWidgetValue(values, dataPath, widgetId);
1991
+ if (value !== null && value !== undefined && typeof value === 'object' && !Array.isArray(value)) {
1992
+ value = extractLevelValueFromStored(value, geoConfig);
1993
+ }
1994
+ return value;
1995
+ }
1996
+ /**
1997
+ * Write the in-memory geo hierarchy builder state into Redux at the shared dataPath.
1998
+ */
1999
+ function applySharedGeoHierarchyToValues(baseValues, groupId, dataPath, widgetId) {
2000
+ const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
2001
+ if (!dataPath || typeof dataPath !== 'string') {
2002
+ return baseValues;
2003
+ }
2004
+ const inner = hierarchyJson?.geo_code_hierarchy_json;
2005
+ const lowestId = hierarchyJson?.geo_lowest_level_value_id;
2006
+ if (dataPath.endsWith('.geo_code_hierarchy_json')) {
2007
+ const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
2008
+ let finalUpdatedValues = setWidgetValue(baseValues, dataPath, widgetId, inner);
2009
+ finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, lowestId);
2010
+ return finalUpdatedValues;
2011
+ }
2012
+ return setWidgetValue(baseValues, dataPath, widgetId, inner);
2013
+ }
2014
+ class GeoHierarchyBuilder {
2015
+ constructor() {
2016
+ this.hierarchies = new Map();
2017
+ }
2018
+ /**
2019
+ * Get or create hierarchy state for a group
2020
+ */
2021
+ getHierarchy(groupId = 'default') {
2022
+ if (!this.hierarchies.has(groupId)) {
2023
+ this.hierarchies.set(groupId, {
2024
+ levels: new Map(),
2025
+ order: [],
2026
+ });
2027
+ }
2028
+ return this.hierarchies.get(groupId);
2029
+ }
2030
+ /**
2031
+ * Add a level to the hierarchy
2032
+ */
2033
+ addLevel(level, level_value_id, level_value_mnemonic, groupId = 'default') {
2034
+ const hierarchy = this.getHierarchy(groupId);
2035
+ // If level already exists, remove it and everything after it
2036
+ const existingIndex = hierarchy.order.indexOf(level);
2037
+ if (existingIndex >= 0) {
2038
+ // Remove this level and all subsequent levels
2039
+ const levelsToRemove = hierarchy.order.slice(existingIndex);
2040
+ levelsToRemove.forEach((l) => {
2041
+ hierarchy.levels.delete(l);
2042
+ hierarchy.order = hierarchy.order.filter((o) => o !== l);
2043
+ });
2044
+ }
2045
+ // Add new level
2046
+ hierarchy.levels.set(level, {
2047
+ level,
2048
+ level_value_id,
2049
+ level_value_mnemonic,
2050
+ });
2051
+ hierarchy.order.push(level);
2052
+ }
2053
+ /**
2054
+ * Remove a level and all levels below it
2055
+ */
2056
+ removeLevelAndBelow(level, groupId = 'default') {
2057
+ const hierarchy = this.getHierarchy(groupId);
2058
+ const index = hierarchy.order.indexOf(level);
2059
+ if (index >= 0) {
2060
+ // Remove this level and all subsequent levels
2061
+ const levelsToRemove = hierarchy.order.slice(index);
2062
+ levelsToRemove.forEach((l) => {
2063
+ hierarchy.levels.delete(l);
2064
+ hierarchy.order = hierarchy.order.filter((o) => o !== l);
2065
+ });
2066
+ }
2067
+ }
2068
+ /**
2069
+ * Build hierarchy JSON structure
2070
+ */
2071
+ buildHierarchyJson(groupId = 'default') {
2072
+ const hierarchy = this.getHierarchy(groupId);
2073
+ if (hierarchy.order.length === 0) {
2074
+ return null;
2075
+ }
2076
+ const hierarchyArray = hierarchy.order.map((level) => {
2077
+ const data = hierarchy.levels.get(level);
2078
+ return {
2079
+ level: data.level,
2080
+ level_value_id: data.level_value_id,
2081
+ level_value_mnemonic: data.level_value_mnemonic,
2082
+ };
2083
+ });
2084
+ const lowestLevel = hierarchy.order[hierarchy.order.length - 1];
2085
+ const lowestLevelData = hierarchy.levels.get(lowestLevel);
2086
+ return {
2087
+ geo_lowest_level_value_id: lowestLevelData.level_value_id,
2088
+ geo_code_hierarchy_json: {
2089
+ hierarchy: hierarchyArray,
2090
+ lowest_level_value_id: lowestLevelData.level_value_id,
2091
+ },
2092
+ };
2093
+ }
2094
+ /**
2095
+ * Clear hierarchy for a group
2096
+ */
2097
+ clear(groupId = 'default') {
2098
+ this.hierarchies.delete(groupId);
2099
+ }
2100
+ /**
2101
+ * Clear all hierarchies
2102
+ */
2103
+ clearAll() {
2104
+ this.hierarchies.clear();
2105
+ }
2106
+ /**
2107
+ * Get current levels for a group
2108
+ */
2109
+ getLevels(groupId = 'default') {
2110
+ const hierarchy = this.getHierarchy(groupId);
2111
+ return hierarchy.order.map((level) => hierarchy.levels.get(level));
2112
+ }
2113
+ }
2114
+ // Singleton instance
2115
+ const geoHierarchyBuilder = new GeoHierarchyBuilder();
2116
+ /** Sentinel written to Redux when a geo level is cleared by cascade (distinct from "never set"). */
2117
+ const GEO_LEVEL_CLEARED = null;
2118
+ const geoWidgetParentRegistry = new Map();
2119
+ function registerGeoWidgetParent(widgetId, parentWidgetId) {
2120
+ geoWidgetParentRegistry.set(widgetId, parentWidgetId || null);
2121
+ }
2122
+ function unregisterGeoWidgetParent(widgetId) {
2123
+ geoWidgetParentRegistry.delete(widgetId);
2124
+ }
2125
+ /** True when changedWidgetId is any upstream geo parent of widgetId (not only immediate parent). */
2126
+ function isUpstreamGeoAncestor(changedWidgetId, widgetId, immediateParentWidgetId) {
2127
+ if (changedWidgetId === widgetId) {
2128
+ return false;
2129
+ }
2130
+ let cursor = immediateParentWidgetId;
2131
+ while (cursor) {
2132
+ if (cursor === changedWidgetId) {
2133
+ return true;
2134
+ }
2135
+ cursor = geoWidgetParentRegistry.get(cursor) ?? null;
2136
+ }
2137
+ return false;
2138
+ }
2139
+ function readStoredHierarchyLevels(values, dataPath, widgetId) {
2140
+ const stored = getWidgetValue(values, dataPath, widgetId);
2141
+ const hierarchy = stored?.hierarchy || stored?.geo_code_hierarchy_json?.hierarchy;
2142
+ if (!Array.isArray(hierarchy)) {
2143
+ return [];
2144
+ }
2145
+ return hierarchy.filter((entry) => entry?.level && entry.level_value_id);
2146
+ }
2147
+ function builderMatchesStored(groupId, storedLevels) {
2148
+ const builderLevels = geoHierarchyBuilder.getLevels(groupId);
2149
+ if (builderLevels.length !== storedLevels.length) {
2150
+ return false;
2151
+ }
2152
+ return storedLevels.every((stored, index) => {
2153
+ const built = builderLevels[index];
2154
+ return (built.level === stored.level &&
2155
+ String(built.level_value_id) === String(stored.level_value_id));
2156
+ });
2157
+ }
2158
+ /** Seed in-memory builder from persisted hierarchy JSON (edit-mode rehydration). */
2159
+ function seedGeoHierarchyFromValues(values, dataPath, widgetId, groupId) {
2160
+ if (!dataPath || typeof dataPath !== 'string') {
2161
+ return;
2162
+ }
2163
+ const storedLevels = readStoredHierarchyLevels(values, dataPath, widgetId);
2164
+ if (storedLevels.length === 0) {
2165
+ return;
2166
+ }
2167
+ if (builderMatchesStored(groupId, storedLevels)) {
2168
+ return;
2169
+ }
2170
+ geoHierarchyBuilder.clear(groupId);
2171
+ storedLevels.forEach((entry) => {
2172
+ geoHierarchyBuilder.addLevel(entry.level, String(entry.level_value_id), entry.level_value_mnemonic || String(entry.level_value_id), groupId);
2173
+ });
2174
+ }
2175
+ /** Force-clear builder for a group, then seed from Redux/schema values (e.g. after Cancel). */
2176
+ function resetAndSeedGeoHierarchyFromValues(values, dataPath, widgetId, groupId) {
2177
+ geoHierarchyBuilder.clear(groupId);
2178
+ seedGeoHierarchyFromValues(values, dataPath, widgetId, groupId);
2179
+ }
2180
+
1920
2181
  // Define stable empty arrays to avoid selector reference issues
1921
2182
  const EMPTY_ERRORS = [];
1922
2183
  const EMPTY_DATA_SOURCE$1 = [];
@@ -2006,6 +2267,17 @@ const useBaseWidget = (options) => {
2006
2267
  if (isLayoutWidget) {
2007
2268
  return undefined; // Layout widgets don't have values
2008
2269
  }
2270
+ const geoConfig = config['widget-geo-config'];
2271
+ if (geoConfig) {
2272
+ const value = resolveGeoWidgetLevelValue(values, widgetId, config['widget-data-path'], geoConfig);
2273
+ if (userHasSetValueRef.current) {
2274
+ return value;
2275
+ }
2276
+ if (value === null) {
2277
+ return null;
2278
+ }
2279
+ return value !== undefined ? value : config['widget-data-default'];
2280
+ }
2009
2281
  // Try to get value from widgetId first (this should have the actual selected value)
2010
2282
  // For geo widgets with dataPath, widgetId stores the actual ID, while dataPath stores the hierarchy object
2011
2283
  let value = values[widgetId];
@@ -2215,6 +2487,14 @@ const useBaseWidget = (options) => {
2215
2487
  // Track readonly state explicitly to detect changes
2216
2488
  // Use JSON.stringify to create a stable reference for the dependency array
2217
2489
  const isReadonly = config['widget-readonly'] ?? false;
2490
+ // Leaving edit mode (Cancel): allow mirror/rehydration on next Edit
2491
+ React.useEffect(() => {
2492
+ if (config['widget-readonly']) {
2493
+ userHasSetValueRef.current = false;
2494
+ lastMirroredValueRef.current = null;
2495
+ lastDispatchedValueRef.current = null;
2496
+ }
2497
+ }, [config['widget-readonly']]);
2218
2498
  const dataSource = config['widget-data-source'];
2219
2499
  const geoConfig = config['widget-geo-config'];
2220
2500
  // Use ref to store handler to avoid stale closures
@@ -2419,113 +2699,6 @@ const useWidgetCascade = (options) => {
2419
2699
  }, [cascadeConfig, eventBus, dataSource, widgetId, dispatch]);
2420
2700
  };
2421
2701
 
2422
- /**
2423
- * Geo Hierarchy Builder
2424
- * Manages geo hierarchy state and builds hierarchy JSON structure
2425
- */
2426
- class GeoHierarchyBuilder {
2427
- constructor() {
2428
- this.hierarchies = new Map();
2429
- }
2430
- /**
2431
- * Get or create hierarchy state for a group
2432
- */
2433
- getHierarchy(groupId = 'default') {
2434
- if (!this.hierarchies.has(groupId)) {
2435
- this.hierarchies.set(groupId, {
2436
- levels: new Map(),
2437
- order: [],
2438
- });
2439
- }
2440
- return this.hierarchies.get(groupId);
2441
- }
2442
- /**
2443
- * Add a level to the hierarchy
2444
- */
2445
- addLevel(level, level_value_id, level_value_mnemonic, groupId = 'default') {
2446
- const hierarchy = this.getHierarchy(groupId);
2447
- // If level already exists, remove it and everything after it
2448
- const existingIndex = hierarchy.order.indexOf(level);
2449
- if (existingIndex >= 0) {
2450
- // Remove this level and all subsequent levels
2451
- const levelsToRemove = hierarchy.order.slice(existingIndex);
2452
- levelsToRemove.forEach((l) => {
2453
- hierarchy.levels.delete(l);
2454
- hierarchy.order = hierarchy.order.filter((o) => o !== l);
2455
- });
2456
- }
2457
- // Add new level
2458
- hierarchy.levels.set(level, {
2459
- level,
2460
- level_value_id,
2461
- level_value_mnemonic,
2462
- });
2463
- hierarchy.order.push(level);
2464
- }
2465
- /**
2466
- * Remove a level and all levels below it
2467
- */
2468
- removeLevelAndBelow(level, groupId = 'default') {
2469
- const hierarchy = this.getHierarchy(groupId);
2470
- const index = hierarchy.order.indexOf(level);
2471
- if (index >= 0) {
2472
- // Remove this level and all subsequent levels
2473
- const levelsToRemove = hierarchy.order.slice(index);
2474
- levelsToRemove.forEach((l) => {
2475
- hierarchy.levels.delete(l);
2476
- hierarchy.order = hierarchy.order.filter((o) => o !== l);
2477
- });
2478
- }
2479
- }
2480
- /**
2481
- * Build hierarchy JSON structure
2482
- */
2483
- buildHierarchyJson(groupId = 'default') {
2484
- const hierarchy = this.getHierarchy(groupId);
2485
- if (hierarchy.order.length === 0) {
2486
- return null;
2487
- }
2488
- const hierarchyArray = hierarchy.order.map((level) => {
2489
- const data = hierarchy.levels.get(level);
2490
- return {
2491
- level: data.level,
2492
- level_value_id: data.level_value_id,
2493
- level_value_mnemonic: data.level_value_mnemonic,
2494
- };
2495
- });
2496
- const lowestLevel = hierarchy.order[hierarchy.order.length - 1];
2497
- const lowestLevelData = hierarchy.levels.get(lowestLevel);
2498
- return {
2499
- geo_lowest_level_value_id: lowestLevelData.level_value_id,
2500
- geo_code_hierarchy_json: {
2501
- hierarchy: hierarchyArray,
2502
- lowest_level_value_id: lowestLevelData.level_value_id,
2503
- },
2504
- };
2505
- }
2506
- /**
2507
- * Clear hierarchy for a group
2508
- */
2509
- clear(groupId = 'default') {
2510
- this.hierarchies.delete(groupId);
2511
- }
2512
- /**
2513
- * Clear all hierarchies
2514
- */
2515
- clearAll() {
2516
- this.hierarchies.clear();
2517
- }
2518
- /**
2519
- * Get current levels for a group
2520
- */
2521
- getLevels(groupId = 'default') {
2522
- const hierarchy = this.getHierarchy(groupId);
2523
- return hierarchy.order.map((level) => hierarchy.levels.get(level));
2524
- }
2525
- }
2526
- // Singleton instance
2527
- const geoHierarchyBuilder = new GeoHierarchyBuilder();
2528
-
2529
2702
  // Define stable empty array to avoid selector reference issues
2530
2703
  const EMPTY_DATA_SOURCE = [];
2531
2704
  /**
@@ -2545,138 +2718,108 @@ const useGeoWidgetCascade = (options) => {
2545
2718
  : 'default';
2546
2719
  const valuesRef = React.useRef(values);
2547
2720
  const handlerRef = React.useRef(dataSourceRequestHandler);
2721
+ const lastCascadePublishRef = React.useRef(undefined);
2548
2722
  // Keep refs updated
2549
2723
  React.useEffect(() => {
2550
2724
  valuesRef.current = values;
2551
2725
  handlerRef.current = dataSourceRequestHandler;
2552
2726
  }, [values, dataSourceRequestHandler]);
2553
2727
  // Get current value and data source options
2554
- const currentValue = reactRedux.useSelector((state) => {
2555
- // Try to get value from widgetId first (most recent selection)
2556
- let value = state.widget.values[widgetId];
2557
- // If not found in widgetId, try dataPath
2558
- if (value === undefined && dataPath) {
2559
- value = getWidgetValue(state.widget.values, dataPath, widgetId);
2560
- }
2561
- // Extract value if it's a geo hierarchy object
2562
- if (value && typeof value === 'object' && !Array.isArray(value) && geoConfig) {
2563
- const hierarchy = value.hierarchy || value.geo_code_hierarchy_json?.hierarchy;
2564
- if (Array.isArray(hierarchy)) {
2565
- const levelData = hierarchy.find((l) => l.level === geoConfig.level);
2566
- if (levelData) {
2567
- return levelData.level_value_id;
2568
- }
2569
- }
2570
- // Extended fallbacks (matching useBaseWidget)
2571
- if ('geo_lowest_level_value_id' in value) {
2572
- return value.geo_lowest_level_value_id;
2573
- }
2574
- if ('lowest_level_value_id' in value) {
2575
- return value.lowest_level_value_id;
2576
- }
2577
- if (value.geo_code_hierarchy_json?.lowest_level_value_id) {
2578
- return value.geo_code_hierarchy_json.lowest_level_value_id;
2579
- }
2580
- if (value.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
2581
- return value.geo_code_hierarchy_json.geo_lowest_level_value_id;
2582
- }
2583
- }
2584
- return value;
2585
- });
2728
+ const currentValue = reactRedux.useSelector((state) => geoConfig
2729
+ ? resolveGeoWidgetLevelValue(state.widget.values, widgetId, dataPath, geoConfig)
2730
+ : state.widget.values[widgetId]);
2586
2731
  // Memoize selector to avoid returning new array reference
2587
2732
  const dataSourceOptions = reactRedux.useSelector((state) => state.widget.dataSources[widgetId] ?? EMPTY_DATA_SOURCE);
2733
+ // Register parent chain for upstream-ancestor detection (grandparent → grandchild reset)
2734
+ React.useEffect(() => {
2735
+ if (!geoConfig) {
2736
+ return;
2737
+ }
2738
+ registerGeoWidgetParent(widgetId, geoConfig.parentWidgetId);
2739
+ return () => unregisterGeoWidgetParent(widgetId);
2740
+ }, [widgetId, geoConfig]);
2741
+ // Keep in-memory builder in sync with persisted hierarchy (reload, cancel→re-edit, etc.)
2742
+ React.useEffect(() => {
2743
+ if (!geoConfig || typeof dataPath !== 'string') {
2744
+ return;
2745
+ }
2746
+ seedGeoHierarchyFromValues(values, dataPath, widgetId, groupId);
2747
+ }, [geoConfig, dataPath, widgetId, groupId, values]);
2588
2748
  React.useEffect(() => {
2589
2749
  if (!geoConfig || !eventBus || !dataSource || dataSource.type !== 'api') {
2590
2750
  return;
2591
2751
  }
2592
- const { level, isLastLevel, parentWidgetId } = geoConfig;
2593
- // Listen to parent widget changes
2594
- if (parentWidgetId) {
2595
- const handleParentChange = async (event) => {
2596
- if (event.widgetId !== parentWidgetId) {
2597
- return;
2598
- }
2599
- // CRITICAL: Use a small delay to ensure Redux state has been updated
2600
- // This prevents reading stale values from valuesRef
2601
- await new Promise(resolve => setTimeout(resolve, 0));
2602
- const currentValues = valuesRef.current;
2603
- const currentHandler = handlerRef.current;
2604
- // CRITICAL: Try to get parent value from event first, then from Redux
2605
- let parentValue = event.value;
2606
- if (parentValue === undefined || parentValue === null) {
2607
- parentValue = currentValues[parentWidgetId];
2608
- // If not found in top-level values, try to find it via dataPath or dependsOn
2609
- if (parentValue === undefined && dataSource.dependsOn) {
2610
- parentValue = getWidgetValue(currentValues, dataSource.dependsOn, '');
2611
- }
2612
- }
2613
- // Remove this level and all below from hierarchy
2614
- geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2615
- // Clear this widget's value
2616
- // CRITICAL: Only dispatch setValue for THIS widget, not for parent or other widgets
2617
- // setWidgetValue returns the entire updated state, but we only want to update this widget
2618
- if (dataPath) {
2619
- const updatedValues = setWidgetValue(currentValues, dataPath, widgetId, undefined);
2620
- // Only dispatch setValue for this widget's widgetId, not for parent or other widgets
2621
- // This prevents accidentally overwriting the parent widget's value
2622
- // The setWidgetValue function updates the nested structure, but we only want to
2623
- // update the top-level widgetId key, not other keys that might be in updatedValues
2624
- const newWidgetValue = updatedValues[widgetId];
2625
- if (newWidgetValue !== undefined) {
2626
- dispatch(setValue({ widgetId, value: newWidgetValue }));
2627
- }
2628
- else {
2629
- // If widgetId is not in updatedValues, the value was set in a nested path
2630
- // In this case, we need to use setValues to update the entire structure
2631
- // But we need to be careful not to overwrite the parent widget's value
2632
- // Only update keys that are related to this widget's dataPath
2633
- const dataPathStr = typeof dataPath === 'string' ? dataPath : '';
2634
- if (dataPathStr && !dataPathStr.startsWith(parentWidgetId + '.')) {
2635
- // Only update if dataPath doesn't start with parentWidgetId
2636
- // This ensures we don't accidentally overwrite the parent widget's value
2637
- dispatch(setValue({ widgetId, value: undefined }));
2638
- }
2639
- }
2640
- }
2641
- else {
2642
- dispatch(setValue({ widgetId, value: undefined }));
2752
+ const { level, isLastLevel, parentWidgetId: rawParentWidgetId } = geoConfig;
2753
+ const parentWidgetId = rawParentWidgetId || null;
2754
+ if (!parentWidgetId) {
2755
+ return;
2756
+ }
2757
+ const clearThisLevel = (baseValues) => {
2758
+ geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2759
+ if (dataPath) {
2760
+ dispatch(setValues(applySharedGeoHierarchyToValues(baseValues, groupId, dataPath, widgetId)));
2761
+ }
2762
+ dispatch(setValue({ widgetId, value: GEO_LEVEL_CLEARED }));
2763
+ if (!isLastLevel) {
2764
+ eventBus.publish({
2765
+ type: 'widget:change',
2766
+ widgetId,
2767
+ value: GEO_LEVEL_CLEARED,
2768
+ timestamp: Date.now(),
2769
+ });
2770
+ }
2771
+ dispatch(setDataSource({ widgetId, data: [] }));
2772
+ };
2773
+ const handleParentChange = async (event) => {
2774
+ const isDirectParent = event.widgetId === parentWidgetId;
2775
+ const isAncestor = isUpstreamGeoAncestor(event.widgetId, widgetId, parentWidgetId);
2776
+ if (!isDirectParent && !isAncestor) {
2777
+ return;
2778
+ }
2779
+ await new Promise(resolve => setTimeout(resolve, 0));
2780
+ const currentValues = valuesRef.current;
2781
+ const currentHandler = handlerRef.current;
2782
+ // Grandparent (or higher) changed: clear this level; only immediate parent drives reload
2783
+ if (isAncestor && !isDirectParent) {
2784
+ clearThisLevel(currentValues);
2785
+ return;
2786
+ }
2787
+ const parentCleared = event.value === undefined ||
2788
+ event.value === null ||
2789
+ event.value === '' ||
2790
+ event.value === GEO_LEVEL_CLEARED;
2791
+ let parentValue = event.value;
2792
+ if (!parentCleared && (parentValue === undefined || parentValue === null)) {
2793
+ parentValue = currentValues[parentWidgetId];
2794
+ if (parentValue === undefined && dataSource.dependsOn) {
2795
+ parentValue = getWidgetValue(currentValues, dataSource.dependsOn, '');
2643
2796
  }
2644
- // Reload data source with new parent value
2645
- // CRITICAL: Use parentValue from Redux, not event.value
2646
- if (currentHandler && parentValue !== null && parentValue !== undefined) {
2647
- try {
2648
- // Merge the new parent value into current values for the API call
2649
- // This ensures getApiDataSource can find the dependency value
2650
- const updatedValues = {
2651
- ...currentValues,
2652
- [parentWidgetId]: parentValue, // Use Redux value, not event.value
2653
- };
2654
- // Extract level_id from widget-geo-config.level
2655
- const levelId = geoConfig.level;
2656
- const data = await getApiDataSource(dataSource, updatedValues, currentHandler, levelId);
2657
- // Transform to { value, label } format
2658
- const valueKey = dataSource.valueKey || 'level_value_id';
2659
- const labelKey = dataSource.labelKey || 'level_value_mnemonic';
2660
- const transformed = transformDataSourceOptions(data, valueKey, labelKey);
2661
- dispatch(setDataSource({ widgetId, data: transformed }));
2662
- }
2663
- catch (error) {
2664
- console.error('Error reloading geo data source:', error);
2665
- dispatch(setDataSource({ widgetId, data: [] }));
2666
- }
2797
+ }
2798
+ clearThisLevel(currentValues);
2799
+ if (currentHandler && parentValue !== null && parentValue !== undefined && parentValue !== '') {
2800
+ try {
2801
+ const updatedValues = {
2802
+ ...currentValues,
2803
+ [parentWidgetId]: parentValue,
2804
+ };
2805
+ const levelId = geoConfig.level;
2806
+ const data = await getApiDataSource(dataSource, updatedValues, currentHandler, levelId);
2807
+ const valueKey = dataSource.valueKey || 'level_value_id';
2808
+ const labelKey = dataSource.labelKey || 'level_value_mnemonic';
2809
+ const transformed = transformDataSourceOptions(data, valueKey, labelKey);
2810
+ dispatch(setDataSource({ widgetId, data: transformed }));
2667
2811
  }
2668
- else {
2669
- // If parent value is cleared, clear the data source and hierarchy
2670
- geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2812
+ catch (error) {
2813
+ console.error('Error reloading geo data source:', error);
2671
2814
  dispatch(setDataSource({ widgetId, data: [] }));
2672
2815
  }
2673
- };
2674
- const unsubscribe = eventBus.subscribe('widget:change', handleParentChange);
2675
- return () => {
2676
- unsubscribe();
2677
- };
2678
- }
2679
- }, [geoConfig, eventBus, dataSource, widgetId, dataPath, dispatch]);
2816
+ }
2817
+ };
2818
+ const unsubscribe = eventBus.subscribe('widget:change', handleParentChange);
2819
+ return () => {
2820
+ unsubscribe();
2821
+ };
2822
+ }, [geoConfig, eventBus, dataSource, widgetId, dataPath, dispatch, groupId]);
2680
2823
  // Handle value changes to build hierarchy
2681
2824
  React.useEffect(() => {
2682
2825
  if (!geoConfig) {
@@ -2685,22 +2828,20 @@ const useGeoWidgetCascade = (options) => {
2685
2828
  // Skip if value is undefined (it might still be loading or rehydrating)
2686
2829
  // ONLY clear hierarchy if the value is explicitly null or empty string (user action)
2687
2830
  if (currentValue === null || currentValue === '') {
2688
- const { level } = geoConfig;
2831
+ const { level, isLastLevel } = geoConfig;
2689
2832
  geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2690
2833
  // If we have a dataPath, we need to update Redux with the cleared hierarchy
2691
2834
  if (dataPath) {
2692
- const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
2693
- let finalUpdatedValues = valuesRef.current;
2694
- // Use logic similar to the build section below to update the dataPath
2695
- if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
2696
- const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
2697
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
2698
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson?.geo_lowest_level_value_id);
2699
- }
2700
- else {
2701
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
2702
- }
2703
- dispatch(setValues(finalUpdatedValues));
2835
+ dispatch(setValues(applySharedGeoHierarchyToValues(valuesRef.current, groupId, dataPath, widgetId)));
2836
+ }
2837
+ if (!isLastLevel && eventBus && lastCascadePublishRef.current !== GEO_LEVEL_CLEARED) {
2838
+ lastCascadePublishRef.current = GEO_LEVEL_CLEARED;
2839
+ eventBus.publish({
2840
+ type: 'widget:change',
2841
+ widgetId,
2842
+ value: GEO_LEVEL_CLEARED,
2843
+ timestamp: Date.now(),
2844
+ });
2704
2845
  }
2705
2846
  return;
2706
2847
  }
@@ -2751,30 +2892,20 @@ const useGeoWidgetCascade = (options) => {
2751
2892
  // Add level to hierarchy
2752
2893
  geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic, groupId);
2753
2894
  // Build and store hierarchy JSON on every change
2754
- if (dataPath) {
2755
- const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
2756
- if (hierarchyJson) {
2757
- // Fix: Avoid double nesting of geo_code_hierarchy_json
2758
- // If dataPath ends with .geo_code_hierarchy_json, we want to save the content directly to it
2759
- // and save the lowest level ID as a sibling
2760
- let finalUpdatedValues = valuesRef.current;
2761
- if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
2762
- const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
2763
- // Save hierarchy JSON content directly to dataPath (avoiding double nesting)
2764
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
2765
- // Save lowest level ID as sibling
2766
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson.geo_lowest_level_value_id);
2767
- }
2768
- else {
2769
- // Fallback if path doesn't follow the naming convention
2770
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
2771
- }
2772
- // CRITICAL: Use setValues for deep merge instead of replacing root keys with setValue
2773
- // setWidgetValue returns the complete updated state object with all keys preserved
2774
- dispatch(setValues(finalUpdatedValues));
2775
- }
2895
+ if (dataPath && geoHierarchyBuilder.buildHierarchyJson(groupId)) {
2896
+ dispatch(setValues(applySharedGeoHierarchyToValues(valuesRef.current, groupId, dataPath, widgetId)));
2776
2897
  }
2777
- }, [geoConfig, currentValue, widgetId, dataPath, dispatch, dataSourceOptions]);
2898
+ // Notify descendants when this level changes via hierarchy/rehydration (handleChange may not run).
2899
+ if (!isLastLevel && eventBus && lastCascadePublishRef.current !== level_value_id) {
2900
+ lastCascadePublishRef.current = level_value_id;
2901
+ eventBus.publish({
2902
+ type: 'widget:change',
2903
+ widgetId,
2904
+ value: level_value_id,
2905
+ timestamp: Date.now(),
2906
+ });
2907
+ }
2908
+ }, [geoConfig, currentValue, widgetId, dataPath, dispatch, dataSourceOptions, eventBus, groupId]);
2778
2909
  };
2779
2910
 
2780
2911
  class WidgetRegistry {
@@ -7447,7 +7578,18 @@ const TextInputWidget = ({ config }) => {
7447
7578
  };
7448
7579
 
7449
7580
  const NumberInputWidget = ({ config }) => {
7450
- const { value, formattedValue, error, touched, isEnabled, onChange, onBlur, config: widgetConfig, } = useBaseWidget({ config });
7581
+ const resolvedConfig = React.useMemo(() => {
7582
+ const rawDefault = config['widget-data-default'];
7583
+ if (rawDefault === undefined) {
7584
+ return config;
7585
+ }
7586
+ const normalizedDefault = normalizeNumericDefault(rawDefault, config['widget-data-format']);
7587
+ if (normalizedDefault === undefined || normalizedDefault === rawDefault) {
7588
+ return config;
7589
+ }
7590
+ return { ...config, 'widget-data-default': normalizedDefault };
7591
+ }, [config]);
7592
+ const { value, formattedValue, error, touched, isEnabled, onChange, onBlur, config: widgetConfig, } = useBaseWidget({ config: resolvedConfig });
7451
7593
  const { translate, translateConfig } = useWidgetTranslation();
7452
7594
  const formatConfig = widgetConfig['widget-data-format'];
7453
7595
  const validationConfig = widgetConfig['widget-data-validation'];
@@ -8762,16 +8904,65 @@ const DisplayWidget = ({ config }) => {
8762
8904
  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 }) })] }));
8763
8905
  };
8764
8906
 
8907
+ const getDateColumnConstraintError = (column, cellValue, rowValues, translateConfig) => {
8908
+ const displayValue = cellValue && typeof cellValue === 'string' ? cellValue.split('T')[0] : '';
8909
+ if (!displayValue) {
8910
+ return null;
8911
+ }
8912
+ const optionsConfig = column['widget-data-options'];
8913
+ const formatConfig = column['widget-data-format'];
8914
+ const dateConstraint = formatConfig?.dateConstraint || 'any';
8915
+ const minDate = optionsConfig?.minDate;
8916
+ const maxDate = optionsConfig?.maxDate;
8917
+ const minDateField = optionsConfig?.minDateField;
8918
+ const maxDateField = optionsConfig?.maxDateField;
8919
+ const minDateMessage = optionsConfig?.minDateMessage
8920
+ ? translateConfig(optionsConfig.minDateMessage)
8921
+ : undefined;
8922
+ const maxDateMessage = optionsConfig?.maxDateMessage
8923
+ ? translateConfig(optionsConfig.maxDateMessage)
8924
+ : undefined;
8925
+ const resolveSiblingDate = (fieldRef) => {
8926
+ if (!fieldRef) {
8927
+ return undefined;
8928
+ }
8929
+ const raw = getValueByPath(rowValues, fieldRef) ?? rowValues[fieldRef];
8930
+ return resolveDateBoundFromFieldValue(raw);
8931
+ };
8932
+ const effectiveMinDate = mergeMinDateBounds(getMinDate(dateConstraint, minDate), resolveSiblingDate(minDateField));
8933
+ const effectiveMaxDate = mergeMaxDateBounds(getMaxDate(dateConstraint, maxDate), resolveSiblingDate(maxDateField));
8934
+ return validateDateConstraints(displayValue, effectiveMinDate, effectiveMaxDate, dateConstraint, { minDateMessage, maxDateMessage });
8935
+ };
8936
+ const isTableRowDataValid = (rowData, columns, tableReadonly, translateConfig) => {
8937
+ for (const col of columns) {
8938
+ if (tableReadonly || col['widget-readonly'] === true) {
8939
+ continue;
8940
+ }
8941
+ const columnKey = col['column-key'];
8942
+ const cellValue = rowData[columnKey];
8943
+ const widgetErrors = validateWidget(cellValue, col['widget-data-validation'], col['widget-required']);
8944
+ if (widgetErrors.length > 0) {
8945
+ return false;
8946
+ }
8947
+ if ((col.widget || 'text') === 'date') {
8948
+ const dateError = getDateColumnConstraintError(col, cellValue, rowData, translateConfig);
8949
+ if (dateError) {
8950
+ return false;
8951
+ }
8952
+ }
8953
+ }
8954
+ return true;
8955
+ };
8765
8956
  const TableCellSelect = ({ config, value, onValueChange }) => {
8766
8957
  const { translate } = useWidgetTranslation();
8767
8958
  // Use useBaseWidget to get data source options (it handles loading)
8768
8959
  const { dataSourceOptions, loading, config: widgetConfig, } = useBaseWidget({ config });
8769
8960
  const isReadonly = config['widget-readonly'] || false;
8770
- return (jsxRuntimeExports.jsxs("select", { value: value || '', onChange: (e) => onValueChange(e.target.value), disabled: isReadonly || loading, className: `w-full h-[28px] px-2 text-sm border focus:outline-none ${isReadonly || loading ? 'cursor-not-allowed' : ''} table-cell-input`, style: {
8771
- borderRadius: '10px',
8772
- borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
8773
- backgroundColor: isReadonly || loading ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8774
- }, children: [jsxRuntimeExports.jsx("option", { value: "", children: translate('common.select') || 'Select' }), dataSourceOptions.map((option) => (jsxRuntimeExports.jsx("option", { value: option.value, children: option.label }, option.value)))] }));
8961
+ return (jsxRuntimeExports.jsxs("div", { className: "table-cell-field w-full", children: [jsxRuntimeExports.jsxs("select", { value: value || '', onChange: (e) => onValueChange(e.target.value), disabled: isReadonly || loading, className: `w-full h-[28px] px-2 text-sm border focus:outline-none ${isReadonly || loading ? 'cursor-not-allowed' : ''} table-cell-input`, style: {
8962
+ borderRadius: '10px',
8963
+ borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
8964
+ backgroundColor: isReadonly || loading ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8965
+ }, children: [jsxRuntimeExports.jsx("option", { value: "", children: translate('common.select') || 'Select' }), dataSourceOptions.map((option) => (jsxRuntimeExports.jsx("option", { value: option.value, children: option.label }, option.value)))] }), jsxRuntimeExports.jsx("p", { className: "table-cell-field-error", "aria-hidden": "true", children: '\u00a0' })] }));
8775
8966
  };
8776
8967
  const SelectDisplayValue$1 = ({ config, value }) => {
8777
8968
  const { dataSourceOptions, loading } = useBaseWidget({ config });
@@ -8790,11 +8981,11 @@ const TableCellText = ({ config, value, onValueChange }) => {
8790
8981
  config['widget-data-format'];
8791
8982
  const maxLength = config['widget-data-validation']?.maxLength;
8792
8983
  const displayValue = value !== null && value !== undefined ? String(value) : '';
8793
- return (jsxRuntimeExports.jsx("input", { type: "text", value: displayValue, onChange: (e) => onValueChange(e.target.value), disabled: isReadonly, placeholder: placeholder, maxLength: maxLength, className: `w-full h-[28px] px-2 text-sm border focus:outline-none ${isReadonly ? 'cursor-not-allowed' : ''} table-cell-input`, style: {
8794
- borderRadius: '10px',
8795
- borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
8796
- backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8797
- } }));
8984
+ return (jsxRuntimeExports.jsxs("div", { className: "table-cell-field w-full", children: [jsxRuntimeExports.jsx("input", { type: "text", value: displayValue, onChange: (e) => onValueChange(e.target.value), disabled: isReadonly, placeholder: placeholder, maxLength: maxLength, className: `w-full h-[28px] px-2 text-sm border focus:outline-none ${isReadonly ? 'cursor-not-allowed' : ''} table-cell-input`, style: {
8985
+ borderRadius: '10px',
8986
+ borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
8987
+ backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8988
+ } }), jsxRuntimeExports.jsx("p", { className: "table-cell-field-error", "aria-hidden": "true", children: '\u00a0' })] }));
8798
8989
  };
8799
8990
  const TableCellNumber = ({ config, value, onValueChange }) => {
8800
8991
  const isReadonly = config['widget-readonly'] || false;
@@ -8817,11 +9008,11 @@ const TableCellNumber = ({ config, value, onValueChange }) => {
8817
9008
  onValueChange(inputValue);
8818
9009
  }
8819
9010
  };
8820
- return (jsxRuntimeExports.jsx("input", { type: "number", value: displayValue, onChange: handleChange, disabled: isReadonly, placeholder: placeholder, min: validationConfig?.min, max: validationConfig?.max, step: formatConfig?.decimalPlaces ? Math.pow(0.1, formatConfig.decimalPlaces) : undefined, className: `w-full h-[28px] px-2 text-sm border focus:outline-none text-right ${isReadonly ? 'cursor-not-allowed' : ''} table-cell-input`, style: {
8821
- borderRadius: '10px',
8822
- borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
8823
- backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8824
- } }));
9011
+ return (jsxRuntimeExports.jsxs("div", { className: "table-cell-field w-full", children: [jsxRuntimeExports.jsx("input", { type: "number", value: displayValue, onChange: handleChange, disabled: isReadonly, placeholder: placeholder, min: validationConfig?.min, max: validationConfig?.max, step: formatConfig?.decimalPlaces ? Math.pow(0.1, formatConfig.decimalPlaces) : undefined, className: `w-full h-[28px] px-2 text-sm border focus:outline-none text-right ${isReadonly ? 'cursor-not-allowed' : ''} table-cell-input`, style: {
9012
+ borderRadius: '10px',
9013
+ borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
9014
+ backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
9015
+ } }), jsxRuntimeExports.jsx("p", { className: "table-cell-field-error", "aria-hidden": "true", children: '\u00a0' })] }));
8825
9016
  };
8826
9017
  const TableCellDate = ({ config, value, rowValues, onValueChange }) => {
8827
9018
  const { translateConfig } = useWidgetTranslation();
@@ -8880,13 +9071,15 @@ const TableCellDate = ({ config, value, rowValues, onValueChange }) => {
8880
9071
  setConstraintError(error);
8881
9072
  };
8882
9073
  const hasError = Boolean(constraintError);
8883
- return (jsxRuntimeExports.jsxs("div", { className: "w-full", children: [jsxRuntimeExports.jsx("input", { type: "date", value: displayValue, onChange: handleChange, disabled: isReadonly, placeholder: placeholder, min: effectiveMinDate, max: effectiveMaxDate, title: constraintError || translateConfig(config['widget-data-tooltip']), className: `w-full h-[28px] px-2 text-sm border focus:outline-none ${isReadonly ? 'cursor-not-allowed' : ''} ${hasError ? 'border-red-500' : ''} table-cell-input`, style: {
9074
+ return (jsxRuntimeExports.jsxs("div", { className: "table-cell-field w-full", children: [jsxRuntimeExports.jsx("input", { type: "date", value: displayValue, onChange: handleChange, disabled: isReadonly, placeholder: placeholder, min: effectiveMinDate, max: effectiveMaxDate, title: constraintError || translateConfig(config['widget-data-tooltip']), className: `w-full h-[28px] px-2 text-sm border focus:outline-none ${isReadonly ? 'cursor-not-allowed' : ''} table-cell-input`, style: {
8884
9075
  borderRadius: '10px',
8885
9076
  borderColor: hasError
8886
9077
  ? 'var(--owt-color-error, #B91C1C)'
8887
9078
  : 'var(--owt-widget-input-border, #C4C4C4)',
8888
9079
  backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8889
- } }), hasError && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-xs mt-0.5 leading-tight", children: constraintError }))] }));
9080
+ } }), jsxRuntimeExports.jsx("p", { className: "table-cell-field-error text-xs mt-0.5 leading-tight", style: {
9081
+ color: hasError ? 'var(--owt-color-error, #B91C1C)' : 'transparent',
9082
+ }, "aria-live": "polite", children: constraintError ?? '\u00a0' })] }));
8890
9083
  };
8891
9084
  const TableWidget = ({ config }) => {
8892
9085
  const { value, error, touched, isEnabled, onChange, config: widgetConfig, } = useBaseWidget({ config });
@@ -8912,6 +9105,18 @@ const TableWidget = ({ config }) => {
8912
9105
  const isSectionEditMode = !isReadonly && operations.edit;
8913
9106
  // Check if any row is being edited (either manually or via section edit mode)
8914
9107
  const isAnyRowEditing = editingState !== null || isAdding;
9108
+ const canSaveEditingRow = React.useMemo(() => {
9109
+ if (!editingState) {
9110
+ return false;
9111
+ }
9112
+ return isTableRowDataValid(editingState.currentValue, columns, isReadonly, translateConfig);
9113
+ }, [editingState, columns, isReadonly, translateConfig]);
9114
+ const canSaveNewRow = React.useMemo(() => {
9115
+ if (!isAdding || !newRowData) {
9116
+ return false;
9117
+ }
9118
+ return isTableRowDataValid(newRowData, columns, isReadonly, translateConfig);
9119
+ }, [isAdding, newRowData, columns, isReadonly, translateConfig]);
8915
9120
  // Show confirmation dialog
8916
9121
  const showConfirmation = React.useCallback((message, onConfirm, onCancel) => {
8917
9122
  setConfirmationState({
@@ -9001,6 +9206,8 @@ const TableWidget = ({ config }) => {
9001
9206
  const saveEdit = React.useCallback(async () => {
9002
9207
  if (!editingState)
9003
9208
  return;
9209
+ if (!canSaveEditingRow)
9210
+ return;
9004
9211
  const rowData = editingState.currentValue;
9005
9212
  const rowIndex = editingState.rowIndex;
9006
9213
  setLoadingRowIndex(rowIndex);
@@ -9072,7 +9279,7 @@ const TableWidget = ({ config }) => {
9072
9279
  finally {
9073
9280
  setLoadingRowIndex(null);
9074
9281
  }
9075
- }, [editingState, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode, originalRows, columns, widgetConfig, dispatch]);
9282
+ }, [editingState, canSaveEditingRow, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode, originalRows, columns, widgetConfig, dispatch]);
9076
9283
  // Add new row
9077
9284
  const startAdd = React.useCallback(() => {
9078
9285
  // If there's an unsaved edit, cancel it first (no confirmation needed)
@@ -9090,6 +9297,8 @@ const TableWidget = ({ config }) => {
9090
9297
  const saveAdd = React.useCallback(async () => {
9091
9298
  if (!isAdding || !newRowData)
9092
9299
  return;
9300
+ if (!canSaveNewRow)
9301
+ return;
9093
9302
  setLoadingRowIndex(-1); // Use -1 to indicate new row
9094
9303
  try {
9095
9304
  let savedRow = { ...newRowData };
@@ -9120,7 +9329,7 @@ const TableWidget = ({ config }) => {
9120
9329
  finally {
9121
9330
  setLoadingRowIndex(null);
9122
9331
  }
9123
- }, [isAdding, newRowData, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode]);
9332
+ }, [isAdding, newRowData, canSaveNewRow, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode]);
9124
9333
  // Delete row
9125
9334
  const deleteRow = React.useCallback(async (rowIndex) => {
9126
9335
  if (isAnyRowEditing) {
@@ -9398,6 +9607,22 @@ const TableWidget = ({ config }) => {
9398
9607
  box-shadow: 0 0 0 1px var(--owt-widget-input-focus-border, #F07B1A);
9399
9608
  border-color: var(--owt-widget-input-focus-border, #F07B1A);
9400
9609
  }
9610
+
9611
+ /* Keep inputs and action buttons top-aligned when a cell shows validation text */
9612
+ .${tableWidgetId} tr.table-row-editing td {
9613
+ vertical-align: top;
9614
+ }
9615
+
9616
+ .${tableWidgetId} .table-cell-field-error {
9617
+ min-height: 1.125rem;
9618
+ }
9619
+
9620
+ .${tableWidgetId} .table-cell-actions {
9621
+ display: flex;
9622
+ flex-direction: row;
9623
+ gap: 0.5rem;
9624
+ align-items: flex-start;
9625
+ }
9401
9626
  ` }), jsxRuntimeExports.jsxs("div", { className: `table-widget-container ${tableWidgetId}`, children: [confirmationState?.show && (jsxRuntimeExports.jsx("div", { className: "fixed inset-0 flex items-center justify-center z-50", style: { backgroundColor: 'rgba(0,0,0,0.5)' }, children: jsxRuntimeExports.jsxs("div", { className: "rounded-lg p-6 max-w-md w-full mx-4", style: { backgroundColor: 'var(--owt-color-bg, #FFFFFF)' }, children: [jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold mb-4", style: { color: 'var(--owt-color-text, #011627)' }, children: translate('table.confirm') || 'Confirm Action' }), jsxRuntimeExports.jsx("p", { className: "mb-6", style: { color: 'var(--owt-color-text, #011627)' }, children: confirmationState.message }), jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-3", children: [jsxRuntimeExports.jsx("button", { onClick: confirmationState.onCancel, className: "px-4 py-2 text-sm font-medium", style: {
9402
9627
  borderRadius: 'var(--owt-btn-border-radius, 10px)',
9403
9628
  border: '1px solid var(--owt-btn-secondary-border, #C4C4C4)',
@@ -9416,7 +9641,7 @@ const TableWidget = ({ config }) => {
9416
9641
  }, children: translate('table.addRecord') || 'Add New Record' }) })), jsxRuntimeExports.jsx("div", { className: "overflow-x-auto border", style: { borderRadius: 'var(--owt-widget-table-border-radius, 15px)', borderColor: 'var(--owt-widget-table-border-color, #C4C4C4)' }, children: jsxRuntimeExports.jsxs("table", { className: "min-w-full", style: { borderCollapse: 'separate', borderSpacing: 0 }, children: [jsxRuntimeExports.jsx("thead", { style: { backgroundColor: 'var(--owt-widget-table-header-bg, #F6F6F6)' }, children: jsxRuntimeExports.jsxs("tr", { style: { borderBottom: '1px solid var(--owt-widget-table-row-divider, #E4E4E4)' }, children: [columns.map((col) => (jsxRuntimeExports.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium uppercase tracking-wider", style: { color: 'var(--owt-widget-table-header-color, #727474)' }, children: translateConfig(col['widget-label']) }, col['column-key']))), ((operations.edit || operations.remove) && !isReadonly) || isAnyRowEditing || isSectionEditMode ? (jsxRuntimeExports.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium uppercase tracking-wider", style: { color: 'var(--owt-widget-table-header-color, #727474)' }, children: translate('common.actions') || 'Actions' })) : null] }) }), jsxRuntimeExports.jsxs("tbody", { style: { backgroundColor: 'var(--owt-widget-table-body-bg, #FFFFFF)' }, children: [rows.length === 0 && !isAdding && (jsxRuntimeExports.jsx("tr", { children: jsxRuntimeExports.jsxs("td", { colSpan: columns.length + (((operations.edit || operations.remove) && !isReadonly) || isSectionEditMode ? 1 : 0), className: "px-4 py-6 text-center text-sm", style: { color: 'var(--owt-widget-table-empty-color, #727474)' }, children: [translate('table.noData') || 'No records available.', operations.add && !isReadonly && ` ${translate('table.clickToAdd') || 'Click "Add New Record" to add one.'}`] }) })), rows.map((row, rowIndex) => {
9417
9642
  const isEditing = isRowEditing(rowIndex);
9418
9643
  const isLoading = loadingRowIndex === rowIndex;
9419
- return (jsxRuntimeExports.jsxs("tr", { className: isLoading ? 'opacity-50' : '', style: {
9644
+ return (jsxRuntimeExports.jsxs("tr", { className: `${isLoading ? 'opacity-50' : ''}${isEditing ? ' table-row-editing' : ''}`, style: {
9420
9645
  borderBottom: '1px solid var(--owt-widget-table-row-divider, #E4E4E4)',
9421
9646
  backgroundColor: isEditing
9422
9647
  ? 'var(--owt-widget-table-editing-row-bg, #FBE6AA)'
@@ -9427,7 +9652,7 @@ const TableWidget = ({ config }) => {
9427
9652
  return (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: renderCell(rowIndex, col, row) }, col['column-key']));
9428
9653
  }), ((operations.edit || operations.remove) && !isReadonly) || isEditing || isSectionEditMode ? (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", style: { minWidth: '120px' }, children: isEditing ? (
9429
9654
  // Show OK (Save)/Cancel buttons when row is being edited (works in both section edit mode and normal mode)
9430
- jsxRuntimeExports.jsxs("div", { className: "flex flex-row gap-2 items-center", style: { width: '100%' }, children: [jsxRuntimeExports.jsx("button", { type: "button", onClick: saveEdit, disabled: isLoading, className: "px-3 py-1 text-xs font-medium disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap flex-shrink-0", style: {
9655
+ jsxRuntimeExports.jsxs("div", { className: "table-cell-actions", style: { width: '100%' }, children: [jsxRuntimeExports.jsx("button", { type: "button", onClick: saveEdit, disabled: isLoading || !canSaveEditingRow, className: "px-3 py-1 text-xs font-medium disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap flex-shrink-0", style: {
9431
9656
  display: 'inline-block',
9432
9657
  minWidth: '60px',
9433
9658
  backgroundColor: 'var(--owt-color-success, #16A34A)',
@@ -9454,7 +9679,7 @@ const TableWidget = ({ config }) => {
9454
9679
  backgroundColor: 'transparent',
9455
9680
  border: 'none',
9456
9681
  }, children: translate('common.remove') || 'Delete' }))] })) })) : null] }, rowIndex));
9457
- }), isAdding && newRowData && (jsxRuntimeExports.jsxs("tr", { style: { backgroundColor: 'var(--owt-widget-table-editing-row-bg, #FBE6AA)' }, children: [columns.map((col) => (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: renderCell(rows.length, col, { ...newRowData, edit_action: 'ADD' }) }, col['column-key']))), jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [jsxRuntimeExports.jsx("button", { type: "button", onClick: saveAdd, disabled: loadingRowIndex === -1, className: "px-3 py-1 text-xs disabled:opacity-50", style: {
9682
+ }), isAdding && newRowData && (jsxRuntimeExports.jsxs("tr", { className: "table-row-editing", style: { backgroundColor: 'var(--owt-widget-table-editing-row-bg, #FBE6AA)' }, children: [columns.map((col) => (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: renderCell(rows.length, col, { ...newRowData, edit_action: 'ADD' }) }, col['column-key']))), jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: jsxRuntimeExports.jsxs("div", { className: "table-cell-actions", children: [jsxRuntimeExports.jsx("button", { type: "button", onClick: saveAdd, disabled: loadingRowIndex === -1 || !canSaveNewRow, className: "px-3 py-1 text-xs disabled:opacity-50 disabled:cursor-not-allowed", style: {
9458
9683
  borderRadius: 'var(--owt-btn-border-radius, 10px)',
9459
9684
  backgroundColor: 'var(--owt-color-success, #16A34A)',
9460
9685
  color: 'var(--owt-color-bg, #FFFFFF)',
@@ -11752,6 +11977,7 @@ exports.DateTimeInputWidget = DateTimeInputWidget;
11752
11977
  exports.DialogTableWidget = DialogTableWidget;
11753
11978
  exports.DisplayWidget = DisplayWidget;
11754
11979
  exports.FileInputWidget = FileInputWidget;
11980
+ exports.GEO_LEVEL_CLEARED = GEO_LEVEL_CLEARED;
11755
11981
  exports.HeaderSectionWidget = HeaderSectionWidget;
11756
11982
  exports.IdAuthenticationWidget = IdAuthenticationWidget;
11757
11983
  exports.IterableAccordionWidget = IterableAccordionWidget;
@@ -11779,6 +12005,7 @@ exports.WidgetRenderer = WidgetRenderer;
11779
12005
  exports.applyCaseControl = applyCaseControl;
11780
12006
  exports.applyDecimalPrecision = applyDecimalPrecision;
11781
12007
  exports.applyMask = applyMask;
12008
+ exports.applySharedGeoHierarchyToValues = applySharedGeoHierarchyToValues;
11782
12009
  exports.createWidgetStore = createWidgetStore;
11783
12010
  exports.createZodSchema = createZodSchema;
11784
12011
  exports.defaultTheme = defaultTheme;
@@ -11798,13 +12025,20 @@ exports.getValueByPath = getValueByPath;
11798
12025
  exports.getWidgetValue = getWidgetValue;
11799
12026
  exports.initI18n = initI18n;
11800
12027
  exports.isAllowedKey = isAllowedKey;
12028
+ exports.isUpstreamGeoAncestor = isUpstreamGeoAncestor;
12029
+ exports.normalizeNumericDefault = normalizeNumericDefault;
11801
12030
  exports.parseDataPath = parseDataPath;
11802
12031
  exports.parseNumber = parseNumber;
11803
12032
  exports.registerDefaultWidgets = registerDefaultWidgets;
12033
+ exports.registerGeoWidgetParent = registerGeoWidgetParent;
11804
12034
  exports.removeMask = removeMask;
11805
12035
  exports.resetAll = resetAll;
12036
+ exports.resetAndSeedGeoHierarchyFromValues = resetAndSeedGeoHierarchyFromValues;
11806
12037
  exports.resetWidget = resetWidget;
12038
+ exports.resolveGeoWidgetLevelValue = resolveGeoWidgetLevelValue;
11807
12039
  exports.resolveTheme = resolveTheme;
12040
+ exports.resolveWidgetIdValue = resolveWidgetIdValue;
12041
+ exports.seedGeoHierarchyFromValues = seedGeoHierarchyFromValues;
11808
12042
  exports.setDataSource = setDataSource;
11809
12043
  exports.setError = setError;
11810
12044
  exports.setLoading = setLoading;
@@ -11819,6 +12053,7 @@ exports.transformDataSourceOptions = transformDataSourceOptions;
11819
12053
  exports.translatePanelConfig = translatePanelConfig;
11820
12054
  exports.translateUISchema = translateUISchema;
11821
12055
  exports.translateWidgetConfig = translateWidgetConfig;
12056
+ exports.unregisterGeoWidgetParent = unregisterGeoWidgetParent;
11822
12057
  exports.useBaseWidget = useBaseWidget;
11823
12058
  exports.useGeoWidgetCascade = useGeoWidgetCascade;
11824
12059
  exports.useWidgetCascade = useWidgetCascade;