@openg2p/registry-widgets 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/components/PanelRenderer.d.ts +3 -3
  2. package/dist/components/PanelRenderer.d.ts.map +1 -1
  3. package/dist/components/SectionBuilder/JSONEditorPanel.d.ts +15 -0
  4. package/dist/components/SectionBuilder/JSONEditorPanel.d.ts.map +1 -0
  5. package/dist/components/SectionBuilder/PropertyEditor.d.ts +15 -0
  6. package/dist/components/SectionBuilder/PropertyEditor.d.ts.map +1 -0
  7. package/dist/components/SectionBuilder/SectionBuilder.d.ts +13 -0
  8. package/dist/components/SectionBuilder/SectionBuilder.d.ts.map +1 -0
  9. package/dist/components/SectionBuilder/SectionTree.d.ts +26 -0
  10. package/dist/components/SectionBuilder/SectionTree.d.ts.map +1 -0
  11. package/dist/components/SectionBuilder/VisualBuilderPanel.d.ts +22 -0
  12. package/dist/components/SectionBuilder/VisualBuilderPanel.d.ts.map +1 -0
  13. package/dist/components/SectionBuilder/index.d.ts +9 -0
  14. package/dist/components/SectionBuilder/index.d.ts.map +1 -0
  15. package/dist/components/SectionBuilder/schemas.d.ts +1947 -0
  16. package/dist/components/SectionBuilder/schemas.d.ts.map +1 -0
  17. package/dist/components/SectionRenderer.d.ts +5 -5
  18. package/dist/components/SectionRenderer.d.ts.map +1 -1
  19. package/dist/components/SectionsContainer.d.ts +4 -3
  20. package/dist/components/SectionsContainer.d.ts.map +1 -1
  21. package/dist/components/WidgetProvider.d.ts +4 -4
  22. package/dist/components/WidgetProvider.d.ts.map +1 -1
  23. package/dist/components/WidgetRenderer.d.ts +1 -1
  24. package/dist/components/WidgetRenderer.d.ts.map +1 -1
  25. package/dist/events/WidgetEventBus.d.ts +43 -0
  26. package/dist/events/WidgetEventBus.d.ts.map +1 -0
  27. package/dist/events/types.d.ts +27 -0
  28. package/dist/events/types.d.ts.map +1 -0
  29. package/dist/hooks/useBaseWidget.d.ts +2 -2
  30. package/dist/hooks/useBaseWidget.d.ts.map +1 -1
  31. package/dist/hooks/useGeoWidgetCascade.d.ts +12 -0
  32. package/dist/hooks/useGeoWidgetCascade.d.ts.map +1 -0
  33. package/dist/hooks/useWidgetCascade.d.ts +12 -0
  34. package/dist/hooks/useWidgetCascade.d.ts.map +1 -0
  35. package/dist/hooks/useWidgetEventBus.d.ts +9 -0
  36. package/dist/hooks/useWidgetEventBus.d.ts.map +1 -0
  37. package/dist/index.d.ts +253 -20
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.esm.js +3154 -336
  40. package/dist/index.esm.js.map +1 -1
  41. package/dist/index.js +3162 -334
  42. package/dist/index.js.map +1 -1
  43. package/dist/store/widgetSlice.d.ts.map +1 -1
  44. package/dist/types/index.d.ts +38 -3
  45. package/dist/types/index.d.ts.map +1 -1
  46. package/dist/utils/dataSource.d.ts +3 -2
  47. package/dist/utils/dataSource.d.ts.map +1 -1
  48. package/dist/utils/geoHierarchy.d.ts +53 -0
  49. package/dist/utils/geoHierarchy.d.ts.map +1 -0
  50. package/dist/utils/schemaNamespace.d.ts +12 -0
  51. package/dist/utils/schemaNamespace.d.ts.map +1 -0
  52. package/dist/widgets/SelectWidget.d.ts.map +1 -1
  53. package/dist/widgets/TableWidget.d.ts.map +1 -1
  54. package/package.json +20 -12
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ var React = require('react');
5
5
  var reactRedux = require('react-redux');
6
6
  var zod = require('zod');
7
7
  var reactDom = require('react-dom');
8
+ var jsonEditReact = require('json-edit-react');
8
9
  var i18n = require('i18next');
9
10
  var reactI18next = require('react-i18next');
10
11
 
@@ -15,19 +16,58 @@ const initialState = {
15
16
  loading: {},
16
17
  dataSources: {},
17
18
  };
