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