@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.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
  }
@@ -672,6 +690,21 @@ const getFormattedNumberLength = (value, format) => {
672
690
  const formatted = formatNumber(typeof value === 'string' ? parseFloat(value) : value, format);
673
691
  return formatted.length;
674
692
  };
693
+ const normalizeNumericDefault = (defaultValue, format) => {
694
+ if (defaultValue === undefined) {
695
+ return undefined;
696
+ }
697
+ if (defaultValue === null || defaultValue === '') {
698
+ return null;
699
+ }
700
+ const numValue = typeof defaultValue === 'number'
701
+ ? defaultValue
702
+ : parseNumber(String(defaultValue), format);
703
+ if (numValue === null || isNaN(numValue)) {
704
+ return undefined;
705
+ }
706
+ return applyDecimalPrecision(numValue, format);
707
+ };
675
708
 
676
709
  /**
677
710
  * Date input utilities for parsing, formatting, and validation
@@ -1030,24 +1063,11 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
1030
1063
  // dependsOn can be either a data path (e.g., "person.address") or a widget-id
1031
1064
  let depValue = null;
1032
1065
  if (dataSource.dependsOn) {
1033
- // First try as data path
1034
- depValue = getValueByPath(allValues, dataSource.dependsOn);
1035
- // If not found and doesn't contain dots, try as widget-id
1036
- if ((depValue === null || depValue === undefined) && !dataSource.dependsOn.includes('.')) {
1037
- depValue = allValues[dataSource.dependsOn];
1038
- // Smart resolution: If not found at top level, try to find the dependency in the same nested object
1039
- // by looking for other keys in allValues that might contain the dependency.
1040
- // We look for objects that contain both the current widget's path (if we can guess it) and the dependency.
1041
- // But since we don't know the current widget's path here, we search for any object that has this dependency key.
1042
- if (depValue === null || depValue === undefined || depValue === '') {
1043
- for (const val of Object.values(allValues)) {
1044
- if (val && typeof val === 'object' && !Array.isArray(val) && dataSource.dependsOn in val) {
1045
- depValue = val[dataSource.dependsOn];
1046
- if (depValue !== null && depValue !== undefined && depValue !== '')
1047
- break;
1048
- }
1049
- }
1050
- }
1066
+ if (dataSource.dependsOn.includes('.')) {
1067
+ depValue = getValueByPath(allValues, dataSource.dependsOn);
1068
+ }
1069
+ else {
1070
+ depValue = resolveWidgetIdValue(allValues, dataSource.dependsOn);
1051
1071
  }
1052
1072
  if (depValue === null || depValue === undefined || depValue === '') {
1053
1073
  // If dependency is empty, return empty array
@@ -1916,6 +1936,247 @@ const WidgetProvider = ({ store, dataSourceRequestHandler, schemaData, translate
1916
1936
  return content;
1917
1937
  };
1918
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
+
1919
2180
  // Define stable empty arrays to avoid selector reference issues
1920
2181
  const EMPTY_ERRORS = [];
1921
2182
  const EMPTY_DATA_SOURCE$1 = [];
@@ -2005,6 +2266,17 @@ const useBaseWidget = (options) => {
2005
2266
  if (isLayoutWidget) {
2006
2267
  return undefined; // Layout widgets don't have values
2007
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
+ }
2008
2280
  // Try to get value from widgetId first (this should have the actual selected value)
2009
2281
  // For geo widgets with dataPath, widgetId stores the actual ID, while dataPath stores the hierarchy object
2010
2282
  let value = values[widgetId];
@@ -2214,6 +2486,14 @@ const useBaseWidget = (options) => {
2214
2486
  // Track readonly state explicitly to detect changes
2215
2487
  // Use JSON.stringify to create a stable reference for the dependency array
2216
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']]);
2217
2497
  const dataSource = config['widget-data-source'];
2218
2498
  const geoConfig = config['widget-geo-config'];
2219
2499
  // Use ref to store handler to avoid stale closures
@@ -2418,113 +2698,6 @@ const useWidgetCascade = (options) => {
2418
2698
  }, [cascadeConfig, eventBus, dataSource, widgetId, dispatch]);
2419
2699
  };
2420
2700
 
2421
- /**
2422
- * Geo Hierarchy Builder
2423
- * Manages geo hierarchy state and builds hierarchy JSON structure
2424
- */
2425
- class GeoHierarchyBuilder {
2426
- constructor() {
2427
- this.hierarchies = new Map();
2428
- }
2429
- /**
2430
- * Get or create hierarchy state for a group
2431
- */
2432
- getHierarchy(groupId = 'default') {
2433
- if (!this.hierarchies.has(groupId)) {
2434
- this.hierarchies.set(groupId, {
2435
- levels: new Map(),
2436
- order: [],
2437
- });
2438
- }
2439
- return this.hierarchies.get(groupId);
2440
- }
2441
- /**
2442
- * Add a level to the hierarchy
2443
- */
2444
- addLevel(level, level_value_id, level_value_mnemonic, groupId = 'default') {
2445
- const hierarchy = this.getHierarchy(groupId);
2446
- // If level already exists, remove it and everything after it
2447
- const existingIndex = hierarchy.order.indexOf(level);
2448
- if (existingIndex >= 0) {
2449
- // Remove this level and all subsequent levels
2450
- const levelsToRemove = hierarchy.order.slice(existingIndex);
2451
- levelsToRemove.forEach((l) => {
2452
- hierarchy.levels.delete(l);
2453
- hierarchy.order = hierarchy.order.filter((o) => o !== l);
2454
- });
2455
- }
2456
- // Add new level
2457
- hierarchy.levels.set(level, {
2458
- level,
2459
- level_value_id,
2460
- level_value_mnemonic,
2461
- });
2462
- hierarchy.order.push(level);
2463
- }
2464
- /**
2465
- * Remove a level and all levels below it
2466
- */
2467
- removeLevelAndBelow(level, groupId = 'default') {
2468
- const hierarchy = this.getHierarchy(groupId);
2469
- const index = hierarchy.order.indexOf(level);
2470
- if (index >= 0) {
2471
- // Remove this level and all subsequent levels
2472
- const levelsToRemove = hierarchy.order.slice(index);
2473
- levelsToRemove.forEach((l) => {
2474
- hierarchy.levels.delete(l);
2475
- hierarchy.order = hierarchy.order.filter((o) => o !== l);
2476
- });
2477
- }
2478
- }
2479
- /**
2480
- * Build hierarchy JSON structure
2481
- */
2482
- buildHierarchyJson(groupId = 'default') {
2483
- const hierarchy = this.getHierarchy(groupId);
2484
- if (hierarchy.order.length === 0) {
2485
- return null;
2486
- }
2487
- const hierarchyArray = hierarchy.order.map((level) => {
2488
- const data = hierarchy.levels.get(level);
2489
- return {
2490
- level: data.level,
2491
- level_value_id: data.level_value_id,
2492
- level_value_mnemonic: data.level_value_mnemonic,
2493
- };
2494
- });
2495
- const lowestLevel = hierarchy.order[hierarchy.order.length - 1];
2496
- const lowestLevelData = hierarchy.levels.get(lowestLevel);
2497
- return {
2498
- geo_lowest_level_value_id: lowestLevelData.level_value_id,
2499
- geo_code_hierarchy_json: {
2500
- hierarchy: hierarchyArray,
2501
- lowest_level_value_id: lowestLevelData.level_value_id,
2502
- },
2503
- };
2504
- }
2505
- /**
2506
- * Clear hierarchy for a group
2507
- */
2508
- clear(groupId = 'default') {
2509
- this.hierarchies.delete(groupId);
2510
- }
2511
- /**
2512
- * Clear all hierarchies
2513
- */
2514
- clearAll() {
2515
- this.hierarchies.clear();
2516
- }
2517
- /**
2518
- * Get current levels for a group
2519
- */
2520
- getLevels(groupId = 'default') {
2521
- const hierarchy = this.getHierarchy(groupId);
2522
- return hierarchy.order.map((level) => hierarchy.levels.get(level));
2523
- }
2524
- }
2525
- // Singleton instance
2526
- const geoHierarchyBuilder = new GeoHierarchyBuilder();
2527
-
2528
2701
  // Define stable empty array to avoid selector reference issues