19
+ // Track recent setValue calls to detect race conditions
20
+ const recentSetValueCalls = new Map();
18
21
  const widgetSlice = toolkit.createSlice({
19
22
  name: 'widget',
20
23
  initialState,
21
24
  reducers: {
22
25
  setValue: (state, action) => {
23
- state.values[action.payload.widgetId] = action.payload.value;
26
+ const { widgetId, value } = action.payload;
27
+ const previousValue = state.values[widgetId];
28
+ const now = Date.now();
29
+ // Check if we recently set a different value (race condition detection)
30
+ const recentCall = recentSetValueCalls.get(widgetId);
31
+ const isRaceCondition = recentCall &&
32
+ recentCall.timestamp > now - 100 && // Within 100ms
33
+ recentCall.value !== value &&
34
+ recentCall.value === previousValue; // Trying to set back to old value
35
+ // Track this call
36
+ recentSetValueCalls.set(widgetId, { value, timestamp: now });
37
+ // Clean up old entries (older than 1 second)
38
+ for (const [key, call] of recentSetValueCalls.entries()) {
39
+ if (now - call.timestamp > 1000) {
40
+ recentSetValueCalls.delete(key);
41
+ }
42
+ }
43
+ // CRITICAL: Prevent race condition - if we just set a new value, don't allow setting the old value back
44
+ if (isRaceCondition) {
45
+ return; // Don't update the value
46
+ }
47
+ state.values[widgetId] = value;
24
48
  // Clear errors when value changes
25
- if (state.errors[action.payload.widgetId]) {
26
- delete state.errors[action.payload.widgetId];
49
+ if (state.errors[widgetId]) {
50
+ delete state.errors[widgetId];
27
51
  }
28
52
  },
29
53
  setValues: (state, action) => {
30
- state.values = { ...state.values, ...action.payload };
54
+ // setWidgetValue returns the complete updated state object with all keys preserved
55
+ // We need to do a deep merge to preserve nested structures that aren't in the payload
56
+ // But since setWidgetValue already includes all existing data, we can merge at top level
57
+ // However, we need to be careful: if payload has nested objects, we need to deep merge them
58
+ const merged = { ...state.values };
59
+ for (const [key, value] of Object.entries(action.payload)) {
60
+ if (value !== null && typeof value === 'object' && !Array.isArray(value) &&
61
+ merged[key] !== null && typeof merged[key] === 'object' && !Array.isArray(merged[key])) {
62
+ // Deep merge nested objects to preserve properties not in the payload
63
+ merged[key] = { ...merged[key], ...value };
64
+ }
65
+ else {
66
+ // Replace primitives, arrays, or null values, or if target is not an object
67
+ merged[key] = value;
68
+ }
69
+ }
70
+ state.values = merged;
31
71
  },
32
72
  setError: (state, action) => {
33
73
  if (action.payload.errors.length > 0) {
@@ -276,7 +316,7 @@ const validateWidget = (value, validation, required = false) => {
276
316
  }
277
317
  catch (error) {
278
318
  if (error instanceof zod.z.ZodError) {
279
- errors.push(...error.errors.map((e) => e.message));
319
+ errors.push(...error.issues.map((e) => e.message));
280
320
  }
281
321
  else {
282
322
  errors.push('Validation failed');
@@ -735,31 +775,97 @@ const getStaticDataSource = (dataSource) => {
735
775
  };
736
776
  /**
737
777
  * Get API data source options
778
+ * Uses dataSourceRequestHandler to make API calls through host application
738
779
  */
739
- const getApiDataSource = async (dataSource, allValues, apiAdapter) => {
740
- if (!apiAdapter) {
741
- console.warn('API adapter not provided for API data source');
780
+ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler, levelId // Optional level_id from widget-geo-config.level
781
+ ) => {
782
+ if (!dataSourceRequestHandler) {
783
+ console.error('[getApiDataSource] dataSourceRequestHandler is required for API data sources');
742
784
  return [];
743
785
  }
744
786
  try {
745
787
  // Get dependency value if exists
746
- const params = {};
788
+ // dependsOn can be either a data path (e.g., "person.address") or a widget-id
789
+ let depValue = null;
747
790
  if (dataSource.dependsOn) {
748
- const depValue = getValueByPath(allValues, dataSource.dependsOn);
749
- if (depValue !== null && depValue !== undefined && depValue !== '') {
750
- params[dataSource.dependsOn.split('.').pop() || 'filter'] = depValue;
791
+ // First try as data path
792
+ depValue = getValueByPath(allValues, dataSource.dependsOn);
793
+ // If not found and doesn't contain dots, try as widget-id
794
+ if ((depValue === null || depValue === undefined) && !dataSource.dependsOn.includes('.')) {
795
+ depValue = allValues[dataSource.dependsOn];
751
796
  }
752
- else {
797
+ if (depValue === null || depValue === undefined || depValue === '') {
753
798
  // If dependency is empty, return empty array
754
799
  return [];
755
800
  }
756
801
  }
757
- const response = await apiAdapter(dataSource.url, {
758
- method: dataSource.method || 'GET',
759
- headers: dataSource.headers,
760
- body: dataSource.body,
761
- params,
762
- });
802
+ // Build request parameters
803
+ const method = dataSource.method || 'GET';
804
+ // Extract static params from dataSource
805
+ // Include explicit params object and any additional fields (like level_id)
806
+ const staticParams = { ...dataSource.params };
807
+ // Extract additional fields that aren't part of the standard ApiDataSource interface
808
+ // These are fields like level_id that might be directly on the dataSource
809
+ // BUT: level_id should come from widget-geo-config.level, not from dataSource
810
+ const standardFields = ['type', 'service', 'endpoint', 'url', 'method', 'dependsOn', 'valueKey', 'labelKey', 'headers', 'body', 'params', 'level_id'];
811
+ for (const [key, value] of Object.entries(dataSource)) {
812
+ if (!standardFields.includes(key) && value !== undefined && value !== null) {
813
+ staticParams[key] = value;
814
+ }
815
+ }
816
+ // If levelId is provided (from widget-geo-config.level), use it instead of any level_id in dataSource
817
+ if (levelId) {
818
+ staticParams.level_id = levelId;
819
+ }
820
+ // Build request params object
821
+ const requestParams = { ...staticParams };
822
+ // Add dependency value to params
823
+ if (dataSource.dependsOn && depValue !== null && depValue !== undefined) {
824
+ // Extract the actual value ID if depValue is an object
825
+ const parentValueId = typeof depValue === 'object' && depValue !== null
826
+ ? (depValue.level_value_id || depValue.id || depValue.value || depValue)
827
+ : depValue;
828
+ // For geo APIs, use parent_level_value_id
829
+ if (staticParams.level_id) {
830
+ requestParams.parent_level_value_id = parentValueId;
831
+ }
832
+ else {
833
+ // For other APIs, use the dependency field name as param key
834
+ const paramKey = dataSource.dependsOn.split('.').pop() || 'filter';
835
+ requestParams[paramKey] = parentValueId;
836
+ }
837
+ }
838
+ else if (staticParams.level_id) {
839
+ // First level has no parent
840
+ requestParams.parent_level_value_id = null;
841
+ }
842
+ // Get service mnemonic and endpoint (required)
843
+ const service = dataSource.service;
844
+ const endpoint = dataSource.endpoint;
845
+ if (!service) {
846
+ console.error('[getApiDataSource] API data source missing service mnemonic. Use "service" field instead of "url"');
847
+ return [];
848
+ }
849
+ if (!endpoint) {
850
+ console.error('[getApiDataSource] API data source missing endpoint. Use "endpoint" field to specify the operation (e.g., "get_g2p_geo_level_values")');
851
+ return [];
852
+ }
853
+ let response;
854
+ try {
855
+ response = await dataSourceRequestHandler(service, endpoint, method, requestParams, {
856
+ headers: dataSource.headers,
857
+ });
858
+ }
859
+ catch (error) {
860
+ console.error('[getApiDataSource] Handler error:', error);
861
+ throw error;
862
+ }
863
+ // Handle OpenG2P response format (response_body.response_payload)
864
+ if (response && typeof response === 'object') {
865
+ if (response.response_body?.response_payload && Array.isArray(response.response_body.response_payload)) {
866
+ return response.response_body.response_payload;
867
+ }
868
+ }
763
869
  // Handle array response
764
870
  if (Array.isArray(response)) {
765
871
  return response;
@@ -806,49 +912,134 @@ const transformDataSourceOptions = (data, valueKey, labelKey) => {
806
912
  }));
807
913
  };
808
914
 
915
+ const WidgetEventBusContext = React.createContext(null);
916
+ /**
917
+ * Hook to access the widget event bus from context
918
+ */
919
+ const useWidgetEventBus = () => {
920
+ return React.useContext(WidgetEventBusContext);
921
+ };
922
+
809
923
  // Define stable empty arrays to avoid selector reference issues
810
924
  const EMPTY_ERRORS = [];
811
- const EMPTY_DATA_SOURCE = [];
925
+ const EMPTY_DATA_SOURCE$1 = [];
812
926
  const useBaseWidget = (options) => {
813
- const { config, apiAdapter, schemaData, onValueChange } = options;
927
+ const { config, dataSourceRequestHandler, schemaData, onValueChange } = options;
814
928
  const dispatch = reactRedux.useDispatch();
929
+ const eventBus = useWidgetEventBus();
815
930
  const widgetId = config['widget-id'];
816
931
  // Get state from Redux
817
932
  const values = reactRedux.useSelector((state) => state.widget.values);
818
933
  const errors = reactRedux.useSelector((state) => state.widget.errors[widgetId] ?? EMPTY_ERRORS);
819
934
  const touched = reactRedux.useSelector((state) => state.widget.touched[widgetId] || false);
820
935
  const loading = reactRedux.useSelector((state) => state.widget.loading[widgetId] || false);
821
- const dataSourceOptions = reactRedux.useSelector((state) => state.widget.dataSources[widgetId] ?? EMPTY_DATA_SOURCE);
936
+ const dataSourceOptions = reactRedux.useSelector((state) => state.widget.dataSources[widgetId] ?? EMPTY_DATA_SOURCE$1);
822
937
  // Skip value handling for layout widgets (they don't store data values)
823
938
  // Infer layout from widget-type
824
939
  const isLayoutWidget = config['widget-type'] === 'layout';
940
+ // Track if user has explicitly set a value to prevent default from overwriting
941
+ const userHasSetValueRef = React.useRef(false);
942
+ // Use ref for values to avoid stale closures in handleChange
943
+ const valuesRef = React.useRef(values);
944
+ React.useEffect(() => {
945
+ valuesRef.current = values;
946
+ }, [values]);
947
+ // Track last dispatched value to prevent duplicate dispatches
948
+ const lastDispatchedValueRef = React.useRef(null);
825
949
  // Get current value
826
950
  const currentValue = React.useMemo(() => {
827
951
  if (isLayoutWidget) {
828
952
  return undefined; // Layout widgets don't have values
829
953
  }
830
- const value = getWidgetValue(values, config['widget-data-path'], widgetId);
954
+ // Try to get value from widgetId first (this should have the actual selected value)
955
+ // For geo widgets with dataPath, widgetId stores the actual ID, while dataPath stores the hierarchy object
956
+ let value = values[widgetId];
957
+ // If widgetId doesn't have a value, try dataPath
958
+ if (value === undefined && config['widget-data-path']) {
959
+ value = getWidgetValue(values, config['widget-data-path'], widgetId);
960
+ // CRITICAL: For geo widgets with dataPath, the value might be stored as a hierarchy object
961
+ // Extract the actual value (geo_lowest_level_value_id) if it's an object
962
+ const geoConfig = config['widget-geo-config'];
963
+ if (geoConfig?.isLastLevel && value && typeof value === 'object' && !Array.isArray(value)) {
964
+ // If it's a geo hierarchy object, extract the actual value
965
+ if ('geo_lowest_level_value_id' in value) {
966
+ value = value.geo_lowest_level_value_id;
967
+ }
968
+ else if ('value' in value) {
969
+ value = value.value;
970
+ }
971
+ else if ('id' in value) {
972
+ value = value.id;
973
+ }
974
+ }
975
+ }
976
+ // If value is still undefined and user has set a value, try reading from widgetId as backup
977
+ // This handles cases where dataPath lookup might fail temporarily
978
+ if (value === undefined && userHasSetValueRef.current && values[widgetId] !== undefined) {
979
+ value = values[widgetId];
980
+ }
981
+ // If user has explicitly set a value, always return it (even if undefined/null)
982
+ // This prevents the default from overwriting user selections
983
+ if (userHasSetValueRef.current) {
984
+ return value;
985
+ }
986
+ // Only fall back to default if user hasn't set a value yet
987
+ // But check if value is explicitly null (user cleared it) vs undefined (never set)
988
+ if (value === null) {
989
+ return null; // User explicitly cleared it, don't use default
990
+ }
831
991
  return value !== undefined ? value : config['widget-data-default'];
832
992
  }, [values, config, widgetId, isLayoutWidget]);
833
- // Initialize default value (skip for layout widgets)
993
+ // Initialize default value only once on mount (skip for layout widgets)
834
994
  React.useEffect(() => {
835
995
  if (isLayoutWidget) {
836
996
  return;
837
997
  }
838
- if (config['widget-data-default'] !== undefined && currentValue === undefined) {
998
+ // Only initialize default if value is undefined and user hasn't set a value yet
999
+ if (!userHasSetValueRef.current && config['widget-data-default'] !== undefined && currentValue === undefined) {
839
1000
  handleChange(config['widget-data-default'], false);
840
1001
  }
841
1002
  // eslint-disable-next-line react-hooks/exhaustive-deps
842
- }, [isLayoutWidget]); // handleChange and currentValue are stable or handled separately
1003
+ }, [isLayoutWidget]); // Only run once on mount
843
1004
  // Handle value change
1005
+ // CRITICAL: Don't include 'values' in dependency array - it causes the callback to be recreated
1006
+ // every time values change, which can lead to stale closures and double dispatches
844
1007
  const handleChange = React.useCallback((newValue, validate = true) => {
845
- const updatedValues = setWidgetValue(values, config['widget-data-path'], widgetId, newValue);
846
- // Update Redux store
847
- Object.entries(updatedValues).forEach(([key, value]) => {
848
- if (key !== widgetId || value !== values[key]) {
849
- dispatch(setValue({ widgetId: key, value }));
1008
+ // Use valuesRef to get the latest values, not the stale closure value
1009
+ const currentValues = valuesRef.current;
1010
+ const currentValue = currentValues[widgetId] || getWidgetValue(currentValues, config['widget-data-path'], widgetId);
1011
+ // CRITICAL: Prevent setting the same value (avoids unnecessary dispatches and potential loops)
1012
+ if (currentValue === newValue) {
1013
+ return;
1014
+ }
1015
+ // Mark that user has set a value (unless this is the default initialization)
1016
+ if (newValue !== config['widget-data-default'] || userHasSetValueRef.current) {
1017
+ userHasSetValueRef.current = true;
1018
+ }
1019
+ // CRITICAL FIX: If there's no dataPath, just set the value directly
1020
+ // If there's a dataPath, we need to update both the widgetId and the dataPath
1021
+ if (!config['widget-data-path']) {
1022
+ // No dataPath: just set the value directly by widgetId
1023
+ // CRITICAL: Check if we just dispatched this value to prevent duplicate dispatches
1024
+ if (lastDispatchedValueRef.current === newValue) {
1025
+ return;
850
1026
  }
851
- });
1027
+ lastDispatchedValueRef.current = newValue;
1028
+ dispatch(setValue({ widgetId, value: newValue }));
1029
+ }
1030
+ else {
1031
+ // Has dataPath: update both widgetId and dataPath
1032
+ // CRITICAL: Create updated values object with newValue already set
1033
+ // This prevents setWidgetValue from reading stale values
1034
+ const currentValuesWithUpdate = {
1035
+ ...valuesRef.current,
1036
+ [widgetId]: newValue, // Ensure widgetId has the new value
1037
+ };
1038
+ const updatedValues = setWidgetValue(currentValuesWithUpdate, config['widget-data-path'], widgetId, newValue);
1039
+ // setWidgetValue returns the complete updated structure with all existing data preserved
1040
+ // Use setValues to update the entire state with deep merge
1041
+ dispatch(setValues(updatedValues));
1042
+ }
852
1043
  // Validate if needed
853
1044
  if (validate) {
854
1045
  const validationErrors = validateWidget(newValue, config['widget-data-validation'], config['widget-required']);
@@ -858,14 +1049,36 @@ const useBaseWidget = (options) => {
858
1049
  if (onValueChange) {
859
1050
  onValueChange(widgetId, newValue);
860
1051
  }
861
- }, [values, config, widgetId, dispatch, onValueChange]);
1052
+ // Publish widget:change event
1053
+ // Skip publishing for last-level geo widgets (no child widgets waiting)
1054
+ const geoConfig = config['widget-geo-config'];
1055
+ const isLastLevelGeo = geoConfig?.isLastLevel === true;
1056
+ if (eventBus && !isLastLevelGeo) {
1057
+ eventBus.publish({
1058
+ type: 'widget:change',
1059
+ widgetId,
1060
+ value: newValue,
1061
+ timestamp: Date.now(),
1062
+ });
1063
+ }
1064
+ }, [config, widgetId, dispatch, onValueChange, eventBus] // Removed 'values' to prevent stale closures
1065
+ );
862
1066
  // Handle blur
863
1067
  const handleBlur = React.useCallback(() => {
864
1068
  dispatch(setTouched({ widgetId, touched: true }));
865
1069
  // Validate on blur
866
1070
  const validationErrors = validateWidget(currentValue, config['widget-data-validation'], config['widget-required']);
867
1071
  dispatch(setError({ widgetId, errors: validationErrors }));
868
- }, [currentValue, config, widgetId, dispatch]);
1072
+ // Publish widget:blur event
1073
+ if (eventBus) {
1074
+ eventBus.publish({
1075
+ type: 'widget:blur',
1076
+ widgetId,
1077
+ value: currentValue,
1078
+ timestamp: Date.now(),
1079
+ });
1080
+ }
1081
+ }, [currentValue, config, widgetId, dispatch, eventBus]);
869
1082
  // Get field value helper
870
1083
  const getFieldValue = React.useCallback((path) => {
871
1084
  return getWidgetValue(values, path, '');
@@ -895,33 +1108,102 @@ const useBaseWidget = (options) => {
895
1108
  }
896
1109
  return formatValue(currentValue, config['widget-data-format'], config.widget);
897
1110
  }, [currentValue, config]);
1111
+ // Track readonly state explicitly to detect changes
1112
+ // Use JSON.stringify to create a stable reference for the dependency array
1113
+ const isReadonly = config['widget-readonly'] ?? false;
1114
+ const dataSource = config['widget-data-source'];
1115
+ const geoConfig = config['widget-geo-config'];
1116
+ // Use ref to store handler to avoid stale closures
1117
+ const handlerRef = React.useRef(dataSourceRequestHandler);
1118
+ React.useEffect(() => {
1119
+ handlerRef.current = dataSourceRequestHandler;
1120
+ }, [dataSourceRequestHandler]);
1121
+ // Create a stable key for the config to detect changes
1122
+ // This ensures the effect runs when widget-readonly changes
1123
+ const apiService = dataSource?.type === 'api' ? dataSource.service : '';
1124
+ const apiEndpoint = dataSource?.type === 'api' ? dataSource.endpoint : '';
1125
+ const configKey = `${widgetId}-${isReadonly}-${dataSource?.type || 'none'}-${apiService}-${apiEndpoint}`;
898
1126
  // Handle data source loading
899
1127
  React.useEffect(() => {
900
- const dataSource = config['widget-data-source'];
901
1128
  if (!dataSource) {
902
1129
  return;
903
1130
  }
1131
+ // For API data sources, check if widget is readonly
1132
+ // According to PRD: "Level 1 geo widgets load on widget mount or when entering edit mode"
1133
+ // So we should only load API data sources when widget is NOT readonly
1134
+ if (dataSource.type === 'api' && isReadonly) {
1135
+ return;
1136
+ }
1137
+ // For widgets with dependencies, check if dependency value exists
1138
+ if (dataSource.type === 'api' && dataSource.dependsOn) {
1139
+ // Check if dependency value exists
1140
+ let depValue = null;
1141
+ if (dataSource.dependsOn.includes('.')) {
1142
+ depValue = getWidgetValue(values, dataSource.dependsOn, '');
1143
+ }
1144
+ else {
1145
+ depValue = values[dataSource.dependsOn];
1146
+ }
1147
+ // If dependency is empty, don't load (will load when dependency has value)
1148
+ if (depValue === null || depValue === undefined || depValue === '') {
1149
+ return;
1150
+ }
1151
+ }
904
1152
  const loadDataSource = async () => {
1153
+ // Get current handler from ref to avoid stale closures
1154
+ // Also check prop directly as fallback (for initial render or when ref not updated yet)
1155
+ const currentHandler = handlerRef.current || dataSourceRequestHandler;
1156
+ // Only check for handler when we actually need it (inside the async function)
1157
+ // This avoids false errors during React Strict Mode double-invocation
1158
+ // If handler isn't available yet, silently skip - React will retry when it's ready
905
1159
  try {
1160
+ if (dataSource.type === 'api' && !currentHandler) {
1161
+ // Silently skip if handler isn't available yet (common during React Strict Mode double-invocation)
1162
+ // React will call this effect again when the handler is ready
1163
+ return;
1164
+ }
906
1165
  dispatch(setLoading({ widgetId, loading: true }));
907
1166
  let data = [];
908
1167
  if (dataSource.type === 'static') {
909
1168
  data = getStaticDataSource(dataSource);
910
1169
  }
911
1170
  else if (dataSource.type === 'api') {
912
- data = await getApiDataSource(dataSource, values, apiAdapter);
1171
+ if (!currentHandler) {
1172
+ // Silently skip if handler isn't available yet
1173
+ dispatch(setLoading({ widgetId, loading: false }));
1174
+ dispatch(setDataSource({ widgetId, data: [] }));
1175
+ return;
1176
+ }
1177
+ // Extract level_id from widget-geo-config.level if available
1178
+ const levelId = geoConfig?.level;
1179
+ data = await getApiDataSource(dataSource, values, currentHandler, levelId);
913
1180
  }
914
1181
  else if (dataSource.type === 'schema') {
915
1182
  data = getSchemaDataSource(dataSource, schemaData || {});
916
1183
  }
917
1184
  // Transform to { value, label } format
918
- const valueKey = dataSource.type === 'static' ? undefined : dataSource.valueKey;
919
- const labelKey = dataSource.type === 'static' ? undefined : dataSource.labelKey;
1185
+ // For geo widgets, default to level_value_id and level_value_mnemonic
1186
+ let valueKey;
1187
+ let labelKey;
1188
+ if (dataSource.type === 'static') {
1189
+ valueKey = undefined;
1190
+ labelKey = undefined;
1191
+ }
1192
+ else if (geoConfig) {
1193
+ // Geo widgets: default to level_value_id and level_value_mnemonic
1194
+ valueKey = dataSource.valueKey || 'level_value_id';
1195
+ labelKey = dataSource.labelKey || 'level_value_mnemonic';
1196
+ }
1197
+ else {
1198
+ // Non-geo widgets: use specified keys or undefined
1199
+ valueKey = dataSource.valueKey;
1200
+ labelKey = dataSource.labelKey;
1201
+ }
920
1202
  const transformed = transformDataSourceOptions(data, valueKey, labelKey);
921
1203
  dispatch(setDataSource({ widgetId, data: transformed }));
922
1204
  }
923
1205
  catch (error) {
924
- console.error('Error loading data source:', error);
1206
+ console.error(`[useBaseWidget] ERROR loading data source for ${widgetId}:`, error);
925
1207
  dispatch(setDataSource({ widgetId, data: [] }));
926
1208
  }
927
1209
  finally {
@@ -929,7 +1211,9 @@ const useBaseWidget = (options) => {
929
1211
  }
930
1212
  };
931
1213
  loadDataSource();
932
- }, [config['widget-data-source'], values, apiAdapter, schemaData, widgetId, dispatch]);
1214
+ // Use configKey to ensure effect runs when readonly state changes
1215
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1216
+ }, [configKey, values, dataSourceRequestHandler, schemaData, widgetId, dispatch]);
933
1217
  return {
934
1218
  widgetId,
935
1219
  value: currentValue,
@@ -948,6 +1232,357 @@ const useBaseWidget = (options) => {
948
1232
  };
949
1233
  };
950
1234
 
1235
+ /**
1236
+ * Hook for general widget cascade functionality
1237
+ * Handles listening to parent widget changes and reloading data sources
1238
+ */
1239
+ const useWidgetCascade = (options) => {
1240
+ const { config, dataSourceRequestHandler, values } = options;
1241
+ const dispatch = reactRedux.useDispatch();
1242
+ const eventBus = useWidgetEventBus();
1243
+ const widgetId = config['widget-id'];
1244
+ const cascadeConfig = config['widget-cascade'];
1245
+ const dataSource = config['widget-data-source'];
1246
+ const valuesRef = React.useRef(values);
1247
+ const handlerRef = React.useRef(dataSourceRequestHandler);
1248
+ // Keep refs updated
1249
+ React.useEffect(() => {
1250
+ valuesRef.current = values;
1251
+ handlerRef.current = dataSourceRequestHandler;
1252
+ }, [values, dataSourceRequestHandler]);
1253
+ React.useEffect(() => {
1254
+ if (!cascadeConfig || !eventBus || !dataSource || dataSource.type !== 'api') {
1255
+ return;
1256
+ }
1257
+ const { listenTo, onEvent = 'widget:change', clearOnChange = true, reloadOnChange = true, debounce, throttle } = cascadeConfig;
1258
+ if (listenTo.length === 0) {
1259
+ return;
1260
+ }
1261
+ const handleEvent = async (event) => {
1262
+ // Check if this event is from a parent we're listening to
1263
+ if (!listenTo.includes(event.widgetId)) {
1264
+ return;
1265
+ }
1266
+ const currentValues = valuesRef.current;
1267
+ const currentHandler = handlerRef.current;
1268
+ // Clear value if configured
1269
+ if (clearOnChange) {
1270
+ dispatch(setValue({ widgetId, value: undefined }));
1271
+ }
1272
+ // Reload data source if configured
1273
+ if (reloadOnChange && currentHandler) {
1274
+ try {
1275
+ const data = await getApiDataSource(dataSource, currentValues, currentHandler);
1276
+ // Transform to { value, label } format
1277
+ const valueKey = dataSource.valueKey;
1278
+ const labelKey = dataSource.labelKey;
1279
+ const transformed = transformDataSourceOptions(data, valueKey, labelKey);
1280
+ dispatch(setDataSource({ widgetId, data: transformed }));
1281
+ }
1282
+ catch (error) {
1283
+ console.error('Error reloading data source in cascade:', error);
1284
+ dispatch(setDataSource({ widgetId, data: [] }));
1285
+ }
1286
+ }
1287
+ };
1288
+ // Subscribe to events
1289
+ const unsubscribe = eventBus.subscribe(onEvent, handleEvent, { debounce, throttle });
1290
+ return () => {
1291
+ unsubscribe();
1292
+ };
1293
+ }, [cascadeConfig, eventBus, dataSource, widgetId, dispatch]);
1294
+ };
1295
+
1296
+ /**
1297
+ * Geo Hierarchy Builder
1298
+ * Manages geo hierarchy state and builds hierarchy JSON structure
1299
+ */
1300
+ class GeoHierarchyBuilder {
1301
+ constructor() {
1302
+ this.hierarchies = new Map();
1303
+ }
1304
+ /**
1305
+ * Get or create hierarchy state for a group
1306
+ */
1307
+ getHierarchy(groupId = 'default') {
1308
+ if (!this.hierarchies.has(groupId)) {
1309
+ this.hierarchies.set(groupId, {
1310
+ levels: new Map(),
1311
+ order: [],
1312
+ });
1313
+ }
1314
+ return this.hierarchies.get(groupId);
1315
+ }
1316
+ /**
1317
+ * Add a level to the hierarchy
1318
+ */
1319
+ addLevel(level, level_value_id, level_value_mnemonic, groupId = 'default') {
1320
+ const hierarchy = this.getHierarchy(groupId);
1321
+ // If level already exists, remove it and everything after it
1322
+ const existingIndex = hierarchy.order.indexOf(level);
1323
+ if (existingIndex >= 0) {
1324
+ // Remove this level and all subsequent levels
1325
+ const levelsToRemove = hierarchy.order.slice(existingIndex);
1326
+ levelsToRemove.forEach((l) => {
1327
+ hierarchy.levels.delete(l);
1328
+ hierarchy.order = hierarchy.order.filter((o) => o !== l);
1329
+ });
1330
+ }
1331
+ // Add new level
1332
+ hierarchy.levels.set(level, {
1333
+ level,
1334
+ level_value_id,
1335
+ level_value_mnemonic,
1336
+ });
1337
+ hierarchy.order.push(level);
1338
+ }
1339
+ /**
1340
+ * Remove a level and all levels below it
1341
+ */
1342
+ removeLevelAndBelow(level, groupId = 'default') {
1343
+ const hierarchy = this.getHierarchy(groupId);
1344
+ const index = hierarchy.order.indexOf(level);
1345
+ if (index >= 0) {
1346
+ // Remove this level and all subsequent levels
1347
+ const levelsToRemove = hierarchy.order.slice(index);
1348
+ levelsToRemove.forEach((l) => {
1349
+ hierarchy.levels.delete(l);
1350
+ hierarchy.order = hierarchy.order.filter((o) => o !== l);
1351
+ });
1352
+ }
1353
+ }
1354
+ /**
1355
+ * Build hierarchy JSON structure
1356
+ */
1357
+ buildHierarchyJson(groupId = 'default') {
1358
+ const hierarchy = this.getHierarchy(groupId);
1359
+ if (hierarchy.order.length === 0) {
1360
+ return null;
1361
+ }
1362
+ const hierarchyArray = hierarchy.order.map((level) => {
1363
+ const data = hierarchy.levels.get(level);
1364
+ return {
1365
+ level: data.level,
1366
+ level_value_id: data.level_value_id,
1367
+ level_value_mnemonic: data.level_value_mnemonic,
1368
+ };
1369
+ });
1370
+ const lowestLevel = hierarchy.order[hierarchy.order.length - 1];
1371
+ const lowestLevelData = hierarchy.levels.get(lowestLevel);
1372
+ return {
1373
+ geo_lowest_level_value_id: lowestLevelData.level_value_id,
1374
+ geo_code_hierarchy_json: {
1375
+ hierarchy: hierarchyArray,
1376
+ lowest_level_value_id: lowestLevelData.level_value_id,
1377
+ },
1378
+ };
1379
+ }
1380
+ /**
1381
+ * Clear hierarchy for a group
1382
+ */
1383
+ clear(groupId = 'default') {
1384
+ this.hierarchies.delete(groupId);
1385
+ }
1386
+ /**
1387
+ * Clear all hierarchies
1388
+ */
1389
+ clearAll() {
1390
+ this.hierarchies.clear();
1391
+ }
1392
+ /**
1393
+ * Get current levels for a group
1394
+ */
1395
+ getLevels(groupId = 'default') {
1396
+ const hierarchy = this.getHierarchy(groupId);
1397
+ return hierarchy.order.map((level) => hierarchy.levels.get(level));
1398
+ }
1399
+ }
1400
+ // Singleton instance
1401
+ const geoHierarchyBuilder = new GeoHierarchyBuilder();
1402
+
1403
+ // Define stable empty array to avoid selector reference issues
1404
+ const EMPTY_DATA_SOURCE = [];
1405
+ /**
1406
+ * Hook for geo widget cascade functionality
1407
+ * Handles geo hierarchy building and cascade behavior
1408
+ */
1409
+ const useGeoWidgetCascade = (options) => {
1410
+ const { config, dataSourceRequestHandler, values } = options;
1411
+ const dispatch = reactRedux.useDispatch();
1412
+ const eventBus = useWidgetEventBus();
1413
+ const widgetId = config['widget-id'];
1414
+ const geoConfig = config['widget-geo-config'];
1415
+ const dataSource = config['widget-data-source'];
1416
+ const dataPath = config['widget-data-path'];
1417
+ const valuesRef = React.useRef(values);
1418
+ const handlerRef = React.useRef(dataSourceRequestHandler);
1419
+ // Keep refs updated
1420
+ React.useEffect(() => {
1421
+ valuesRef.current = values;
1422
+ handlerRef.current = dataSourceRequestHandler;
1423
+ }, [values, dataSourceRequestHandler]);
1424
+ // Get current value and data source options
1425
+ const currentValue = reactRedux.useSelector((state) => {
1426
+ if (!dataPath) {
1427
+ return state.widget.values[widgetId];
1428
+ }
1429
+ return getWidgetValue(state.widget.values, dataPath, widgetId);
1430
+ });
1431
+ // Memoize selector to avoid returning new array reference
1432
+ const dataSourceOptions = reactRedux.useSelector((state) => state.widget.dataSources[widgetId] ?? EMPTY_DATA_SOURCE);
1433
+ React.useEffect(() => {
1434
+ if (!geoConfig || !eventBus || !dataSource || dataSource.type !== 'api') {
1435
+ return;
1436
+ }
1437
+ const { level, isLastLevel, parentWidgetId } = geoConfig;
1438
+ // Listen to parent widget changes
1439
+ if (parentWidgetId) {
1440
+ const handleParentChange = async (event) => {
1441
+ if (event.widgetId !== parentWidgetId) {
1442
+ return;
1443
+ }
1444
+ // CRITICAL: Use a small delay to ensure Redux state has been updated
1445
+ // This prevents reading stale values from valuesRef
1446
+ await new Promise(resolve => setTimeout(resolve, 0));
1447
+ const currentValues = valuesRef.current;
1448
+ const currentHandler = handlerRef.current;
1449
+ // CRITICAL: Get the parent value from Redux state, not from the event
1450
+ // The event.value might be stale, but Redux state is always current
1451
+ const parentValue = currentValues[parentWidgetId];
1452
+ // Remove this level and all below from hierarchy
1453
+ geoHierarchyBuilder.removeLevelAndBelow(level);
1454
+ // Clear this widget's value
1455
+ // CRITICAL: Only dispatch setValue for THIS widget, not for parent or other widgets
1456
+ // setWidgetValue returns the entire updated state, but we only want to update this widget
1457
+ if (dataPath) {
1458
+ const updatedValues = setWidgetValue(currentValues, dataPath, widgetId, undefined);
1459
+ // Only dispatch setValue for this widget's widgetId, not for parent or other widgets
1460
+ // This prevents accidentally overwriting the parent widget's value
1461
+ // The setWidgetValue function updates the nested structure, but we only want to
1462
+ // update the top-level widgetId key, not other keys that might be in updatedValues
1463
+ const newWidgetValue = updatedValues[widgetId];
1464
+ if (newWidgetValue !== undefined) {
1465
+ dispatch(setValue({ widgetId, value: newWidgetValue }));
1466
+ }
1467
+ else {
1468
+ // If widgetId is not in updatedValues, the value was set in a nested path
1469
+ // In this case, we need to use setValues to update the entire structure
1470
+ // But we need to be careful not to overwrite the parent widget's value
1471
+ // Only update keys that are related to this widget's dataPath
1472
+ const dataPathStr = typeof dataPath === 'string' ? dataPath : '';
1473
+ if (dataPathStr && !dataPathStr.startsWith(parentWidgetId + '.')) {
1474
+ // Only update if dataPath doesn't start with parentWidgetId
1475
+ // This ensures we don't accidentally overwrite the parent widget's value
1476
+ dispatch(setValue({ widgetId, value: undefined }));
1477
+ }
1478
+ }
1479
+ }
1480
+ else {
1481
+ dispatch(setValue({ widgetId, value: undefined }));
1482
+ }
1483
+ // Reload data source with new parent value
1484
+ // CRITICAL: Use parentValue from Redux, not event.value
1485
+ if (currentHandler && parentValue !== null && parentValue !== undefined) {
1486
+ try {
1487
+ // Merge the new parent value into current values for the API call
1488
+ // This ensures getApiDataSource can find the dependency value
1489
+ const updatedValues = {
1490
+ ...currentValues,
1491
+ [parentWidgetId]: parentValue, // Use Redux value, not event.value
1492
+ };
1493
+ // Extract level_id from widget-geo-config.level
1494
+ const levelId = geoConfig.level;
1495
+ const data = await getApiDataSource(dataSource, updatedValues, currentHandler, levelId);
1496
+ // Transform to { value, label } format
1497
+ const valueKey = dataSource.valueKey || 'level_value_id';
1498
+ const labelKey = dataSource.labelKey || 'level_value_mnemonic';
1499
+ const transformed = transformDataSourceOptions(data, valueKey, labelKey);
1500
+ dispatch(setDataSource({ widgetId, data: transformed }));
1501
+ }
1502
+ catch (error) {
1503
+ console.error('Error reloading geo data source:', error);
1504
+ dispatch(setDataSource({ widgetId, data: [] }));
1505
+ }
1506
+ }
1507
+ else {
1508
+ // If parent value is cleared, clear the data source
1509
+ dispatch(setDataSource({ widgetId, data: [] }));
1510
+ }
1511
+ };
1512
+ const unsubscribe = eventBus.subscribe('widget:change', handleParentChange);
1513
+ return () => {
1514
+ unsubscribe();
1515
+ };
1516
+ }
1517
+ }, [geoConfig, eventBus, dataSource, widgetId, dataPath, dispatch]);
1518
+ // Handle value changes to build hierarchy
1519
+ React.useEffect(() => {
1520
+ if (!geoConfig) {
1521
+ return;
1522
+ }
1523
+ // Skip if value is empty/null (but allow 0 and false)
1524
+ if (currentValue === null || currentValue === undefined || currentValue === '') {
1525
+ // If value was cleared, remove this level and below from hierarchy
1526
+ const { level } = geoConfig;
1527
+ geoHierarchyBuilder.removeLevelAndBelow(level);
1528
+ return;
1529
+ }
1530
+ const { level, isLastLevel } = geoConfig;
1531
+ // For last level, check if hierarchy is already built to prevent endless loops
1532
+ if (isLastLevel && dataPath) {
1533
+ const currentHierarchy = getWidgetValue(valuesRef.current, dataPath, widgetId);
1534
+ // If hierarchy JSON is already set and matches current value, skip rebuilding
1535
+ if (currentHierarchy && typeof currentHierarchy === 'object' && currentHierarchy.geo_code_hierarchy_json) {
1536
+ // Check if the lowest level value matches
1537
+ const currentLevelValue = typeof currentValue === 'object'
1538
+ ? (currentValue.level_value_id || currentValue.id || currentValue.value)
1539
+ : currentValue;
1540
+ if (currentHierarchy.geo_lowest_level_value_id === currentLevelValue) {
1541
+ return; // Hierarchy already built for this value, skip
1542
+ }
1543
+ }
1544
+ }
1545
+ // Extract level_value_id and level_value_mnemonic from current value
1546
+ // The value could be the ID itself or an object with id/name
1547
+ let level_value_id;
1548
+ let level_value_mnemonic;
1549
+ if (typeof currentValue === 'string' || typeof currentValue === 'number') {
1550
+ // Value is just the ID, need to find mnemonic from data source
1551
+ level_value_id = String(currentValue);
1552
+ // Try to get mnemonic from data source options
1553
+ const option = dataSourceOptions.find((opt) => opt.value === currentValue);
1554
+ level_value_mnemonic = option?.label || String(currentValue);
1555
+ }
1556
+ else if (currentValue && typeof currentValue === 'object') {
1557
+ level_value_id = currentValue.level_value_id || currentValue.id || currentValue.value;
1558
+ level_value_mnemonic = currentValue.level_value_mnemonic || currentValue.name || currentValue.label;
1559
+ }
1560
+ else {
1561
+ return;
1562
+ }
1563
+ // When a widget's own value changes, remove this level and all below from hierarchy first
1564
+ // This ensures that when level 1 changes, we clear the hierarchy and rebuild from scratch
1565
+ // The addLevel method already handles removing existing levels, but we explicitly clear to be safe
1566
+ geoHierarchyBuilder.removeLevelAndBelow(level);
1567
+ // Add level to hierarchy
1568
+ geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic);
1569
+ // If this is the last level, build and store hierarchy JSON
1570
+ if (isLastLevel && dataPath) {
1571
+ const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson();
1572
+ if (hierarchyJson) {
1573
+ // Store both geo_lowest_level_value_id and geo_code_hierarchy_json
1574
+ const updatedValues = setWidgetValue(valuesRef.current, dataPath, widgetId, {
1575
+ geo_lowest_level_value_id: hierarchyJson.geo_lowest_level_value_id,
1576
+ geo_code_hierarchy_json: hierarchyJson.geo_code_hierarchy_json,
1577
+ });
1578
+ Object.entries(updatedValues).forEach(([key, value]) => {
1579
+ dispatch(setValue({ widgetId: key, value }));
1580
+ });
1581
+ }
1582
+ }
1583
+ }, [geoConfig, currentValue, widgetId, dataPath, dispatch, dataSourceOptions]);
1584
+ };
1585
+
951
1586
  var jsxRuntime = {exports: {}};
952
1587
 
953
1588
  var reactJsxRuntime_production = {};
@@ -1450,42 +2085,208 @@ class WidgetRegistry {
1450
2085
  // Singleton instance
1451
2086
  const widgetRegistry = new WidgetRegistry();
1452
2087
 
2088
+ /**
2089
+ * Widget Event Bus
2090
+ * Provides a publish/subscribe mechanism for widget communication
2091
+ * Supports debounce and throttle for performance optimization
2092
+ */
2093
+ class WidgetEventBus {
2094
+ constructor() {
2095
+ this.subscriptions = new Map();
2096
+ this.throttleTimers = new Map();
2097
+ }
2098
+ /**
2099
+ * Subscribe to an event type
2100
+ * @param eventType The type of event to listen to
2101
+ * @param handler The handler function
2102
+ * @param options Debounce/throttle options
2103
+ * @returns Unsubscribe function
2104
+ */
2105
+ subscribe(eventType, handler, options) {
2106
+ if (!this.subscriptions.has(eventType)) {
2107
+ this.subscriptions.set(eventType, new Map());
2108
+ }
2109
+ const eventSubscriptions = this.subscriptions.get(eventType);
2110
+ const subscriptionId = `${Date.now()}-${Math.random()}`;
2111
+ const subscription = {
2112
+ handler,
2113
+ debounce: options?.debounce,
2114
+ throttle: options?.throttle,
2115
+ };
2116
+ if (!eventSubscriptions.has(subscriptionId)) {
2117
+ eventSubscriptions.set(subscriptionId, []);
2118
+ }
2119
+ eventSubscriptions.get(subscriptionId).push(subscription);
2120
+ // Return unsubscribe function
2121
+ return () => {
2122
+ const subs = eventSubscriptions.get(subscriptionId);
2123
+ if (subs) {
2124
+ const index = subs.indexOf(subscription);
2125
+ if (index > -1) {
2126
+ subs.splice(index, 1);
2127
+ if (subs.length === 0) {
2128
+ eventSubscriptions.delete(subscriptionId);
2129
+ }
2130
+ }
2131
+ }
2132
+ };
2133
+ }
2134
+ /**
2135
+ * Publish an event
2136
+ * @param event The event to publish
2137
+ */
2138
+ publish(event) {
2139
+ const eventSubscriptions = this.subscriptions.get(event.type);
2140
+ if (!eventSubscriptions) {
2141
+ return;
2142
+ }
2143
+ eventSubscriptions.forEach((subscriptions) => {
2144
+ subscriptions.forEach((subscription) => {
2145
+ this.executeHandler(subscription, event);
2146
+ });
2147
+ });
2148
+ }
2149
+ /**
2150
+ * Execute handler with debounce/throttle support
2151
+ */
2152
+ executeHandler(subscription, event) {
2153
+ const { handler, debounce, throttle } = subscription;
2154
+ // Clear existing debounce timer if any
2155
+ if (subscription.debounceTimer) {
2156
+ clearTimeout(subscription.debounceTimer);
2157
+ }
2158
+ // Handle throttle
2159
+ if (throttle && throttle > 0) {
2160
+ const throttleKey = `${event.type}-${event.widgetId}`;
2161
+ const now = Date.now();
2162
+ const lastCallTime = subscription.lastCallTime || 0;
2163
+ if (now - lastCallTime < throttle) {
2164
+ // Still in throttle period, schedule for later
2165
+ if (this.throttleTimers.has(throttleKey)) {
2166
+ return; // Already scheduled
2167
+ }
2168
+ const remainingTime = throttle - (now - lastCallTime);
2169
+ const timer = setTimeout(() => {
2170
+ subscription.lastCallTime = Date.now();
2171
+ handler(event);
2172
+ this.throttleTimers.delete(throttleKey);
2173
+ }, remainingTime);
2174
+ this.throttleTimers.set(throttleKey, timer);
2175
+ return;
2176
+ }
2177
+ subscription.lastCallTime = now;
2178
+ }
2179
+ // Handle debounce
2180
+ if (debounce && debounce > 0) {
2181
+ subscription.debounceTimer = setTimeout(() => {
2182
+ handler(event);
2183
+ subscription.debounceTimer = undefined;
2184
+ }, debounce);
2185
+ return;
2186
+ }
2187
+ // Execute immediately if no debounce/throttle
2188
+ handler(event);
2189
+ }
2190
+ /**
2191
+ * Clear all subscriptions
2192
+ */
2193
+ clear() {
2194
+ // Clear all timers
2195
+ this.subscriptions.forEach((eventSubscriptions) => {
2196
+ eventSubscriptions.forEach((subscriptions) => {
2197
+ subscriptions.forEach((sub) => {
2198
+ if (sub.debounceTimer) {
2199
+ clearTimeout(sub.debounceTimer);
2200
+ }
2201
+ });
2202
+ });
2203
+ });
2204
+ this.throttleTimers.forEach((timer) => clearTimeout(timer));
2205
+ this.throttleTimers.clear();
2206
+ this.subscriptions.clear();
2207
+ }
2208
+ }
2209
+
1453
2210
  const WidgetContext = React.createContext({
1454
- apiAdapter: undefined,
2211
+ dataSourceRequestHandler: undefined,
1455
2212
  schemaData: undefined,
1456
2213
  translate: undefined,
1457
2214
  });
1458
2215
  const useWidgetContext = () => {
1459
2216
  return React.useContext(WidgetContext);
1460
2217
  };
1461
- const WidgetProvider = ({ store, apiAdapter, schemaData, translate, children, }) => {
2218
+ const WidgetProvider = ({ store, dataSourceRequestHandler, schemaData, translate, children, }) => {
1462
2219
  const widgetStore = React.useMemo(() => store || createWidgetStore(), [store]);
1463
- // Sync schemaData to Redux store
2220
+ // Create event bus instance (one per provider)
2221
+ const eventBus = React.useMemo(() => new WidgetEventBus(), []);
2222
+ // Memoize context value to prevent unnecessary re-renders
2223
+ const contextValue = React.useMemo(() => ({
2224
+ dataSourceRequestHandler,
2225
+ schemaData,
2226
+ translate,
2227
+ }), [dataSourceRequestHandler, schemaData, translate]);
2228
+ // Warn if dataSourceRequestHandler is missing (will cause issues with API data sources)
2229
+ React.useEffect(() => {
2230
+ if (!dataSourceRequestHandler) {
2231
+ console.warn('[WidgetProvider] dataSourceRequestHandler is not provided. ' +
2232
+ 'Widgets with API data sources will not be able to load data. ' +
2233
+ 'Please provide dataSourceRequestHandler prop to WidgetProvider.');
2234
+ }
2235
+ }, [dataSourceRequestHandler]);
2236
+ // Cleanup event bus on unmount
2237
+ React.useEffect(() => {
2238
+ return () => {
2239
+ eventBus.clear();
2240
+ };
2241
+ }, [eventBus]);
2242
+ // Initialize schemaData to Redux store (only on mount)
2243
+ // This prevents overwriting user changes when schemaData prop changes
1464
2244
  React.useEffect(() => {
1465
2245
  if (schemaData) {
1466
2246
  widgetStore.dispatch(setValues(schemaData));
1467
2247
  }
1468
- }, [schemaData, widgetStore]);
1469
- const content = (jsxRuntimeExports.jsx(reactRedux.Provider, { store: widgetStore, children: jsxRuntimeExports.jsx(WidgetContext.Provider, { value: { apiAdapter, schemaData, translate }, children: children }) }));
2248
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2249
+ }, []); // Only run on mount
2250
+ const content = (jsxRuntimeExports.jsx(reactRedux.Provider, { store: widgetStore, children: jsxRuntimeExports.jsx(WidgetContext.Provider, { value: contextValue, children: jsxRuntimeExports.jsx(WidgetEventBusContext.Provider, { value: eventBus, children: children }) }) }));
1470
2251
  return content;
1471
2252
  };
1472
2253
 
1473
- const WidgetRenderer = ({ config, apiAdapter: propApiAdapter, schemaData: propSchemaData, onValueChange, defaultComponent, }) => {
2254
+ const WidgetRenderer = ({ config, dataSourceRequestHandler: propDataSourceRequestHandler, schemaData: propSchemaData, onValueChange, defaultComponent, }) => {
1474
2255
  // Use context values as fallback
1475
2256
  const context = useWidgetContext();
1476
- const apiAdapter = propApiAdapter || context.apiAdapter;
2257
+ const dataSourceRequestHandler = propDataSourceRequestHandler || context.dataSourceRequestHandler;
1477
2258
  const schemaData = propSchemaData || context.schemaData;
2259
+ if (!dataSourceRequestHandler && config['widget-data-source']?.type === 'api') {
2260
+ console.error(`[WidgetRenderer] dataSourceRequestHandler is required for widget ${config['widget-id']} with API data source`);
2261
+ }
2262
+ // Get values from Redux for cascade hooks
2263
+ const values = reactRedux.useSelector((state) => state.widget.values);
1478
2264
  const widgetContext = useBaseWidget({
1479
2265
  config,
1480
- apiAdapter,
2266
+ dataSourceRequestHandler,
1481
2267
  schemaData,
1482
2268
  onValueChange,
1483
2269
  });
2270
+ // Apply cascade hooks if configured (only if handler is available)
2271
+ if (dataSourceRequestHandler) {
2272
+ useWidgetCascade({
2273
+ config,
2274
+ dataSourceRequestHandler,
2275
+ values,
2276
+ });
2277
+ useGeoWidgetCascade({
2278
+ config,
2279
+ dataSourceRequestHandler,
2280
+ values,
2281
+ });
2282
+ }
1484
2283
  // Don't render if not visible
1485
2284
  if (!widgetContext.isVisible) {
1486
2285
  return null;
1487
2286
  }
1488
2287
  // Render widget using registry
2288
+ // Don't use key based on readonly state - it causes remounting which resets userHasSetValueRef
2289
+ // The readonly state is already handled in the widget components themselves
1489
2290
  return (jsxRuntimeExports.jsx("div", { className: "widget-container", "data-widget-id": widgetContext.widgetId, style: { marginBottom: 0 }, children: widgetRegistry.render(config, widgetContext, defaultComponent) }));
1490
2291
  };
1491
2292
 
@@ -1588,36 +2389,42 @@ const useWidgetTranslation = () => {
1588
2389
  * - Nested panels (for layout composition)
1589
2390
  * - Widgets (for actual form inputs/controls)
1590
2391
  */
1591
- const PanelRenderer = ({ panel, apiAdapter, schemaData, onValueChange, isEditMode = false, }) => {
2392
+ const PanelRenderer = ({ panel, dataSourceRequestHandler, schemaData, onValueChange, isEditMode = false, }) => {
1592
2393
  const { translateConfig } = useWidgetTranslation();
1593
2394
  const orientation = panel['panel-orientation'] || 'vertical';
1594
2395
  const nestedPanels = panel.panels || [];
1595
2396
  const widgets = panel.widgets || [];
1596
2397
  // For horizontal orientation, use grid for equal-width columns
1597
- // Dynamic grid based on number of nested panels
2398
+ // Dynamic grid based on number of nested panels and their column spans
1598
2399
  // For vertical orientation, use flex column
1599
2400
  const getContainerClassAndStyle = () => {
1600
2401
  if (orientation === 'horizontal' && nestedPanels.length > 0) {
1601
- const numPanels = nestedPanels.length;
1602
- // Use predefined grid classes based on number of panels (1-5)
2402
+ // Calculate total columns needed based on panel column spans
2403
+ // Sum up all column spans, or use panel count if no spans specified
2404
+ let totalColumns = 0;
2405
+ nestedPanels.forEach(panel => {
2406
+ const columnSpan = panel['panel-column-span'] || 1;
2407
+ totalColumns += columnSpan;
2408
+ });
2409
+ // Ensure at least as many columns as panels (for panels without explicit span)
2410
+ totalColumns = Math.max(totalColumns, nestedPanels.length);
2411
+ // Use predefined grid classes for common cases (1-5)
1603
2412
  // For more than 5, use inline style
1604
2413
  // Removed gap to allow borders to show properly
1605
- console.log('numPanels', numPanels);
1606
- if (numPanels <= 5) {
1607
- const gridClasses = {
1608
- 1: 'grid grid-cols-1',
1609
- 2: 'grid grid-cols-2',
1610
- 3: 'grid grid-cols-3',
1611
- 4: 'grid grid-cols-4',
1612
- 5: 'grid grid-cols-5',
2414
+ if (totalColumns <= 5) {
2415
+ // For grid with column spans, we need to use inline styles to set minmax
2416
+ // This ensures each column is at least 200px wide
2417
+ return {
2418
+ className: 'grid',
2419
+ style: { gridTemplateColumns: `repeat(${totalColumns}, minmax(200px, 1fr))` },
1613
2420
  };
1614
- return { className: gridClasses[numPanels] || 'grid', style: {} };
1615
2421
  }
1616
2422
  else {
1617
- // For more than 5 panels, use inline style
2423
+ // For more than 5 columns, use inline style
2424
+ // Use minmax(200px, 1fr) to ensure minimum 200px per column
1618
2425
  return {
1619
2426
  className: 'grid',
1620
- style: { gridTemplateColumns: `repeat(${numPanels}, minmax(0, 1fr))` },
2427
+ style: { gridTemplateColumns: `repeat(${totalColumns}, minmax(200px, 1fr))` },
1621
2428
  };
1622
2429
  }
1623
2430
  }
@@ -1637,15 +2444,44 @@ const PanelRenderer = ({ panel, apiAdapter, schemaData, onValueChange, isEditMod
1637
2444
  }, children: [nestedPanels.map((nestedPanel, index) => {
1638
2445
  const isLastPanel = index === nestedPanels.length - 1;
1639
2446
  const isFirstPanel = index === 0;
1640
- const horizontalStyle = orientation === 'horizontal'
1641
- ? {
1642
- minWidth: '200px',
1643
- paddingRight: !isLastPanel ? '40px' : undefined,
1644
- paddingLeft: !isFirstPanel ? '40px' : undefined,
1645
- position: 'relative',
2447
+ const nestedOrientation = nestedPanel['panel-orientation'] || 'vertical';
2448
+ const columnSpan = nestedPanel['panel-column-span'];
2449
+ // Calculate style for nested panel based on orientation and column span
2450
+ const getNestedPanelStyle = () => {
2451
+ if (orientation === 'horizontal') {
2452
+ // When nested inside horizontal panel, check for column span
2453
+ const baseStyle = {
2454
+ minWidth: '200px',
2455
+ paddingRight: !isLastPanel ? '40px' : undefined,
2456
+ paddingLeft: !isFirstPanel ? '40px' : undefined,
2457
+ position: 'relative',
2458
+ };
2459
+ // If vertical panel has column span, use CSS grid-column-span
2460
+ if (nestedOrientation === 'vertical' && columnSpan && columnSpan > 1) {
2461
+ return {
2462
+ ...baseStyle,
2463
+ gridColumn: `span ${columnSpan}`,
2464
+ minWidth: 'auto', // Remove minWidth constraint when spanning columns
2465
+ };
2466
+ }
2467
+ return baseStyle;
2468
+ }
2469
+ else {
2470
+ // Vertical panel nested in vertical panel
2471
+ if (columnSpan && columnSpan > 1) {
2472
+ // If column span is specified, calculate width based on 200px per column
2473
+ const width = columnSpan * 200;
2474
+ return {
2475
+ width: `${width}px`,
2476
+ maxWidth: '100%',
2477
+ flexShrink: 0,
2478
+ };
2479
+ }
2480
+ return { width: '100%' };
1646
2481
  }
1647
- : { width: '100%' };
1648
- return (jsxRuntimeExports.jsx(React.Fragment, { children: jsxRuntimeExports.jsxs("div", { className: orientation === 'horizontal' ? 'min-w-200 relative' : 'w-full', style: horizontalStyle, children: [jsxRuntimeExports.jsx(PanelRenderer, { panel: nestedPanel, apiAdapter: apiAdapter, schemaData: schemaData, onValueChange: onValueChange, isEditMode: isEditMode }), orientation === 'horizontal' && !isLastPanel && (jsxRuntimeExports.jsx("div", { style: {
2482
+ };
2483
+ const nestedPanelStyle = getNestedPanelStyle();
2484
+ return (jsxRuntimeExports.jsx(React.Fragment, { children: jsxRuntimeExports.jsxs("div", { className: orientation === 'horizontal' ? 'min-w-200 relative' : 'w-full', style: nestedPanelStyle, "data-panel-column-span": columnSpan || undefined, children: [jsxRuntimeExports.jsx(PanelRenderer, { panel: nestedPanel, dataSourceRequestHandler: dataSourceRequestHandler, schemaData: schemaData, onValueChange: onValueChange, isEditMode: isEditMode }), orientation === 'horizontal' && !isLastPanel && (jsxRuntimeExports.jsx("div", { style: {
1649
2485
  position: 'absolute',
1650
2486
  right: 0,
1651
2487
  top: 0,
@@ -1653,7 +2489,11 @@ const PanelRenderer = ({ panel, apiAdapter, schemaData, onValueChange, isEditMod
1653
2489
  width: '1px',
1654
2490
  backgroundColor: isEditMode ? '#F2BA1A' : '#D1D5DB',
1655
2491
  } }))] }) }, nestedPanel['panel-id'] || `panel-${index}`));
1656
- }), widgets.map((widgetConfig, index) => (jsxRuntimeExports.jsx(WidgetRenderer, { config: widgetConfig, apiAdapter: apiAdapter, schemaData: schemaData, onValueChange: onValueChange }, widgetConfig['widget-id'] || `widget-${index}`)))] }));
2492
+ }), widgets.map((widgetConfig, index) => {
2493
+ // Don't use readonly state in key - it causes remounting which resets userHasSetValueRef
2494
+ // The readonly state is already handled in the widget components themselves
2495
+ return (jsxRuntimeExports.jsx(WidgetRenderer, { config: widgetConfig, dataSourceRequestHandler: dataSourceRequestHandler, schemaData: schemaData, onValueChange: onValueChange }, widgetConfig['widget-id'] || `widget-${index}`));
2496
+ })] }));
1657
2497
  // Render panel without card styling (panel-type removed from schema)
1658
2498
  // Vertical panels will have constrained width via CSS in SectionRenderer
1659
2499
  // Horizontal panels take full width
@@ -2247,6 +3087,104 @@ const FileInputWidget = ({ config }) => {
2247
3087
  } }, `modal-${previewFile ? (previewFile instanceof File ? previewFile.name : previewFile) : 'none'}`)] }));
2248
3088
  };
2249
3089
 
3090
+ /**
3091
+ * Namespace a data path by adding a namespace prefix
3092
+ */
3093
+ const namespaceDataPath = (dataPath, namespace) => {
3094
+ if (!dataPath)
3095
+ return dataPath;
3096
+ if (typeof dataPath === 'string') {
3097
+ // Add namespace prefix to the data path
3098
+ return `${namespace}.${dataPath}`;
3099
+ }
3100
+ // Multi-path: namespace each path
3101
+ const namespaced = {};
3102
+ for (const [key, path] of Object.entries(dataPath)) {
3103
+ namespaced[key] = `${namespace}.${path}`;
3104
+ }
3105
+ return namespaced;
3106
+ };
3107
+ /**
3108
+ * Recursively namespace widget IDs and data paths in a widget configuration
3109
+ * This ensures unique widget IDs and data paths when the same section is rendered multiple times
3110
+ */
3111
+ const namespaceWidgetConfig = (widgetConfig, namespace) => {
3112
+ const namespaced = { ...widgetConfig };
3113
+ // Namespace the widget-id
3114
+ if (namespaced['widget-id']) {
3115
+ namespaced['widget-id'] = `${namespace}__${namespaced['widget-id']}`;
3116
+ }
3117
+ // Namespace the widget-data-path to ensure values are stored separately
3118
+ if (namespaced['widget-data-path']) {
3119
+ namespaced['widget-data-path'] = namespaceDataPath(namespaced['widget-data-path'], namespace);
3120
+ }
3121
+ // Recursively namespace nested widgets (for layout widgets)
3122
+ if (namespaced.widgets && Array.isArray(namespaced.widgets)) {
3123
+ namespaced.widgets = namespaced.widgets.map((widget) => namespaceWidgetConfig(widget, namespace));
3124
+ }
3125
+ // Namespace widget-item (for array/group widgets)
3126
+ if (namespaced['widget-item']) {
3127
+ namespaced['widget-item'] = namespaceWidgetConfig(namespaced['widget-item'], namespace);
3128
+ }
3129
+ // Namespace widget-data-columns (for table widgets)
3130
+ if (namespaced['widget-data-columns'] && Array.isArray(namespaced['widget-data-columns'])) {
3131
+ namespaced['widget-data-columns'] = namespaced['widget-data-columns'].map((column) => {
3132
+ const namespacedColumn = { ...column };
3133
+ // Namespace column data paths if they exist (columns only support string paths, not multi-path)
3134
+ if (namespacedColumn['widget-data-path'] && typeof namespacedColumn['widget-data-path'] === 'string') {
3135
+ namespacedColumn['widget-data-path'] = namespaceDataPath(namespacedColumn['widget-data-path'], namespace); // Safe cast since we checked it's a string
3136
+ }
3137
+ return namespacedColumn;
3138
+ });
3139
+ }
3140
+ return namespaced;
3141
+ };
3142
+ /**
3143
+ * Recursively namespace widget IDs in a panel configuration
3144
+ */
3145
+ const namespacePanelConfig = (panel, namespace) => {
3146
+ const namespaced = { ...panel };
3147
+ // Recursively namespace nested panels
3148
+ if (namespaced.panels && Array.isArray(namespaced.panels)) {
3149
+ namespaced.panels = namespaced.panels.map((p) => namespacePanelConfig(p, namespace));
3150
+ }
3151
+ // Namespace widgets in panel
3152
+ if (namespaced.widgets && Array.isArray(namespaced.widgets)) {
3153
+ namespaced.widgets = namespaced.widgets.map((widget) => namespaceWidgetConfig(widget, namespace));
3154
+ }
3155
+ return namespaced;
3156
+ };
3157
+ /**
3158
+ * Namespace widget IDs and data paths in a section configuration
3159
+ * This ensures unique widget IDs and data paths when the same section is rendered multiple times
3160
+ * (e.g., in CRView mode showing old and new records side by side)
3161
+ *
3162
+ * @param section - Section configuration to namespace
3163
+ * @param namespace - Namespace prefix to add to widget IDs and data paths (e.g., "old", "new", "instance-1")
3164
+ * @returns Namespaced section configuration
3165
+ */
3166
+ const namespaceSectionConfig = (section, namespace) => {
3167
+ const namespaced = { ...section };
3168
+ // Namespace the section-id as well to ensure uniqueness
3169
+ if (namespaced['section-id']) {
3170
+ namespaced['section-id'] = `${namespace}__${namespaced['section-id']}`;
3171
+ }
3172
+ // Recursively namespace panels
3173
+ if (namespaced.panels && Array.isArray(namespaced.panels)) {
3174
+ namespaced.panels = namespaced.panels.map((panel) => namespacePanelConfig(panel, namespace));
3175
+ }
3176
+ // Namespace supporting documents data paths
3177
+ if (namespaced['section-supporting-documents'] && Array.isArray(namespaced['section-supporting-documents'])) {
3178
+ namespaced['section-supporting-documents'] = namespaced['section-supporting-documents'].map((doc) => ({
3179
+ ...doc,
3180
+ 'document-data-path': doc['document-data-path']
3181
+ ? `${namespace}.${doc['document-data-path']}`
3182
+ : doc['document-data-path'],
3183
+ }));
3184
+ }
3185
+ return namespaced;
3186
+ };
3187
+
2250
3188
  /**
2251
3189
  * Renders a section with its panels
2252
3190
  *
@@ -2255,14 +3193,59 @@ const FileInputWidget = ({ config }) => {
2255
3193
  * - Panels wrap when they exceed available width
2256
3194
  * - Sections can sit side-by-side if there's space
2257
3195
  */
2258
- const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridColumnSpan, onSectionSave, hideEditButton = false, mode = 'RegistryView', }) => {
3196
+ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequestHandler, schemaData, onValueChange, gridColumnSpan, onSectionSave, hideEditButton = false, mode = 'RegistryView', namespace, }) => {
2259
3197
  const { translateConfig, translate } = useWidgetTranslation();
2260
- const { schemaData: contextSchemaData } = useWidgetContext();
3198
+ const { schemaData: contextSchemaData, dataSourceRequestHandler: contextDataSourceRequestHandler } = useWidgetContext();
2261
3199
  const store = reactRedux.useStore();
2262
3200
  const dispatch = reactRedux.useDispatch();
3201
+ // Use prop handler if provided, otherwise fall back to context
3202
+ const dataSourceRequestHandler = propDataSourceRequestHandler || contextDataSourceRequestHandler;
2263
3203
  // Get CRView data from schemaData (prefer prop over context, then Redux store)
2264
3204
  const currentSchemaData = schemaData || contextSchemaData || {};
2265
3205
  const storeValues = reactRedux.useSelector((state) => state.widget?.values || {});
3206
+ // Namespace the section if namespace is provided
3207
+ // This ensures unique widget IDs when the same section is rendered multiple times
3208
+ const namespacedSection = React.useMemo(() => {
3209
+ if (namespace) {
3210
+ return namespaceSectionConfig(section, namespace);
3211
+ }
3212
+ return section;
3213
+ }, [section, namespace]);
3214
+ // Create namespaced schemaData if namespace is provided
3215
+ // This ensures widgets can read initial values from schemaData at namespaced paths
3216
+ const namespacedSchemaData = React.useMemo(() => {
3217
+ if (!namespace || !currentSchemaData) {
3218
+ return schemaData;
3219
+ }
3220
+ // Create a namespaced version of schemaData by copying values to namespaced paths
3221
+ const namespaced = { ...currentSchemaData };
3222
+ // Copy all top-level keys to namespaced paths
3223
+ Object.keys(currentSchemaData).forEach(key => {
3224
+ const namespacedKey = `${namespace}.${key}`;
3225
+ if (!(namespacedKey in namespaced)) {
3226
+ namespaced[namespacedKey] = currentSchemaData[key];
3227
+ }
3228
+ });
3229
+ // Also handle nested objects - copy nested values to namespaced paths
3230
+ const copyNestedValues = (obj, prefix = '') => {
3231
+ if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
3232
+ Object.keys(obj).forEach(key => {
3233
+ const fullPath = prefix ? `${prefix}.${key}` : key;
3234
+ const namespacedPath = `${namespace}.${fullPath}`;
3235
+ if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
3236
+ copyNestedValues(obj[key], fullPath);
3237
+ // Also set the nested object at the namespaced path
3238
+ setValueByPath(namespaced, namespacedPath, obj[key]);
3239
+ }
3240
+ else {
3241
+ setValueByPath(namespaced, namespacedPath, obj[key]);
3242
+ }
3243
+ });
3244
+ }
3245
+ };
3246
+ copyNestedValues(currentSchemaData);
3247
+ return namespaced;
3248
+ }, [namespace, schemaData, currentSchemaData]);
2266
3249
  const crViewData = React.useMemo(() => {
2267
3250
  if (mode !== 'CRView')
2268
3251
  return null;
@@ -2275,17 +3258,16 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
2275
3258
  approvedBy: getValueByPath(dataSource, 'approvedBy') || getValueByPath(dataSource, 'approved_by'),
2276
3259
  approvedDate: getValueByPath(dataSource, 'approvedDate') || getValueByPath(dataSource, 'approved_date'),
2277
3260
  };
2278
- // Debug logging (can be removed in production)
2279
- if (mode === 'CRView') {
2280
- console.log('CRView Data Source:', { dataSource, result, currentSchemaData, storeValues });
2281
- }
2282
3261
  return result;
2283
3262
  }, [mode, currentSchemaData, storeValues]);
2284
- const sectionId = section['section-id'];
3263
+ // Use namespaced section for rendering
3264
+ const sectionToRender = namespacedSection;
3265
+ const sectionId = sectionToRender['section-id'];
2285
3266
  const gridId = `section-panels-${sectionId}`;
2286
3267
  const sectionClassId = `section-${sectionId}`;
2287
3268
  // Recursively count all vertical panels, especially those nested inside horizontal panels
2288
3269
  // Typically: horizontal panels at first level contain vertical panels at second level
3270
+ // Accounts for panel-column-span: a panel with column-span 3 counts as 3 columns
2289
3271
  const countVerticalPanels = (panels) => {
2290
3272
  let count = 0;
2291
3273
  for (const panel of panels) {
@@ -2295,8 +3277,9 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
2295
3277
  count += countVerticalPanels(panel.panels);
2296
3278
  }
2297
3279
  else if (orientation === 'vertical') {
2298
- // Count this vertical panel
2299
- count += 1;
3280
+ // Count this vertical panel, accounting for column span
3281
+ const columnSpan = panel['panel-column-span'] || 1;
3282
+ count += columnSpan;
2300
3283
  // Also recursively count vertical panels nested inside this vertical panel
2301
3284
  if (panel.panels && panel.panels.length > 0) {
2302
3285
  count += countVerticalPanels(panel.panels);
@@ -2343,9 +3326,9 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
2343
3326
  }
2344
3327
  return null;
2345
3328
  };
2346
- const hasTableWidget = checkForTableWidget(section.panels);
2347
- const tableWidgetColumnSpan = getTableWidgetColumnSpan(section.panels);
2348
- const verticalPanelsCount = countVerticalPanels(section.panels);
3329
+ const hasTableWidget = checkForTableWidget(sectionToRender.panels);
3330
+ const tableWidgetColumnSpan = getTableWidgetColumnSpan(sectionToRender.panels);
3331
+ const verticalPanelsCount = countVerticalPanels(sectionToRender.panels);
2349
3332
  // If section contains a table widget with explicit column span, use it
2350
3333
  // Otherwise, if it has a table widget, ensure it spans at least 2 columns
2351
3334
  // Otherwise, use the vertical panel count
@@ -2355,7 +3338,7 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
2355
3338
  // Check if table widget has explicit column span (not default)
2356
3339
  const hasExplicitTableSpan = tableWidgetColumnSpan !== null;
2357
3340
  // Supporting documents configuration
2358
- const supportingDocuments = section['section-supporting-documents'] || [];
3341
+ const supportingDocuments = sectionToRender['section-supporting-documents'] || [];
2359
3342
  const hasSupportingDocuments = supportingDocuments.length > 0;
2360
3343
  // Edit mode state
2361
3344
  const [isEditMode, setIsEditMode] = React.useState(false);
@@ -2393,21 +3376,24 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
2393
3376
  }, [isEditMode]);
2394
3377
  // Recursively modify panels to set readonly based on edit mode
2395
3378
  const makePanelsEditable = (panels, editable) => {
2396
- const sectionEditable = section['section-editable'] === true;
3379
+ const sectionEditable = sectionToRender['section-editable'] === true;
2397
3380
  return panels.map(panel => {
2398
3381
  const modifiedPanel = {
2399
3382
  ...panel,
2400
3383
  panels: panel.panels ? makePanelsEditable(panel.panels, editable) : undefined,
2401
- widgets: panel.widgets?.map(widget => ({
2402
- ...widget,
3384
+ widgets: panel.widgets?.map(widget => {
2403
3385
  // When NOT in edit mode (editable = false), set all widgets to readonly
2404
3386
  // When in edit mode (editable = true):
2405
3387
  // - If section-editable is true, force widgets to be editable (override widget-readonly)
2406
3388
  // - Otherwise, respect original readonly setting
2407
- 'widget-readonly': editable
3389
+ const newReadonly = editable
2408
3390
  ? (sectionEditable ? false : (widget['widget-readonly'] || false))
2409
- : true,
2410
- })),
3391
+ : true;
3392
+ return {
3393
+ ...widget,
3394
+ 'widget-readonly': newReadonly,
3395
+ };
3396
+ }),
2411
3397
  };
2412
3398
  return modifiedPanel;
2413
3399
  });
@@ -2416,10 +3402,10 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
2416
3402
  const editableSection = React.useMemo(() => {
2417
3403
  // Always apply readonly/editable state based on edit mode
2418
3404
  return {
2419
- ...section,
2420
- panels: makePanelsEditable(section.panels, isEditMode),
3405
+ ...sectionToRender,
3406
+ panels: makePanelsEditable(sectionToRender.panels, isEditMode),
2421
3407
  };
2422
- }, [section, isEditMode]);
3408
+ }, [sectionToRender, isEditMode]);
2423
3409
  // Handle edit button click
2424
3410
  const handleEdit = () => {
2425
3411
  // Capture height BEFORE entering edit mode to preserve space
@@ -2486,9 +3472,9 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
2486
3472
  width: `${editSectionPosition.width}px`,
2487
3473
  maxHeight: '90vh',
2488
3474
  overflowY: 'auto',
2489
- }, children: [section['section-title'] && (jsxRuntimeExports.jsx("h2", { className: "text-xl font-semibold mb-4", style: { fontFamily: 'Roboto, sans-serif', marginTop: '35px' }, children: translateConfig(section['section-title']) })), jsxRuntimeExports.jsxs("div", { id: editGridId, className: "section-panels", children: [editableSection.panels.map((panel, index) => {
3475
+ }, children: [sectionToRender['section-title'] && (jsxRuntimeExports.jsx("h2", { className: "text-xl font-semibold mb-4", style: { fontFamily: 'Roboto, sans-serif', marginTop: '35px' }, children: translateConfig(sectionToRender['section-title']) })), jsxRuntimeExports.jsxs("div", { id: editGridId, className: "section-panels", children: [editableSection.panels.map((panel, index) => {
2490
3476
  const isLastPanel = index === editableSection.panels.length - 1;
2491
- return (jsxRuntimeExports.jsx("div", { className: `panel-wrapper ${isLastPanel ? 'last-panel-wrapper' : ''}`, children: jsxRuntimeExports.jsx(PanelRenderer, { panel: panel, apiAdapter: apiAdapter, schemaData: schemaData, onValueChange: onValueChange, isEditMode: true }) }, panel['panel-id'] || `section-panel-${index}`));
3477
+ return (jsxRuntimeExports.jsx("div", { className: `panel-wrapper ${isLastPanel ? 'last-panel-wrapper' : ''}`, children: jsxRuntimeExports.jsx(PanelRenderer, { panel: panel, dataSourceRequestHandler: dataSourceRequestHandler, schemaData: namespacedSchemaData, onValueChange: onValueChange, isEditMode: true }) }, panel['panel-id'] || `section-panel-${index}`));
2492
3478
  }), hasSupportingDocuments && (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("hr", { className: "my-4 w-full", style: { height: '1px', backgroundColor: '#F2BA1A', border: 'none' } }), jsxRuntimeExports.jsxs("div", { className: "supporting-documents-container", children: [jsxRuntimeExports.jsxs("button", { type: "button", onClick: () => setIsDocumentsExpanded(!isDocumentsExpanded), className: "supporting-documents-title-button w-full flex items-center text-left", children: [jsxRuntimeExports.jsx("span", { className: "font-semibold", style: { fontFamily: 'Roboto, sans-serif', fontSize: '16px' }, children: translate('common.supportedDocuments') || 'Supported Documents' }), jsxRuntimeExports.jsx("svg", { className: `w-5 h-5 text-[#ED7C22] transition-transform ml-2 ${isDocumentsExpanded ? 'rotate-180' : ''}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntimeExports.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }) })] }), isDocumentsExpanded && (jsxRuntimeExports.jsx("div", { className: "supporting-documents-grid mt-4", children: supportingDocuments.map((doc, index) => {
2493
3479
  const docConfig = createDocumentWidgetConfig(doc, sectionId, index);
2494
3480
  return (jsxRuntimeExports.jsx("div", { className: "supporting-document-item", children: jsxRuntimeExports.jsx(FileInputWidget, { config: docConfig }) }, `${sectionId}-doc-${index}`));
@@ -2506,27 +3492,62 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
2506
3492
  });
2507
3493
  return widgets;
2508
3494
  };
2509
- const buildSectionSnapshot = (widgets, sourceData) => {
3495
+ const trackSectionChages = (widgets, sourceData, useNamespacedPaths = false) => {
2510
3496
  const snapshot = {};
3497
+ let hasTable = false;
3498
+ const recordId = Object.keys(sourceData)[0];
2511
3499
  widgets.forEach(widget => {
2512
- const dataPath = widget['widget-data-path'];
2513
- if (!dataPath)
3500
+ const originalDataPath = widget['widget-data-path'];
3501
+ if (!originalDataPath)
2514
3502
  return;
3503
+ if (widget['widget-type'] === 'table' || widget['widget-type'] === 'simple-table') {
3504
+ hasTable = true;
3505
+ }
3506
+ // If namespace was used and we're reading from store, use namespaced paths
3507
+ useNamespacedPaths && namespace && originalDataPath
3508
+ ? (typeof originalDataPath === 'string'
3509
+ ? `${namespace}.${originalDataPath}`
3510
+ : Object.fromEntries(Object.entries(originalDataPath).map(([key, path]) => [key, `${namespace}.${path}`])))
3511
+ : originalDataPath;
3512
+ // Always store snapshot using original paths (for change tracking)
2515
3513
  // Handle multi-path (object) or single path (string)
2516
- if (typeof dataPath === 'object') {
2517
- // Multi-path: store each path separately
2518
- Object.entries(dataPath).forEach(([key, path]) => {
3514
+ if (typeof originalDataPath === 'object') {
3515
+ // Multi-path: store each path separately using original paths
3516
+ Object.entries(originalDataPath).forEach(([key, path]) => {
2519
3517
  if (typeof path === 'string') {
2520
- snapshot[path] = getValueByPath(sourceData, path);
3518
+ // Read from source using namespaced path if needed
3519
+ const readPath = useNamespacedPaths && namespace ? `${namespace}.${path}` : path;
3520
+ snapshot[path] = getValueByPath(sourceData, readPath);
2521
3521
  }
2522
3522
  });
2523
3523
  }
2524
- else if (typeof dataPath === 'string') {
2525
- snapshot[dataPath] = getValueByPath(sourceData, dataPath);
3524
+ else if (typeof originalDataPath === 'string') {
3525
+ // Read from source using namespaced path if needed
3526
+ const readPath = useNamespacedPaths && namespace ? `${namespace}.${originalDataPath}` : originalDataPath;
3527
+ snapshot[originalDataPath] = getValueByPath(sourceData, readPath);
2526
3528
  }
2527
3529
  });
2528
- return snapshot;
3530
+ if (hasTable === false) {
3531
+ const cleanedSnapshot = {};
3532
+ Object.entries(snapshot).forEach(([key, value]) => {
3533
+ const removedFirstLevelPath = key.includes('.')
3534
+ ? key.split('.').slice(1).join('.')
3535
+ : key;
3536
+ cleanedSnapshot[removedFirstLevelPath] = value;
3537
+ });
3538
+ return [
3539
+ { ...sourceData[recordId],
3540
+ ...cleanedSnapshot,
3541
+ edit_action: "UPDATE"
3542
+ }
3543
+ ];
3544
+ }
3545
+ const recordEntry = Object.entries(snapshot).find(([key, value]) => key.endsWith('.records') && Array.isArray(value));
3546
+ return recordEntry ? recordEntry[1] : snapshot;
2529
3547
  };
3548
+ // Get original section (without namespace) for building snapshots
3549
+ // This ensures we use the original data paths when saving
3550
+ const originalSection = section;
2530
3551
  // Handle save button click
2531
3552
  const handleSave = async () => {
2532
3553
  if (!store || !onSectionSave) {
@@ -2534,31 +3555,37 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
2534
3555
  setIsEditMode(false);
2535
3556
  return;
2536
3557
  }
2537
- const sectionWidgets = collectWidgets(section.panels);
3558
+ // Use original section (without namespace) for collecting widgets
3559
+ // This ensures we use the original widget IDs and data paths
3560
+ const sectionWidgets = collectWidgets(originalSection.panels);
2538
3561
  const currentState = store.getState().widget;
2539
3562
  const currentSchemaData = currentState.values || {};
3563
+ // schema data before section change
2540
3564
  const oldSchemaData = schemaData || contextSchemaData;
2541
- const oldSectionValue = buildSectionSnapshot(sectionWidgets, oldSchemaData);
2542
- const newSectionValue = buildSectionSnapshot(sectionWidgets, currentSchemaData);
3565
+ // schema data after section change
3566
+ const newSchemaData = trackSectionChages(sectionWidgets, currentSchemaData);
2543
3567
  // Include supporting documents in the snapshot if they exist
3568
+ const sectionFiles = [];
2544
3569
  if (hasSupportingDocuments) {
2545
- supportingDocuments.forEach((doc, index) => {
2546
- const dataPath = doc['document-data-path'];
2547
- const oldValue = getValueByPath(oldSchemaData, dataPath);
2548
- const newValue = getValueByPath(currentSchemaData, dataPath);
2549
- oldSectionValue[dataPath] = oldValue;
2550
- newSectionValue[dataPath] = newValue;
3570
+ // Use original section's supporting documents to get original data paths
3571
+ const originalSupportingDocuments = originalSection['section-supporting-documents'] || [];
3572
+ originalSupportingDocuments.forEach((doc) => {
3573
+ const originalDataPath = doc['document-data-path'];
3574
+ // Read new value from store (with namespace if used)
3575
+ const storeDataPath = namespace && originalDataPath
3576
+ ? `${namespace}.${originalDataPath}`
3577
+ : originalDataPath;
3578
+ sectionFiles.push(getValueByPath(currentSchemaData, storeDataPath));
2551
3579
  });
2552
3580
  }
2553
- if (JSON.stringify(oldSectionValue) !== JSON.stringify(newSectionValue)) {
2554
- const changes = {
2555
- section_id: sectionId,
2556
- section_schema: section,
2557
- old_section_value: oldSectionValue,
2558
- new_section_value: newSectionValue,
2559
- };
3581
+ if (JSON.stringify(oldSchemaData) !== JSON.stringify(newSchemaData)) {
2560
3582
  try {
2561
- await onSectionSave(changes);
3583
+ const sectionchanges = {
3584
+ section_id: originalSection['section-id'],
3585
+ records: [...newSchemaData],
3586
+ files: [...sectionFiles]
3587
+ };
3588
+ await onSectionSave(sectionchanges);
2562
3589
  }
2563
3590
  catch (error) {
2564
3591
  console.error('Section Changes Save failed', error);
@@ -2569,40 +3596,58 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
2569
3596
  // Handle cancel button click
2570
3597
  const handleCancel = () => {
2571
3598
  // Revert values in store to original schema data
2572
- const sectionWidgets = collectWidgets(section.panels);
3599
+ // Use original section (without namespace) for collecting widgets
3600
+ const sectionWidgets = collectWidgets(originalSection.panels);
2573
3601
  const oldSchemaData = schemaData || contextSchemaData;
2574
3602
  const currentStoreValues = store.getState().widget.values;
2575
3603
  let newStoreValues = currentStoreValues;
2576
3604
  sectionWidgets.forEach(widget => {
2577
- const widgetId = widget['widget-id'];
2578
- const dataPath = widget['widget-data-path'];
2579
- if (widgetId && dataPath) {
3605
+ const originalWidgetId = widget['widget-id'];
3606
+ // If namespace was used, we need to use namespaced widget ID and data path
3607
+ const namespacedWidgetId = namespace ? `${namespace}__${originalWidgetId}` : originalWidgetId;
3608
+ const widgetId = namespacedWidgetId;
3609
+ const originalDataPath = widget['widget-data-path'];
3610
+ // If namespace was used, data path in store is namespaced, but we read from original schema using original path
3611
+ const storeDataPath = namespace && originalDataPath
3612
+ ? (typeof originalDataPath === 'string'
3613
+ ? `${namespace}.${originalDataPath}`
3614
+ : Object.fromEntries(Object.entries(originalDataPath).map(([key, path]) => [key, `${namespace}.${path}`])))
3615
+ : originalDataPath;
3616
+ if (widgetId && originalDataPath) {
2580
3617
  // Handle multi-path (object) or single path (string)
3618
+ // Read from original schema data using original paths
2581
3619
  let oldValue;
2582
- if (typeof dataPath === 'object') {
3620
+ if (typeof originalDataPath === 'object') {
2583
3621
  // Multi-path: get values for each path
2584
3622
  oldValue = {};
2585
- Object.entries(dataPath).forEach(([key, path]) => {
3623
+ Object.entries(originalDataPath).forEach(([key, path]) => {
2586
3624
  if (typeof path === 'string') {
2587
3625
  oldValue[key] = getValueByPath(oldSchemaData, path);
2588
3626
  }
2589
3627
  });
2590
3628
  }
2591
- else if (typeof dataPath === 'string') {
2592
- oldValue = getValueByPath(oldSchemaData, dataPath);
3629
+ else if (typeof originalDataPath === 'string') {
3630
+ oldValue = getValueByPath(oldSchemaData, originalDataPath);
2593
3631
  }
3632
+ // Set in store using namespaced data path (if namespace was used)
2594
3633
  if (oldValue !== undefined) {
2595
- newStoreValues = setWidgetValue(newStoreValues, dataPath, widgetId, oldValue);
3634
+ newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
2596
3635
  }
2597
3636
  }
2598
3637
  });
2599
3638
  // Also revert supporting documents if any
2600
3639
  if (hasSupportingDocuments) {
2601
- supportingDocuments.forEach((doc, index) => {
3640
+ // Use original section's supporting documents to get original data paths
3641
+ const originalSupportingDocuments = originalSection['section-supporting-documents'] || [];
3642
+ originalSupportingDocuments.forEach((doc, index) => {
2602
3643
  const widgetId = `supporting-doc-${sectionId}-${index}`;
2603
- const dataPath = doc['document-data-path'];
2604
- const oldValue = getValueByPath(oldSchemaData, dataPath);
2605
- newStoreValues = setWidgetValue(newStoreValues, dataPath, widgetId, oldValue);
3644
+ const originalDataPath = doc['document-data-path'];
3645
+ // If namespace was used, data path in store is namespaced
3646
+ const storeDataPath = namespace && originalDataPath
3647
+ ? `${namespace}.${originalDataPath}`
3648
+ : originalDataPath;
3649
+ const oldValue = getValueByPath(oldSchemaData, originalDataPath);
3650
+ newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
2606
3651
  });
2607
3652
  }
2608
3653
  if (newStoreValues !== currentStoreValues) {
@@ -2617,11 +3662,13 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
2617
3662
  (documentType === 'image' ? 'image/*' :
2618
3663
  documentType === 'pdf' ? '.pdf' :
2619
3664
  '*/*');
3665
+ // Use the namespaced section ID for widget ID to ensure uniqueness
3666
+ const widgetId = `supporting-doc-${sectionId}-${index}`;
2620
3667
  return {
2621
3668
  widget: 'file',
2622
3669
  'widget-type': 'input',
2623
3670
  'widget-label': doc['document-label'] || doc['document-data-path'] || `Document ${index + 1}`,
2624
- 'widget-id': `supporting-doc-${sectionId}-${index}`,
3671
+ 'widget-id': widgetId,
2625
3672
  'widget-data-path': doc['document-data-path'],
2626
3673
  'widget-required': doc['document-required'] || false,
2627
3674
  'widget-readonly': false,
@@ -2777,7 +3824,7 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
2777
3824
  minHeight: 'auto',
2778
3825
  height: 'auto'
2779
3826
  }),
2780
- }, children: [section['section-title'] && (jsxRuntimeExports.jsx("h2", { className: "text-xl font-semibold mb-4", style: { marginTop: '35px' }, children: translateConfig(section['section-title']) })), jsxRuntimeExports.jsxs("div", { id: gridId, className: "section-panels", style: mode === 'RegistryView' && hideEditButton ? { paddingBottom: '40px' } : {}, children: [editableSection.panels.map((panel, index) => (jsxRuntimeExports.jsx("div", { className: "panel-wrapper", children: jsxRuntimeExports.jsx(PanelRenderer, { panel: panel, apiAdapter: apiAdapter, schemaData: schemaData, onValueChange: onValueChange }) }, panel['panel-id'] || `section-panel-${index}`))), mode === 'CRView' && crViewData && (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("hr", { className: "border-gray-300 w-full", style: { height: '1px', marginTop: '20px', marginBottom: '0px' } }), jsxRuntimeExports.jsxs("div", { className: "cr-view-container", style: {
3827
+ }, children: [sectionToRender['section-title'] && (jsxRuntimeExports.jsx("h2", { className: "text-xl font-semibold mb-4", style: { marginTop: '35px' }, children: translateConfig(sectionToRender['section-title']) })), jsxRuntimeExports.jsxs("div", { id: gridId, className: "section-panels", style: mode === 'RegistryView' && hideEditButton ? { paddingBottom: '40px' } : {}, children: [editableSection.panels.map((panel, index) => (jsxRuntimeExports.jsx("div", { className: "panel-wrapper", children: jsxRuntimeExports.jsx(PanelRenderer, { panel: panel, dataSourceRequestHandler: dataSourceRequestHandler, schemaData: namespacedSchemaData, onValueChange: onValueChange }) }, panel['panel-id'] || `section-panel-${index}`))), mode === 'CRView' && crViewData && (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("hr", { className: "border-gray-300 w-full", style: { height: '1px', marginTop: '20px', marginBottom: '0px' } }), jsxRuntimeExports.jsxs("div", { className: "cr-view-container", style: {
2781
3828
  marginTop: '20px',
2782
3829
  paddingBottom: '30px',
2783
3830
  display: 'flex',
@@ -2845,130 +3892,1807 @@ const hasTableWidget = (panels) => {
2845
3892
  }
2846
3893
  }
2847
3894
  }
2848
- // Recursively check nested panels
2849
- if (panel.panels) {
2850
- if (hasTableWidget(panel.panels)) {
3895
+ // Recursively check nested panels
3896
+ if (panel.panels) {
3897
+ if (hasTableWidget(panel.panels)) {
3898
+ return true;
3899
+ }
3900
+ }
3901
+ }
3902
+ return false;
3903
+ };
3904
+ /**
3905
+ * Recursively get table widget column span from panels
3906
+ */
3907
+ const getTableWidgetColumnSpan = (panels) => {
3908
+ for (const panel of panels) {
3909
+ // Check widgets in this panel
3910
+ if (panel.widgets) {
3911
+ for (const widget of panel.widgets) {
3912
+ if (widget.widget === 'table' || widget['widget-type'] === 'table') {
3913
+ // Return the widget's column span if specified, otherwise null
3914
+ return widget['widget-column-span'] || null;
3915
+ }
3916
+ }
3917
+ }
3918
+ // Recursively check nested panels
3919
+ if (panel.panels) {
3920
+ const nestedSpan = getTableWidgetColumnSpan(panel.panels);
3921
+ if (nestedSpan !== null) {
3922
+ return nestedSpan;
3923
+ }
3924
+ }
3925
+ }
3926
+ return null;
3927
+ };
3928
+ /**
3929
+ * Recursively count all vertical panels in a section
3930
+ * Handles nested structure: horizontal panels containing vertical panels
3931
+ * Accounts for panel-column-span: a panel with column-span 3 counts as 3 columns
3932
+ */
3933
+ const countVerticalPanels = (panels) => {
3934
+ let count = 0;
3935
+ for (const panel of panels) {
3936
+ const orientation = panel['panel-orientation'] || 'vertical';
3937
+ if (orientation === 'horizontal' && panel.panels) {
3938
+ // For horizontal panels, count all vertical panels nested inside
3939
+ count += countVerticalPanels(panel.panels);
3940
+ }
3941
+ else if (orientation === 'vertical') {
3942
+ // Count this vertical panel, accounting for column span
3943
+ const columnSpan = panel['panel-column-span'] || 1;
3944
+ count += columnSpan;
3945
+ // Also recursively count vertical panels nested inside this vertical panel
3946
+ if (panel.panels && panel.panels.length > 0) {
3947
+ count += countVerticalPanels(panel.panels);
3948
+ }
3949
+ }
3950
+ }
3951
+ return count;
3952
+ };
3953
+ /**
3954
+ * Container component that renders multiple sections
3955
+ *
3956
+ * Layout behavior:
3957
+ * - Uses CSS Grid for proper alignment
3958
+ * - Each grid column = 200px (one vertical panel width)
3959
+ * - Sections span columns based on their total vertical panel count
3960
+ * - All sections align to the same grid, ensuring right-side alignment
3961
+ * - Handles nested structure: multiple horizontal panels, each with multiple vertical panels
3962
+ */
3963
+ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceRequestHandler, schemaData, onValueChange, className = '', onSectionSave, hideEditButton = false, mode = 'RegistryView', namespace, }) => {
3964
+ // Get dataSourceRequestHandler from context if not provided as prop
3965
+ const { dataSourceRequestHandler: contextDataSourceRequestHandler } = useWidgetContext();
3966
+ const dataSourceRequestHandler = propDataSourceRequestHandler || contextDataSourceRequestHandler;
3967
+ // Warn if dataSourceRequestHandler is missing
3968
+ React.useEffect(() => {
3969
+ if (!dataSourceRequestHandler) {
3970
+ console.warn('[SectionsContainer] ⚠️ dataSourceRequestHandler is not provided. ' +
3971
+ 'Sections with widgets that have API data sources will not be able to load data. ' +
3972
+ 'Please provide dataSourceRequestHandler prop to SectionsContainer or WidgetProvider.');
3973
+ }
3974
+ }, [dataSourceRequestHandler]);
3975
+ // Find the maximum number of vertical panels across all sections
3976
+ // This determines the grid size (minimum 3 columns)
3977
+ // Also account for table widgets and their explicit column spans
3978
+ const maxVerticalPanels = Math.max(...sections.map(section => {
3979
+ const panelCount = countVerticalPanels(section.panels);
3980
+ const tableWidgetSpan = getTableWidgetColumnSpan(section.panels);
3981
+ // Use explicit table widget span if specified, otherwise use default logic
3982
+ return tableWidgetSpan !== null
3983
+ ? Math.max(panelCount, tableWidgetSpan)
3984
+ : (hasTableWidget(section.panels) ? Math.max(panelCount, 2) : panelCount);
3985
+ }), 3 // Minimum 3 columns
3986
+ );
3987
+ const containerId = 'sections-container-grid';
3988
+ return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
3989
+ #${containerId} {
3990
+ display: grid;
3991
+ /* Flexible columns: minimum 200px, but can grow equally to fill width */
3992
+ grid-template-columns: repeat(${maxVerticalPanels}, minmax(200px, 1fr));
3993
+ gap: 1.5rem;
3994
+ width: 100%;
3995
+ align-items: start;
3996
+ }
3997
+
3998
+ /* Sections with table widgets should expand to fill available space only if no explicit span */
3999
+ #${containerId} > .section[data-has-table="true"][data-has-explicit-span="false"] {
4000
+ grid-column: 1 / -1; /* Span all columns */
4001
+ width: 100%;
4002
+ }
4003
+
4004
+ /* Sections with explicit table widget span - inline style will handle grid-column */
4005
+ /* This rule ensures width is 100% but doesn't override grid-column */
4006
+ #${containerId} > .section[data-has-explicit-span="true"] {
4007
+ width: 100%;
4008
+ }
4009
+
4010
+ /* Responsive: on smaller screens, use auto-fit for flexibility */
4011
+ @media (max-width: 1023px) {
4012
+ #${containerId} {
4013
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
4014
+ }
4015
+ }
4016
+ ` }), jsxRuntimeExports.jsx("div", { id: containerId, className: `sections-container ${className}`, children: sections.map((section, index) => {
4017
+ // Determine namespace for this section
4018
+ const sectionNamespace = namespace
4019
+ ? (typeof namespace === 'string' ? namespace : namespace(section['section-id'], index))
4020
+ : undefined;
4021
+ // Check if section has explicit column span
4022
+ if (section['section-column-span']) {
4023
+ return (jsxRuntimeExports.jsx(SectionRenderer, { section: section, dataSourceRequestHandler: dataSourceRequestHandler, schemaData: schemaData, onValueChange: onValueChange, gridColumnSpan: section['section-column-span'], onSectionSave: onSectionSave, hideEditButton: hideEditButton, mode: mode, namespace: sectionNamespace }, section['section-id']));
4024
+ }
4025
+ const verticalPanelsCount = countVerticalPanels(section.panels);
4026
+ const tableWidgetColumnSpan = getTableWidgetColumnSpan(section.panels);
4027
+ const containsTable = hasTableWidget(section.panels);
4028
+ // If section contains a table widget with explicit column span, use it
4029
+ // Otherwise, if it has a table widget, ensure it spans at least 2 columns
4030
+ // Otherwise, use the vertical panel count
4031
+ const columnSpan = tableWidgetColumnSpan !== null
4032
+ ? tableWidgetColumnSpan
4033
+ : (containsTable ? Math.max(verticalPanelsCount, 2) : verticalPanelsCount);
4034
+ return (jsxRuntimeExports.jsx(SectionRenderer, { section: section, dataSourceRequestHandler: dataSourceRequestHandler, schemaData: schemaData, onValueChange: onValueChange, gridColumnSpan: columnSpan, onSectionSave: onSectionSave, hideEditButton: hideEditButton, mode: mode, namespace: sectionNamespace }, section['section-id']));
4035
+ }) })] }));
4036
+ };
4037
+
4038
+ /**
4039
+ * JSON Schema definitions for Section Builder validation
4040
+ * These schemas are used by json-edit-react to provide validation and constraints
4041
+ */
4042
+ const WIDGET_TYPES = [
4043
+ 'text',
4044
+ 'textarea',
4045
+ 'number',
4046
+ 'boolean',
4047
+ 'date',
4048
+ 'datetime',
4049
+ 'select',
4050
+ 'radio',
4051
+ 'checkbox',
4052
+ 'file',
4053
+ 'simple-table',
4054
+ 'table',
4055
+ 'array-widget',
4056
+ 'iterable-accordion',
4057
+ 'phone',
4058
+ 'currency',
4059
+ 'display',
4060
+ 'profile',
4061
+ ];
4062
+ const ORIENTATIONS = ['horizontal', 'vertical'];
4063
+ const CONDITION_OPERATORS = [
4064
+ 'equals',
4065
+ 'notEquals',
4066
+ 'notEmpty',
4067
+ 'empty',
4068
+ 'greaterThan',
4069
+ 'lessThan',
4070
+ 'contains',
4071
+ 'notContains',
4072
+ ];
4073
+ const DATA_SOURCE_TYPES = ['static', 'api', 'schema'];
4074
+ const VALIDATION_TYPES = ['email', 'phone', 'url'];
4075
+ const CHARACTER_TYPES = [
4076
+ 'any',
4077
+ 'alphabetic',
4078
+ 'alphanumeric',
4079
+ 'numeric',
4080
+ 'numeric-decimal',
4081
+ 'custom',
4082
+ ];
4083
+ const CASE_CONTROLS = ['none', 'lowercase', 'uppercase', 'capitalize'];
4084
+ const NUMERIC_TYPES = ['integer', 'decimal'];
4085
+ const BOOLEAN_REPRESENTATIONS = ['true-false', 'yes-no', 'on-off', 'custom'];
4086
+ const BOOLEAN_CONTROL_TYPES = ['checkbox', 'radio', 'toggle'];
4087
+
4088
+ // Inject styles to constrain json-edit-react container
4089
+ if (typeof document !== 'undefined') {
4090
+ const styleId = 'json-editor-constraints';
4091
+ if (!document.getElementById(styleId)) {
4092
+ const style = document.createElement('style');
4093
+ style.id = styleId;
4094
+ style.textContent = `
4095
+ .json-editor-scroll-container {
4096
+ display: flex !important;
4097
+ flex-direction: column !important;
4098
+ height: 100% !important;
4099
+ max-height: 100% !important;
4100
+ overflow: auto !important;
4101
+ }
4102
+ .json-editor-scroll-container .jer-editor-container {
4103
+ max-width: 100% !important;
4104
+ width: 100% !important;
4105
+ height: auto !important;
4106
+ max-height: none !important;
4107
+ flex-shrink: 0 !important;
4108
+ background-color: white !important;
4109
+ }
4110
+ .json-editor-scroll-container .jer-component {
4111
+ width: 100% !important;
4112
+ }
4113
+ `;
4114
+ document.head.appendChild(style);
4115
+ }
4116
+ }
4117
+ /**
4118
+ * JSON Editor Panel - Left side of Section Builder
4119
+ */
4120
+ const JSONEditorPanel = ({ section, onChange, onReset, onSelectNode, context = 'section', }) => {
4121
+ const [jsonData, setJsonData] = React.useState(section);
4122
+ const [validationErrors, setValidationErrors] = React.useState([]);
4123
+ const [rawJsonView, setRawJsonView] = React.useState(false);
4124
+ const [rawJsonText, setRawJsonText] = React.useState('');
4125
+ const [showPreview, setShowPreview] = React.useState(false);
4126
+ const [editorKey, setEditorKey] = React.useState(0); // Key to force JsonEditor re-render on reset
4127
+ // Store the original section when component mounts or section prop changes
4128
+ const originalSectionRef = React.useRef(section);
4129
+ // Get WidgetProvider context for preview modal (optional - may not be available)
4130
+ let widgetContext;
4131
+ try {
4132
+ widgetContext = useWidgetContext();
4133
+ }
4134
+ catch {
4135
+ widgetContext = {
4136
+ dataSourceRequestHandler: undefined,
4137
+ schemaData: undefined,
4138
+ translate: undefined,
4139
+ };
4140
+ }
4141
+ // Create a store for the preview modal if we're not in a Provider
4142
+ // This ensures SectionRenderer has access to Redux
4143
+ const previewStore = React.useMemo(() => createWidgetStore(), []);
4144
+ // Track if this is the initial mount
4145
+ const isInitialMount = React.useRef(true);
4146
+ React.useEffect(() => {
4147
+ // Only update original section on initial mount (when page loads)
4148
+ // This ensures reset works until save is clicked
4149
+ // Don't update original when user makes edits (those come through onChange)
4150
+ if (isInitialMount.current) {
4151
+ originalSectionRef.current = JSON.parse(JSON.stringify(section)); // Deep copy
4152
+ isInitialMount.current = false;
4153
+ }
4154
+ // Always sync the display with the section prop (for external updates like reset from parent)
4155
+ setJsonData(section);
4156
+ setRawJsonText(JSON.stringify(section, null, 2));
4157
+ // Force JsonEditor to update when section prop changes (e.g., from parent reset)
4158
+ setEditorKey(prev => prev + 1);
4159
+ }, [section]);
4160
+ // Reset to original section
4161
+ const handleReset = React.useCallback(() => {
4162
+ // If parent provides onReset, use it (this will reset both JSON editor and visual builder)
4163
+ if (onReset) {
4164
+ onReset();
4165
+ // Also force JsonEditor to remount to ensure it picks up the reset
4166
+ setEditorKey(prev => prev + 1);
4167
+ return;
4168
+ }
4169
+ // Fallback: reset only this panel (for standalone usage)
4170
+ const original = JSON.parse(JSON.stringify(originalSectionRef.current)); // Deep copy to ensure new reference
4171
+ // Update state immediately
4172
+ setJsonData(original);
4173
+ setRawJsonText(JSON.stringify(original, null, 2));
4174
+ // Force JsonEditor to completely remount by changing key
4175
+ // This is critical because json-edit-react maintains internal state that doesn't sync with props
4176
+ setEditorKey(prev => prev + 1);
4177
+ // Notify parent
4178
+ onChange(original);
4179
+ }, [onChange, onReset]);
4180
+ // Handle Escape key to close preview
4181
+ React.useEffect(() => {
4182
+ if (!showPreview)
4183
+ return;
4184
+ const handleEscape = (e) => {
4185
+ if (e.key === 'Escape') {
4186
+ setShowPreview(false);
4187
+ }
4188
+ };
4189
+ window.addEventListener('keydown', handleEscape);
4190
+ return () => window.removeEventListener('keydown', handleEscape);
4191
+ }, [showPreview]);
4192
+ // Handle clicks in JSON editor to select corresponding node in visual builder
4193
+ React.useEffect(() => {
4194
+ if (!onSelectNode || rawJsonView)
4195
+ return; // Only work in tree view, not raw JSON view
4196
+ const handleJsonEditorClick = (e) => {
4197
+ const mouseEvent = e;
4198
+ const target = mouseEvent.target;
4199
+ // Find the key text element (json-edit-react uses .jer-key-text class)
4200
+ const keyElement = target.closest('.jer-key-text') ||
4201
+ target.querySelector('.jer-key-text') ||
4202
+ (target.classList.contains('jer-key-text') ? target : null);
4203
+ if (!keyElement)
4204
+ return;
4205
+ const keyText = keyElement.textContent?.trim();
4206
+ if (!keyText)
4207
+ return;
4208
+ // Remove colon if present
4209
+ const keyName = keyText.replace(':', '').trim();
4210
+ // Map key names to node types and find the corresponding node
4211
+ let nodeId = null;
4212
+ let nodeType = null;
4213
+ if (keyName === 'section-id') {
4214
+ // Find the section-id value
4215
+ const keyRow = keyElement.closest('.jer-collection-header-row, .jer-value-row');
4216
+ if (keyRow) {
4217
+ const valueElement = keyRow.querySelector('.jer-value-text, .jer-string-value');
4218
+ if (valueElement) {
4219
+ nodeId = valueElement.textContent?.replace(/^"|"$/g, '').trim() || section['section-id'];
4220
+ nodeType = 'section';
4221
+ }
4222
+ }
4223
+ }
4224
+ else if (keyName === 'panel-id') {
4225
+ // Find the panel-id value in the current panel object
4226
+ const panelContainer = keyElement.closest('.jer-collection-component');
4227
+ if (panelContainer) {
4228
+ const valueElement = panelContainer.querySelector('.jer-value-text, .jer-string-value');
4229
+ if (valueElement) {
4230
+ // Try to find panel-id value in this panel
4231
+ const allKeys = panelContainer.querySelectorAll('.jer-key-text');
4232
+ for (const key of Array.from(allKeys)) {
4233
+ if (key.textContent?.includes('panel-id')) {
4234
+ const keyRow = key.closest('.jer-collection-header-row, .jer-value-row');
4235
+ if (keyRow) {
4236
+ const valElement = keyRow.querySelector('.jer-value-text, .jer-string-value');
4237
+ if (valElement) {
4238
+ nodeId = valElement.textContent?.replace(/^"|"$/g, '').trim() || null;
4239
+ nodeType = 'panel';
4240
+ break;
4241
+ }
4242
+ }
4243
+ }
4244
+ }
4245
+ }
4246
+ }
4247
+ }
4248
+ else if (keyName === 'widget-id') {
4249
+ // Find the widget-id value in the current widget object
4250
+ const widgetContainer = keyElement.closest('.jer-collection-component');
4251
+ if (widgetContainer) {
4252
+ const allKeys = widgetContainer.querySelectorAll('.jer-key-text');
4253
+ for (const key of Array.from(allKeys)) {
4254
+ if (key.textContent?.includes('widget-id')) {
4255
+ const keyRow = key.closest('.jer-collection-header-row, .jer-value-row');
4256
+ if (keyRow) {
4257
+ const valElement = keyRow.querySelector('.jer-value-text, .jer-string-value');
4258
+ if (valElement) {
4259
+ nodeId = valElement.textContent?.replace(/^"|"$/g, '').trim() || null;
4260
+ nodeType = 'widget';
4261
+ break;
4262
+ }
4263
+ }
4264
+ }
4265
+ }
4266
+ }
4267
+ }
4268
+ // If we found a node, select it in the visual builder
4269
+ if (nodeId && nodeType) {
4270
+ onSelectNode(nodeId, nodeType);
4271
+ }
4272
+ };
4273
+ // Add click listener to the JSON editor container
4274
+ const editorContainer = document.querySelector('.json-editor-scroll-container');
4275
+ if (editorContainer) {
4276
+ editorContainer.addEventListener('click', handleJsonEditorClick);
4277
+ return () => {
4278
+ editorContainer.removeEventListener('click', handleJsonEditorClick);
4279
+ };
4280
+ }
4281
+ }, [onSelectNode, rawJsonView, section]);
4282
+ // Make section editable for preview - remove readonly flags from widgets
4283
+ const makeSectionEditable = React.useCallback((section) => {
4284
+ const processWidget = (widget) => {
4285
+ if (!widget || typeof widget !== 'object')
4286
+ return widget;
4287
+ const editableWidget = {
4288
+ ...widget,
4289
+ 'widget-readonly': false, // Make all widgets editable in preview
4290
+ };
4291
+ // Process nested widgets
4292
+ if (widget.widgets && Array.isArray(widget.widgets)) {
4293
+ editableWidget.widgets = widget.widgets.map(processWidget);
4294
+ }
4295
+ if (widget['widget-item']) {
4296
+ editableWidget['widget-item'] = processWidget(widget['widget-item']);
4297
+ }
4298
+ // Process table columns
4299
+ if (widget['widget-data-columns'] && Array.isArray(widget['widget-data-columns'])) {
4300
+ editableWidget['widget-data-columns'] = widget['widget-data-columns'].map((col) => {
4301
+ if (col && typeof col === 'object' && col.widget) {
4302
+ return processWidget(col);
4303
+ }
4304
+ return col;
4305
+ });
4306
+ }
4307
+ return editableWidget;
4308
+ };
4309
+ const processPanel = (panel) => {
4310
+ if (!panel || typeof panel !== 'object')
4311
+ return panel;
4312
+ const editablePanel = { ...panel };
4313
+ if (panel.widgets && Array.isArray(panel.widgets)) {
4314
+ editablePanel.widgets = panel.widgets.map(processWidget);
4315
+ }
4316
+ if (panel.panels && Array.isArray(panel.panels)) {
4317
+ editablePanel.panels = panel.panels.map(processPanel);
4318
+ }
4319
+ return editablePanel;
4320
+ };
4321
+ return {
4322
+ ...section,
4323
+ 'section-editable': true,
4324
+ panels: section.panels ? section.panels.map(processPanel) : [],
4325
+ };
4326
+ }, []);
4327
+ // Auto-populate widget-type based on widget selection
4328
+ const autoPopulateWidgetType = React.useCallback((data) => {
4329
+ if (!data || typeof data !== 'object')
4330
+ return data;
4331
+ const processWidget = (widget) => {
4332
+ if (!widget || typeof widget !== 'object')
4333
+ return widget;
4334
+ const widgetType = widget.widget;
4335
+ if (widgetType && !widget['widget-type']) {
4336
+ // Auto-determine widget-type based on widget name
4337
+ const widgetTypeMap = {
4338
+ 'text': 'input',
4339
+ 'textarea': 'input',
4340
+ 'number': 'input',
4341
+ 'boolean': 'input',
4342
+ 'date': 'input',
4343
+ 'datetime': 'input',
4344
+ 'select': 'input',
4345
+ 'radio': 'input',
4346
+ 'checkbox': 'input',
4347
+ 'file': 'input',
4348
+ 'phone': 'input',
4349
+ 'currency': 'input',
4350
+ 'display': 'input',
4351
+ 'table': 'table',
4352
+ 'simple-table': 'table',
4353
+ 'array-widget': 'group',
4354
+ 'iterable-accordion': 'group',
4355
+ 'profile': 'layout',
4356
+ };
4357
+ widget = {
4358
+ ...widget,
4359
+ 'widget-type': widgetTypeMap[widgetType] || 'input',
4360
+ };
4361
+ }
4362
+ // Process nested widgets
4363
+ if (widget.widgets && Array.isArray(widget.widgets)) {
4364
+ widget = {
4365
+ ...widget,
4366
+ widgets: widget.widgets.map(processWidget),
4367
+ };
4368
+ }
4369
+ // Process widget-item
4370
+ if (widget['widget-item']) {
4371
+ widget = {
4372
+ ...widget,
4373
+ 'widget-item': processWidget(widget['widget-item']),
4374
+ };
4375
+ }
4376
+ // Process table columns
4377
+ if (widget['widget-data-columns'] && Array.isArray(widget['widget-data-columns'])) {
4378
+ widget = {
4379
+ ...widget,
4380
+ 'widget-data-columns': widget['widget-data-columns'].map((col) => {
4381
+ if (col && typeof col === 'object' && col.widget && !col['widget-type']) {
4382
+ const widgetTypeMap = {
4383
+ 'text': 'input',
4384
+ 'number': 'input',
4385
+ 'date': 'input',
4386
+ 'select': 'input',
4387
+ 'boolean': 'input',
4388
+ };
4389
+ return {
4390
+ ...col,
4391
+ 'widget-type': widgetTypeMap[col.widget] || 'input',
4392
+ };
4393
+ }
4394
+ return col;
4395
+ }),
4396
+ };
4397
+ }
4398
+ return widget;
4399
+ };
4400
+ const processPanel = (panel) => {
4401
+ if (!panel || typeof panel !== 'object')
4402
+ return panel;
4403
+ let processed = { ...panel };
4404
+ // Process widgets in panel
4405
+ if (processed.widgets && Array.isArray(processed.widgets)) {
4406
+ processed.widgets = processed.widgets.map(processWidget);
4407
+ }
4408
+ // Process nested panels
4409
+ if (processed.panels && Array.isArray(processed.panels)) {
4410
+ processed.panels = processed.panels.map(processPanel);
4411
+ }
4412
+ return processed;
4413
+ };
4414
+ // Process section
4415
+ if (data.panels && Array.isArray(data.panels)) {
4416
+ return {
4417
+ ...data,
4418
+ panels: data.panels.map(processPanel),
4419
+ };
4420
+ }
4421
+ return data;
4422
+ }, []);
4423
+ const handleJsonChange = React.useCallback((data) => {
4424
+ // json-edit-react may wrap the data in a "root" key - unwrap it if present
4425
+ let unwrappedData = data?.root ? data.root : data;
4426
+ // Auto-populate widget-type for widgets that don't have it
4427
+ unwrappedData = autoPopulateWidgetType(unwrappedData);
4428
+ setJsonData(unwrappedData);
4429
+ setRawJsonText(JSON.stringify(unwrappedData, null, 2));
4430
+ // Basic validation
4431
+ const errors = [];
4432
+ if (!unwrappedData['section-id']) {
4433
+ errors.push('section-id is required');
4434
+ }
4435
+ if (!unwrappedData.panels || !Array.isArray(unwrappedData.panels)) {
4436
+ errors.push('panels must be an array');
4437
+ }
4438
+ setValidationErrors(errors);
4439
+ // Only update if valid
4440
+ if (errors.length === 0) {
4441
+ onChange(unwrappedData);
4442
+ }
4443
+ }, [onChange, autoPopulateWidgetType]);
4444
+ const handleRawJsonChange = React.useCallback((text) => {
4445
+ setRawJsonText(text);
4446
+ try {
4447
+ const parsed = JSON.parse(text);
4448
+ const errors = [];
4449
+ if (!parsed['section-id']) {
4450
+ errors.push('section-id is required');
4451
+ }
4452
+ if (!parsed.panels || !Array.isArray(parsed.panels)) {
4453
+ errors.push('panels must be an array');
4454
+ }
4455
+ setValidationErrors(errors);
4456
+ // Auto-populate widget-type
4457
+ const processed = autoPopulateWidgetType(parsed);
4458
+ if (errors.length === 0) {
4459
+ setJsonData(processed);
4460
+ onChange(processed);
4461
+ }
4462
+ }
4463
+ catch (error) {
4464
+ setValidationErrors([`Invalid JSON: ${error instanceof Error ? error.message : 'Parse error'}`]);
4465
+ }
4466
+ }, [onChange, autoPopulateWidgetType]);
4467
+ const toggleRawJsonView = React.useCallback(() => {
4468
+ if (!rawJsonView) {
4469
+ // Switching to raw view - update text from current data
4470
+ setRawJsonText(JSON.stringify(jsonData, null, 2));
4471
+ }
4472
+ setRawJsonView(!rawJsonView);
4473
+ }, [rawJsonView, jsonData]);
4474
+ // Create enum configuration for json-edit-react
4475
+ // This maps field paths to their allowed enum values
4476
+ const enumConfig = React.useCallback(() => {
4477
+ return {
4478
+ // Section level
4479
+ 'section-id': undefined, // string, no enum
4480
+ 'section-title': undefined, // string, no enum
4481
+ 'section-editable': undefined, // boolean, no enum
4482
+ 'section-column-span': undefined, // number, no enum
4483
+ // Panel level - can be nested in panels array
4484
+ 'panel-id': undefined, // string, no enum
4485
+ 'panel-orientation': ORIENTATIONS, // enum: ['horizontal', 'vertical']
4486
+ 'panel-column-span': undefined, // number, no enum
4487
+ // Widget level - can be nested in widgets array or widget-item
4488
+ 'widget': WIDGET_TYPES, // enum: all widget types
4489
+ 'widget-type': ['input', 'layout', 'table', 'group'], // enum
4490
+ 'widget-id': undefined, // string, no enum
4491
+ 'widget-label': undefined, // string, no enum
4492
+ 'widget-orientation': ORIENTATIONS, // enum: ['horizontal', 'vertical']
4493
+ 'widget-required': undefined, // boolean, no enum
4494
+ 'widget-readonly': undefined, // boolean, no enum
4495
+ // Widget data source type
4496
+ 'widget-data-source.type': DATA_SOURCE_TYPES, // enum: ['static', 'api', 'schema']
4497
+ 'widget-data-source.method': ['GET', 'POST', 'PUT', 'DELETE'], // HTTP methods
4498
+ // Widget validation
4499
+ 'widget-data-validation.validationType': VALIDATION_TYPES, // enum: ['email', 'phone', 'url']
4500
+ // Widget format options
4501
+ 'widget-data-format.inputType': ['text', 'email', 'password', 'number', 'tel', 'url', 'search', 'file'],
4502
+ 'widget-data-format.characterType': CHARACTER_TYPES,
4503
+ 'widget-data-format.caseControl': CASE_CONTROLS,
4504
+ 'widget-data-format.numericType': NUMERIC_TYPES,
4505
+ 'widget-data-format.roundingMode': ['round', 'truncate'],
4506
+ 'widget-data-format.textAlign': ['left', 'right'],
4507
+ 'widget-data-format.booleanRepresentation': BOOLEAN_REPRESENTATIONS,
4508
+ 'widget-data-format.booleanControlType': BOOLEAN_CONTROL_TYPES,
4509
+ 'widget-data-format.layout': ['vertical', 'horizontal', 'grid'],
4510
+ 'widget-data-format.inputMethod': ['picker', 'manual', 'hybrid'],
4511
+ 'widget-data-format.dateConstraint': ['any', 'past-only', 'future-only'],
4512
+ 'widget-data-format.dateTimeConstraint': ['any', 'past-only', 'future-only'],
4513
+ // Widget options
4514
+ 'widget-data-options.action': ['show', 'hide', 'enable', 'disable'],
4515
+ 'widget-data-options.condition.operator': CONDITION_OPERATORS,
4516
+ };
4517
+ }, []);
4518
+ return (jsxRuntimeExports.jsxs("div", { style: {
4519
+ display: 'flex',
4520
+ flexDirection: 'column',
4521
+ height: '100%',
4522
+ width: '100%',
4523
+ minHeight: 0,
4524
+ borderRight: '0px',
4525
+ }, children: [jsxRuntimeExports.jsxs("div", { style: {
4526
+ padding: '15px 20px',
4527
+ background: '#ffffff',
4528
+ display: 'flex',
4529
+ justifyContent: 'space-between',
4530
+ alignItems: 'center',
4531
+ }, children: [jsxRuntimeExports.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '12px' }, children: [jsxRuntimeExports.jsx("div", { style: { fontWeight: 600, fontSize: '16px', color: '#2c3e50' }, children: "JSON Editor" }), validationErrors.length === 0 ? (jsxRuntimeExports.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '6px', color: '#28a745' }, children: [jsxRuntimeExports.jsx("div", { style: {
4532
+ width: '12px',
4533
+ height: '12px',
4534
+ borderRadius: '50%',
4535
+ background: '#28a745',
4536
+ } }), jsxRuntimeExports.jsx("span", { style: { fontSize: '12px' }, children: "Valid JSON Schema" })] })) : (jsxRuntimeExports.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '6px', color: '#e74c3c' }, children: [jsxRuntimeExports.jsx("div", { style: {
4537
+ width: '12px',
4538
+ height: '12px',
4539
+ borderRadius: '50%',
4540
+ background: '#e74c3c',
4541
+ } }), jsxRuntimeExports.jsx("span", { style: { fontSize: '12px' }, children: "Validation Errors" })] }))] }), jsxRuntimeExports.jsxs("div", { style: {
4542
+ display: 'flex',
4543
+ alignItems: 'center',
4544
+ gap: '8px',
4545
+ }, children: [jsxRuntimeExports.jsxs("button", { onClick: handleReset, style: {
4546
+ padding: '6px 12px',
4547
+ border: '1px solid #ddd',
4548
+ borderRadius: '4px',
4549
+ background: 'white',
4550
+ color: '#666',
4551
+ cursor: 'pointer',
4552
+ display: 'flex',
4553
+ alignItems: 'center',
4554
+ gap: '6px',
4555
+ fontSize: '12px',
4556
+ fontWeight: 500,
4557
+ transition: 'all 0.2s',
4558
+ }, onMouseEnter: (e) => {
4559
+ e.currentTarget.style.background = '#f8f9fa';
4560
+ e.currentTarget.style.borderColor = '#999';
4561
+ }, onMouseLeave: (e) => {
4562
+ e.currentTarget.style.background = 'white';
4563
+ e.currentTarget.style.borderColor = '#ddd';
4564
+ }, title: "Reset to original JSON", children: [jsxRuntimeExports.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntimeExports.jsx("path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }), jsxRuntimeExports.jsx("path", { d: "M21 3v5h-5" }), jsxRuntimeExports.jsx("path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }), jsxRuntimeExports.jsx("path", { d: "M3 21v-5h5" })] }), "Reset"] }), jsxRuntimeExports.jsxs("button", { onClick: () => setShowPreview(true), style: {
4565
+ padding: '6px 12px',
4566
+ border: '1px solid #ddd',
4567
+ borderRadius: '4px',
4568
+ background: 'white',
4569
+ color: '#666',
4570
+ cursor: 'pointer',
4571
+ display: 'flex',
4572
+ alignItems: 'center',
4573
+ gap: '6px',
4574
+ fontSize: '12px',
4575
+ fontWeight: 500,
4576
+ }, title: "Preview Section", children: [jsxRuntimeExports.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntimeExports.jsx("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }), jsxRuntimeExports.jsx("circle", { cx: "12", cy: "12", r: "3" })] }), "Preview"] }), jsxRuntimeExports.jsx("span", { style: {
4577
+ fontSize: '12px',
4578
+ color: !rawJsonView ? '#007bff' : '#6c757d',
4579
+ fontWeight: !rawJsonView ? 600 : 400,
4580
+ transition: 'color 0.2s',
4581
+ }, children: "Tree" }), jsxRuntimeExports.jsx("div", { onClick: toggleRawJsonView, style: {
4582
+ position: 'relative',
4583
+ width: '44px',
4584
+ height: '24px',
4585
+ background: rawJsonView ? '#007bff' : '#ccc',
4586
+ borderRadius: '12px',
4587
+ cursor: 'pointer',
4588
+ transition: 'background 0.2s',
4589
+ }, children: jsxRuntimeExports.jsx("div", { style: {
4590
+ position: 'absolute',
4591
+ top: '2px',
4592
+ left: rawJsonView ? '22px' : '2px',
4593
+ width: '20px',
4594
+ height: '20px',
4595
+ background: 'white',
4596
+ borderRadius: '50%',
4597
+ transition: 'left 0.2s',
4598
+ boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
4599
+ } }) }), jsxRuntimeExports.jsx("span", { style: {
4600
+ fontSize: '12px',
4601
+ color: rawJsonView ? '#007bff' : '#6c757d',
4602
+ fontWeight: rawJsonView ? 600 : 400,
4603
+ transition: 'color 0.2s',
4604
+ }, children: "Raw" })] })] }), jsxRuntimeExports.jsx("div", { style: {
4605
+ flex: 1,
4606
+ minHeight: 0,
4607
+ maxHeight: '100%',
4608
+ overflow: 'hidden',
4609
+ background: 'white',
4610
+ border: '1px solid #E1E1E1',
4611
+ borderRadius: '10px',
4612
+ padding: rawJsonView ? '0' : '20px',
4613
+ position: 'relative',
4614
+ display: 'flex',
4615
+ flexDirection: 'column',
4616
+ }, children: rawJsonView ? (jsxRuntimeExports.jsx("textarea", { value: rawJsonText, onChange: (e) => handleRawJsonChange(e.target.value), style: {
4617
+ width: '100%',
4618
+ height: '100%',
4619
+ background: 'white',
4620
+ color: '#333',
4621
+ border: 'none',
4622
+ padding: '20px',
4623
+ fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, "source-code-pro", monospace',
4624
+ fontSize: '13px',
4625
+ lineHeight: '1.5',
4626
+ resize: 'none',
4627
+ outline: 'none',
4628
+ boxSizing: 'border-box',
4629
+ borderRadius: '10px',
4630
+ }, spellCheck: false })) : (jsxRuntimeExports.jsx("div", { className: "json-editor-scroll-container", style: {
4631
+ width: '100%',
4632
+ height: '100%',
4633
+ minHeight: 0,
4634
+ maxHeight: '100%',
4635
+ overflow: 'auto',
4636
+ position: 'relative',
4637
+ display: 'flex',
4638
+ flexDirection: 'column',
4639
+ flex: '1 1 0',
4640
+ }, children: jsxRuntimeExports.jsx(jsonEditReact.JsonEditor, { data: jsonData, setData: handleJsonChange, ...{ enumOptions: enumConfig() } }, `editor-${editorKey}`) })) }), showPreview && reactDom.createPortal(jsxRuntimeExports.jsx("div", { className: "section-builder-preview-backdrop", style: {
4641
+ position: 'fixed',
4642
+ top: 0,
4643
+ left: 0,
4644
+ right: 0,
4645
+ bottom: 0,
4646
+ background: 'rgba(0, 0, 0, 0.5)',
4647
+ zIndex: 10000,
4648
+ display: 'flex',
4649
+ alignItems: 'center',
4650
+ justifyContent: 'center',
4651
+ padding: '20px',
4652
+ }, onClick: () => setShowPreview(false), children: jsxRuntimeExports.jsxs("div", { className: "section-builder-preview-modal", style: {
4653
+ background: 'white',
4654
+ borderRadius: '8px',
4655
+ width: '100%',
4656
+ minWidth: '700px', // Ensure enough width for 600px content + padding
4657
+ maxWidth: '90vw',
4658
+ height: '90vh',
4659
+ maxHeight: '90vh',
4660
+ display: 'flex',
4661
+ flexDirection: 'column',
4662
+ boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
4663
+ }, onClick: (e) => e.stopPropagation(), children: [jsxRuntimeExports.jsxs("div", { className: "section-builder-preview-header", style: {
4664
+ padding: '15px 20px',
4665
+ borderBottom: '1px solid #ddd',
4666
+ display: 'flex',
4667
+ justifyContent: 'space-between',
4668
+ alignItems: 'center',
4669
+ background: '#f8f9fa',
4670
+ }, children: [jsxRuntimeExports.jsx("h2", { className: "section-builder-preview-title", style: { margin: 0, fontSize: '18px', fontWeight: 600, color: '#2c3e50' }, children: "Section Preview" }), jsxRuntimeExports.jsx("button", { className: "section-builder-preview-close", onClick: () => setShowPreview(false), style: {
4671
+ padding: '6px 12px',
4672
+ border: 'none',
4673
+ borderRadius: '4px',
4674
+ background: '#e74c3c',
4675
+ color: 'white',
4676
+ cursor: 'pointer',
4677
+ fontSize: '14px',
4678
+ fontWeight: 600,
4679
+ }, children: "Close" })] }), jsxRuntimeExports.jsx("div", { className: "section-builder-preview-content", style: {
4680
+ flex: 1,
4681
+ overflow: 'auto',
4682
+ padding: '20px',
4683
+ }, children: jsxRuntimeExports.jsx(reactRedux.Provider, { store: previewStore, children: jsxRuntimeExports.jsx(WidgetProvider, { store: previewStore, dataSourceRequestHandler: widgetContext.dataSourceRequestHandler, schemaData: widgetContext.schemaData, translate: widgetContext.translate, children: jsxRuntimeExports.jsx("div", { style: { position: 'relative', width: '100%', height: '100%' }, children: jsxRuntimeExports.jsx(SectionRenderer, { section: makeSectionEditable(jsonData), hideEditButton: true, onValueChange: (widgetId, value) => {
4684
+ // Handle value changes in preview (optional - for tracking)
4685
+ console.log('Preview value changed:', widgetId, value);
4686
+ } }) }) }) }) })] }) }), document.body)] }));
4687
+ };
4688
+
4689
+ /**
4690
+ * Recursively build tree nodes from panel structure
4691
+ */
4692
+ function buildTreeNodesFromPanels(panels, parent) {
4693
+ const nodes = [];
4694
+ panels.forEach((panel) => {
4695
+ const panelNode = {
4696
+ type: 'panel',
4697
+ id: panel['panel-id'],
4698
+ label: `Panel: ${panel['panel-id']}`,
4699
+ data: panel,
4700
+ parent: parent,
4701
+ children: [],
4702
+ };
4703
+ // Add nested panels recursively
4704
+ if (panel.panels && panel.panels.length > 0) {
4705
+ panelNode.children = panelNode.children || [];
4706
+ panelNode.children.push(...buildTreeNodesFromPanels(panel.panels, panelNode));
4707
+ }
4708
+ // Add widgets
4709
+ if (panel.widgets && panel.widgets.length > 0) {
4710
+ panelNode.children = panelNode.children || [];
4711
+ panel.widgets.forEach((widget) => {
4712
+ panelNode.children.push({
4713
+ type: 'widget',
4714
+ id: widget['widget-id'],
4715
+ label: `Widget: ${widget['widget-id']} (${widget.widget})`,
4716
+ data: widget,
4717
+ parent: panelNode,
4718
+ });
4719
+ });
4720
+ }
4721
+ nodes.push(panelNode);
4722
+ });
4723
+ return nodes;
4724
+ }
4725
+ /**
4726
+ * Tree view component for section structure
4727
+ */
4728
+ const SectionTree = ({ section, selectedNode, onSelectNode, onAddPanel, onAddWidget, onDeleteNode, onDuplicateNode, }) => {
4729
+ const treeNodes = React.useMemo(() => {
4730
+ if (section.panels && section.panels.length > 0) {
4731
+ return buildTreeNodesFromPanels(section.panels);
4732
+ }
4733
+ return [];
4734
+ }, [section]);
4735
+ const renderTreeNode = (node, level = 0) => {
4736
+ const isSelected = selectedNode?.id === node.id && selectedNode?.type === node.type;
4737
+ const indent = level * 20;
4738
+ const getNodeStyles = () => {
4739
+ const baseStyles = {
4740
+ marginLeft: `${indent}px`,
4741
+ marginTop: '4px',
4742
+ padding: '8px 12px',
4743
+ borderRadius: '4px',
4744
+ cursor: 'pointer',
4745
+ position: 'relative',
4746
+ transition: 'background 0.2s',
4747
+ display: 'flex',
4748
+ alignItems: 'center',
4749
+ justifyContent: 'space-between',
4750
+ };
4751
+ if (isSelected) {
4752
+ switch (node.type) {
4753
+ case 'section':
4754
+ return { ...baseStyles, background: '#e3f2fd', border: '1px solid #2196f3' };
4755
+ case 'panel':
4756
+ return { ...baseStyles, background: '#fff3e0', border: '1px solid #ff9800' };
4757
+ case 'widget':
4758
+ return { ...baseStyles, background: '#e8f5e9', border: '1px solid #4caf50' };
4759
+ }
4760
+ }
4761
+ switch (node.type) {
4762
+ case 'section':
4763
+ return { ...baseStyles, background: '#e3f2fd', border: '1px solid #2196f3' };
4764
+ case 'panel':
4765
+ return { ...baseStyles, background: '#fff3e0', border: '1px solid #ff9800' };
4766
+ case 'widget':
4767
+ return { ...baseStyles, background: '#e8f5e9', border: '1px solid #4caf50' };
4768
+ }
4769
+ return baseStyles;
4770
+ };
4771
+ const getIcon = () => {
4772
+ switch (node.type) {
4773
+ case 'section':
4774
+ return '📁';
4775
+ case 'panel':
4776
+ return '📦';
4777
+ case 'widget':
4778
+ return '🔧';
4779
+ }
4780
+ };
4781
+ const getActionButtonColor = () => {
4782
+ switch (node.type) {
4783
+ case 'section':
4784
+ return '#2196f3';
4785
+ case 'panel':
4786
+ return '#ff9800';
4787
+ case 'widget':
4788
+ return '#4caf50';
4789
+ }
4790
+ };
4791
+ return (jsxRuntimeExports.jsxs("div", { children: [jsxRuntimeExports.jsxs("div", { style: getNodeStyles(), onClick: (e) => {
4792
+ e.stopPropagation();
4793
+ onSelectNode(node);
4794
+ }, onMouseEnter: (e) => {
4795
+ const target = e.currentTarget;
4796
+ const actionBtn = target.querySelector('.tree-action-btn');
4797
+ if (actionBtn)
4798
+ actionBtn.style.opacity = '1';
4799
+ }, onMouseLeave: (e) => {
4800
+ const target = e.currentTarget;
4801
+ const actionBtn = target.querySelector('.tree-action-btn');
4802
+ if (actionBtn)
4803
+ actionBtn.style.opacity = '0';
4804
+ }, children: [jsxRuntimeExports.jsxs("span", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [jsxRuntimeExports.jsx("span", { children: getIcon() }), jsxRuntimeExports.jsx("span", { style: { fontSize: '13px' }, children: node.label })] }), jsxRuntimeExports.jsx("button", { className: "tree-action-btn", onClick: (e) => {
4805
+ e.stopPropagation();
4806
+ onSelectNode(node);
4807
+ }, style: {
4808
+ width: '24px',
4809
+ height: '24px',
4810
+ borderRadius: '50%',
4811
+ border: 'none',
4812
+ background: getActionButtonColor(),
4813
+ color: 'white',
4814
+ cursor: 'pointer',
4815
+ fontSize: '12px',
4816
+ opacity: isSelected ? '1' : '0',
4817
+ transition: 'opacity 0.2s',
4818
+ display: 'flex',
4819
+ alignItems: 'center',
4820
+ justifyContent: 'center',
4821
+ }, children: "\u2699" })] }), node.children && node.children.length > 0 && (jsxRuntimeExports.jsx("div", { style: { marginLeft: `${indent + 20}px` }, children: node.children.map((child) => renderTreeNode(child, level + 1)) }))] }, node.id));
4822
+ };
4823
+ return (jsxRuntimeExports.jsxs("div", { style: {
4824
+ padding: '15px',
4825
+ overflowY: 'auto',
4826
+ background: '#f8f9fa',
4827
+ height: '100%',
4828
+ }, children: [jsxRuntimeExports.jsx("h3", { style: { fontSize: '14px', marginBottom: '15px', color: '#2c3e50' }, children: "Section Structure" }), jsxRuntimeExports.jsxs("div", { style: {
4829
+ padding: '8px 12px',
4830
+ borderRadius: '4px',
4831
+ background: '#e3f2fd',
4832
+ border: '1px solid #2196f3',
4833
+ marginBottom: '10px',
4834
+ cursor: 'pointer',
4835
+ display: 'flex',
4836
+ alignItems: 'center',
4837
+ justifyContent: 'space-between',
4838
+ }, onClick: () => {
4839
+ const sectionNode = {
4840
+ type: 'section',
4841
+ id: section['section-id'],
4842
+ label: `Section: ${section['section-id']}`,
4843
+ data: section,
4844
+ children: treeNodes,
4845
+ };
4846
+ onSelectNode(sectionNode);
4847
+ }, children: [jsxRuntimeExports.jsxs("span", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [jsxRuntimeExports.jsx("span", { children: "\uD83D\uDCC1" }), jsxRuntimeExports.jsxs("span", { style: { fontSize: '13px', fontWeight: 600 }, children: ["Section: ", section['section-id']] })] }), jsxRuntimeExports.jsx("button", { onClick: (e) => {
4848
+ e.stopPropagation();
4849
+ const sectionNode = {
4850
+ type: 'section',
4851
+ id: section['section-id'],
4852
+ label: `Section: ${section['section-id']}`,
4853
+ data: section,
4854
+ children: treeNodes,
4855
+ };
4856
+ onSelectNode(sectionNode);
4857
+ }, style: {
4858
+ width: '24px',
4859
+ height: '24px',
4860
+ borderRadius: '50%',
4861
+ border: 'none',
4862
+ background: '#2196f3',
4863
+ color: 'white',
4864
+ cursor: 'pointer',
4865
+ fontSize: '12px',
4866
+ }, children: "\u2699" })] }), treeNodes.map((node) => renderTreeNode(node, 0))] }));
4867
+ };
4868
+
4869
+ /**
4870
+ * Property editor component for editing selected node properties
4871
+ */
4872
+ const PropertyEditor = ({ node, onChange, onDelete, onDuplicate, }) => {
4873
+ const [localData, setLocalData] = React.useState(null);
4874
+ React.useEffect(() => {
4875
+ if (node) {
4876
+ setLocalData({ ...node.data });
4877
+ }
4878
+ }, [node]);
4879
+ if (!node || !localData) {
4880
+ return (jsxRuntimeExports.jsx("div", { style: {
4881
+ padding: '15px',
4882
+ background: '#f8f9fa',
4883
+ height: '100%',
4884
+ display: 'flex',
4885
+ alignItems: 'center',
4886
+ justifyContent: 'center',
4887
+ color: '#666',
4888
+ }, children: "Select an item to edit properties" }));
4889
+ }
4890
+ const handleChange = (field, value) => {
4891
+ const updated = { ...localData, [field]: value };
4892
+ setLocalData(updated);
4893
+ onChange(node, updated);
4894
+ };
4895
+ const handleNestedChange = (field, nestedField, value) => {
4896
+ const updated = {
4897
+ ...localData,
4898
+ [field]: {
4899
+ ...(localData[field] || {}),
4900
+ [nestedField]: value,
4901
+ },
4902
+ };
4903
+ setLocalData(updated);
4904
+ onChange(node, updated);
4905
+ };
4906
+ const ensureNestedObject = (field) => {
4907
+ if (!localData[field]) {
4908
+ const updated = {
4909
+ ...localData,
4910
+ [field]: {},
4911
+ };
4912
+ setLocalData(updated);
4913
+ onChange(node, updated);
4914
+ }
4915
+ };
4916
+ const renderSectionProperties = () => {
4917
+ const section = localData;
4918
+ return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsxs("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: ["Section ID ", jsxRuntimeExports.jsx("span", { style: { color: '#e74c3c' }, children: "*" })] }), jsxRuntimeExports.jsx("input", { type: "text", value: section['section-id'] || '', onChange: (e) => handleChange('section-id', e.target.value), style: {
4919
+ width: '100%',
4920
+ padding: '8px 12px',
4921
+ border: '1px solid #ccc',
4922
+ borderRadius: '4px',
4923
+ fontSize: '12px',
4924
+ } })] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: "Section Title" }), jsxRuntimeExports.jsx("input", { type: "text", value: section['section-title'] || '', onChange: (e) => handleChange('section-title', e.target.value), style: {
4925
+ width: '100%',
4926
+ padding: '8px 12px',
4927
+ border: '1px solid #ccc',
4928
+ borderRadius: '4px',
4929
+ fontSize: '12px',
4930
+ } })] }), jsxRuntimeExports.jsx("div", { style: { marginBottom: '20px' }, children: jsxRuntimeExports.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px' }, children: [jsxRuntimeExports.jsx("input", { type: "checkbox", checked: section['section-editable'] || false, onChange: (e) => handleChange('section-editable', e.target.checked) }), jsxRuntimeExports.jsx("span", { children: "Editable" })] }) }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: "Column Span" }), jsxRuntimeExports.jsx("input", { type: "number", value: section['section-column-span'] || 1, onChange: (e) => handleChange('section-column-span', parseInt(e.target.value) || 1), min: "1", style: {
4931
+ width: '100%',
4932
+ padding: '8px 12px',
4933
+ border: '1px solid #ccc',
4934
+ borderRadius: '4px',
4935
+ fontSize: '12px',
4936
+ } })] })] }));
4937
+ };
4938
+ const renderPanelProperties = () => {
4939
+ const panel = localData;
4940
+ return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsxs("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: ["Panel ID ", jsxRuntimeExports.jsx("span", { style: { color: '#e74c3c' }, children: "*" })] }), jsxRuntimeExports.jsx("input", { type: "text", value: panel['panel-id'] || '', onChange: (e) => handleChange('panel-id', e.target.value), style: {
4941
+ width: '100%',
4942
+ padding: '8px 12px',
4943
+ border: '1px solid #ccc',
4944
+ borderRadius: '4px',
4945
+ fontSize: '12px',
4946
+ } })] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: "Orientation" }), jsxRuntimeExports.jsx("select", { value: panel['panel-orientation'] || 'vertical', onChange: (e) => handleChange('panel-orientation', e.target.value), style: {
4947
+ width: '100%',
4948
+ padding: '8px 12px',
4949
+ border: '1px solid #ccc',
4950
+ borderRadius: '4px',
4951
+ fontSize: '12px',
4952
+ background: 'white',
4953
+ }, children: ORIENTATIONS.map((opt) => (jsxRuntimeExports.jsx("option", { value: opt, children: opt }, opt))) })] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: "Column Span" }), jsxRuntimeExports.jsx("input", { type: "number", value: panel['panel-column-span'] || 1, onChange: (e) => handleChange('panel-column-span', parseInt(e.target.value) || 1), min: "1", style: {
4954
+ width: '100%',
4955
+ padding: '8px 12px',
4956
+ border: '1px solid #ccc',
4957
+ borderRadius: '4px',
4958
+ fontSize: '12px',
4959
+ } })] })] }));
4960
+ };
4961
+ const renderWidgetProperties = () => {
4962
+ const widget = localData;
4963
+ return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsxs("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: ["Widget Type ", jsxRuntimeExports.jsx("span", { style: { color: '#e74c3c' }, children: "*" })] }), jsxRuntimeExports.jsx("select", { value: widget.widget || '', onChange: (e) => {
4964
+ const newWidgetType = e.target.value;
4965
+ // When widget type changes, preserve common fields but clear widget-specific ones
4966
+ const updated = {
4967
+ ...widget,
4968
+ widget: newWidgetType,
4969
+ };
4970
+ // Clear widget-specific fields that don't apply to the new type
4971
+ if (!['select', 'radio', 'checkbox'].includes(newWidgetType)) {
4972
+ delete updated['widget-data-source'];
4973
+ }
4974
+ if (newWidgetType !== 'table') {
4975
+ delete updated['widget-data-columns'];
4976
+ }
4977
+ setLocalData(updated);
4978
+ onChange(node, updated);
4979
+ }, style: {
4980
+ width: '100%',
4981
+ padding: '8px 12px',
4982
+ border: '1px solid #ccc',
4983
+ borderRadius: '4px',
4984
+ fontSize: '12px',
4985
+ background: 'white',
4986
+ }, children: WIDGET_TYPES.map((type) => (jsxRuntimeExports.jsx("option", { value: type, children: type }, type))) })] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsxs("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: ["Widget ID ", jsxRuntimeExports.jsx("span", { style: { color: '#e74c3c' }, children: "*" })] }), jsxRuntimeExports.jsx("input", { type: "text", value: widget['widget-id'] || '', onChange: (e) => handleChange('widget-id', e.target.value), style: {
4987
+ width: '100%',
4988
+ padding: '8px 12px',
4989
+ border: '1px solid #ccc',
4990
+ borderRadius: '4px',
4991
+ fontSize: '12px',
4992
+ } })] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: "Widget Label" }), jsxRuntimeExports.jsx("input", { type: "text", value: widget['widget-label'] || '', onChange: (e) => handleChange('widget-label', e.target.value), style: {
4993
+ width: '100%',
4994
+ padding: '8px 12px',
4995
+ border: '1px solid #ccc',
4996
+ borderRadius: '4px',
4997
+ fontSize: '12px',
4998
+ }, placeholder: "Enter label..." })] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: "Data Path" }), jsxRuntimeExports.jsx("input", { type: "text", value: typeof widget['widget-data-path'] === 'string' ? widget['widget-data-path'] : '', onChange: (e) => handleChange('widget-data-path', e.target.value), style: {
4999
+ width: '100%',
5000
+ padding: '8px 12px',
5001
+ border: '1px solid #ccc',
5002
+ borderRadius: '4px',
5003
+ fontSize: '12px',
5004
+ }, placeholder: "person.name" })] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: "Placeholder" }), jsxRuntimeExports.jsx("input", { type: "text", value: widget['widget-data-placeholder'] || '', onChange: (e) => handleChange('widget-data-placeholder', e.target.value), style: {
5005
+ width: '100%',
5006
+ padding: '8px 12px',
5007
+ border: '1px solid #ccc',
5008
+ borderRadius: '4px',
5009
+ fontSize: '12px',
5010
+ }, placeholder: "Enter placeholder..." })] }), jsxRuntimeExports.jsx("div", { style: { marginBottom: '20px' }, children: jsxRuntimeExports.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px' }, children: [jsxRuntimeExports.jsx("input", { type: "checkbox", checked: widget['widget-required'] || false, onChange: (e) => handleChange('widget-required', e.target.checked) }), jsxRuntimeExports.jsx("span", { children: "Required" })] }) }), jsxRuntimeExports.jsx("div", { style: { marginBottom: '20px' }, children: jsxRuntimeExports.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px' }, children: [jsxRuntimeExports.jsx("input", { type: "checkbox", checked: widget['widget-readonly'] || false, onChange: (e) => handleChange('widget-readonly', e.target.checked) }), jsxRuntimeExports.jsx("span", { children: "Readonly" })] }) }), ['select', 'radio', 'checkbox'].includes(widget.widget) && (jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px', padding: '10px', background: '#e3f2fd', borderRadius: '4px' }, children: [jsxRuntimeExports.jsxs("label", { style: { display: 'block', marginBottom: '8px', fontSize: '12px', fontWeight: 600 }, children: ["Data Source (Required for ", widget.widget, ")"] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '10px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Source Type" }), jsxRuntimeExports.jsxs("select", { value: widget['widget-data-source']?.type || 'static', onChange: (e) => {
5011
+ ensureNestedObject('widget-data-source');
5012
+ handleNestedChange('widget-data-source', 'type', e.target.value);
5013
+ }, style: {
5014
+ width: '100%',
5015
+ padding: '6px 8px',
5016
+ border: '1px solid #ccc',
5017
+ borderRadius: '4px',
5018
+ fontSize: '11px',
5019
+ background: 'white',
5020
+ }, children: [jsxRuntimeExports.jsx("option", { value: "static", children: "Static" }), jsxRuntimeExports.jsx("option", { value: "api", children: "API" }), jsxRuntimeExports.jsx("option", { value: "schema", children: "Schema" })] })] }), widget['widget-data-source']?.type === 'static' && (jsxRuntimeExports.jsxs("div", { children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Options (JSON array format: see placeholder below)" }), jsxRuntimeExports.jsx("textarea", { value: JSON.stringify(widget['widget-data-source']?.options || [], null, 2), onChange: (e) => {
5021
+ try {
5022
+ const parsed = JSON.parse(e.target.value);
5023
+ handleNestedChange('widget-data-source', 'options', parsed);
5024
+ }
5025
+ catch {
5026
+ // Invalid JSON, ignore
5027
+ }
5028
+ }, style: {
5029
+ width: '100%',
5030
+ padding: '6px 8px',
5031
+ border: '1px solid #ccc',
5032
+ borderRadius: '4px',
5033
+ fontSize: '11px',
5034
+ fontFamily: 'monospace',
5035
+ minHeight: '80px',
5036
+ }, placeholder: '[{"value": "opt1", "label": "Option 1"}]' })] })), widget['widget-data-source']?.type === 'api' && (() => {
5037
+ const apiSource = widget['widget-data-source'];
5038
+ return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("div", { style: { marginBottom: '10px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "API URL" }), jsxRuntimeExports.jsx("input", { type: "text", value: apiSource.url || '', onChange: (e) => handleNestedChange('widget-data-source', 'url', e.target.value), style: {
5039
+ width: '100%',
5040
+ padding: '6px 8px',
5041
+ border: '1px solid #ccc',
5042
+ borderRadius: '4px',
5043
+ fontSize: '11px',
5044
+ }, placeholder: "https://api.example.com/options" })] }), jsxRuntimeExports.jsxs("div", { children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Value/Label Keys (e.g., \"id\"/\"name\")" }), jsxRuntimeExports.jsxs("div", { style: { display: 'flex', gap: '8px' }, children: [jsxRuntimeExports.jsx("input", { type: "text", value: apiSource.valueKey || '', onChange: (e) => handleNestedChange('widget-data-source', 'valueKey', e.target.value), style: {
5045
+ flex: 1,
5046
+ padding: '6px 8px',
5047
+ border: '1px solid #ccc',
5048
+ borderRadius: '4px',
5049
+ fontSize: '11px',
5050
+ }, placeholder: "value key" }), jsxRuntimeExports.jsx("input", { type: "text", value: apiSource.labelKey || '', onChange: (e) => handleNestedChange('widget-data-source', 'labelKey', e.target.value), style: {
5051
+ flex: 1,
5052
+ padding: '6px 8px',
5053
+ border: '1px solid #ccc',
5054
+ borderRadius: '4px',
5055
+ fontSize: '11px',
5056
+ }, placeholder: "label key" })] })] })] }));
5057
+ })()] })), widget.widget === 'table' && (jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px', padding: '10px', background: '#fff3e0', borderRadius: '4px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '8px', fontSize: '12px', fontWeight: 600 }, children: "Table Columns" }), jsxRuntimeExports.jsx("div", { style: { fontSize: '11px', color: '#666', marginBottom: '10px' }, children: "Configure columns in JSON editor or add via code" }), jsxRuntimeExports.jsx("textarea", { value: JSON.stringify(widget['widget-data-columns'] || [], null, 2), onChange: (e) => {
5058
+ try {
5059
+ const parsed = JSON.parse(e.target.value);
5060
+ handleChange('widget-data-columns', parsed);
5061
+ }
5062
+ catch {
5063
+ // Invalid JSON, ignore
5064
+ }
5065
+ }, style: {
5066
+ width: '100%',
5067
+ padding: '6px 8px',
5068
+ border: '1px solid #ccc',
5069
+ borderRadius: '4px',
5070
+ fontSize: '11px',
5071
+ fontFamily: 'monospace',
5072
+ minHeight: '100px',
5073
+ }, placeholder: '[{"column-key": "col1", "widget-label": "Column 1", "widget": "text"}]' })] })), widget.widget === 'number' && (jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px', padding: '10px', background: '#f3e5f5', borderRadius: '4px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '8px', fontSize: '12px', fontWeight: 600 }, children: "Number Validation" }), jsxRuntimeExports.jsxs("div", { style: { display: 'flex', gap: '10px', marginBottom: '10px' }, children: [jsxRuntimeExports.jsxs("div", { style: { flex: 1 }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Min Value" }), jsxRuntimeExports.jsx("input", { type: "number", value: widget['widget-data-validation']?.min ?? '', onChange: (e) => handleNestedChange('widget-data-validation', 'min', e.target.value ? parseFloat(e.target.value) : undefined), style: {
5074
+ width: '100%',
5075
+ padding: '6px 8px',
5076
+ border: '1px solid #ccc',
5077
+ borderRadius: '4px',
5078
+ fontSize: '11px',
5079
+ } })] }), jsxRuntimeExports.jsxs("div", { style: { flex: 1 }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Max Value" }), jsxRuntimeExports.jsx("input", { type: "number", value: widget['widget-data-validation']?.max ?? '', onChange: (e) => handleNestedChange('widget-data-validation', 'max', e.target.value ? parseFloat(e.target.value) : undefined), style: {
5080
+ width: '100%',
5081
+ padding: '6px 8px',
5082
+ border: '1px solid #ccc',
5083
+ borderRadius: '4px',
5084
+ fontSize: '11px',
5085
+ } })] })] })] })), ['date', 'datetime'].includes(widget.widget) && (jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px', padding: '10px', background: '#e8f5e9', borderRadius: '4px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '8px', fontSize: '12px', fontWeight: 600 }, children: "Date Range Options" }), jsxRuntimeExports.jsxs("div", { style: { display: 'flex', gap: '10px', marginBottom: '10px' }, children: [jsxRuntimeExports.jsxs("div", { style: { flex: 1 }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Min Date" }), jsxRuntimeExports.jsx("input", { type: "date", value: widget['widget-data-options']?.minDate || '', onChange: (e) => handleNestedChange('widget-data-options', 'minDate', e.target.value || undefined), style: {
5086
+ width: '100%',
5087
+ padding: '6px 8px',
5088
+ border: '1px solid #ccc',
5089
+ borderRadius: '4px',
5090
+ fontSize: '11px',
5091
+ } })] }), jsxRuntimeExports.jsxs("div", { style: { flex: 1 }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Max Date" }), jsxRuntimeExports.jsx("input", { type: "date", value: widget['widget-data-options']?.maxDate || '', onChange: (e) => handleNestedChange('widget-data-options', 'maxDate', e.target.value || undefined), style: {
5092
+ width: '100%',
5093
+ padding: '6px 8px',
5094
+ border: '1px solid #ccc',
5095
+ borderRadius: '4px',
5096
+ fontSize: '11px',
5097
+ } })] })] }), jsxRuntimeExports.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '11px' }, children: [jsxRuntimeExports.jsx("input", { type: "checkbox", checked: widget['widget-data-options']?.showCalendar || false, onChange: (e) => handleNestedChange('widget-data-options', 'showCalendar', e.target.checked) }), jsxRuntimeExports.jsx("span", { children: "Show Calendar" })] })] })), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px', padding: '10px', background: '#f0f0f0', borderRadius: '4px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '8px', fontSize: '12px', fontWeight: 600 }, children: "Validation" }), jsxRuntimeExports.jsx("div", { style: { marginBottom: '10px' }, children: jsxRuntimeExports.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px' }, children: [jsxRuntimeExports.jsx("input", { type: "checkbox", checked: widget['widget-data-validation']?.required || false, onChange: (e) => {
5098
+ if (!widget['widget-data-validation']) {
5099
+ handleChange('widget-data-validation', { required: e.target.checked });
5100
+ }
5101
+ else {
5102
+ handleNestedChange('widget-data-validation', 'required', e.target.checked);
5103
+ }
5104
+ } }), jsxRuntimeExports.jsx("span", { children: "Required" })] }) }), ['text', 'textarea'].includes(widget.widget) && (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("div", { style: { marginBottom: '10px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Min Length" }), jsxRuntimeExports.jsx("input", { type: "number", value: widget['widget-data-validation']?.minLength || '', onChange: (e) => {
5105
+ const validation = widget['widget-data-validation'] || {};
5106
+ handleChange('widget-data-validation', {
5107
+ ...validation,
5108
+ minLength: e.target.value ? parseInt(e.target.value) : undefined,
5109
+ });
5110
+ }, style: {
5111
+ width: '100%',
5112
+ padding: '6px 8px',
5113
+ border: '1px solid #ccc',
5114
+ borderRadius: '4px',
5115
+ fontSize: '11px',
5116
+ } })] }), jsxRuntimeExports.jsxs("div", { children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Max Length" }), jsxRuntimeExports.jsx("input", { type: "number", value: widget['widget-data-validation']?.maxLength || '', onChange: (e) => {
5117
+ const validation = widget['widget-data-validation'] || {};
5118
+ handleChange('widget-data-validation', {
5119
+ ...validation,
5120
+ maxLength: e.target.value ? parseInt(e.target.value) : undefined,
5121
+ });
5122
+ }, style: {
5123
+ width: '100%',
5124
+ padding: '6px 8px',
5125
+ border: '1px solid #ccc',
5126
+ borderRadius: '4px',
5127
+ fontSize: '11px',
5128
+ } })] })] }))] })] }));
5129
+ };
5130
+ const getHeaderColor = () => {
5131
+ switch (node.type) {
5132
+ case 'section':
5133
+ return '#2196f3';
5134
+ case 'panel':
5135
+ return '#ff9800';
5136
+ case 'widget':
5137
+ return '#4caf50';
5138
+ }
5139
+ };
5140
+ return (jsxRuntimeExports.jsxs("div", { style: {
5141
+ padding: '15px',
5142
+ overflowY: 'auto',
5143
+ background: '#f8f9fa',
5144
+ height: '100%',
5145
+ }, children: [jsxRuntimeExports.jsx("h3", { style: { fontSize: '14px', marginBottom: '15px', color: '#2c3e50' }, children: "Properties" }), jsxRuntimeExports.jsxs("div", { style: {
5146
+ background: getHeaderColor(),
5147
+ color: 'white',
5148
+ padding: '12px',
5149
+ borderRadius: '4px',
5150
+ marginBottom: '20px',
5151
+ fontWeight: 600,
5152
+ fontSize: '13px',
5153
+ }, children: [node.type === 'section' && `Section: ${node.data['section-id']}`, node.type === 'panel' && `Panel: ${node.data['panel-id']}`, node.type === 'widget' && `Widget: ${node.data['widget-id']}`] }), node.type === 'section' && renderSectionProperties(), node.type === 'panel' && renderPanelProperties(), node.type === 'widget' && renderWidgetProperties(), jsxRuntimeExports.jsxs("div", { style: { display: 'flex', gap: '10px', marginTop: '30px' }, children: [jsxRuntimeExports.jsx("button", { onClick: () => onDelete(node), style: {
5154
+ flex: 1,
5155
+ padding: '10px',
5156
+ border: 'none',
5157
+ borderRadius: '4px',
5158
+ background: '#f44336',
5159
+ color: 'white',
5160
+ fontWeight: 600,
5161
+ cursor: 'pointer',
5162
+ fontSize: '12px',
5163
+ }, children: "Delete" }), jsxRuntimeExports.jsx("button", { onClick: () => onDuplicate(node), style: {
5164
+ flex: 1,
5165
+ padding: '10px',
5166
+ border: 'none',
5167
+ borderRadius: '4px',
5168
+ background: '#ff9800',
5169
+ color: 'white',
5170
+ fontWeight: 600,
5171
+ cursor: 'pointer',
5172
+ fontSize: '12px',
5173
+ }, children: "Duplicate" })] })] }));
5174
+ };
5175
+
5176
+ /**
5177
+ * Visual Builder Panel - Right side of Section Builder
5178
+ */
5179
+ const VisualBuilderPanel = ({ section, selectedNode, onSelectNode, onSectionChange, onAddPanel, onAddWidget, onDeleteNode, onDuplicateNode, onSave, isMaximized = false, onToggleMaximize, }) => {
5180
+ // Validate section before saving
5181
+ const validateSection = (sectionToValidate) => {
5182
+ const errors = [];
5183
+ if (!sectionToValidate['section-id']) {
5184
+ errors.push('Section ID is required');
5185
+ }
5186
+ if (!sectionToValidate.panels || sectionToValidate.panels.length === 0) {
5187
+ errors.push('Section must have at least one panel');
5188
+ }
5189
+ // Validate panels
5190
+ const validatePanels = (panels) => {
5191
+ panels.forEach((panel, index) => {
5192
+ if (!panel['panel-id']) {
5193
+ errors.push(`Panel at index ${index} is missing panel-id`);
5194
+ }
5195
+ if (panel.panels) {
5196
+ validatePanels(panel.panels);
5197
+ }
5198
+ if (panel.widgets) {
5199
+ panel.widgets.forEach((widget, widgetIndex) => {
5200
+ if (!widget['widget-id']) {
5201
+ errors.push(`Widget at panel ${panel['panel-id'] || index}, index ${widgetIndex} is missing widget-id`);
5202
+ }
5203
+ if (!widget.widget) {
5204
+ errors.push(`Widget ${widget['widget-id'] || widgetIndex} is missing widget type`);
5205
+ }
5206
+ });
5207
+ }
5208
+ });
5209
+ };
5210
+ if (sectionToValidate.panels) {
5211
+ validatePanels(sectionToValidate.panels);
5212
+ }
5213
+ return {
5214
+ isValid: errors.length === 0,
5215
+ errors,
5216
+ };
5217
+ };
5218
+ const handleSave = () => {
5219
+ const validation = validateSection(section);
5220
+ if (!validation.isValid) {
5221
+ console.error('Section validation failed:', validation.errors);
5222
+ alert(`Cannot save section. Please fix the following errors:\n\n${validation.errors.join('\n')}`);
5223
+ return;
5224
+ }
5225
+ if (onSave) {
5226
+ try {
5227
+ onSave(section);
5228
+ }
5229
+ catch (error) {
5230
+ console.error('Error saving section:', error);
5231
+ alert('An error occurred while saving the section. Please check the console for details.');
5232
+ }
5233
+ }
5234
+ };
5235
+ const handleNodeChange = (node, updates) => {
5236
+ // Create a deep copy of the section
5237
+ const updatedSection = JSON.parse(JSON.stringify(section));
5238
+ // Find and update the node in the section structure
5239
+ const updateInSection = (current, targetId, targetType) => {
5240
+ if (targetType === 'section' && current['section-id'] === targetId) {
5241
+ Object.assign(current, updates);
2851
5242
  return true;
2852
5243
  }
5244
+ if (current.panels) {
5245
+ for (const panel of current.panels) {
5246
+ if (targetType === 'panel' && panel['panel-id'] === targetId) {
5247
+ Object.assign(panel, updates);
5248
+ return true;
5249
+ }
5250
+ if (updateInSection(panel, targetId, targetType)) {
5251
+ return true;
5252
+ }
5253
+ if (panel.widgets) {
5254
+ for (const widget of panel.widgets) {
5255
+ if (targetType === 'widget' && widget['widget-id'] === targetId) {
5256
+ Object.assign(widget, updates);
5257
+ return true;
5258
+ }
5259
+ }
5260
+ }
5261
+ }
5262
+ }
5263
+ if (current.widgets) {
5264
+ for (const widget of current.widgets) {
5265
+ if (targetType === 'widget' && widget['widget-id'] === targetId) {
5266
+ Object.assign(widget, updates);
5267
+ return true;
5268
+ }
5269
+ }
5270
+ }
5271
+ return false;
5272
+ };
5273
+ updateInSection(updatedSection, node.id, node.type);
5274
+ onSectionChange(updatedSection);
5275
+ };
5276
+ const handleAddPanel = () => {
5277
+ if (selectedNode) {
5278
+ if (selectedNode.type === 'section' || selectedNode.type === 'panel') {
5279
+ onAddPanel(selectedNode.id, selectedNode.type);
5280
+ }
2853
5281
  }
2854
- }
2855
- return false;
2856
- };
2857
- /**
2858
- * Recursively get table widget column span from panels
2859
- */
2860
- const getTableWidgetColumnSpan = (panels) => {
2861
- for (const panel of panels) {
2862
- // Check widgets in this panel
2863
- if (panel.widgets) {
2864
- for (const widget of panel.widgets) {
2865
- if (widget.widget === 'table' || widget['widget-type'] === 'table') {
2866
- // Return the widget's column span if specified, otherwise null
2867
- return widget['widget-column-span'] || null;
5282
+ else {
5283
+ // Add to root section
5284
+ onAddPanel(section['section-id'], 'section');
5285
+ }
5286
+ };
5287
+ const handleAddWidget = () => {
5288
+ if (selectedNode) {
5289
+ if (selectedNode.type === 'panel') {
5290
+ onAddWidget(selectedNode.id);
5291
+ }
5292
+ else if (selectedNode.type === 'section') {
5293
+ // Find first panel or create one
5294
+ if (section.panels && section.panels.length > 0) {
5295
+ onAddWidget(section.panels[0]['panel-id']);
5296
+ }
5297
+ else {
5298
+ // Create a panel first, then add widget
5299
+ const newPanel = {
5300
+ 'panel-id': `panel-${Date.now()}`,
5301
+ 'panel-orientation': 'vertical',
5302
+ widgets: [],
5303
+ };
5304
+ const updatedSection = {
5305
+ ...section,
5306
+ panels: [...(section.panels || []), newPanel],
5307
+ };
5308
+ onSectionChange(updatedSection);
5309
+ onAddWidget(newPanel['panel-id']);
2868
5310
  }
2869
5311
  }
2870
5312
  }
2871
- // Recursively check nested panels
2872
- if (panel.panels) {
2873
- const nestedSpan = getTableWidgetColumnSpan(panel.panels);
2874
- if (nestedSpan !== null) {
2875
- return nestedSpan;
5313
+ else {
5314
+ // Add to first panel or create one
5315
+ if (section.panels && section.panels.length > 0) {
5316
+ onAddWidget(section.panels[0]['panel-id']);
2876
5317
  }
2877
5318
  }
2878
- }
2879
- return null;
5319
+ };
5320
+ return (jsxRuntimeExports.jsxs("div", { style: {
5321
+ display: 'flex',
5322
+ flexDirection: 'column',
5323
+ height: '100%',
5324
+ width: '100%',
5325
+ minHeight: 0,
5326
+ }, children: [jsxRuntimeExports.jsxs("div", { style: {
5327
+ padding: '15px 20px',
5328
+ background: '#ffffff',
5329
+ borderBottom: '1px solid #ddd',
5330
+ display: 'flex',
5331
+ justifyContent: 'space-between',
5332
+ alignItems: 'center',
5333
+ }, children: [jsxRuntimeExports.jsx("div", { style: { fontWeight: 600, fontSize: '16px', color: '#2c3e50' }, children: "Visual Builder" }), jsxRuntimeExports.jsxs("div", { style: { display: 'flex', gap: '10px' }, children: [jsxRuntimeExports.jsx("button", { onClick: handleAddPanel, style: {
5334
+ padding: '8px 16px',
5335
+ border: 'none',
5336
+ borderRadius: '4px',
5337
+ background: '#2196f3',
5338
+ color: 'white',
5339
+ fontWeight: 600,
5340
+ cursor: 'pointer',
5341
+ fontSize: '12px',
5342
+ whiteSpace: 'nowrap',
5343
+ }, children: "+ Add Panel" }), jsxRuntimeExports.jsx("button", { onClick: handleAddWidget, style: {
5344
+ padding: '8px 16px',
5345
+ border: 'none',
5346
+ borderRadius: '4px',
5347
+ background: '#4caf50',
5348
+ color: 'white',
5349
+ fontWeight: 600,
5350
+ cursor: 'pointer',
5351
+ fontSize: '12px',
5352
+ whiteSpace: 'nowrap',
5353
+ }, children: "+ Add Widget" }), onSave && (jsxRuntimeExports.jsx("button", { onClick: handleSave, style: {
5354
+ padding: '8px 16px',
5355
+ border: 'none',
5356
+ borderRadius: '4px',
5357
+ background: '#ff9800',
5358
+ color: 'white',
5359
+ fontWeight: 600,
5360
+ cursor: 'pointer',
5361
+ fontSize: '12px',
5362
+ whiteSpace: 'nowrap',
5363
+ }, children: "Save" })), onToggleMaximize && (jsxRuntimeExports.jsx("button", { onClick: onToggleMaximize, style: {
5364
+ padding: '8px',
5365
+ border: 'none',
5366
+ borderRadius: '4px',
5367
+ background: 'transparent',
5368
+ color: '#666',
5369
+ cursor: 'pointer',
5370
+ display: 'flex',
5371
+ alignItems: 'center',
5372
+ justifyContent: 'center',
5373
+ width: '32px',
5374
+ height: '32px',
5375
+ }, title: isMaximized ? 'Minimize' : 'Maximize', children: isMaximized ? (jsxRuntimeExports.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsxRuntimeExports.jsx("path", { d: "M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3" }) })) : (jsxRuntimeExports.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsxRuntimeExports.jsx("path", { d: "M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" }) })) }))] })] }), jsxRuntimeExports.jsxs("div", { style: {
5376
+ flex: 1,
5377
+ display: 'flex',
5378
+ overflow: 'hidden',
5379
+ minHeight: 0, // Important for flex children to respect overflow
5380
+ }, children: [jsxRuntimeExports.jsx("div", { style: {
5381
+ width: '45%',
5382
+ borderRight: '1px solid #ddd',
5383
+ display: 'flex',
5384
+ flexDirection: 'column',
5385
+ minHeight: 0, // Important for flex children to respect overflow
5386
+ overflow: 'hidden',
5387
+ }, children: jsxRuntimeExports.jsx(SectionTree, { section: section, selectedNode: selectedNode, onSelectNode: onSelectNode, onAddPanel: onAddPanel, onAddWidget: onAddWidget, onDeleteNode: onDeleteNode, onDuplicateNode: onDuplicateNode }) }), jsxRuntimeExports.jsx("div", { style: {
5388
+ width: '55%',
5389
+ display: 'flex',
5390
+ flexDirection: 'column',
5391
+ minHeight: 0, // Important for flex children to respect overflow
5392
+ overflow: 'hidden',
5393
+ }, children: jsxRuntimeExports.jsx(PropertyEditor, { node: selectedNode, onChange: handleNodeChange, onDelete: onDeleteNode, onDuplicate: onDuplicateNode }) })] })] }));
2880
5394
  };
