@openg2p/registry-widgets 1.1.2-dev.0 → 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.esm.js CHANGED
@@ -173,15 +173,33 @@ const parseDataPath = (dataPath) => {
173
173
  /**
174
174
  * Get value from widget state using data path
175
175
  */
176
+ /**
177
+ * Resolve a widget-id reference in Redux values.
178
+ * Supports namespaced ids (e.g. "rv-section-0__region_code" when ref is "region_code").
179
+ */
180
+ const resolveWidgetIdValue = (values, ref) => {
181
+ if (!ref) {
182
+ return undefined;
183
+ }
184
+ if (ref.includes('.')) {
185
+ return getValueByPath(values, ref);
186
+ }
187
+ if (Object.prototype.hasOwnProperty.call(values, ref)) {
188
+ return values[ref];
189
+ }
190
+ const suffix = `__${ref}`;
191
+ for (const [key, val] of Object.entries(values)) {
192
+ if (key.endsWith(suffix)) {
193
+ return val;
194
+ }
195
+ }
196
+ return undefined;
197
+ };
176
198
  const getWidgetValue = (values, dataPath, widgetId) => {
177
199
  if (!dataPath) {
178
200
  // Fallback to widget-id if no data path
179
201
  return values[widgetId];
180
202
  }
181
- if (widgetId == "user-profile") {
182
- console.log('values', values);
183
- console.log('dataPath', dataPath);
184
- }
185
203
  if (typeof dataPath === 'string') {
186
204
  return getValueByPath(values, dataPath);
187
205
  }
@@ -1045,24 +1063,11 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
1045
1063
  // dependsOn can be either a data path (e.g., "person.address") or a widget-id
1046
1064
  let depValue = null;
1047
1065
  if (dataSource.dependsOn) {
1048
- // First try as data path
1049
- depValue = getValueByPath(allValues, dataSource.dependsOn);
1050
- // If not found and doesn't contain dots, try as widget-id
1051
- if ((depValue === null || depValue === undefined) && !dataSource.dependsOn.includes('.')) {
1052
- depValue = allValues[dataSource.dependsOn];
1053
- // Smart resolution: If not found at top level, try to find the dependency in the same nested object
1054
- // by looking for other keys in allValues that might contain the dependency.
1055
- // We look for objects that contain both the current widget's path (if we can guess it) and the dependency.
1056
- // But since we don't know the current widget's path here, we search for any object that has this dependency key.
1057
- if (depValue === null || depValue === undefined || depValue === '') {
1058
- for (const val of Object.values(allValues)) {
1059
- if (val && typeof val === 'object' && !Array.isArray(val) && dataSource.dependsOn in val) {
1060
- depValue = val[dataSource.dependsOn];
1061
- if (depValue !== null && depValue !== undefined && depValue !== '')
1062
- break;
1063
- }
1064
- }
1065
- }
1066
+ if (dataSource.dependsOn.includes('.')) {
1067
+ depValue = getValueByPath(allValues, dataSource.dependsOn);
1068
+ }
1069
+ else {
1070
+ depValue = resolveWidgetIdValue(allValues, dataSource.dependsOn);
1066
1071
  }
1067
1072
  if (depValue === null || depValue === undefined || depValue === '') {
1068
1073
  // If dependency is empty, return empty array
@@ -1931,6 +1936,247 @@ const WidgetProvider = ({ store, dataSourceRequestHandler, schemaData, translate
1931
1936
  return content;
1932
1937
  };
1933
1938
 
1939
+ /**
1940
+ * Geo Hierarchy Builder
1941
+ * Manages geo hierarchy state and builds hierarchy JSON structure
1942
+ */
1943
+ function extractLevelValueFromStored(value, geoConfig) {
1944
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1945
+ return value;
1946
+ }
1947
+ const hierarchy = value.hierarchy || value.geo_code_hierarchy_json?.hierarchy;
1948
+ if (Array.isArray(hierarchy)) {
1949
+ const levelData = hierarchy.find((l) => l.level === geoConfig.level);
1950
+ if (levelData) {
1951
+ return levelData.level_value_id;
1952
+ }
1953
+ // Level absent from hierarchy (e.g. cleared by upstream cascade) — do not use lowest-level fallback
1954
+ return undefined;
1955
+ }
1956
+ if ('geo_lowest_level_value_id' in value) {
1957
+ return value.geo_lowest_level_value_id;
1958
+ }
1959
+ if ('lowest_level_value_id' in value) {
1960
+ return value.lowest_level_value_id;
1961
+ }
1962
+ if (value.geo_code_hierarchy_json?.lowest_level_value_id) {
1963
+ return value.geo_code_hierarchy_json.lowest_level_value_id;
1964
+ }
1965
+ if (value.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
1966
+ return value.geo_code_hierarchy_json.geo_lowest_level_value_id;
1967
+ }
1968
+ return undefined;
1969
+ }
1970
+ /**
1971
+ * Resolve the display value for a geo level widget.
1972
+ * When widgetId is explicitly set in Redux (including cleared undefined/null), do not
1973
+ * fall back to shared hierarchy dataPath — that stale path was keeping grandchildren visible.
1974
+ */
1975
+ function resolveGeoWidgetLevelValue(values, widgetId, dataPath, geoConfig) {
1976
+ if (Object.prototype.hasOwnProperty.call(values, widgetId)) {
1977
+ let value = values[widgetId];
1978
+ if (value === undefined || value === null || value === '') {
1979
+ return value;
1980
+ }
1981
+ if (typeof value === 'object' && !Array.isArray(value)) {
1982
+ return extractLevelValueFromStored(value, geoConfig);
1983
+ }
1984
+ return value;
1985
+ }
1986
+ if (!dataPath) {
1987
+ return undefined;
1988
+ }
1989
+ let value = getWidgetValue(values, dataPath, widgetId);
1990
+ if (value !== null && value !== undefined && typeof value === 'object' && !Array.isArray(value)) {
1991
+ value = extractLevelValueFromStored(value, geoConfig);
1992
+ }
1993
+ return value;
1994
+ }
1995
+ /**
1996
+ * Write the in-memory geo hierarchy builder state into Redux at the shared dataPath.
1997
+ */
1998
+ function applySharedGeoHierarchyToValues(baseValues, groupId, dataPath, widgetId) {
1999
+ const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
2000
+ if (!dataPath || typeof dataPath !== 'string') {
2001
+ return baseValues;
2002
+ }
2003
+ const inner = hierarchyJson?.geo_code_hierarchy_json;
2004
+ const lowestId = hierarchyJson?.geo_lowest_level_value_id;
2005
+ if (dataPath.endsWith('.geo_code_hierarchy_json')) {
2006
+ const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
2007
+ let finalUpdatedValues = setWidgetValue(baseValues, dataPath, widgetId, inner);
2008
+ finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, lowestId);
2009
+ return finalUpdatedValues;
2010
+ }
2011
+ return setWidgetValue(baseValues, dataPath, widgetId, inner);
2012
+ }
2013
+ class GeoHierarchyBuilder {
2014
+ constructor() {
2015
+ this.hierarchies = new Map();
2016
+ }
2017
+ /**
2018
+ * Get or create hierarchy state for a group
2019
+ */
2020
+ getHierarchy(groupId = 'default') {
2021
+ if (!this.hierarchies.has(groupId)) {
2022
+ this.hierarchies.set(groupId, {
2023
+ levels: new Map(),
2024
+ order: [],
2025
+ });
2026
+ }
2027
+ return this.hierarchies.get(groupId);
2028
+ }
2029
+ /**
2030
+ * Add a level to the hierarchy
2031
+ */
2032
+ addLevel(level, level_value_id, level_value_mnemonic, groupId = 'default') {
2033
+ const hierarchy = this.getHierarchy(groupId);
2034
+ // If level already exists, remove it and everything after it
2035
+ const existingIndex = hierarchy.order.indexOf(level);
2036
+ if (existingIndex >= 0) {
2037
+ // Remove this level and all subsequent levels
2038
+ const levelsToRemove = hierarchy.order.slice(existingIndex);
2039
+ levelsToRemove.forEach((l) => {
2040
+ hierarchy.levels.delete(l);
2041
+ hierarchy.order = hierarchy.order.filter((o) => o !== l);
2042
+ });
2043
+ }
2044
+ // Add new level
2045
+ hierarchy.levels.set(level, {
2046
+ level,
2047
+ level_value_id,
2048
+ level_value_mnemonic,
2049
+ });
2050
+ hierarchy.order.push(level);
2051
+ }
2052
+ /**
2053
+ * Remove a level and all levels below it
2054
+ */
2055
+ removeLevelAndBelow(level, groupId = 'default') {
2056
+ const hierarchy = this.getHierarchy(groupId);
2057
+ const index = hierarchy.order.indexOf(level);
2058
+ if (index >= 0) {
2059
+ // Remove this level and all subsequent levels
2060
+ const levelsToRemove = hierarchy.order.slice(index);
2061
+ levelsToRemove.forEach((l) => {
2062
+ hierarchy.levels.delete(l);
2063
+ hierarchy.order = hierarchy.order.filter((o) => o !== l);
2064
+ });
2065
+ }
2066
+ }
2067
+ /**
2068
+ * Build hierarchy JSON structure
2069
+ */
2070
+ buildHierarchyJson(groupId = 'default') {
2071
+ const hierarchy = this.getHierarchy(groupId);
2072
+ if (hierarchy.order.length === 0) {
2073
+ return null;
2074
+ }
2075
+ const hierarchyArray = hierarchy.order.map((level) => {
2076
+ const data = hierarchy.levels.get(level);
2077
+ return {
2078
+ level: data.level,
2079
+ level_value_id: data.level_value_id,
2080
+ level_value_mnemonic: data.level_value_mnemonic,
2081
+ };
2082
+ });
2083
+ const lowestLevel = hierarchy.order[hierarchy.order.length - 1];
2084
+ const lowestLevelData = hierarchy.levels.get(lowestLevel);
2085
+ return {
2086
+ geo_lowest_level_value_id: lowestLevelData.level_value_id,
2087
+ geo_code_hierarchy_json: {
2088
+ hierarchy: hierarchyArray,
2089
+ lowest_level_value_id: lowestLevelData.level_value_id,
2090
+ },
2091
+ };
2092
+ }
2093
+ /**
2094
+ * Clear hierarchy for a group
2095
+ */
2096
+ clear(groupId = 'default') {
2097
+ this.hierarchies.delete(groupId);
2098
+ }
2099
+ /**
2100
+ * Clear all hierarchies
2101
+ */
2102
+ clearAll() {
2103
+ this.hierarchies.clear();
2104
+ }
2105
+ /**
2106
+ * Get current levels for a group
2107
+ */
2108
+ getLevels(groupId = 'default') {
2109
+ const hierarchy = this.getHierarchy(groupId);
2110
+ return hierarchy.order.map((level) => hierarchy.levels.get(level));
2111
+ }
2112
+ }
2113
+ // Singleton instance
2114
+ const geoHierarchyBuilder = new GeoHierarchyBuilder();
2115
+ /** Sentinel written to Redux when a geo level is cleared by cascade (distinct from "never set"). */
2116
+ const GEO_LEVEL_CLEARED = null;
2117
+ const geoWidgetParentRegistry = new Map();
2118
+ function registerGeoWidgetParent(widgetId, parentWidgetId) {
2119
+ geoWidgetParentRegistry.set(widgetId, parentWidgetId || null);
2120
+ }
2121
+ function unregisterGeoWidgetParent(widgetId) {
2122
+ geoWidgetParentRegistry.delete(widgetId);
2123
+ }
2124
+ /** True when changedWidgetId is any upstream geo parent of widgetId (not only immediate parent). */
2125
+ function isUpstreamGeoAncestor(changedWidgetId, widgetId, immediateParentWidgetId) {
2126
+ if (changedWidgetId === widgetId) {
2127
+ return false;
2128
+ }
2129
+ let cursor = immediateParentWidgetId;
2130
+ while (cursor) {
2131
+ if (cursor === changedWidgetId) {
2132
+ return true;
2133
+ }
2134
+ cursor = geoWidgetParentRegistry.get(cursor) ?? null;
2135
+ }
2136
+ return false;
2137
+ }
2138
+ function readStoredHierarchyLevels(values, dataPath, widgetId) {
2139
+ const stored = getWidgetValue(values, dataPath, widgetId);
2140
+ const hierarchy = stored?.hierarchy || stored?.geo_code_hierarchy_json?.hierarchy;
2141
+ if (!Array.isArray(hierarchy)) {
2142
+ return [];
2143
+ }
2144
+ return hierarchy.filter((entry) => entry?.level && entry.level_value_id);
2145
+ }
2146
+ function builderMatchesStored(groupId, storedLevels) {
2147
+ const builderLevels = geoHierarchyBuilder.getLevels(groupId);
2148
+ if (builderLevels.length !== storedLevels.length) {
2149
+ return false;
2150
+ }
2151
+ return storedLevels.every((stored, index) => {
2152
+ const built = builderLevels[index];
2153
+ return (built.level === stored.level &&
2154
+ String(built.level_value_id) === String(stored.level_value_id));
2155
+ });
2156
+ }
2157
+ /** Seed in-memory builder from persisted hierarchy JSON (edit-mode rehydration). */
2158
+ function seedGeoHierarchyFromValues(values, dataPath, widgetId, groupId) {
2159
+ if (!dataPath || typeof dataPath !== 'string') {
2160
+ return;
2161
+ }
2162
+ const storedLevels = readStoredHierarchyLevels(values, dataPath, widgetId);
2163
+ if (storedLevels.length === 0) {
2164
+ return;
2165
+ }
2166
+ if (builderMatchesStored(groupId, storedLevels)) {
2167
+ return;
2168
+ }
2169
+ geoHierarchyBuilder.clear(groupId);
2170
+ storedLevels.forEach((entry) => {
2171
+ geoHierarchyBuilder.addLevel(entry.level, String(entry.level_value_id), entry.level_value_mnemonic || String(entry.level_value_id), groupId);
2172
+ });
2173
+ }
2174
+ /** Force-clear builder for a group, then seed from Redux/schema values (e.g. after Cancel). */
2175
+ function resetAndSeedGeoHierarchyFromValues(values, dataPath, widgetId, groupId) {
2176
+ geoHierarchyBuilder.clear(groupId);
2177
+ seedGeoHierarchyFromValues(values, dataPath, widgetId, groupId);
2178
+ }
2179
+
1934
2180
  // Define stable empty arrays to avoid selector reference issues
1935
2181
  const EMPTY_ERRORS = [];
1936
2182
  const EMPTY_DATA_SOURCE$1 = [];
@@ -2020,6 +2266,17 @@ const useBaseWidget = (options) => {
2020
2266
  if (isLayoutWidget) {
2021
2267
  return undefined; // Layout widgets don't have values
2022
2268
  }
2269
+ const geoConfig = config['widget-geo-config'];
2270
+ if (geoConfig) {
2271
+ const value = resolveGeoWidgetLevelValue(values, widgetId, config['widget-data-path'], geoConfig);
2272
+ if (userHasSetValueRef.current) {
2273
+ return value;
2274
+ }
2275
+ if (value === null) {
2276
+ return null;
2277
+ }
2278
+ return value !== undefined ? value : config['widget-data-default'];
2279
+ }
2023
2280
  // Try to get value from widgetId first (this should have the actual selected value)
2024
2281
  // For geo widgets with dataPath, widgetId stores the actual ID, while dataPath stores the hierarchy object
2025
2282
  let value = values[widgetId];
@@ -2229,6 +2486,14 @@ const useBaseWidget = (options) => {
2229
2486
  // Track readonly state explicitly to detect changes
2230
2487
  // Use JSON.stringify to create a stable reference for the dependency array
2231
2488
  const isReadonly = config['widget-readonly'] ?? false;
2489
+ // Leaving edit mode (Cancel): allow mirror/rehydration on next Edit
2490
+ useEffect(() => {
2491
+ if (config['widget-readonly']) {
2492
+ userHasSetValueRef.current = false;
2493
+ lastMirroredValueRef.current = null;
2494
+ lastDispatchedValueRef.current = null;
2495
+ }
2496
+ }, [config['widget-readonly']]);
2232
2497
  const dataSource = config['widget-data-source'];
2233
2498
  const geoConfig = config['widget-geo-config'];
2234
2499
  // Use ref to store handler to avoid stale closures
@@ -2433,113 +2698,6 @@ const useWidgetCascade = (options) => {
2433
2698
  }, [cascadeConfig, eventBus, dataSource, widgetId, dispatch]);
2434
2699
  };
2435
2700
 
2436
- /**
2437
- * Geo Hierarchy Builder
2438
- * Manages geo hierarchy state and builds hierarchy JSON structure
2439
- */
2440
- class GeoHierarchyBuilder {
2441
- constructor() {
2442
- this.hierarchies = new Map();
2443
- }
2444
- /**
2445
- * Get or create hierarchy state for a group
2446
- */
2447
- getHierarchy(groupId = 'default') {
2448
- if (!this.hierarchies.has(groupId)) {
2449
- this.hierarchies.set(groupId, {
2450
- levels: new Map(),
2451
- order: [],
2452
- });
2453
- }
2454
- return this.hierarchies.get(groupId);
2455
- }
2456
- /**
2457
- * Add a level to the hierarchy
2458
- */
2459
- addLevel(level, level_value_id, level_value_mnemonic, groupId = 'default') {
2460
- const hierarchy = this.getHierarchy(groupId);
2461
- // If level already exists, remove it and everything after it
2462
- const existingIndex = hierarchy.order.indexOf(level);
2463
- if (existingIndex >= 0) {
2464
- // Remove this level and all subsequent levels
2465
- const levelsToRemove = hierarchy.order.slice(existingIndex);
2466
- levelsToRemove.forEach((l) => {
2467
- hierarchy.levels.delete(l);
2468
- hierarchy.order = hierarchy.order.filter((o) => o !== l);
2469
- });
2470
- }
2471
- // Add new level
2472
- hierarchy.levels.set(level, {
2473
- level,
2474
- level_value_id,
2475
- level_value_mnemonic,
2476
- });
2477
- hierarchy.order.push(level);
2478
- }
2479
- /**
2480
- * Remove a level and all levels below it
2481
- */
2482
- removeLevelAndBelow(level, groupId = 'default') {
2483
- const hierarchy = this.getHierarchy(groupId);
2484
- const index = hierarchy.order.indexOf(level);
2485
- if (index >= 0) {
2486
- // Remove this level and all subsequent levels
2487
- const levelsToRemove = hierarchy.order.slice(index);
2488
- levelsToRemove.forEach((l) => {
2489
- hierarchy.levels.delete(l);
2490
- hierarchy.order = hierarchy.order.filter((o) => o !== l);
2491
- });
2492
- }
2493
- }
2494
- /**
2495
- * Build hierarchy JSON structure
2496
- */
2497
- buildHierarchyJson(groupId = 'default') {
2498
- const hierarchy = this.getHierarchy(groupId);
2499
- if (hierarchy.order.length === 0) {
2500
- return null;
2501
- }
2502
- const hierarchyArray = hierarchy.order.map((level) => {
2503
- const data = hierarchy.levels.get(level);
2504
- return {
2505
- level: data.level,
2506
- level_value_id: data.level_value_id,
2507
- level_value_mnemonic: data.level_value_mnemonic,
2508
- };
2509
- });
2510
- const lowestLevel = hierarchy.order[hierarchy.order.length - 1];
2511
- const lowestLevelData = hierarchy.levels.get(lowestLevel);
2512
- return {
2513
- geo_lowest_level_value_id: lowestLevelData.level_value_id,
2514
- geo_code_hierarchy_json: {
2515
- hierarchy: hierarchyArray,
2516
- lowest_level_value_id: lowestLevelData.level_value_id,
2517
- },
2518
- };
2519
- }
2520
- /**
2521
- * Clear hierarchy for a group
2522
- */
2523
- clear(groupId = 'default') {
2524
- this.hierarchies.delete(groupId);
2525
- }
2526
- /**
2527
- * Clear all hierarchies
2528
- */
2529
- clearAll() {
2530
- this.hierarchies.clear();
2531
- }
2532
- /**
2533
- * Get current levels for a group
2534
- */
2535
- getLevels(groupId = 'default') {
2536
- const hierarchy = this.getHierarchy(groupId);
2537
- return hierarchy.order.map((level) => hierarchy.levels.get(level));
2538
- }
2539
- }
2540
- // Singleton instance
2541
- const geoHierarchyBuilder = new GeoHierarchyBuilder();
2542
-
2543
2701
  // Define stable empty array to avoid selector reference issues
2544
2702
  const EMPTY_DATA_SOURCE = [];
2545
2703
  /**
@@ -2559,138 +2717,108 @@ const useGeoWidgetCascade = (options) => {
2559
2717
  : 'default';
2560
2718
  const valuesRef = useRef(values);
2561
2719
  const handlerRef = useRef(dataSourceRequestHandler);
2720
+ const lastCascadePublishRef = useRef(undefined);
2562
2721
  // Keep refs updated
2563
2722
  useEffect(() => {
2564
2723
  valuesRef.current = values;
2565
2724
  handlerRef.current = dataSourceRequestHandler;
2566
2725
  }, [values, dataSourceRequestHandler]);
2567
2726
  // Get current value and data source options
2568
- const currentValue = useSelector((state) => {
2569
- // Try to get value from widgetId first (most recent selection)
2570
- let value = state.widget.values[widgetId];
2571
- // If not found in widgetId, try dataPath
2572
- if (value === undefined && dataPath) {
2573
- value = getWidgetValue(state.widget.values, dataPath, widgetId);
2574
- }
2575
- // Extract value if it's a geo hierarchy object
2576
- if (value && typeof value === 'object' && !Array.isArray(value) && geoConfig) {
2577
- const hierarchy = value.hierarchy || value.geo_code_hierarchy_json?.hierarchy;
2578
- if (Array.isArray(hierarchy)) {
2579
- const levelData = hierarchy.find((l) => l.level === geoConfig.level);
2580
- if (levelData) {
2581
- return levelData.level_value_id;
2582
- }
2583
- }
2584
- // Extended fallbacks (matching useBaseWidget)
2585
- if ('geo_lowest_level_value_id' in value) {
2586
- return value.geo_lowest_level_value_id;
2587
- }
2588
- if ('lowest_level_value_id' in value) {
2589
- return value.lowest_level_value_id;
2590
- }
2591
- if (value.geo_code_hierarchy_json?.lowest_level_value_id) {
2592
- return value.geo_code_hierarchy_json.lowest_level_value_id;
2593
- }
2594
- if (value.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
2595
- return value.geo_code_hierarchy_json.geo_lowest_level_value_id;
2596
- }
2597
- }
2598
- return value;
2599
- });
2727
+ const currentValue = useSelector((state) => geoConfig
2728
+ ? resolveGeoWidgetLevelValue(state.widget.values, widgetId, dataPath, geoConfig)
2729
+ : state.widget.values[widgetId]);
2600
2730
  // Memoize selector to avoid returning new array reference
2601
2731
  const dataSourceOptions = useSelector((state) => state.widget.dataSources[widgetId] ?? EMPTY_DATA_SOURCE);
2732
+ // Register parent chain for upstream-ancestor detection (grandparent → grandchild reset)
2733
+ useEffect(() => {
2734
+ if (!geoConfig) {
2735
+ return;
2736
+ }
2737
+ registerGeoWidgetParent(widgetId, geoConfig.parentWidgetId);
2738
+ return () => unregisterGeoWidgetParent(widgetId);
2739
+ }, [widgetId, geoConfig]);
2740
+ // Keep in-memory builder in sync with persisted hierarchy (reload, cancel→re-edit, etc.)
2741
+ useEffect(() => {
2742
+ if (!geoConfig || typeof dataPath !== 'string') {
2743
+ return;
2744
+ }
2745
+ seedGeoHierarchyFromValues(values, dataPath, widgetId, groupId);
2746
+ }, [geoConfig, dataPath, widgetId, groupId, values]);
2602
2747
  useEffect(() => {
2603
2748
  if (!geoConfig || !eventBus || !dataSource || dataSource.type !== 'api') {
2604
2749
  return;
2605
2750
  }
2606
- const { level, isLastLevel, parentWidgetId } = geoConfig;
2607
- // Listen to parent widget changes
2608
- if (parentWidgetId) {
2609
- const handleParentChange = async (event) => {
2610
- if (event.widgetId !== parentWidgetId) {
2611
- return;
2612
- }
2613
- // CRITICAL: Use a small delay to ensure Redux state has been updated
2614
- // This prevents reading stale values from valuesRef
2615
- await new Promise(resolve => setTimeout(resolve, 0));
2616
- const currentValues = valuesRef.current;
2617
- const currentHandler = handlerRef.current;
2618
- // CRITICAL: Try to get parent value from event first, then from Redux
2619
- let parentValue = event.value;
2620
- if (parentValue === undefined || parentValue === null) {
2621
- parentValue = currentValues[parentWidgetId];
2622
- // If not found in top-level values, try to find it via dataPath or dependsOn
2623
- if (parentValue === undefined && dataSource.dependsOn) {
2624
- parentValue = getWidgetValue(currentValues, dataSource.dependsOn, '');
2625
- }
2626
- }
2627
- // Remove this level and all below from hierarchy
2628
- geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2629
- // Clear this widget's value
2630
- // CRITICAL: Only dispatch setValue for THIS widget, not for parent or other widgets
2631
- // setWidgetValue returns the entire updated state, but we only want to update this widget
2632
- if (dataPath) {
2633
- const updatedValues = setWidgetValue(currentValues, dataPath, widgetId, undefined);
2634
- // Only dispatch setValue for this widget's widgetId, not for parent or other widgets
2635
- // This prevents accidentally overwriting the parent widget's value
2636
- // The setWidgetValue function updates the nested structure, but we only want to
2637
- // update the top-level widgetId key, not other keys that might be in updatedValues
2638
- const newWidgetValue = updatedValues[widgetId];
2639
- if (newWidgetValue !== undefined) {
2640
- dispatch(setValue({ widgetId, value: newWidgetValue }));
2641
- }
2642
- else {
2643
- // If widgetId is not in updatedValues, the value was set in a nested path
2644
- // In this case, we need to use setValues to update the entire structure
2645
- // But we need to be careful not to overwrite the parent widget's value
2646
- // Only update keys that are related to this widget's dataPath
2647
- const dataPathStr = typeof dataPath === 'string' ? dataPath : '';
2648
- if (dataPathStr && !dataPathStr.startsWith(parentWidgetId + '.')) {
2649
- // Only update if dataPath doesn't start with parentWidgetId
2650
- // This ensures we don't accidentally overwrite the parent widget's value
2651
- dispatch(setValue({ widgetId, value: undefined }));
2652
- }
2653
- }
2654
- }
2655
- else {
2656
- dispatch(setValue({ widgetId, value: undefined }));
2751
+ const { level, isLastLevel, parentWidgetId: rawParentWidgetId } = geoConfig;
2752
+ const parentWidgetId = rawParentWidgetId || null;
2753
+ if (!parentWidgetId) {
2754
+ return;
2755
+ }
2756
+ const clearThisLevel = (baseValues) => {
2757
+ geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2758
+ if (dataPath) {
2759
+ dispatch(setValues(applySharedGeoHierarchyToValues(baseValues, groupId, dataPath, widgetId)));
2760
+ }
2761
+ dispatch(setValue({ widgetId, value: GEO_LEVEL_CLEARED }));
2762
+ if (!isLastLevel) {
2763
+ eventBus.publish({
2764
+ type: 'widget:change',
2765
+ widgetId,
2766
+ value: GEO_LEVEL_CLEARED,
2767
+ timestamp: Date.now(),
2768
+ });
2769
+ }
2770
+ dispatch(setDataSource({ widgetId, data: [] }));
2771
+ };
2772
+ const handleParentChange = async (event) => {
2773
+ const isDirectParent = event.widgetId === parentWidgetId;
2774
+ const isAncestor = isUpstreamGeoAncestor(event.widgetId, widgetId, parentWidgetId);
2775
+ if (!isDirectParent && !isAncestor) {
2776
+ return;
2777
+ }
2778
+ await new Promise(resolve => setTimeout(resolve, 0));
2779
+ const currentValues = valuesRef.current;
2780
+ const currentHandler = handlerRef.current;
2781
+ // Grandparent (or higher) changed: clear this level; only immediate parent drives reload
2782
+ if (isAncestor && !isDirectParent) {
2783
+ clearThisLevel(currentValues);
2784
+ return;
2785
+ }
2786
+ const parentCleared = event.value === undefined ||
2787
+ event.value === null ||
2788
+ event.value === '' ||
2789
+ event.value === GEO_LEVEL_CLEARED;
2790
+ let parentValue = event.value;
2791
+ if (!parentCleared && (parentValue === undefined || parentValue === null)) {
2792
+ parentValue = currentValues[parentWidgetId];
2793
+ if (parentValue === undefined && dataSource.dependsOn) {
2794
+ parentValue = getWidgetValue(currentValues, dataSource.dependsOn, '');
2657
2795
  }
2658
- // Reload data source with new parent value
2659
- // CRITICAL: Use parentValue from Redux, not event.value
2660
- if (currentHandler && parentValue !== null && parentValue !== undefined) {
2661
- try {
2662
- // Merge the new parent value into current values for the API call
2663
- // This ensures getApiDataSource can find the dependency value
2664
- const updatedValues = {
2665
- ...currentValues,
2666
- [parentWidgetId]: parentValue, // Use Redux value, not event.value
2667
- };
2668
- // Extract level_id from widget-geo-config.level
2669
- const levelId = geoConfig.level;
2670
- const data = await getApiDataSource(dataSource, updatedValues, currentHandler, levelId);
2671
- // Transform to { value, label } format
2672
- const valueKey = dataSource.valueKey || 'level_value_id';
2673
- const labelKey = dataSource.labelKey || 'level_value_mnemonic';
2674
- const transformed = transformDataSourceOptions(data, valueKey, labelKey);
2675
- dispatch(setDataSource({ widgetId, data: transformed }));
2676
- }
2677
- catch (error) {
2678
- console.error('Error reloading geo data source:', error);
2679
- dispatch(setDataSource({ widgetId, data: [] }));
2680
- }
2796
+ }
2797
+ clearThisLevel(currentValues);
2798
+ if (currentHandler && parentValue !== null && parentValue !== undefined && parentValue !== '') {
2799
+ try {
2800
+ const updatedValues = {
2801
+ ...currentValues,
2802
+ [parentWidgetId]: parentValue,
2803
+ };
2804
+ const levelId = geoConfig.level;
2805
+ const data = await getApiDataSource(dataSource, updatedValues, currentHandler, levelId);
2806
+ const valueKey = dataSource.valueKey || 'level_value_id';
2807
+ const labelKey = dataSource.labelKey || 'level_value_mnemonic';
2808
+ const transformed = transformDataSourceOptions(data, valueKey, labelKey);
2809
+ dispatch(setDataSource({ widgetId, data: transformed }));
2681
2810
  }
2682
- else {
2683
- // If parent value is cleared, clear the data source and hierarchy
2684
- geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2811
+ catch (error) {
2812
+ console.error('Error reloading geo data source:', error);
2685
2813
  dispatch(setDataSource({ widgetId, data: [] }));
2686
2814
  }
2687
- };
2688
- const unsubscribe = eventBus.subscribe('widget:change', handleParentChange);
2689
- return () => {
2690
- unsubscribe();
2691
- };
2692
- }
2693
- }, [geoConfig, eventBus, dataSource, widgetId, dataPath, dispatch]);
2815
+ }
2816
+ };
2817
+ const unsubscribe = eventBus.subscribe('widget:change', handleParentChange);
2818
+ return () => {
2819
+ unsubscribe();
2820
+ };
2821
+ }, [geoConfig, eventBus, dataSource, widgetId, dataPath, dispatch, groupId]);
2694
2822
  // Handle value changes to build hierarchy
2695
2823
  useEffect(() => {
2696
2824
  if (!geoConfig) {
@@ -2699,22 +2827,20 @@ const useGeoWidgetCascade = (options) => {
2699
2827
  // Skip if value is undefined (it might still be loading or rehydrating)
2700
2828
  // ONLY clear hierarchy if the value is explicitly null or empty string (user action)
2701
2829
  if (currentValue === null || currentValue === '') {
2702
- const { level } = geoConfig;
2830
+ const { level, isLastLevel } = geoConfig;
2703
2831
  geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2704
2832
  // If we have a dataPath, we need to update Redux with the cleared hierarchy
2705
2833
  if (dataPath) {
2706
- const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
2707
- let finalUpdatedValues = valuesRef.current;
2708
- // Use logic similar to the build section below to update the dataPath
2709
- if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
2710
- const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
2711
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
2712
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson?.geo_lowest_level_value_id);
2713
- }
2714
- else {
2715
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
2716
- }
2717
- dispatch(setValues(finalUpdatedValues));
2834
+ dispatch(setValues(applySharedGeoHierarchyToValues(valuesRef.current, groupId, dataPath, widgetId)));
2835
+ }
2836
+ if (!isLastLevel && eventBus && lastCascadePublishRef.current !== GEO_LEVEL_CLEARED) {
2837
+ lastCascadePublishRef.current = GEO_LEVEL_CLEARED;
2838
+ eventBus.publish({
2839
+ type: 'widget:change',
2840
+ widgetId,
2841
+ value: GEO_LEVEL_CLEARED,
2842
+ timestamp: Date.now(),
2843
+ });
2718
2844
  }
2719
2845
  return;
2720
2846
  }
@@ -2765,30 +2891,20 @@ const useGeoWidgetCascade = (options) => {
2765
2891
  // Add level to hierarchy
2766
2892
  geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic, groupId);
2767
2893
  // Build and store hierarchy JSON on every change
2768
- if (dataPath) {
2769
- const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
2770
- if (hierarchyJson) {
2771
- // Fix: Avoid double nesting of geo_code_hierarchy_json
2772
- // If dataPath ends with .geo_code_hierarchy_json, we want to save the content directly to it
2773
- // and save the lowest level ID as a sibling
2774
- let finalUpdatedValues = valuesRef.current;
2775
- if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
2776
- const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
2777
- // Save hierarchy JSON content directly to dataPath (avoiding double nesting)
2778
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
2779
- // Save lowest level ID as sibling
2780
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson.geo_lowest_level_value_id);
2781
- }
2782
- else {
2783
- // Fallback if path doesn't follow the naming convention
2784
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
2785
- }
2786
- // CRITICAL: Use setValues for deep merge instead of replacing root keys with setValue
2787
- // setWidgetValue returns the complete updated state object with all keys preserved
2788
- dispatch(setValues(finalUpdatedValues));
2789
- }
2894
+ if (dataPath && geoHierarchyBuilder.buildHierarchyJson(groupId)) {
2895
+ dispatch(setValues(applySharedGeoHierarchyToValues(valuesRef.current, groupId, dataPath, widgetId)));
2896
+ }
2897
+ // Notify descendants when this level changes via hierarchy/rehydration (handleChange may not run).
2898
+ if (!isLastLevel && eventBus && lastCascadePublishRef.current !== level_value_id) {
2899
+ lastCascadePublishRef.current = level_value_id;
2900
+ eventBus.publish({
2901
+ type: 'widget:change',
2902
+ widgetId,
2903
+ value: level_value_id,
2904
+ timestamp: Date.now(),
2905
+ });
2790
2906
  }
2791
- }, [geoConfig, currentValue, widgetId, dataPath, dispatch, dataSourceOptions]);
2907
+ }, [geoConfig, currentValue, widgetId, dataPath, dispatch, dataSourceOptions, eventBus, groupId]);
2792
2908
  };
2793
2909
 
2794
2910
  class WidgetRegistry {
@@ -8787,16 +8903,65 @@ const DisplayWidget = ({ config }) => {
8787
8903
  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 }) })] }));
8788
8904
  };
8789
8905
 
8906
+ const getDateColumnConstraintError = (column, cellValue, rowValues, translateConfig) => {
8907
+ const displayValue = cellValue && typeof cellValue === 'string' ? cellValue.split('T')[0] : '';
8908
+ if (!displayValue) {
8909
+ return null;
8910
+ }
8911
+ const optionsConfig = column['widget-data-options'];
8912
+ const formatConfig = column['widget-data-format'];
8913
+ const dateConstraint = formatConfig?.dateConstraint || 'any';
8914
+ const minDate = optionsConfig?.minDate;
8915
+ const maxDate = optionsConfig?.maxDate;
8916
+ const minDateField = optionsConfig?.minDateField;
8917
+ const maxDateField = optionsConfig?.maxDateField;
8918
+ const minDateMessage = optionsConfig?.minDateMessage
8919
+ ? translateConfig(optionsConfig.minDateMessage)
8920
+ : undefined;
8921
+ const maxDateMessage = optionsConfig?.maxDateMessage
8922
+ ? translateConfig(optionsConfig.maxDateMessage)
8923
+ : undefined;
8924
+ const resolveSiblingDate = (fieldRef) => {
8925
+ if (!fieldRef) {
8926
+ return undefined;
8927
+ }
8928
+ const raw = getValueByPath(rowValues, fieldRef) ?? rowValues[fieldRef];
8929
+ return resolveDateBoundFromFieldValue(raw);
8930
+ };
8931
+ const effectiveMinDate = mergeMinDateBounds(getMinDate(dateConstraint, minDate), resolveSiblingDate(minDateField));
8932
+ const effectiveMaxDate = mergeMaxDateBounds(getMaxDate(dateConstraint, maxDate), resolveSiblingDate(maxDateField));
8933
+ return validateDateConstraints(displayValue, effectiveMinDate, effectiveMaxDate, dateConstraint, { minDateMessage, maxDateMessage });
8934
+ };
8935
+ const isTableRowDataValid = (rowData, columns, tableReadonly, translateConfig) => {
8936
+ for (const col of columns) {
8937
+ if (tableReadonly || col['widget-readonly'] === true) {
8938
+ continue;
8939
+ }
8940
+ const columnKey = col['column-key'];
8941
+ const cellValue = rowData[columnKey];
8942
+ const widgetErrors = validateWidget(cellValue, col['widget-data-validation'], col['widget-required']);
8943
+ if (widgetErrors.length > 0) {
8944
+ return false;
8945
+ }
8946
+ if ((col.widget || 'text') === 'date') {
8947
+ const dateError = getDateColumnConstraintError(col, cellValue, rowData, translateConfig);
8948
+ if (dateError) {
8949
+ return false;
8950
+ }
8951
+ }
8952
+ }
8953
+ return true;
8954
+ };
8790
8955
  const TableCellSelect = ({ config, value, onValueChange }) => {
8791
8956
  const { translate } = useWidgetTranslation();
8792
8957
  // Use useBaseWidget to get data source options (it handles loading)
8793
8958
  const { dataSourceOptions, loading, config: widgetConfig, } = useBaseWidget({ config });
8794
8959
  const isReadonly = config['widget-readonly'] || false;
8795
- 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: {
8796
- borderRadius: '10px',
8797
- borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
8798
- backgroundColor: isReadonly || loading ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8799
- }, children: [jsxRuntimeExports.jsx("option", { value: "", children: translate('common.select') || 'Select' }), dataSourceOptions.map((option) => (jsxRuntimeExports.jsx("option", { value: option.value, children: option.label }, option.value)))] }));
8960
+ 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: {
8961
+ borderRadius: '10px',
8962
+ borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
8963
+ backgroundColor: isReadonly || loading ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8964
+ }, 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' })] }));
8800
8965
  };
8801
8966
  const SelectDisplayValue$1 = ({ config, value }) => {
8802
8967
  const { dataSourceOptions, loading } = useBaseWidget({ config });
@@ -8815,11 +8980,11 @@ const TableCellText = ({ config, value, onValueChange }) => {
8815
8980
  config['widget-data-format'];
8816
8981
  const maxLength = config['widget-data-validation']?.maxLength;
8817
8982
  const displayValue = value !== null && value !== undefined ? String(value) : '';
8818
- 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: {
8819
- borderRadius: '10px',
8820
- borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
8821
- backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8822
- } }));
8983
+ 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: {
8984
+ borderRadius: '10px',
8985
+ borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
8986
+ backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8987
+ } }), jsxRuntimeExports.jsx("p", { className: "table-cell-field-error", "aria-hidden": "true", children: '\u00a0' })] }));
8823
8988
  };
8824
8989
  const TableCellNumber = ({ config, value, onValueChange }) => {
8825
8990
  const isReadonly = config['widget-readonly'] || false;
@@ -8842,11 +9007,11 @@ const TableCellNumber = ({ config, value, onValueChange }) => {
8842
9007
  onValueChange(inputValue);
8843
9008
  }
8844
9009
  };
8845
- 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: {
8846
- borderRadius: '10px',
8847
- borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
8848
- backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8849
- } }));
9010
+ 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: {
9011
+ borderRadius: '10px',
9012
+ borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
9013
+ backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
9014
+ } }), jsxRuntimeExports.jsx("p", { className: "table-cell-field-error", "aria-hidden": "true", children: '\u00a0' })] }));
8850
9015
  };
8851
9016
  const TableCellDate = ({ config, value, rowValues, onValueChange }) => {
8852
9017
  const { translateConfig } = useWidgetTranslation();
@@ -8905,13 +9070,15 @@ const TableCellDate = ({ config, value, rowValues, onValueChange }) => {
8905
9070
  setConstraintError(error);
8906
9071
  };
8907
9072
  const hasError = Boolean(constraintError);
8908
- 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: {
9073
+ 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: {
8909
9074
  borderRadius: '10px',
8910
9075
  borderColor: hasError
8911
9076
  ? 'var(--owt-color-error, #B91C1C)'
8912
9077
  : 'var(--owt-widget-input-border, #C4C4C4)',
8913
9078
  backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8914
- } }), hasError && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-xs mt-0.5 leading-tight", children: constraintError }))] }));
9079
+ } }), jsxRuntimeExports.jsx("p", { className: "table-cell-field-error text-xs mt-0.5 leading-tight", style: {
9080
+ color: hasError ? 'var(--owt-color-error, #B91C1C)' : 'transparent',
9081
+ }, "aria-live": "polite", children: constraintError ?? '\u00a0' })] }));
8915
9082
  };
8916
9083
  const TableWidget = ({ config }) => {
8917
9084
  const { value, error, touched, isEnabled, onChange, config: widgetConfig, } = useBaseWidget({ config });
@@ -8937,6 +9104,18 @@ const TableWidget = ({ config }) => {
8937
9104
  const isSectionEditMode = !isReadonly && operations.edit;
8938
9105
  // Check if any row is being edited (either manually or via section edit mode)
8939
9106
  const isAnyRowEditing = editingState !== null || isAdding;
9107
+ const canSaveEditingRow = useMemo(() => {
9108
+ if (!editingState) {
9109
+ return false;
9110
+ }
9111
+ return isTableRowDataValid(editingState.currentValue, columns, isReadonly, translateConfig);
9112
+ }, [editingState, columns, isReadonly, translateConfig]);
9113
+ const canSaveNewRow = useMemo(() => {
9114
+ if (!isAdding || !newRowData) {
9115
+ return false;
9116
+ }
9117
+ return isTableRowDataValid(newRowData, columns, isReadonly, translateConfig);
9118
+ }, [isAdding, newRowData, columns, isReadonly, translateConfig]);
8940
9119
  // Show confirmation dialog
8941
9120
  const showConfirmation = useCallback((message, onConfirm, onCancel) => {
8942
9121
  setConfirmationState({
@@ -9026,6 +9205,8 @@ const TableWidget = ({ config }) => {
9026
9205
  const saveEdit = useCallback(async () => {
9027
9206
  if (!editingState)
9028
9207
  return;
9208
+ if (!canSaveEditingRow)
9209
+ return;
9029
9210
  const rowData = editingState.currentValue;
9030
9211
  const rowIndex = editingState.rowIndex;
9031
9212
  setLoadingRowIndex(rowIndex);
@@ -9097,7 +9278,7 @@ const TableWidget = ({ config }) => {
9097
9278
  finally {
9098
9279
  setLoadingRowIndex(null);
9099
9280
  }
9100
- }, [editingState, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode, originalRows, columns, widgetConfig, dispatch]);
9281
+ }, [editingState, canSaveEditingRow, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode, originalRows, columns, widgetConfig, dispatch]);
9101
9282
  // Add new row
9102
9283
  const startAdd = useCallback(() => {
9103
9284
  // If there's an unsaved edit, cancel it first (no confirmation needed)
@@ -9115,6 +9296,8 @@ const TableWidget = ({ config }) => {
9115
9296
  const saveAdd = useCallback(async () => {
9116
9297
  if (!isAdding || !newRowData)
9117
9298
  return;
9299
+ if (!canSaveNewRow)
9300
+ return;
9118
9301
  setLoadingRowIndex(-1); // Use -1 to indicate new row
9119
9302
  try {
9120
9303
  let savedRow = { ...newRowData };
@@ -9145,7 +9328,7 @@ const TableWidget = ({ config }) => {
9145
9328
  finally {
9146
9329
  setLoadingRowIndex(null);
9147
9330
  }
9148
- }, [isAdding, newRowData, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode]);
9331
+ }, [isAdding, newRowData, canSaveNewRow, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode]);
9149
9332
  // Delete row
9150
9333
  const deleteRow = useCallback(async (rowIndex) => {
9151
9334
  if (isAnyRowEditing) {
@@ -9423,6 +9606,22 @@ const TableWidget = ({ config }) => {
9423
9606
  box-shadow: 0 0 0 1px var(--owt-widget-input-focus-border, #F07B1A);
9424
9607
  border-color: var(--owt-widget-input-focus-border, #F07B1A);
9425
9608
  }
9609
+
9610
+ /* Keep inputs and action buttons top-aligned when a cell shows validation text */
9611
+ .${tableWidgetId} tr.table-row-editing td {
9612
+ vertical-align: top;
9613
+ }
9614
+
9615
+ .${tableWidgetId} .table-cell-field-error {
9616
+ min-height: 1.125rem;
9617
+ }
9618
+
9619
+ .${tableWidgetId} .table-cell-actions {
9620
+ display: flex;
9621
+ flex-direction: row;
9622
+ gap: 0.5rem;
9623
+ align-items: flex-start;
9624
+ }
9426
9625
  ` }), 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: {
9427
9626
  borderRadius: 'var(--owt-btn-border-radius, 10px)',
9428
9627
  border: '1px solid var(--owt-btn-secondary-border, #C4C4C4)',
@@ -9441,7 +9640,7 @@ const TableWidget = ({ config }) => {
9441
9640
  }, 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) => {
9442
9641
  const isEditing = isRowEditing(rowIndex);
9443
9642
  const isLoading = loadingRowIndex === rowIndex;
9444
- return (jsxRuntimeExports.jsxs("tr", { className: isLoading ? 'opacity-50' : '', style: {
9643
+ return (jsxRuntimeExports.jsxs("tr", { className: `${isLoading ? 'opacity-50' : ''}${isEditing ? ' table-row-editing' : ''}`, style: {
9445
9644
  borderBottom: '1px solid var(--owt-widget-table-row-divider, #E4E4E4)',
9446
9645
  backgroundColor: isEditing
9447
9646
  ? 'var(--owt-widget-table-editing-row-bg, #FBE6AA)'
@@ -9452,7 +9651,7 @@ const TableWidget = ({ config }) => {
9452
9651
  return (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: renderCell(rowIndex, col, row) }, col['column-key']));
9453
9652
  }), ((operations.edit || operations.remove) && !isReadonly) || isEditing || isSectionEditMode ? (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", style: { minWidth: '120px' }, children: isEditing ? (
9454
9653
  // Show OK (Save)/Cancel buttons when row is being edited (works in both section edit mode and normal mode)
9455
- 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: {
9654
+ 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: {
9456
9655
  display: 'inline-block',
9457
9656
  minWidth: '60px',
9458
9657
  backgroundColor: 'var(--owt-color-success, #16A34A)',
@@ -9479,7 +9678,7 @@ const TableWidget = ({ config }) => {
9479
9678
  backgroundColor: 'transparent',
9480
9679
  border: 'none',
9481
9680
  }, children: translate('common.remove') || 'Delete' }))] })) })) : null] }, rowIndex));
9482
- }), 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: {
9681
+ }), 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: {
9483
9682
  borderRadius: 'var(--owt-btn-border-radius, 10px)',
9484
9683
  backgroundColor: 'var(--owt-color-success, #16A34A)',
9485
9684
  color: 'var(--owt-color-bg, #FFFFFF)',
@@ -11767,5 +11966,5 @@ const translateUISchema = (schema, translate) => {
11767
11966
  };
11768
11967
  };
11769
11968
 
11770
- export { ArrayWidget, BooleanWidget, CheckboxWidget, CurrencyInputWidget, DateInputWidget, DateTimeInputWidget, DialogTableWidget, 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, normalizeNumericDefault, 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 };
11969
+ export { ArrayWidget, BooleanWidget, CheckboxWidget, CurrencyInputWidget, DateInputWidget, DateTimeInputWidget, DialogTableWidget, DisplayWidget, FileInputWidget, GEO_LEVEL_CLEARED, 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, applySharedGeoHierarchyToValues, createWidgetStore, createZodSchema, defaultTheme, evaluateCondition, filterByCharacterType, formatCurrency, formatDate, formatNumber, formatPhone, formatValue, geoHierarchyBuilder, getApiDataSource, getFormattedNumberLength, getSchemaDataSource, getStaticDataSource, getValueByPath, getWidgetValue, initI18n, isAllowedKey, isUpstreamGeoAncestor, normalizeNumericDefault, parseDataPath, parseNumber, registerDefaultWidgets, registerGeoWidgetParent, removeMask, resetAll, resetAndSeedGeoHierarchyFromValues, resetWidget, resolveGeoWidgetLevelValue, resolveTheme, resolveWidgetIdValue, seedGeoHierarchyFromValues, setDataSource, setError, setLoading, setTouched, setValue, setValueByPath, setValues, setWidgetValue, shouldEnableWidget, shouldShowWidget, transformDataSourceOptions, translatePanelConfig, translateUISchema, translateWidgetConfig, unregisterGeoWidgetParent, useBaseWidget, useGeoWidgetCascade, useWidgetCascade, useWidgetContext, useWidgetEventBus, useWidgetTheme, useWidgetTranslation, validateNumericValue, validateWidget, widgetRegistry };
11771
11970
  //# sourceMappingURL=index.esm.js.map