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