5395
+
2881
5396
  /**
2882
- * Recursively count all vertical panels in a section
2883
- * Handles nested structure: horizontal panels containing vertical panels
5397
+ * Main Section Builder Component
5398
+ * Provides dual-panel interface for editing section JSON
2884
5399
  */
2885
- const countVerticalPanels = (panels) => {
2886
- let count = 0;
2887
- for (const panel of panels) {
2888
- const orientation = panel['panel-orientation'] || 'vertical';
2889
- if (orientation === 'horizontal' && panel.panels) {
2890
- // For horizontal panels, count all vertical panels nested inside
2891
- count += countVerticalPanels(panel.panels);
2892
- }
2893
- else if (orientation === 'vertical') {
2894
- // Count this vertical panel
2895
- count += 1;
2896
- // Also recursively count vertical panels nested inside this vertical panel
2897
- if (panel.panels && panel.panels.length > 0) {
2898
- count += countVerticalPanels(panel.panels);
5400
+ const SectionBuilder = ({ initialSection, onChange, onSave, }) => {
5401
+ const defaultSection = {
5402
+ 'section-id': 'new-section',
5403
+ 'section-title': '',
5404
+ 'section-editable': false,
5405
+ panels: [],
5406
+ };
5407
+ const [section, setSection] = React.useState(initialSection || defaultSection);
5408
+ const [selectedNode, setSelectedNode] = React.useState(null);
5409
+ const [isMaximized, setIsMaximized] = React.useState(false);
5410
+ // Store the original section for reset functionality
5411
+ const originalSectionRef = React.useRef(initialSection ? JSON.parse(JSON.stringify(initialSection)) : defaultSection);
5412
+ const isInitialMount = React.useRef(true);
5413
+ React.useEffect(() => {
5414
+ if (initialSection) {
5415
+ // Only update original on initial mount
5416
+ if (isInitialMount.current) {
5417
+ originalSectionRef.current = JSON.parse(JSON.stringify(initialSection));
5418
+ isInitialMount.current = false;
2899
5419
  }
5420
+ setSection(initialSection);
2900
5421
  }
2901
- }
2902
- return count;
2903
- };
2904
- /**
2905
- * Container component that renders multiple sections
2906
- *
2907
- * Layout behavior:
2908
- * - Uses CSS Grid for proper alignment
2909
- * - Each grid column = 200px (one vertical panel width)
2910
- * - Sections span columns based on their total vertical panel count
2911
- * - All sections align to the same grid, ensuring right-side alignment
2912
- * - Handles nested structure: multiple horizontal panels, each with multiple vertical panels
2913
- */
2914
- const SectionsContainer = ({ sections, apiAdapter, schemaData, onValueChange, className = '', onSectionSave, hideEditButton = false, mode = 'RegistryView', }) => {
2915
- // Find the maximum number of vertical panels across all sections
2916
- // This determines the grid size (minimum 3 columns)
2917
- // Also account for table widgets and their explicit column spans
2918
- const maxVerticalPanels = Math.max(...sections.map(section => {
2919
- const panelCount = countVerticalPanels(section.panels);
2920
- const tableWidgetSpan = getTableWidgetColumnSpan(section.panels);
2921
- // Use explicit table widget span if specified, otherwise use default logic
2922
- return tableWidgetSpan !== null
2923
- ? Math.max(panelCount, tableWidgetSpan)
2924
- : (hasTableWidget(section.panels) ? Math.max(panelCount, 2) : panelCount);
2925
- }), 3 // Minimum 3 columns
2926
- );
2927
- const containerId = 'sections-container-grid';
2928
- return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
2929
- #${containerId} {
2930
- display: grid;
2931
- /* Flexible columns: minimum 200px, but can grow equally to fill width */
2932
- grid-template-columns: repeat(${maxVerticalPanels}, minmax(200px, 1fr));
2933
- gap: 1.5rem;
2934
- width: 100%;
2935
- align-items: start;
5422
+ }, [initialSection]);
5423
+ const handleSectionChange = React.useCallback((updatedSection) => {
5424
+ setSection(updatedSection);
5425
+ if (onChange) {
5426
+ onChange(updatedSection);
2936
5427
  }
2937
-
2938
- /* Sections with table widgets should expand to fill available space only if no explicit span */
2939
- #${containerId} > .section[data-has-table="true"][data-has-explicit-span="false"] {
2940
- grid-column: 1 / -1; /* Span all columns */
2941
- width: 100%;
5428
+ }, [onChange]);
5429
+ // Reset to original section - resets both JSON editor and visual builder
5430
+ const handleReset = React.useCallback(() => {
5431
+ const original = JSON.parse(JSON.stringify(originalSectionRef.current));
5432
+ setSection(original);
5433
+ setSelectedNode(null); // Clear selection on reset
5434
+ if (onChange) {
5435
+ onChange(original);
2942
5436
  }
2943
-
2944
- /* Sections with explicit table widget span - inline style will handle grid-column */
2945
- /* This rule ensures width is 100% but doesn't override grid-column */
2946
- #${containerId} > .section[data-has-explicit-span="true"] {
2947
- width: 100%;
5437
+ }, [onChange]);
5438
+ // Handle node selection from JSON editor
5439
+ const handleSelectNodeFromJson = React.useCallback((nodeId, nodeType) => {
5440
+ // Find the corresponding node in the section structure
5441
+ const findNode = () => {
5442
+ if (nodeType === 'section') {
5443
+ return {
5444
+ type: 'section',
5445
+ id: section['section-id'],
5446
+ label: `Section: ${section['section-id']}`,
5447
+ data: section,
5448
+ };
5449
+ }
5450
+ // Recursively search for panel or widget
5451
+ const searchInPanels = (panels, parent) => {
5452
+ for (const panel of panels) {
5453
+ if (nodeType === 'panel' && panel['panel-id'] === nodeId) {
5454
+ return {
5455
+ type: 'panel',
5456
+ id: panel['panel-id'],
5457
+ label: `Panel: ${panel['panel-id']}`,
5458
+ data: panel,
5459
+ parent: parent,
5460
+ };
5461
+ }
5462
+ // Check nested panels
5463
+ if (panel.panels) {
5464
+ const panelNode = {
5465
+ type: 'panel',
5466
+ id: panel['panel-id'],
5467
+ label: `Panel: ${panel['panel-id']}`,
5468
+ data: panel,
5469
+ parent: parent,
5470
+ };
5471
+ const found = searchInPanels(panel.panels, panelNode);
5472
+ if (found)
5473
+ return found;
5474
+ }
5475
+ // Check widgets
5476
+ if (panel.widgets) {
5477
+ const panelNode = {
5478
+ type: 'panel',
5479
+ id: panel['panel-id'],
5480
+ label: `Panel: ${panel['panel-id']}`,
5481
+ data: panel,
5482
+ parent: parent,
5483
+ };
5484
+ for (const widget of panel.widgets) {
5485
+ if (nodeType === 'widget' && widget['widget-id'] === nodeId) {
5486
+ return {
5487
+ type: 'widget',
5488
+ id: widget['widget-id'],
5489
+ label: `Widget: ${widget['widget-id']} (${widget.widget})`,
5490
+ data: widget,
5491
+ parent: panelNode,
5492
+ };
5493
+ }
5494
+ }
5495
+ }
5496
+ }
5497
+ return null;
5498
+ };
5499
+ if (section.panels) {
5500
+ return searchInPanels(section.panels);
5501
+ }
5502
+ return null;
5503
+ };
5504
+ const node = findNode();
5505
+ if (node) {
5506
+ setSelectedNode(node);
5507
+ }
5508
+ }, [section]);
5509
+ const handleAddPanel = React.useCallback((parentId, parentType) => {
5510
+ const updatedSection = JSON.parse(JSON.stringify(section));
5511
+ const newPanel = {
5512
+ 'panel-id': `panel-${Date.now()}`,
5513
+ 'panel-orientation': 'vertical',
5514
+ widgets: [],
5515
+ };
5516
+ if (parentType === 'section') {
5517
+ updatedSection.panels = [...(updatedSection.panels || []), newPanel];
5518
+ }
5519
+ else if (parentType === 'panel') {
5520
+ const addPanelToParent = (panels) => {
5521
+ for (const panel of panels) {
5522
+ if (panel['panel-id'] === parentId) {
5523
+ panel.panels = [...(panel.panels || []), newPanel];
5524
+ return true;
5525
+ }
5526
+ if (panel.panels && addPanelToParent(panel.panels)) {
5527
+ return true;
5528
+ }
5529
+ }
5530
+ return false;
5531
+ };
5532
+ if (updatedSection.panels) {
5533
+ addPanelToParent(updatedSection.panels);
5534
+ }
5535
+ }
5536
+ handleSectionChange(updatedSection);
5537
+ }, [section, handleSectionChange]);
5538
+ const handleAddWidget = React.useCallback((parentId) => {
5539
+ const updatedSection = JSON.parse(JSON.stringify(section));
5540
+ const newWidget = {
5541
+ widget: 'text',
5542
+ 'widget-id': `widget-${Date.now()}`,
5543
+ 'widget-label': 'New Widget',
5544
+ 'widget-data-path': '',
5545
+ };
5546
+ const addWidgetToPanel = (panels) => {
5547
+ for (const panel of panels) {
5548
+ if (panel['panel-id'] === parentId) {
5549
+ panel.widgets = [...(panel.widgets || []), newWidget];
5550
+ return true;
5551
+ }
5552
+ if (panel.panels && addWidgetToPanel(panel.panels)) {
5553
+ return true;
5554
+ }
5555
+ }
5556
+ return false;
5557
+ };
5558
+ if (updatedSection.panels) {
5559
+ addWidgetToPanel(updatedSection.panels);
5560
+ }
5561
+ handleSectionChange(updatedSection);
5562
+ }, [section, handleSectionChange]);
5563
+ const handleDeleteNode = React.useCallback((node) => {
5564
+ const updatedSection = JSON.parse(JSON.stringify(section));
5565
+ if (node.type === 'section') {
5566
+ // Can't delete section, but can reset it
5567
+ return;
2948
5568
  }
2949
-
2950
- /* Responsive: on smaller screens, use auto-fit for flexibility */
2951
- @media (max-width: 1023px) {
2952
- #${containerId} {
2953
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
2954
- }
5569
+ const deleteFromSection = (current) => {
5570
+ if (current.panels) {
5571
+ const panelIndex = current.panels.findIndex((p) => p['panel-id'] === node.id);
5572
+ if (panelIndex !== -1 && node.type === 'panel') {
5573
+ current.panels.splice(panelIndex, 1);
5574
+ return true;
5575
+ }
5576
+ for (const panel of current.panels) {
5577
+ if (panel['panel-id'] === node.id && node.type === 'panel') {
5578
+ // This shouldn't happen due to findIndex above, but handle nested case
5579
+ const index = current.panels.indexOf(panel);
5580
+ if (index !== -1) {
5581
+ current.panels.splice(index, 1);
5582
+ return true;
5583
+ }
5584
+ }
5585
+ if (panel.widgets) {
5586
+ const widgetIndex = panel.widgets.findIndex((w) => w['widget-id'] === node.id);
5587
+ if (widgetIndex !== -1 && node.type === 'widget') {
5588
+ panel.widgets.splice(widgetIndex, 1);
5589
+ return true;
5590
+ }
5591
+ }
5592
+ if (panel.panels && deleteFromSection(panel)) {
5593
+ return true;
5594
+ }
5595
+ }
5596
+ }
5597
+ return false;
5598
+ };
5599
+ deleteFromSection(updatedSection);
5600
+ handleSectionChange(updatedSection);
5601
+ setSelectedNode(null);
5602
+ }, [section, handleSectionChange]);
5603
+ const handleDuplicateNode = React.useCallback((node) => {
5604
+ const updatedSection = JSON.parse(JSON.stringify(section));
5605
+ if (node.type === 'section') {
5606
+ return;
2955
5607
  }
2956
- ` }), jsxRuntimeExports.jsx("div", { id: containerId, className: `sections-container ${className}`, children: sections.map((section) => {
2957
- // Check if section has explicit column span
2958
- if (section['section-column-span']) {
2959
- return (jsxRuntimeExports.jsx(SectionRenderer, { section: section, apiAdapter: apiAdapter, schemaData: schemaData, onValueChange: onValueChange, gridColumnSpan: section['section-column-span'], onSectionSave: onSectionSave, hideEditButton: hideEditButton, mode: mode }, section['section-id']));
5608
+ const duplicateInSection = (current) => {
5609
+ if (current.panels) {
5610
+ for (const panel of current.panels) {
5611
+ if (panel['panel-id'] === node.id && node.type === 'panel') {
5612
+ const duplicated = {
5613
+ ...panel,
5614
+ 'panel-id': `${panel['panel-id']}-copy-${Date.now()}`,
5615
+ };
5616
+ const index = current.panels.indexOf(panel);
5617
+ current.panels.splice(index + 1, 0, duplicated);
5618
+ return true;
2960
5619
  }
2961
- const verticalPanelsCount = countVerticalPanels(section.panels);
2962
- const tableWidgetColumnSpan = getTableWidgetColumnSpan(section.panels);
2963
- const containsTable = hasTableWidget(section.panels);
2964
- // If section contains a table widget with explicit column span, use it
2965
- // Otherwise, if it has a table widget, ensure it spans at least 2 columns
2966
- // Otherwise, use the vertical panel count
2967
- const columnSpan = tableWidgetColumnSpan !== null
2968
- ? tableWidgetColumnSpan
2969
- : (containsTable ? Math.max(verticalPanelsCount, 2) : verticalPanelsCount);
2970
- return (jsxRuntimeExports.jsx(SectionRenderer, { section: section, apiAdapter: apiAdapter, schemaData: schemaData, onValueChange: onValueChange, gridColumnSpan: columnSpan, onSectionSave: onSectionSave, hideEditButton: hideEditButton, mode: mode }, section['section-id']));
2971
- }) })] }));
5620
+ if (panel.widgets) {
5621
+ for (const widget of panel.widgets) {
5622
+ if (widget['widget-id'] === node.id && node.type === 'widget') {
5623
+ const duplicated = {
5624
+ ...widget,
5625
+ 'widget-id': `${widget['widget-id']}-copy-${Date.now()}`,
5626
+ };
5627
+ const index = panel.widgets.indexOf(widget);
5628
+ panel.widgets.splice(index + 1, 0, duplicated);
5629
+ return true;
5630
+ }
5631
+ }
5632
+ }
5633
+ if (panel.panels && duplicateInSection(panel)) {
5634
+ return true;
5635
+ }
5636
+ }
5637
+ }
5638
+ return false;
5639
+ };
5640
+ duplicateInSection(updatedSection);
5641
+ handleSectionChange(updatedSection);
5642
+ }, [section, handleSectionChange]);
5643
+ const toggleMaximize = React.useCallback(() => {
5644
+ setIsMaximized((prev) => !prev);
5645
+ }, []);
5646
+ // Handle Escape key to exit fullscreen
5647
+ React.useEffect(() => {
5648
+ if (!isMaximized)
5649
+ return;
5650
+ const handleEscape = (e) => {
5651
+ if (e.key === 'Escape') {
5652
+ setIsMaximized(false);
5653
+ }
5654
+ };
5655
+ window.addEventListener('keydown', handleEscape);
5656
+ return () => window.removeEventListener('keydown', handleEscape);
5657
+ }, [isMaximized]);
5658
+ return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [isMaximized && (jsxRuntimeExports.jsx("div", { style: {
5659
+ position: 'fixed',
5660
+ top: 0,
5661
+ left: 0,
5662
+ right: 0,
5663
+ bottom: 0,
5664
+ background: 'rgba(0, 0, 0, 0.5)',
5665
+ zIndex: 9998,
5666
+ }, onClick: toggleMaximize })), jsxRuntimeExports.jsxs("div", { style: {
5667
+ display: 'flex',
5668
+ height: isMaximized ? '100vh' : '100%',
5669
+ width: isMaximized ? '100vw' : '100%',
5670
+ minHeight: 0,
5671
+ background: '#FFFFFF',
5672
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
5673
+ overflow: 'hidden',
5674
+ border: 'none',
5675
+ position: isMaximized ? 'fixed' : 'relative',
5676
+ top: isMaximized ? 0 : 'auto',
5677
+ left: isMaximized ? 0 : 'auto',
5678
+ zIndex: isMaximized ? 9999 : 'auto',
5679
+ }, children: [jsxRuntimeExports.jsx("div", { style: {
5680
+ width: '50%',
5681
+ height: '100%',
5682
+ minHeight: 0,
5683
+ overflow: 'hidden',
5684
+ display: 'flex',
5685
+ padding: '10px 10px 10px 10px',
5686
+ flexDirection: 'column',
5687
+ borderRight: '0px',
5688
+ }, children: jsxRuntimeExports.jsx(JSONEditorPanel, { section: section, onChange: handleSectionChange, onReset: handleReset, onSelectNode: handleSelectNodeFromJson }) }), jsxRuntimeExports.jsx("div", { style: {
5689
+ width: '50%',
5690
+ height: '100%',
5691
+ minHeight: 0,
5692
+ overflow: 'hidden',
5693
+ display: 'flex',
5694
+ flexDirection: 'column',
5695
+ }, children: jsxRuntimeExports.jsx(VisualBuilderPanel, { section: section, selectedNode: selectedNode, onSelectNode: setSelectedNode, onSectionChange: handleSectionChange, onAddPanel: handleAddPanel, onAddWidget: handleAddWidget, onDeleteNode: handleDeleteNode, onDuplicateNode: handleDuplicateNode, onSave: onSave, isMaximized: isMaximized, onToggleMaximize: toggleMaximize }) })] })] }));
2972
5696
  };
2973
5697
 
2974
5698
  /**
@@ -4401,6 +7125,14 @@ const DateTimeInputWidget = ({ config }) => {
4401
7125
  const SelectWidget = ({ config }) => {
4402
7126
  const { value, error, touched, isEnabled, onChange, onBlur, dataSourceOptions, loading, config: widgetConfig, } = useBaseWidget({ config });
4403
7127
  const { translate, translateConfig } = useWidgetTranslation();
7128
+ // For readonly mode, render as display text showing only the selected label
7129
+ if (widgetConfig['widget-readonly']) {
7130
+ const label = translateConfig(widgetConfig['widget-label']);
7131
+ // Find the selected option's label
7132
+ const selectedOption = dataSourceOptions.find((option) => option.value === value);
7133
+ const displayValue = selectedOption ? selectedOption.label : (value || '-');
7134
+ return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] SelectDisplayWidget flex flex-col sm:flex-row sm:items-start", children: [label && (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' }, children: [label, ":"] })), jsxRuntimeExports.jsx("div", { className: "flex-1", children: jsxRuntimeExports.jsx("div", { className: "text-base text-gray-900 font-medium", children: displayValue }) })] }));
7135
+ }
4404
7136
  return (jsxRuntimeExports.jsx("div", { className: "mb-[10px]", children: jsxRuntimeExports.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-start", children: [jsxRuntimeExports.jsxs("label", { className: "text-base font-medium text-gray-700 md:min-w-[120px] sm:pr-4 sm:pt-1 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, children: [translateConfig(widgetConfig['widget-label']), widgetConfig['widget-required'] && (jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-1", children: "*" }))] }), jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntimeExports.jsxs("select", { value: value || '', onChange: (e) => onChange(e.target.value), onBlur: onBlur, disabled: !isEnabled || loading || widgetConfig['widget-readonly'], className: `w-full sm:w-[180px] max-w-full h-[30px] px-3 border shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${(touched && error.length > 0) || (widgetConfig['widget-required'] && (!value || value === ''))
4405
7137
  ? 'border-red-500 focus:ring-red-500 focus:border-red-500'
4406
7138
  : 'border-gray-300'} ${!isEnabled || loading || widgetConfig['widget-readonly'] ? 'bg-gray-100 cursor-not-allowed' : 'bg-white'}`, style: { borderRadius: '10px' }, title: translateConfig(widgetConfig['widget-data-tooltip']), children: [jsxRuntimeExports.jsx("option", { value: "", children: translate('common.select') }), dataSourceOptions.map((option) => (jsxRuntimeExports.jsx("option", { value: option.value, children: option.label }, option.value)))] }), loading && (jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500 mt-1", children: translate('common.loadingOptions') })), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-sm mt-1", children: error[0] }))] })] }) }));
@@ -4801,6 +7533,17 @@ const TableCellSelect = ({ config, value, onValueChange }) => {
4801
7533
  const isReadonly = config['widget-readonly'] || false;
4802
7534
  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 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 ${isReadonly || loading ? 'bg-gray-100 cursor-not-allowed' : 'bg-white'} border-gray-300`, style: { borderRadius: '10px' }, children: [jsxRuntimeExports.jsx("option", { value: "", children: translate('common.select') || 'Select' }), dataSourceOptions.map((option) => (jsxRuntimeExports.jsx("option", { value: option.value, children: option.label }, option.value)))] }));
4803
7535
  };
7536
+ const SelectDisplayValue = ({ config, value }) => {
7537
+ const { dataSourceOptions, loading } = useBaseWidget({ config });
7538
+ if (loading) {
7539
+ return jsxRuntimeExports.jsx("span", { children: "-" });
7540
+ }
7541
+ if (value === null || value === undefined || value === '') {
7542
+ return jsxRuntimeExports.jsx("span", { children: "-" });
7543
+ }
7544
+ const selectedOption = dataSourceOptions.find((option) => option.value === value);
7545
+ return jsxRuntimeExports.jsx("span", { children: selectedOption ? selectedOption.label : String(value) });
7546
+ };
4804
7547
  const TableCellText = ({ config, value, onValueChange }) => {
4805
7548
  const isReadonly = config['widget-readonly'] || false;
4806
7549
  const placeholder = config['widget-data-placeholder'] || '';
@@ -4835,8 +7578,9 @@ const TableCellNumber = ({ config, value, onValueChange }) => {
4835
7578
  const TableWidget = ({ config }) => {
4836
7579
  const { value, error, touched, isEnabled, onChange, config: widgetConfig, } = useBaseWidget({ config });
4837
7580
  const { translate, translateConfig } = useWidgetTranslation();
4838
- const { apiAdapter } = useWidgetContext();
7581
+ const { dataSourceRequestHandler } = useWidgetContext();
4839
7582
  const dispatch = reactRedux.useDispatch();
7583
+ const storeValues = reactRedux.useSelector((state) => state.widget?.values || {});
4840
7584
  const rows = Array.isArray(value) ? value : [];
4841
7585
  const columns = widgetConfig['widget-data-columns'] || [];
4842
7586
  const operations = widgetConfig['widget-data-operations'] || {};
@@ -4848,10 +7592,13 @@ const TableWidget = ({ config }) => {
4848
7592
  const [confirmationState, setConfirmationState] = React.useState(null);
4849
7593
  const [isAdding, setIsAdding] = React.useState(false);
4850
7594
  const [newRowData, setNewRowData] = React.useState(null);
4851
- // When section is in edit mode (isReadonly is false), all rows are automatically editable
7595
+ // Track original rows when entering section edit mode for edit_action tracking
7596
+ const [originalRows, setOriginalRows] = React.useState(null);
7597
+ // When section is in edit mode (isReadonly is false), rows can be edited individually
7598
+ // But they are NOT automatically editable - user must click Edit button for each row
4852
7599
  const isSectionEditMode = !isReadonly && operations.edit;
4853
7600
  // Check if any row is being edited (either manually or via section edit mode)
4854
- const isAnyRowEditing = editingState !== null || isAdding || isSectionEditMode;
7601
+ const isAnyRowEditing = editingState !== null || isAdding;
4855
7602
  // Show confirmation dialog
4856
7603
  const showConfirmation = React.useCallback((message, onConfirm, onCancel) => {
4857
7604
  setConfirmationState({
@@ -4917,19 +7664,8 @@ const TableWidget = ({ config }) => {
4917
7664
  }, [isAnyRowEditing, rows, showConfirmation, cancelEdit, translate]);
4918
7665
  // Update cell value during edit
4919
7666
  const updateCellValue = React.useCallback((columnKey, newValue, rowIndex) => {
4920
- if (isSectionEditMode && rowIndex !== undefined) {
4921
- // When section is in edit mode, update the row directly
4922
- const newRows = [...rows];
4923
- if (!newRows[rowIndex]) {
4924
- newRows[rowIndex] = {};
4925
- }
4926
- newRows[rowIndex] = {
4927
- ...newRows[rowIndex],
4928
- [columnKey]: newValue,
4929
- };
4930
- onChange(newRows);
4931
- }
4932
- else if (editingState) {
7667
+ if (editingState && rowIndex !== undefined) {
7668
+ // Update editing state (works for both section edit mode and normal mode)
4933
7669
  setEditingState({
4934
7670
  ...editingState,
4935
7671
  currentValue: {
@@ -4937,6 +7673,9 @@ const TableWidget = ({ config }) => {
4937
7673
  [columnKey]: newValue,
4938
7674
  },
4939
7675
  });
7676
+ // Also update Redux store for the cell widget
7677
+ const cellWidgetId = `${widgetConfig['widget-id']}-row-${rowIndex}-col-${columnKey}`;
7678
+ dispatch(setValue({ widgetId: cellWidgetId, value: newValue }));
4940
7679
  }
4941
7680
  else if (isAdding && newRowData) {
4942
7681
  setNewRowData({
@@ -4944,7 +7683,7 @@ const TableWidget = ({ config }) => {
4944
7683
  [columnKey]: newValue,
4945
7684
  });
4946
7685
  }
4947
- }, [editingState, isAdding, newRowData, isSectionEditMode, rows, onChange]);
7686
+ }, [editingState, isAdding, newRowData, widgetConfig, dispatch]);
4948
7687
  // Save edited row
4949
7688
  const saveEdit = React.useCallback(async () => {
4950
7689
  if (!editingState)
@@ -4954,23 +7693,62 @@ const TableWidget = ({ config }) => {
4954
7693
  setLoadingRowIndex(rowIndex);
4955
7694
  try {
4956
7695
  // If API config exists, make API call
4957
- if (apiAdapter && apiConfig.edit) {
7696
+ // TODO: Update to use dataSourceRequestHandler pattern
7697
+ if (dataSourceRequestHandler && apiConfig.edit) {
4958
7698
  const editConfig = apiConfig.edit;
4959
- let url = editConfig.url || '';
4960
- // Replace {id} placeholder if present
4961
- if (rowData.id !== undefined) {
4962
- url = url.replace('{id}', rowData.id);
4963
- }
4964
- await apiAdapter(url, {
4965
- method: editConfig.method || 'PUT',
4966
- headers: editConfig.headers || { 'Content-Type': 'application/json' },
4967
- body: rowData,
4968
- });
7699
+ // Extract service and endpoint from URL if possible, or use config
7700
+ // For now, API operations in TableWidget are disabled
7701
+ console.warn('[TableWidget] API edit operations require migration to dataSourceRequestHandler pattern');
4969
7702
  }
4970
7703
  // Update local state
4971
7704
  const newRows = [...rows];
4972
- newRows[rowIndex] = rowData;
7705
+ const currentRow = newRows[rowIndex] || {};
7706
+ const wasDeleted = currentRow.edit_action === 'DELETE';
7707
+ // Determine edit_action (for color coding)
7708
+ let editAction = currentRow.edit_action;
7709
+ if (isSectionEditMode) {
7710
+ // If row was deleted but is being saved, un-delete it
7711
+ if (wasDeleted) {
7712
+ // Check if this row exists in original rows
7713
+ if (originalRows) {
7714
+ const rowId = rowData.id;
7715
+ const existsInOriginal = rowId !== undefined
7716
+ ? originalRows.some(or => or.id === rowId)
7717
+ : rowIndex < originalRows.length;
7718
+ editAction = existsInOriginal ? 'UPDATE' : 'ADD';
7719
+ }
7720
+ else {
7721
+ editAction = 'UPDATE';
7722
+ }
7723
+ }
7724
+ else if (!editAction && originalRows) {
7725
+ // Check if this row exists in original rows
7726
+ const rowId = rowData.id;
7727
+ const existsInOriginal = rowId !== undefined
7728
+ ? originalRows.some(or => or.id === rowId)
7729
+ : rowIndex < originalRows.length;
7730
+ editAction = existsInOriginal ? 'UPDATE' : 'ADD';
7731
+ }
7732
+ else if (!editAction) {
7733
+ editAction = 'UPDATE';
7734
+ }
7735
+ }
7736
+ else {
7737
+ // In non-section edit mode, mark as UPDATE if not already set
7738
+ if (!editAction && !wasDeleted) {
7739
+ editAction = 'UPDATE';
7740
+ }
7741
+ }
7742
+ newRows[rowIndex] = {
7743
+ ...rowData,
7744
+ ...(editAction ? { edit_action: editAction } : {}),
7745
+ };
4973
7746
  onChange(newRows);
7747
+ // Clear editing state and reset widget values in Redux
7748
+ columns.forEach((col) => {
7749
+ const cellWidgetId = `${widgetConfig['widget-id']}-row-${rowIndex}-col-${col['column-key']}`;
7750
+ dispatch(resetWidget(cellWidgetId));
7751
+ });
4974
7752
  setEditingState(null);
4975
7753
  }
4976
7754
  catch (error) {
@@ -4981,7 +7759,7 @@ const TableWidget = ({ config }) => {
4981
7759
  finally {
4982
7760
  setLoadingRowIndex(null);
4983
7761
  }
4984
- }, [editingState, rows, onChange, apiAdapter, apiConfig, translate]);
7762
+ }, [editingState, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode, originalRows, columns, widgetConfig, dispatch]);
4985
7763
  // Add new row
4986
7764
  const startAdd = React.useCallback(() => {
4987
7765
  // If there's an unsaved edit, cancel it first (no confirmation needed)
@@ -5003,18 +7781,20 @@ const TableWidget = ({ config }) => {
5003
7781
  try {
5004
7782
  let savedRow = { ...newRowData };
5005
7783
  // If API config exists, make API call
5006
- if (apiAdapter && apiConfig.add) {
7784
+ // TODO: Update to use dataSourceRequestHandler pattern
7785
+ if (dataSourceRequestHandler && apiConfig.add) {
5007
7786
  const addConfig = apiConfig.add;
5008
- const response = await apiAdapter(addConfig.url || '', {
5009
- method: addConfig.method || 'POST',
5010
- headers: addConfig.headers || { 'Content-Type': 'application/json' },
5011
- body: newRowData,
5012
- });
5013
- // Use response data if available (might contain generated ID)
7787
+ // Extract service and endpoint from URL if possible, or use config
7788
+ // For now, API operations in TableWidget are disabled
7789
+ console.warn('[TableWidget] API add operations require migration to dataSourceRequestHandler pattern');
7790
+ // Use newRowData as response for now
7791
+ const response = newRowData;
5014
7792
  if (response && typeof response === 'object') {
5015
7793
  savedRow = { ...savedRow, ...response };
5016
7794
  }
5017
7795
  }
7796
+ // Mark new row with edit_action: 'ADD' (for color coding)
7797
+ savedRow = { ...savedRow, edit_action: 'ADD' };
5018
7798
  // Add to local state
5019
7799
  onChange([...rows, savedRow]);
5020
7800
  setIsAdding(false);
@@ -5027,7 +7807,7 @@ const TableWidget = ({ config }) => {
5027
7807
  finally {
5028
7808
  setLoadingRowIndex(null);
5029
7809
  }
5030
- }, [isAdding, newRowData, rows, onChange, apiAdapter, apiConfig, translate]);
7810
+ }, [isAdding, newRowData, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode]);
5031
7811
  // Delete row
5032
7812
  const deleteRow = React.useCallback(async (rowIndex) => {
5033
7813
  if (isAnyRowEditing) {
@@ -5043,25 +7823,31 @@ const TableWidget = ({ config }) => {
5043
7823
  }
5044
7824
  }, [isAnyRowEditing, showConfirmation, cancelEdit, translate]);
5045
7825
  const performDelete = React.useCallback(async (rowIndex) => {
5046
- const row = rows[rowIndex];
7826
+ rows[rowIndex];
5047
7827
  setLoadingRowIndex(rowIndex);
5048
7828
  try {
5049
7829
  // If API config exists, make API call
5050
- if (apiAdapter && apiConfig.delete) {
7830
+ // TODO: Update to use dataSourceRequestHandler pattern
7831
+ if (dataSourceRequestHandler && apiConfig.delete) {
5051
7832
  const deleteConfig = apiConfig.delete;
5052
- let url = deleteConfig.url || '';
5053
- // Replace {id} placeholder if present
5054
- if (row.id !== undefined) {
5055
- url = url.replace('{id}', row.id);
5056
- }
5057
- await apiAdapter(url, {
5058
- method: deleteConfig.method || 'DELETE',
5059
- headers: deleteConfig.headers || {},
5060
- });
7833
+ // Extract service and endpoint from URL if possible, or use config
7834
+ // For now, API operations in TableWidget are disabled
7835
+ console.warn('[TableWidget] API delete operations require migration to dataSourceRequestHandler pattern');
7836
+ }
7837
+ // In section edit mode, mark row as deleted instead of removing it
7838
+ if (isSectionEditMode) {
7839
+ const newRows = [...rows];
7840
+ newRows[rowIndex] = {
7841
+ ...newRows[rowIndex],
7842
+ edit_action: 'DELETE',
7843
+ };
7844
+ onChange(newRows);
7845
+ }
7846
+ else {
7847
+ // Remove from local state (non-section edit mode)
7848
+ const newRows = rows.filter((_, i) => i !== rowIndex);
7849
+ onChange(newRows);
5061
7850
  }
5062
- // Remove from local state
5063
- const newRows = rows.filter((_, i) => i !== rowIndex);
5064
- onChange(newRows);
5065
7851
  }
5066
7852
  catch (error) {
5067
7853
  console.error('Error deleting record:', error);
@@ -5070,28 +7856,37 @@ const TableWidget = ({ config }) => {
5070
7856
  finally {
5071
7857
  setLoadingRowIndex(null);
5072
7858
  }
5073
- }, [rows, onChange, apiAdapter, apiConfig, translate]);
7859
+ }, [rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode]);
5074
7860
  // Get cell value (from editing state or row data)
5075
7861
  const getCellValue = React.useCallback((rowIndex, columnKey) => {
5076
- // When section is in edit mode, use row data directly
5077
- if (isSectionEditMode) {
5078
- return rows[rowIndex]?.[columnKey];
5079
- }
7862
+ // When a specific row is being edited (either in section edit mode or normal mode)
5080
7863
  if (editingState && editingState.rowIndex === rowIndex) {
7864
+ // Check Redux store first for most up-to-date value
7865
+ const cellWidgetId = `${widgetConfig['widget-id']}-row-${rowIndex}-col-${columnKey}`;
7866
+ const storeValue = storeValues[cellWidgetId];
7867
+ if (storeValue !== undefined) {
7868
+ return storeValue;
7869
+ }
5081
7870
  return editingState.currentValue[columnKey];
5082
7871
  }
5083
7872
  if (isAdding && rowIndex === rows.length) {
5084
7873
  return newRowData?.[columnKey];
5085
7874
  }
5086
7875
  return rows[rowIndex]?.[columnKey];
5087
- }, [editingState, isAdding, rows, newRowData, isSectionEditMode]);
7876
+ }, [editingState, isAdding, rows, newRowData, widgetConfig, storeValues]);
5088
7877
  // Get formatted display value for a cell
5089
7878
  const getDisplayValue = React.useCallback((rowIndex, column) => {
5090
7879
  const columnKey = column['column-key'];
5091
7880
  const cellValue = getCellValue(rowIndex, columnKey);
7881
+ const widgetType = column.widget || 'text';
5092
7882
  if (cellValue === null || cellValue === undefined || cellValue === '') {
5093
7883
  return '-';
5094
7884
  }
7885
+ // For select widgets, we'll use SelectDisplayValue component instead
7886
+ // This function is kept for other widget types
7887
+ if (widgetType === 'select') {
7888
+ return null; // Will be handled by SelectDisplayValue component
7889
+ }
5095
7890
  // Use formatValue if format config exists
5096
7891
  if (column['widget-data-format']) {
5097
7892
  return formatValue(cellValue, column['widget-data-format'], column.widget);
@@ -5099,29 +7894,22 @@ const TableWidget = ({ config }) => {
5099
7894
  return cellValue?.toString() || '-';
5100
7895
  }, [getCellValue]);
5101
7896
  // Check if row is being edited
5102
- // When section is in edit mode, all rows are editable
7897
+ // In section edit mode, only the row with active editingState is editable
5103
7898
  const isRowEditing = React.useCallback((rowIndex) => {
5104
- if (isSectionEditMode) {
5105
- return true; // All rows are editable when section is in edit mode
5106
- }
5107
7899
  return editingState?.rowIndex === rowIndex || (isAdding && rowIndex === rows.length);
5108
- }, [editingState, isAdding, rows.length, isSectionEditMode]);
5109
- // Set cell widget value in Redux when entering edit mode
7900
+ }, [editingState, isAdding, rows.length]);
7901
+ // Store original rows when entering section edit mode (for edit_action tracking)
5110
7902
  React.useEffect(() => {
5111
- if (isSectionEditMode) {
5112
- // When section is in edit mode, set values for all rows
5113
- rows.forEach((row, rowIndex) => {
5114
- columns.forEach((col) => {
5115
- const columnKey = col['column-key'];
5116
- const cellWidgetId = `${widgetConfig['widget-id']}-row-${rowIndex}-col-${columnKey}`;
5117
- const cellValue = row[columnKey];
5118
- const defaultValue = cellValue !== undefined ? cellValue : (col['widget-data-default'] ?? '');
5119
- // Set value in Redux store
5120
- dispatch(setValue({ widgetId: cellWidgetId, value: defaultValue }));
5121
- });
5122
- });
7903
+ if (isSectionEditMode && originalRows === null) {
7904
+ setOriginalRows(JSON.parse(JSON.stringify(rows))); // Deep clone
7905
+ }
7906
+ else if (!isSectionEditMode && originalRows !== null) {
7907
+ setOriginalRows(null);
5123
7908
  }
5124
- else if (editingState) {
7909
+ }, [isSectionEditMode, rows, originalRows]);
7910
+ // Set cell widget value in Redux when entering edit mode for a specific row
7911
+ React.useEffect(() => {
7912
+ if (editingState) {
5125
7913
  columns.forEach((col) => {
5126
7914
  const columnKey = col['column-key'];
5127
7915
  const cellWidgetId = `${widgetConfig['widget-id']}-row-${editingState.rowIndex}-col-${columnKey}`;
@@ -5131,7 +7919,7 @@ const TableWidget = ({ config }) => {
5131
7919
  dispatch(setValue({ widgetId: cellWidgetId, value: defaultValue }));
5132
7920
  });
5133
7921
  }
5134
- }, [isSectionEditMode, editingState, columns, widgetConfig, dispatch, rows]);
7922
+ }, [editingState, columns, widgetConfig, dispatch]);
5135
7923
  React.useEffect(() => {
5136
7924
  if (isAdding && newRowData) {
5137
7925
  columns.forEach((col) => {
@@ -5178,18 +7966,48 @@ const TableWidget = ({ config }) => {
5178
7966
  } }) }));
5179
7967
  }, [widgetConfig, updateCellValue]);
5180
7968
  // Render cell content (widget in edit mode, formatted value in view mode)
5181
- const renderCell = React.useCallback((rowIndex, column) => {
7969
+ const renderCell = React.useCallback((rowIndex, column, row) => {
5182
7970
  const columnKey = column['column-key'];
5183
7971
  const isEditing = isRowEditing(rowIndex);
5184
7972
  const cellValue = getCellValue(rowIndex, columnKey);
5185
7973
  const columnReadonly = column['widget-readonly'] === true;
7974
+ // Get color styling based on edit_action
7975
+ const getCellStyle = () => {
7976
+ if (isEditing)
7977
+ return {}; // No special styling when editing
7978
+ const editAction = row?.edit_action;
7979
+ if (editAction === 'ADD') {
7980
+ return { color: '#16a34a' }; // green-600
7981
+ }
7982
+ else if (editAction === 'DELETE') {
7983
+ return { color: '#dc2626', textDecoration: 'line-through' }; // red-600 with strikethrough
7984
+ }
7985
+ else if (editAction === 'UPDATE') {
7986
+ return { color: '#ea580c' }; // orange-600
7987
+ }
7988
+ return {};
7989
+ };
5186
7990
  if (isEditing) {
5187
7991
  // Use lightweight cell renderer
5188
7992
  return renderTableCell(rowIndex, column, cellValue, columnReadonly);
5189
7993
  }
5190
7994
  else {
5191
- // Display formatted value in view mode
5192
- return (jsxRuntimeExports.jsx("div", { className: "text-sm text-gray-900", children: getDisplayValue(rowIndex, column) }));
7995
+ // Display formatted value in view mode with color styling
7996
+ const widgetType = column.widget || 'text';
7997
+ const displayValue = getDisplayValue(rowIndex, column);
7998
+ // For select widgets, use SelectDisplayValue component to show label
7999
+ if (widgetType === 'select' && displayValue === null) {
8000
+ const cellWidgetId = `${widgetConfig['widget-id']}-row-${rowIndex}-col-${columnKey}`;
8001
+ const cellConfig = {
8002
+ ...column,
8003
+ 'widget-id': cellWidgetId,
8004
+ 'widget-label': '',
8005
+ 'widget-readonly': true,
8006
+ 'widget-data-path': undefined,
8007
+ };
8008
+ return (jsxRuntimeExports.jsx("div", { className: "text-sm", style: getCellStyle(), children: jsxRuntimeExports.jsx(SelectDisplayValue, { config: cellConfig, value: cellValue }) }));
8009
+ }
8010
+ return (jsxRuntimeExports.jsx("div", { className: "text-sm", style: getCellStyle(), children: displayValue }));
5193
8011
  }
5194
8012
  }, [isRowEditing, getCellValue, getDisplayValue, renderTableCell]);
5195
8013
  const tableWidgetId = `table-widget-${widgetConfig['widget-id']}`;
@@ -5249,11 +8067,13 @@ const TableWidget = ({ config }) => {
5249
8067
  .${tableWidgetId} button {
5250
8068
  border-radius: 10px !important;
5251
8069
  }
5252
- ` }), jsxRuntimeExports.jsxs("div", { className: `table-widget-container ${tableWidgetId}`, children: [confirmationState?.show && (jsxRuntimeExports.jsx("div", { className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50", children: jsxRuntimeExports.jsxs("div", { className: "bg-white rounded-lg p-6 max-w-md w-full mx-4", children: [jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold mb-4", children: translate('table.confirm') || 'Confirm Action' }), jsxRuntimeExports.jsx("p", { className: "text-gray-700 mb-6", 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 text-gray-700 bg-gray-200 hover:bg-gray-300", style: { borderRadius: '15px' }, children: translate('common.cancel') || 'Cancel' }), jsxRuntimeExports.jsx("button", { onClick: confirmationState.onConfirm, className: "px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700", style: { borderRadius: '15px' }, children: translate('table.discard') || 'Discard & Continue' })] })] }) })), operations.add && !isReadonly && isEnabled && (jsxRuntimeExports.jsx("div", { className: "flex justify-end mb-2", children: jsxRuntimeExports.jsx("button", { type: "button", onClick: startAdd, disabled: loadingRowIndex !== null, className: "px-3 py-1 text-sm bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed", style: { borderRadius: '15px' }, children: translate('table.addRecord') || 'Add New Record' }) })), rows.length === 0 && !isAdding ? (jsxRuntimeExports.jsxs("div", { className: "text-gray-500 text-sm py-4 text-center border border-gray-300", style: { borderRadius: '15px' }, children: [translate('table.noData') || 'No records available.', operations.add && !isReadonly && ` ${translate('table.clickToAdd') || 'Click "Add New Record" to add one.'}`] })) : (jsxRuntimeExports.jsx("div", { className: "overflow-x-auto border border-gray-300", style: { borderRadius: '15px' }, children: jsxRuntimeExports.jsxs("table", { className: "min-w-full divide-y divide-gray-200", children: [jsxRuntimeExports.jsx("thead", { className: "bg-gray-50", children: jsxRuntimeExports.jsxs("tr", { children: [columns.map((col) => (jsxRuntimeExports.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider", children: translateConfig(col['widget-label']) }, col['column-key']))), ((operations.edit || operations.remove) && !isReadonly) || isAnyRowEditing ? (jsxRuntimeExports.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider", children: translate('common.actions') || 'Actions' })) : null] }) }), jsxRuntimeExports.jsxs("tbody", { className: "bg-white divide-y divide-gray-200", children: [rows.map((row, rowIndex) => {
8070
+ ` }), jsxRuntimeExports.jsxs("div", { className: `table-widget-container ${tableWidgetId}`, children: [confirmationState?.show && (jsxRuntimeExports.jsx("div", { className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50", children: jsxRuntimeExports.jsxs("div", { className: "bg-white rounded-lg p-6 max-w-md w-full mx-4", children: [jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold mb-4", children: translate('table.confirm') || 'Confirm Action' }), jsxRuntimeExports.jsx("p", { className: "text-gray-700 mb-6", 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 text-gray-700 bg-gray-200 hover:bg-gray-300", style: { borderRadius: '15px' }, children: translate('common.cancel') || 'Cancel' }), jsxRuntimeExports.jsx("button", { onClick: confirmationState.onConfirm, className: "px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700", style: { borderRadius: '15px' }, children: translate('table.discard') || 'Discard & Continue' })] })] }) })), operations.add && !isReadonly && isEnabled && (isSectionEditMode || !isAnyRowEditing) && (jsxRuntimeExports.jsx("div", { className: "flex justify-end mb-2", children: jsxRuntimeExports.jsx("button", { type: "button", onClick: startAdd, disabled: loadingRowIndex !== null, className: "px-3 py-1 text-sm bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed", style: { borderRadius: '15px' }, children: translate('table.addRecord') || 'Add New Record' }) })), rows.length === 0 && !isAdding ? (jsxRuntimeExports.jsxs("div", { className: "text-gray-500 text-sm py-4 text-center border border-gray-300", style: { borderRadius: '15px' }, children: [translate('table.noData') || 'No records available.', operations.add && !isReadonly && ` ${translate('table.clickToAdd') || 'Click "Add New Record" to add one.'}`] })) : (jsxRuntimeExports.jsx("div", { className: "overflow-x-auto border border-gray-300", style: { borderRadius: '15px' }, children: jsxRuntimeExports.jsxs("table", { className: "min-w-full divide-y divide-gray-200", children: [jsxRuntimeExports.jsx("thead", { className: "bg-gray-50", children: jsxRuntimeExports.jsxs("tr", { children: [columns.map((col) => (jsxRuntimeExports.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider", 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 text-gray-500 uppercase tracking-wider", children: translate('common.actions') || 'Actions' })) : null] }) }), jsxRuntimeExports.jsxs("tbody", { className: "bg-white divide-y divide-gray-200", children: [rows.map((row, rowIndex) => {
5253
8071
  const isEditing = isRowEditing(rowIndex);
5254
8072
  const isLoading = loadingRowIndex === rowIndex;
5255
- return (jsxRuntimeExports.jsxs("tr", { className: isEditing ? 'bg-blue-50' : isLoading ? 'opacity-50' : '', children: [columns.map((col) => (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: renderCell(rowIndex, col) }, col['column-key']))), ((operations.edit || operations.remove) && !isReadonly) || isEditing ? (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", style: { minWidth: '120px' }, children: isEditing && !isSectionEditMode ? (
5256
- // Show Save/Cancel buttons only for manual row editing (not section edit mode)
8073
+ return (jsxRuntimeExports.jsxs("tr", { className: isEditing ? 'bg-blue-50' : isLoading ? 'opacity-50' : row.edit_action === 'DELETE' ? 'bg-red-50' : '', children: [columns.map((col) => {
8074
+ return (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: renderCell(rowIndex, col, row) }, col['column-key']));
8075
+ }), ((operations.edit || operations.remove) && !isReadonly) || isEditing || isSectionEditMode ? (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", style: { minWidth: '120px' }, children: isEditing ? (
8076
+ // Show OK (Save)/Cancel buttons when row is being edited (works in both section edit mode and normal mode)
5257
8077
  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 hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap flex-shrink-0", style: {
5258
8078
  display: 'inline-block',
5259
8079
  minWidth: '60px',
@@ -5261,12 +8081,10 @@ const TableWidget = ({ config }) => {
5261
8081
  color: '#ffffff', // white text
5262
8082
  border: 'none',
5263
8083
  borderRadius: '15px'
5264
- }, children: "Save" }), jsxRuntimeExports.jsx("button", { type: "button", onClick: cancelEdit, disabled: isLoading, className: "px-3 py-1 text-xs font-medium bg-gray-200 text-gray-700 hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap flex-shrink-0", style: { display: 'inline-block', minWidth: '60px', borderRadius: '15px' }, children: "Cancel" })] })) : !isSectionEditMode ? (
5265
- // Show Edit/Delete buttons only when not in section edit mode
5266
- jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [operations.edit && (jsxRuntimeExports.jsx("button", { type: "button", onClick: () => startEdit(rowIndex), disabled: isAnyRowEditing || isLoading, className: "px-3 py-1 text-xs text-blue-600 hover:text-blue-800 hover:bg-blue-50 disabled:opacity-50 disabled:cursor-not-allowed", style: { borderRadius: '15px' }, children: translate('common.edit') || 'Edit' })), operations.remove && (jsxRuntimeExports.jsx("button", { type: "button", onClick: () => deleteRow(rowIndex), disabled: isAnyRowEditing || isLoading, className: "px-3 py-1 text-xs text-red-600 hover:text-red-800 hover:bg-red-50 disabled:opacity-50 disabled:cursor-not-allowed", style: { borderRadius: '15px' }, children: translate('common.remove') || 'Delete' }))] })) : (
5267
- // In section edit mode, show only Delete button if remove is enabled
5268
- operations.remove && (jsxRuntimeExports.jsx("div", { className: "flex gap-2", children: jsxRuntimeExports.jsx("button", { type: "button", onClick: () => deleteRow(rowIndex), disabled: isLoading, className: "px-3 py-1 text-xs text-red-600 hover:text-red-800 hover:bg-red-50 disabled:opacity-50 disabled:cursor-not-allowed", style: { borderRadius: '15px' }, children: translate('common.remove') || 'Delete' }) }))) })) : null] }, rowIndex));
5269
- }), isAdding && newRowData && (jsxRuntimeExports.jsxs("tr", { className: "bg-blue-50", children: [columns.map((col) => (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: renderCell(rows.length, col) }, 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 bg-green-600 text-white hover:bg-green-700 disabled:opacity-50", style: { borderRadius: '15px' }, children: translate('common.save') || 'Save' }), jsxRuntimeExports.jsx("button", { type: "button", onClick: () => {
8084
+ }, children: translate('common.ok') || 'OK' }), jsxRuntimeExports.jsx("button", { type: "button", onClick: cancelEdit, disabled: isLoading, className: "px-3 py-1 text-xs font-medium bg-gray-200 text-gray-700 hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap flex-shrink-0", style: { display: 'inline-block', minWidth: '60px', borderRadius: '15px' }, children: translate('common.cancel') || 'Cancel' })] })) : (
8085
+ // Show Edit/Delete buttons when row is not being edited
8086
+ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [operations.edit && (jsxRuntimeExports.jsx("button", { type: "button", onClick: () => startEdit(rowIndex), disabled: isAnyRowEditing || isLoading, className: "px-3 py-1 text-xs text-blue-600 hover:text-blue-800 hover:bg-blue-50 disabled:opacity-50 disabled:cursor-not-allowed", style: { borderRadius: '15px' }, children: translate('common.edit') || 'Edit' })), operations.remove && (jsxRuntimeExports.jsx("button", { type: "button", onClick: () => deleteRow(rowIndex), disabled: isAnyRowEditing || isLoading, className: "px-3 py-1 text-xs text-red-600 hover:text-red-800 hover:bg-red-50 disabled:opacity-50 disabled:cursor-not-allowed", style: { borderRadius: '15px' }, children: translate('common.remove') || 'Delete' }))] })) })) : null] }, rowIndex));
8087
+ }), isAdding && newRowData && (jsxRuntimeExports.jsxs("tr", { className: "bg-blue-50", 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 bg-green-600 text-white hover:bg-green-700 disabled:opacity-50", style: { borderRadius: '15px' }, children: translate('common.save') || 'Save' }), jsxRuntimeExports.jsx("button", { type: "button", onClick: () => {
5270
8088
  setIsAdding(false);
5271
8089
  setNewRowData(null);
5272
8090
  }, disabled: loadingRowIndex === -1, className: "px-3 py-1 text-xs bg-gray-200 text-gray-700 hover:bg-gray-300 disabled:opacity-50", style: { borderRadius: '15px' }, children: translate('common.cancel') || 'Cancel' })] }) })] }))] })] }) })), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-sm mt-1", children: error[0] }))] })] }));