2529
2702
  const EMPTY_DATA_SOURCE = [];
2530
2703
  /**
@@ -2544,138 +2717,108 @@ const useGeoWidgetCascade = (options) => {
2544
2717
  : 'default';
2545
2718
  const valuesRef = useRef(values);
2546
2719
  const handlerRef = useRef(dataSourceRequestHandler);
2720
+ const lastCascadePublishRef = useRef(undefined);
2547
2721
  // Keep refs updated
2548
2722
  useEffect(() => {
2549
2723
  valuesRef.current = values;
2550
2724
  handlerRef.current = dataSourceRequestHandler;
2551
2725
  }, [values, dataSourceRequestHandler]);
2552
2726
  // Get current value and data source options
2553
- const currentValue = useSelector((state) => {
2554
- // Try to get value from widgetId first (most recent selection)
2555
- let value = state.widget.values[widgetId];
2556
- // If not found in widgetId, try dataPath
2557
- if (value === undefined && dataPath) {
2558
- value = getWidgetValue(state.widget.values, dataPath, widgetId);
2559
- }
2560
- // Extract value if it's a geo hierarchy object
2561
- if (value && typeof value === 'object' && !Array.isArray(value) && geoConfig) {
2562
- const hierarchy = value.hierarchy || value.geo_code_hierarchy_json?.hierarchy;
2563
- if (Array.isArray(hierarchy)) {
2564
- const levelData = hierarchy.find((l) => l.level === geoConfig.level);
2565
- if (levelData) {
2566
- return levelData.level_value_id;
2567
- }
2568
- }
2569
- // Extended fallbacks (matching useBaseWidget)
2570
- if ('geo_lowest_level_value_id' in value) {
2571
- return value.geo_lowest_level_value_id;
2572
- }
2573
- if ('lowest_level_value_id' in value) {
2574
- return value.lowest_level_value_id;
2575
- }
2576
- if (value.geo_code_hierarchy_json?.lowest_level_value_id) {
2577
- return value.geo_code_hierarchy_json.lowest_level_value_id;
2578
- }
2579
- if (value.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
2580
- return value.geo_code_hierarchy_json.geo_lowest_level_value_id;
2581
- }
2582
- }
2583
- return value;
2584
- });
2727
+ const currentValue = useSelector((state) => geoConfig
2728
+ ? resolveGeoWidgetLevelValue(state.widget.values, widgetId, dataPath, geoConfig)
2729
+ : state.widget.values[widgetId]);
2585
2730
  // Memoize selector to avoid returning new array reference
2586
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]);
2587
2747
  useEffect(() => {
2588
2748
  if (!geoConfig || !eventBus || !dataSource || dataSource.type !== 'api') {
2589
2749
  return;
2590
2750
  }
2591
- const { level, isLastLevel, parentWidgetId } = geoConfig;
2592
- // Listen to parent widget changes
2593
- if (parentWidgetId) {
2594
- const handleParentChange = async (event) => {
2595
- if (event.widgetId !== parentWidgetId) {
2596
- return;
2597
- }
2598
- // CRITICAL: Use a small delay to ensure Redux state has been updated
2599
- // This prevents reading stale values from valuesRef
2600
- await new Promise(resolve => setTimeout(resolve, 0));
2601
- const currentValues = valuesRef.current;
2602
- const currentHandler = handlerRef.current;
2603
- // CRITICAL: Try to get parent value from event first, then from Redux
2604
- let parentValue = event.value;
2605
- if (parentValue === undefined || parentValue === null) {
2606
- parentValue = currentValues[parentWidgetId];
2607
- // If not found in top-level values, try to find it via dataPath or dependsOn
2608
- if (parentValue === undefined && dataSource.dependsOn) {
2609
- parentValue = getWidgetValue(currentValues, dataSource.dependsOn, '');
2610
- }
2611
- }
2612
- // Remove this level and all below from hierarchy
2613
- geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2614
- // Clear this widget's value
2615
- // CRITICAL: Only dispatch setValue for THIS widget, not for parent or other widgets
2616
- // setWidgetValue returns the entire updated state, but we only want to update this widget
2617
- if (dataPath) {
2618
- const updatedValues = setWidgetValue(currentValues, dataPath, widgetId, undefined);
2619
- // Only dispatch setValue for this widget's widgetId, not for parent or other widgets
2620
- // This prevents accidentally overwriting the parent widget's value
2621
- // The setWidgetValue function updates the nested structure, but we only want to
2622
- // update the top-level widgetId key, not other keys that might be in updatedValues
2623
- const newWidgetValue = updatedValues[widgetId];
2624
- if (newWidgetValue !== undefined) {
2625
- dispatch(setValue({ widgetId, value: newWidgetValue }));
2626
- }
2627
- else {
2628
- // If widgetId is not in updatedValues, the value was set in a nested path
2629
- // In this case, we need to use setValues to update the entire structure
2630
- // But we need to be careful not to overwrite the parent widget's value
2631
- // Only update keys that are related to this widget's dataPath
2632
- const dataPathStr = typeof dataPath === 'string' ? dataPath : '';
2633
- if (dataPathStr && !dataPathStr.startsWith(parentWidgetId + '.')) {
2634
- // Only update if dataPath doesn't start with parentWidgetId
2635
- // This ensures we don't accidentally overwrite the parent widget's value
2636
- dispatch(setValue({ widgetId, value: undefined }));
2637
- }
2638
- }
2639
- }
2640
- else {
2641
- 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, '');
2642
2795
  }
2643
- // Reload data source with new parent value
2644
- // CRITICAL: Use parentValue from Redux, not event.value
2645
- if (currentHandler && parentValue !== null && parentValue !== undefined) {
2646
- try {
2647
- // Merge the new parent value into current values for the API call
2648
- // This ensures getApiDataSource can find the dependency value
2649
- const updatedValues = {
2650
- ...currentValues,
2651
- [parentWidgetId]: parentValue, // Use Redux value, not event.value
2652
- };
2653
- // Extract level_id from widget-geo-config.level
2654
- const levelId = geoConfig.level;
2655
- const data = await getApiDataSource(dataSource, updatedValues, currentHandler, levelId);
2656
- // Transform to { value, label } format
2657
- const valueKey = dataSource.valueKey || 'level_value_id';
2658
- const labelKey = dataSource.labelKey || 'level_value_mnemonic';
2659
- const transformed = transformDataSourceOptions(data, valueKey, labelKey);
2660
- dispatch(setDataSource({ widgetId, data: transformed }));
2661
- }
2662
- catch (error) {
2663
- console.error('Error reloading geo data source:', error);
2664
- dispatch(setDataSource({ widgetId, data: [] }));
2665
- }
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 }));
2666
2810
  }
2667
- else {
2668
- // If parent value is cleared, clear the data source and hierarchy
2669
- geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2811
+ catch (error) {
2812
+ console.error('Error reloading geo data source:', error);
2670
2813
  dispatch(setDataSource({ widgetId, data: [] }));
2671
2814
  }
2672
- };
2673
- const unsubscribe = eventBus.subscribe('widget:change', handleParentChange);
2674
- return () => {
2675
- unsubscribe();
2676
- };
2677
- }
2678
- }, [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]);
2679
2822
  // Handle value changes to build hierarchy
2680
2823
  useEffect(() => {
2681
2824
  if (!geoConfig) {
@@ -2684,22 +2827,20 @@ const useGeoWidgetCascade = (options) => {
2684
2827
  // Skip if value is undefined (it might still be loading or rehydrating)
2685
2828
  // ONLY clear hierarchy if the value is explicitly null or empty string (user action)
2686
2829
  if (currentValue === null || currentValue === '') {
2687
- const { level } = geoConfig;
2830
+ const { level, isLastLevel } = geoConfig;
2688
2831
  geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2689
2832
  // If we have a dataPath, we need to update Redux with the cleared hierarchy
2690
2833
  if (dataPath) {
2691
- const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
2692
- let finalUpdatedValues = valuesRef.current;
2693
- // Use logic similar to the build section below to update the dataPath
2694
- if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
2695
- const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
2696
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
2697
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson?.geo_lowest_level_value_id);
2698
- }
2699
- else {
2700
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
2701
- }
2702
- 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
+ });
2703
2844
  }
2704
2845
  return;
2705
2846
  }
@@ -2750,30 +2891,20 @@ const useGeoWidgetCascade = (options) => {
2750
2891
  // Add level to hierarchy
2751
2892
  geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic, groupId);
2752
2893
  // Build and store hierarchy JSON on every change
2753
- if (dataPath) {
2754
- const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
2755
- if (hierarchyJson) {
2756
- // Fix: Avoid double nesting of geo_code_hierarchy_json
2757
- // If dataPath ends with .geo_code_hierarchy_json, we want to save the content directly to it
2758
- // and save the lowest level ID as a sibling
2759
- let finalUpdatedValues = valuesRef.current;
2760
- if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
2761
- const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
2762
- // Save hierarchy JSON content directly to dataPath (avoiding double nesting)
2763
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
2764
- // Save lowest level ID as sibling
2765
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson.geo_lowest_level_value_id);
2766
- }
2767
- else {
2768
- // Fallback if path doesn't follow the naming convention
2769
- finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
2770
- }
2771
- // CRITICAL: Use setValues for deep merge instead of replacing root keys with setValue
2772
- // setWidgetValue returns the complete updated state object with all keys preserved
2773
- dispatch(setValues(finalUpdatedValues));
2774
- }
2894
+ if (dataPath && geoHierarchyBuilder.buildHierarchyJson(groupId)) {
2895
+ dispatch(setValues(applySharedGeoHierarchyToValues(valuesRef.current, groupId, dataPath, widgetId)));
2775
2896
  }
2776
- }, [geoConfig, currentValue, widgetId, dataPath, dispatch, dataSourceOptions]);
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
+ });
2906
+ }
2907
+ }, [geoConfig, currentValue, widgetId, dataPath, dispatch, dataSourceOptions, eventBus, groupId]);
2777
2908
  };
2778
2909
 
2779
2910
  class WidgetRegistry {
@@ -7446,7 +7577,18 @@ const TextInputWidget = ({ config }) => {
7446
7577
  };
7447
7578
 
7448
7579
  const NumberInputWidget = ({ config }) => {
7449
- const { value, formattedValue, error, touched, isEnabled, onChange, onBlur, config: widgetConfig, } = useBaseWidget({ config });
7580
+ const resolvedConfig = useMemo(() => {
7581
+ const rawDefault = config['widget-data-default'];
7582
+ if (rawDefault === undefined) {
7583
+ return config;
7584
+ }
7585
+ const normalizedDefault = normalizeNumericDefault(rawDefault, config['widget-data-format']);
7586
+ if (normalizedDefault === undefined || normalizedDefault === rawDefault) {
7587
+ return config;
7588
+ }
7589
+ return { ...config, 'widget-data-default': normalizedDefault };
7590
+ }, [config]);
7591
+ const { value, formattedValue, error, touched, isEnabled, onChange, onBlur, config: widgetConfig, } = useBaseWidget({ config: resolvedConfig });
7450
7592
  const { translate, translateConfig } = useWidgetTranslation();
7451
7593
  const formatConfig = widgetConfig['widget-data-format'];
7452
7594
  const validationConfig = widgetConfig['widget-data-validation'];
@@ -8761,16 +8903,65 @@ const DisplayWidget = ({ config }) => {
8761
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 }) })] }));
8762
8904
  };
8763
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
+ };
8764
8955
  const TableCellSelect = ({ config, value, onValueChange }) => {
8765
8956
  const { translate } = useWidgetTranslation();
8766
8957
  // Use useBaseWidget to get data source options (it handles loading)
8767
8958
  const { dataSourceOptions, loading, config: widgetConfig, } = useBaseWidget({ config });
8768
8959
  const isReadonly = config['widget-readonly'] || false;
8769
- 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: {
8770
- borderRadius: '10px',
8771
- borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
8772
- backgroundColor: isReadonly || loading ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8773
- }, 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' })] }));
8774
8965
  };
8775
8966
  const SelectDisplayValue$1 = ({ config, value }) => {
8776
8967
  const { dataSourceOptions, loading } = useBaseWidget({ config });
@@ -8789,11 +8980,11 @@ const TableCellText = ({ config, value, onValueChange }) => {
8789
8980
  config['widget-data-format'];
8790
8981
  const maxLength = config['widget-data-validation']?.maxLength;
8791
8982
  const displayValue = value !== null && value !== undefined ? String(value) : '';
8792
- 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: {
8793
- borderRadius: '10px',
8794
- borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
8795
- backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8796
- } }));
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' })] }));
8797
8988
  };
8798
8989
  const TableCellNumber = ({ config, value, onValueChange }) => {
8799
8990
  const isReadonly = config['widget-readonly'] || false;
@@ -8816,11 +9007,11 @@ const TableCellNumber = ({ config, value, onValueChange }) => {
8816
9007
  onValueChange(inputValue);
8817
9008
  }
8818
9009
  };
8819
- 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: {
8820
- borderRadius: '10px',
8821
- borderColor: 'var(--owt-widget-input-border, #C4C4C4)',
8822
- backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8823
- } }));
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' })] }));
8824
9015
  };
8825
9016
  const TableCellDate = ({ config, value, rowValues, onValueChange }) => {
8826
9017
  const { translateConfig } = useWidgetTranslation();
@@ -8879,13 +9070,15 @@ const TableCellDate = ({ config, value, rowValues, onValueChange }) => {
8879
9070
  setConstraintError(error);
8880
9071
  };
8881
9072
  const hasError = Boolean(constraintError);
8882
- 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: {
8883
9074
  borderRadius: '10px',
8884
9075
  borderColor: hasError
8885
9076
  ? 'var(--owt-color-error, #B91C1C)'
8886
9077
  : 'var(--owt-widget-input-border, #C4C4C4)',
8887
9078
  backgroundColor: isReadonly ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8888
- } }), 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' })] }));
8889
9082
  };
8890
9083
  const TableWidget = ({ config }) => {
8891
9084
  const { value, error, touched, isEnabled, onChange, config: widgetConfig, } = useBaseWidget({ config });
@@ -8911,6 +9104,18 @@ const TableWidget = ({ config }) => {
8911
9104
  const isSectionEditMode = !isReadonly && operations.edit;
8912
9105
  // Check if any row is being edited (either manually or via section edit mode)
8913
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]);
8914
9119
  // Show confirmation dialog
8915
9120
  const showConfirmation = useCallback((message, onConfirm, onCancel) => {
8916
9121
  setConfirmationState({
@@ -9000,6 +9205,8 @@ const TableWidget = ({ config }) => {
9000
9205
  const saveEdit = useCallback(async () => {
9001
9206
  if (!editingState)
9002
9207
  return;
9208
+ if (!canSaveEditingRow)
9209
+ return;
9003
9210
  const rowData = editingState.currentValue;
9004
9211
  const rowIndex = editingState.rowIndex;
9005
9212
  setLoadingRowIndex(rowIndex);
@@ -9071,7 +9278,7 @@ const TableWidget = ({ config }) => {
9071
9278
  finally {
9072
9279
  setLoadingRowIndex(null);
9073
9280
  }
9074
- }, [editingState, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode, originalRows, columns, widgetConfig, dispatch]);
9281
+ }, [editingState, canSaveEditingRow, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode, originalRows, columns, widgetConfig, dispatch]);
9075
9282
  // Add new row
9076
9283
  const startAdd = useCallback(() => {
9077
9284
  // If there's an unsaved edit, cancel it first (no confirmation needed)
@@ -9089,6 +9296,8 @@ const TableWidget = ({ config }) => {
9089
9296
  const saveAdd = useCallback(async () => {
9090
9297
  if (!isAdding || !newRowData)
9091
9298
  return;
9299
+ if (!canSaveNewRow)
9300
+ return;
9092
9301
  setLoadingRowIndex(-1); // Use -1 to indicate new row
9093
9302
  try {
9094
9303
  let savedRow = { ...newRowData };
@@ -9119,7 +9328,7 @@ const TableWidget = ({ config }) => {
9119
9328
  finally {
9120
9329
  setLoadingRowIndex(null);
9121
9330
  }
9122
- }, [isAdding, newRowData, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode]);
9331
+ }, [isAdding, newRowData, canSaveNewRow, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode]);
9123
9332
  // Delete row
9124
9333
  const deleteRow = useCallback(async (rowIndex) => {
9125
9334
  if (isAnyRowEditing) {
@@ -9397,6 +9606,22 @@ const TableWidget = ({ config }) => {
9397
9606
  box-shadow: 0 0 0 1px var(--owt-widget-input-focus-border, #F07B1A);
9398
9607
  border-color: var(--owt-widget-input-focus-border, #F07B1A);
9399
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
+ }
9400
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: {
9401
9626
  borderRadius: 'var(--owt-btn-border-radius, 10px)',
9402
9627
  border: '1px solid var(--owt-btn-secondary-border, #C4C4C4)',
@@ -9415,7 +9640,7 @@ const TableWidget = ({ config }) => {
9415
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) => {
9416
9641
  const isEditing = isRowEditing(rowIndex);
9417
9642
  const isLoading = loadingRowIndex === rowIndex;
9418
- return (jsxRuntimeExports.jsxs("tr", { className: isLoading ? 'opacity-50' : '', style: {
9643
+ return (jsxRuntimeExports.jsxs("tr", { className: `${isLoading ? 'opacity-50' : ''}${isEditing ? ' table-row-editing' : ''}`, style: {
9419
9644
  borderBottom: '1px solid var(--owt-widget-table-row-divider, #E4E4E4)',
9420
9645
  backgroundColor: isEditing
9421
9646
  ? 'var(--owt-widget-table-editing-row-bg, #FBE6AA)'
@@ -9426,7 +9651,7 @@ const TableWidget = ({ config }) => {
9426
9651
  return (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: renderCell(rowIndex, col, row) }, col['column-key']));
9427
9652
  }), ((operations.edit || operations.remove) && !isReadonly) || isEditing || isSectionEditMode ? (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", style: { minWidth: '120px' }, children: isEditing ? (
9428
9653
  // Show OK (Save)/Cancel buttons when row is being edited (works in both section edit mode and normal mode)
9429
- 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: {
9430
9655
  display: 'inline-block',
9431
9656
  minWidth: '60px',
9432
9657
  backgroundColor: 'var(--owt-color-success, #16A34A)',
@@ -9453,7 +9678,7 @@ const TableWidget = ({ config }) => {
9453
9678
  backgroundColor: 'transparent',
9454
9679
  border: 'none',
9455
9680
  }, children: translate('common.remove') || 'Delete' }))] })) })) : null] }, rowIndex));
9456
- }), 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: {
9457
9682
  borderRadius: 'var(--owt-btn-border-radius, 10px)',
9458
9683
  backgroundColor: 'var(--owt-color-success, #16A34A)',
9459
9684
  color: 'var(--owt-color-bg, #FFFFFF)',
@@ -11741,5 +11966,5 @@ const translateUISchema = (schema, translate) => {
11741
11966
  };
11742
11967
  };
11743
11968
 
11744
- 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, 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 };
11745
11970
  //# sourceMappingURL=index.esm.js.map