@@ -5937,18 +8755,24 @@ exports.DateTimeInputWidget = DateTimeInputWidget;
5937
8755
  exports.DisplayWidget = DisplayWidget;
5938
8756
  exports.FileInputWidget = FileInputWidget;
5939
8757
  exports.IterableAccordionWidget = IterableAccordionWidget;
8758
+ exports.JSONEditorPanel = JSONEditorPanel;
5940
8759
  exports.NumberInputWidget = NumberInputWidget;
5941
8760
  exports.PanelRenderer = PanelRenderer;
5942
8761
  exports.PhoneInputWidget = PhoneInputWidget;
5943
8762
  exports.ProfileWidget = ProfileWidget;
8763
+ exports.PropertyEditor = PropertyEditor;
5944
8764
  exports.RadioWidget = RadioWidget;
8765
+ exports.SectionBuilder = SectionBuilder;
5945
8766
  exports.SectionRenderer = SectionRenderer;
8767
+ exports.SectionTree = SectionTree;
5946
8768
  exports.SectionsContainer = SectionsContainer;
5947
8769
  exports.SelectWidget = SelectWidget;
5948
8770
  exports.SimpleTableWidget = SimpleTableWidget;
5949
8771
  exports.TableWidget = TableWidget;
5950
8772
  exports.TextAreaWidget = TextAreaWidget;
5951
8773
  exports.TextInputWidget = TextInputWidget;
8774
+ exports.VisualBuilderPanel = VisualBuilderPanel;
8775
+ exports.WidgetEventBus = WidgetEventBus;
5952
8776
  exports.WidgetProvider = WidgetProvider;
5953
8777
  exports.WidgetRenderer = WidgetRenderer;
5954
8778
  exports.applyCaseControl = applyCaseControl;
@@ -5963,6 +8787,7 @@ exports.formatDate = formatDate;
5963
8787
  exports.formatNumber = formatNumber;
5964
8788
  exports.formatPhone = formatPhone;
5965
8789
  exports.formatValue = formatValue;
8790
+ exports.geoHierarchyBuilder = geoHierarchyBuilder;
5966
8791
  exports.getApiDataSource = getApiDataSource;
5967
8792
  exports.getFormattedNumberLength = getFormattedNumberLength;
5968
8793
  exports.getSchemaDataSource = getSchemaDataSource;
@@ -5992,7 +8817,10 @@ exports.translatePanelConfig = translatePanelConfig;
5992
8817
  exports.translateUISchema = translateUISchema;
5993
8818
  exports.translateWidgetConfig = translateWidgetConfig;
5994
8819
  exports.useBaseWidget = useBaseWidget;
8820
+ exports.useGeoWidgetCascade = useGeoWidgetCascade;
8821
+ exports.useWidgetCascade = useWidgetCascade;
5995
8822
  exports.useWidgetContext = useWidgetContext;
8823
+ exports.useWidgetEventBus = useWidgetEventBus;
5996
8824
  exports.useWidgetTranslation = useWidgetTranslation;
5997
8825
  exports.validateNumericValue = validateNumericValue;
5998
8826
  exports.validateWidget = validateWidget;