@openg2p/registry-widgets 0.1.1 → 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 +2 -0
- package/dist/components/SectionBuilder/JSONEditorPanel.d.ts.map +1 -1
- package/dist/components/SectionBuilder/PropertyEditor.d.ts.map +1 -1
- package/dist/components/SectionBuilder/SectionBuilder.d.ts.map +1 -1
- package/dist/components/SectionBuilder/VisualBuilderPanel.d.ts +3 -0
- package/dist/components/SectionBuilder/VisualBuilderPanel.d.ts.map +1 -1
- package/dist/components/SectionRenderer.d.ts +2 -2
- package/dist/components/SectionRenderer.d.ts.map +1 -1
- package/dist/components/SectionsContainer.d.ts +3 -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 +249 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +2601 -168
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2609 -166
- 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/widgets/RadioWidget.d.ts.map +1 -1
- package/dist/widgets/TableWidget.d.ts.map +1 -1
- package/package.json +17 -9
package/dist/index.esm.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createSlice, configureStore } from '@reduxjs/toolkit';
|
|
2
|
-
import React, {
|
|
2
|
+
import React, { useContext, useRef, useEffect, useMemo, useCallback, createContext, useState } from 'react';
|
|
3
3
|
import { useDispatch, useSelector, Provider, useStore } from 'react-redux';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
|
+
import { JsonEditor } from 'json-edit-react';
|
|
6
7
|
import i18n from 'i18next';
|
|
7
8
|
export { default as i18n } from 'i18next';
|
|
8
9
|
import { initReactI18next } from 'react-i18next';
|
|
@@ -14,19 +15,58 @@ const initialState = {
|
|
|
14
15
|
loading: {},
|
|
15
16
|
dataSources: {},
|
|
16
17
|
};
|
|
18
|
+
// Track recent setValue calls to detect race conditions
|
|
19
|
+
const recentSetValueCalls = new Map();
|
|
17
20
|
const widgetSlice = createSlice({
|
|
18
21
|
name: 'widget',
|
|
19
22
|
initialState,
|
|
20
23
|
reducers: {
|
|
21
24
|
setValue: (state, action) => {
|
|
22
|
-
|
|
25
|
+
const { widgetId, value } = action.payload;
|
|
26
|
+
const previousValue = state.values[widgetId];
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
// Check if we recently set a different value (race condition detection)
|
|
29
|
+
const recentCall = recentSetValueCalls.get(widgetId);
|
|
30
|
+
const isRaceCondition = recentCall &&
|
|
31
|
+
recentCall.timestamp > now - 100 && // Within 100ms
|
|
32
|
+
recentCall.value !== value &&
|
|
33
|
+
recentCall.value === previousValue; // Trying to set back to old value
|
|
34
|
+
// Track this call
|
|
35
|
+
recentSetValueCalls.set(widgetId, { value, timestamp: now });
|
|
36
|
+
// Clean up old entries (older than 1 second)
|
|
37
|
+
for (const [key, call] of recentSetValueCalls.entries()) {
|
|
38
|
+
if (now - call.timestamp > 1000) {
|
|
39
|
+
recentSetValueCalls.delete(key);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// CRITICAL: Prevent race condition - if we just set a new value, don't allow setting the old value back
|
|
43
|
+
if (isRaceCondition) {
|
|
44
|
+
return; // Don't update the value
|
|
45
|
+
}
|
|
46
|
+
state.values[widgetId] = value;
|
|
23
47
|
// Clear errors when value changes
|
|
24
|
-
if (state.errors[
|
|
25
|
-
delete state.errors[
|
|
48
|
+
if (state.errors[widgetId]) {
|
|
49
|
+
delete state.errors[widgetId];
|
|
26
50
|
}
|
|
27
51
|
},
|
|
28
52
|
setValues: (state, action) => {
|
|
29
|
-
|
|
53
|
+
// setWidgetValue returns the complete updated state object with all keys preserved
|
|
54
|
+
// We need to do a deep merge to preserve nested structures that aren't in the payload
|
|
55
|
+
// But since setWidgetValue already includes all existing data, we can merge at top level
|
|
56
|
+
// However, we need to be careful: if payload has nested objects, we need to deep merge them
|
|
57
|
+
const merged = { ...state.values };
|
|
58
|
+
for (const [key, value] of Object.entries(action.payload)) {
|
|
59
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value) &&
|
|
60
|
+
merged[key] !== null && typeof merged[key] === 'object' && !Array.isArray(merged[key])) {
|
|
61
|
+
// Deep merge nested objects to preserve properties not in the payload
|
|
62
|
+
merged[key] = { ...merged[key], ...value };
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Replace primitives, arrays, or null values, or if target is not an object
|
|
66
|
+
merged[key] = value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
state.values = merged;
|
|
30
70
|
},
|
|
31
71
|
setError: (state, action) => {
|
|
32
72
|
if (action.payload.errors.length > 0) {
|
|
@@ -275,7 +315,7 @@ const validateWidget = (value, validation, required = false) => {
|
|
|
275
315
|
}
|
|
276
316
|
catch (error) {
|
|
277
317
|
if (error instanceof z.ZodError) {
|
|
278
|
-
errors.push(...error.
|
|
318
|
+
errors.push(...error.issues.map((e) => e.message));
|
|
279
319
|
}
|
|
280
320
|
else {
|
|
281
321
|
errors.push('Validation failed');
|
|
@@ -734,31 +774,97 @@ const getStaticDataSource = (dataSource) => {
|
|
|
734
774
|
};
|
|
735
775
|
/**
|
|
736
776
|
* Get API data source options
|
|
777
|
+
* Uses dataSourceRequestHandler to make API calls through host application
|
|
737
778
|
*/
|
|
738
|
-
const getApiDataSource = async (dataSource, allValues,
|
|
739
|
-
|
|
740
|
-
|
|
779
|
+
const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler, levelId // Optional level_id from widget-geo-config.level
|
|
780
|
+
) => {
|
|
781
|
+
if (!dataSourceRequestHandler) {
|
|
782
|
+
console.error('[getApiDataSource] dataSourceRequestHandler is required for API data sources');
|
|
741
783
|
return [];
|
|
742
784
|
}
|
|
743
785
|
try {
|
|
744
786
|
// Get dependency value if exists
|
|
745
|
-
|
|
787
|
+
// dependsOn can be either a data path (e.g., "person.address") or a widget-id
|
|
788
|
+
let depValue = null;
|
|
746
789
|
if (dataSource.dependsOn) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
790
|
+
// First try as data path
|
|
791
|
+
depValue = getValueByPath(allValues, dataSource.dependsOn);
|
|
792
|
+
// If not found and doesn't contain dots, try as widget-id
|
|
793
|
+
if ((depValue === null || depValue === undefined) && !dataSource.dependsOn.includes('.')) {
|
|
794
|
+
depValue = allValues[dataSource.dependsOn];
|
|
750
795
|
}
|
|
751
|
-
|
|
796
|
+
if (depValue === null || depValue === undefined || depValue === '') {
|
|
752
797
|
// If dependency is empty, return empty array
|
|
753
798
|
return [];
|
|
754
799
|
}
|
|
755
800
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
801
|
+
// Build request parameters
|
|
802
|
+
const method = dataSource.method || 'GET';
|
|
803
|
+
// Extract static params from dataSource
|
|
804
|
+
// Include explicit params object and any additional fields (like level_id)
|
|
805
|
+
const staticParams = { ...dataSource.params };
|
|
806
|
+
// Extract additional fields that aren't part of the standard ApiDataSource interface
|
|
807
|
+
// These are fields like level_id that might be directly on the dataSource
|
|
808
|
+
// BUT: level_id should come from widget-geo-config.level, not from dataSource
|
|
809
|
+
const standardFields = ['type', 'service', 'endpoint', 'url', 'method', 'dependsOn', 'valueKey', 'labelKey', 'headers', 'body', 'params', 'level_id'];
|
|
810
|
+
for (const [key, value] of Object.entries(dataSource)) {
|
|
811
|
+
if (!standardFields.includes(key) && value !== undefined && value !== null) {
|
|
812
|
+
staticParams[key] = value;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
// If levelId is provided (from widget-geo-config.level), use it instead of any level_id in dataSource
|
|
816
|
+
if (levelId) {
|
|
817
|
+
staticParams.level_id = levelId;
|
|
818
|
+
}
|
|
819
|
+
// Build request params object
|
|
820
|
+
const requestParams = { ...staticParams };
|
|
821
|
+
// Add dependency value to params
|
|
822
|
+
if (dataSource.dependsOn && depValue !== null && depValue !== undefined) {
|
|
823
|
+
// Extract the actual value ID if depValue is an object
|
|
824
|
+
const parentValueId = typeof depValue === 'object' && depValue !== null
|
|
825
|
+
? (depValue.level_value_id || depValue.id || depValue.value || depValue)
|
|
826
|
+
: depValue;
|
|
827
|
+
// For geo APIs, use parent_level_value_id
|
|
828
|
+
if (staticParams.level_id) {
|
|
829
|
+
requestParams.parent_level_value_id = parentValueId;
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
// For other APIs, use the dependency field name as param key
|
|
833
|
+
const paramKey = dataSource.dependsOn.split('.').pop() || 'filter';
|
|
834
|
+
requestParams[paramKey] = parentValueId;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
else if (staticParams.level_id) {
|
|
838
|
+
// First level has no parent
|
|
839
|
+
requestParams.parent_level_value_id = null;
|
|
840
|
+
}
|
|
841
|
+
// Get service mnemonic and endpoint (required)
|
|
842
|
+
const service = dataSource.service;
|
|
843
|
+
const endpoint = dataSource.endpoint;
|
|
844
|
+
if (!service) {
|
|
845
|
+
console.error('[getApiDataSource] API data source missing service mnemonic. Use "service" field instead of "url"');
|
|
846
|
+
return [];
|
|
847
|
+
}
|
|
848
|
+
if (!endpoint) {
|
|
849
|
+
console.error('[getApiDataSource] API data source missing endpoint. Use "endpoint" field to specify the operation (e.g., "get_g2p_geo_level_values")');
|
|
850
|
+
return [];
|
|
851
|
+
}
|
|
852
|
+
let response;
|
|
853
|
+
try {
|
|
854
|
+
response = await dataSourceRequestHandler(service, endpoint, method, requestParams, {
|
|
855
|
+
headers: dataSource.headers,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
console.error('[getApiDataSource] Handler error:', error);
|
|
860
|
+
throw error;
|
|
861
|
+
}
|
|
862
|
+
// Handle OpenG2P response format (response_body.response_payload)
|
|
863
|
+
if (response && typeof response === 'object') {
|
|
864
|
+
if (response.response_body?.response_payload && Array.isArray(response.response_body.response_payload)) {
|
|
865
|
+
return response.response_body.response_payload;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
762
868
|
// Handle array response
|
|
763
869
|
if (Array.isArray(response)) {
|
|
764
870
|
return response;
|
|
@@ -805,49 +911,134 @@ const transformDataSourceOptions = (data, valueKey, labelKey) => {
|
|
|
805
911
|
}));
|
|
806
912
|
};
|
|
807
913
|
|
|
914
|
+
const WidgetEventBusContext = React.createContext(null);
|
|
915
|
+
/**
|
|
916
|
+
* Hook to access the widget event bus from context
|
|
917
|
+
*/
|
|
918
|
+
const useWidgetEventBus = () => {
|
|
919
|
+
return useContext(WidgetEventBusContext);
|
|
920
|
+
};
|
|
921
|
+
|
|
808
922
|
// Define stable empty arrays to avoid selector reference issues
|
|
809
923
|
const EMPTY_ERRORS = [];
|
|
810
|
-
const EMPTY_DATA_SOURCE = [];
|
|
924
|
+
const EMPTY_DATA_SOURCE$1 = [];
|
|
811
925
|
const useBaseWidget = (options) => {
|
|
812
|
-
const { config,
|
|
926
|
+
const { config, dataSourceRequestHandler, schemaData, onValueChange } = options;
|
|
813
927
|
const dispatch = useDispatch();
|
|
928
|
+
const eventBus = useWidgetEventBus();
|
|
814
929
|
const widgetId = config['widget-id'];
|
|
815
930
|
// Get state from Redux
|
|
816
931
|
const values = useSelector((state) => state.widget.values);
|
|
817
932
|
const errors = useSelector((state) => state.widget.errors[widgetId] ?? EMPTY_ERRORS);
|
|
818
933
|
const touched = useSelector((state) => state.widget.touched[widgetId] || false);
|
|
819
934
|
const loading = useSelector((state) => state.widget.loading[widgetId] || false);
|
|
820
|
-
const dataSourceOptions = useSelector((state) => state.widget.dataSources[widgetId] ?? EMPTY_DATA_SOURCE);
|
|
935
|
+
const dataSourceOptions = useSelector((state) => state.widget.dataSources[widgetId] ?? EMPTY_DATA_SOURCE$1);
|
|
821
936
|
// Skip value handling for layout widgets (they don't store data values)
|
|
822
937
|
// Infer layout from widget-type
|
|
823
938
|
const isLayoutWidget = config['widget-type'] === 'layout';
|
|
939
|
+
// Track if user has explicitly set a value to prevent default from overwriting
|
|
940
|
+
const userHasSetValueRef = useRef(false);
|
|
941
|
+
// Use ref for values to avoid stale closures in handleChange
|
|
942
|
+
const valuesRef = useRef(values);
|
|
943
|
+
useEffect(() => {
|
|
944
|
+
valuesRef.current = values;
|
|
945
|
+
}, [values]);
|
|
946
|
+
// Track last dispatched value to prevent duplicate dispatches
|
|
947
|
+
const lastDispatchedValueRef = useRef(null);
|
|
824
948
|
// Get current value
|
|
825
949
|
const currentValue = useMemo(() => {
|
|
826
950
|
if (isLayoutWidget) {
|
|
827
951
|
return undefined; // Layout widgets don't have values
|
|
828
952
|
}
|
|
829
|
-
|
|
953
|
+
// Try to get value from widgetId first (this should have the actual selected value)
|
|
954
|
+
// For geo widgets with dataPath, widgetId stores the actual ID, while dataPath stores the hierarchy object
|
|
955
|
+
let value = values[widgetId];
|
|
956
|
+
// If widgetId doesn't have a value, try dataPath
|
|
957
|
+
if (value === undefined && config['widget-data-path']) {
|
|
958
|
+
value = getWidgetValue(values, config['widget-data-path'], widgetId);
|
|
959
|
+
// CRITICAL: For geo widgets with dataPath, the value might be stored as a hierarchy object
|
|
960
|
+
// Extract the actual value (geo_lowest_level_value_id) if it's an object
|
|
961
|
+
const geoConfig = config['widget-geo-config'];
|
|
962
|
+
if (geoConfig?.isLastLevel && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
963
|
+
// If it's a geo hierarchy object, extract the actual value
|
|
964
|
+
if ('geo_lowest_level_value_id' in value) {
|
|
965
|
+
value = value.geo_lowest_level_value_id;
|
|
966
|
+
}
|
|
967
|
+
else if ('value' in value) {
|
|
968
|
+
value = value.value;
|
|
969
|
+
}
|
|
970
|
+
else if ('id' in value) {
|
|
971
|
+
value = value.id;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
// If value is still undefined and user has set a value, try reading from widgetId as backup
|
|
976
|
+
// This handles cases where dataPath lookup might fail temporarily
|
|
977
|
+
if (value === undefined && userHasSetValueRef.current && values[widgetId] !== undefined) {
|
|
978
|
+
value = values[widgetId];
|
|
979
|
+
}
|
|
980
|
+
// If user has explicitly set a value, always return it (even if undefined/null)
|
|
981
|
+
// This prevents the default from overwriting user selections
|
|
982
|
+
if (userHasSetValueRef.current) {
|
|
983
|
+
return value;
|
|
984
|
+
}
|
|
985
|
+
// Only fall back to default if user hasn't set a value yet
|
|
986
|
+
// But check if value is explicitly null (user cleared it) vs undefined (never set)
|
|
987
|
+
if (value === null) {
|
|
988
|
+
return null; // User explicitly cleared it, don't use default
|
|
989
|
+
}
|
|
830
990
|
return value !== undefined ? value : config['widget-data-default'];
|
|
831
991
|
}, [values, config, widgetId, isLayoutWidget]);
|
|
832
|
-
// Initialize default value (skip for layout widgets)
|
|
992
|
+
// Initialize default value only once on mount (skip for layout widgets)
|
|
833
993
|
useEffect(() => {
|
|
834
994
|
if (isLayoutWidget) {
|
|
835
995
|
return;
|
|
836
996
|
}
|
|
837
|
-
|
|
997
|
+
// Only initialize default if value is undefined and user hasn't set a value yet
|
|
998
|
+
if (!userHasSetValueRef.current && config['widget-data-default'] !== undefined && currentValue === undefined) {
|
|
838
999
|
handleChange(config['widget-data-default'], false);
|
|
839
1000
|
}
|
|
840
1001
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
841
|
-
}, [isLayoutWidget]); //
|
|
1002
|
+
}, [isLayoutWidget]); // Only run once on mount
|
|
842
1003
|
// Handle value change
|
|
1004
|
+
// CRITICAL: Don't include 'values' in dependency array - it causes the callback to be recreated
|
|
1005
|
+
// every time values change, which can lead to stale closures and double dispatches
|
|
843
1006
|
const handleChange = useCallback((newValue, validate = true) => {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1007
|
+
// Use valuesRef to get the latest values, not the stale closure value
|
|
1008
|
+
const currentValues = valuesRef.current;
|
|
1009
|
+
const currentValue = currentValues[widgetId] || getWidgetValue(currentValues, config['widget-data-path'], widgetId);
|
|
1010
|
+
// CRITICAL: Prevent setting the same value (avoids unnecessary dispatches and potential loops)
|
|
1011
|
+
if (currentValue === newValue) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
// Mark that user has set a value (unless this is the default initialization)
|
|
1015
|
+
if (newValue !== config['widget-data-default'] || userHasSetValueRef.current) {
|
|
1016
|
+
userHasSetValueRef.current = true;
|
|
1017
|
+
}
|
|
1018
|
+
// CRITICAL FIX: If there's no dataPath, just set the value directly
|
|
1019
|
+
// If there's a dataPath, we need to update both the widgetId and the dataPath
|
|
1020
|
+
if (!config['widget-data-path']) {
|
|
1021
|
+
// No dataPath: just set the value directly by widgetId
|
|
1022
|
+
// CRITICAL: Check if we just dispatched this value to prevent duplicate dispatches
|
|
1023
|
+
if (lastDispatchedValueRef.current === newValue) {
|
|
1024
|
+
return;
|
|
849
1025
|
}
|
|
850
|
-
|
|
1026
|
+
lastDispatchedValueRef.current = newValue;
|
|
1027
|
+
dispatch(setValue({ widgetId, value: newValue }));
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
// Has dataPath: update both widgetId and dataPath
|
|
1031
|
+
// CRITICAL: Create updated values object with newValue already set
|
|
1032
|
+
// This prevents setWidgetValue from reading stale values
|
|
1033
|
+
const currentValuesWithUpdate = {
|
|
1034
|
+
...valuesRef.current,
|
|
1035
|
+
[widgetId]: newValue, // Ensure widgetId has the new value
|
|
1036
|
+
};
|
|
1037
|
+
const updatedValues = setWidgetValue(currentValuesWithUpdate, config['widget-data-path'], widgetId, newValue);
|
|
1038
|
+
// setWidgetValue returns the complete updated structure with all existing data preserved
|
|
1039
|
+
// Use setValues to update the entire state with deep merge
|
|
1040
|
+
dispatch(setValues(updatedValues));
|
|
1041
|
+
}
|
|
851
1042
|
// Validate if needed
|
|
852
1043
|
if (validate) {
|
|
853
1044
|
const validationErrors = validateWidget(newValue, config['widget-data-validation'], config['widget-required']);
|
|
@@ -857,14 +1048,36 @@ const useBaseWidget = (options) => {
|
|
|
857
1048
|
if (onValueChange) {
|
|
858
1049
|
onValueChange(widgetId, newValue);
|
|
859
1050
|
}
|
|
860
|
-
|
|
1051
|
+
// Publish widget:change event
|
|
1052
|
+
// Skip publishing for last-level geo widgets (no child widgets waiting)
|
|
1053
|
+
const geoConfig = config['widget-geo-config'];
|
|
1054
|
+
const isLastLevelGeo = geoConfig?.isLastLevel === true;
|
|
1055
|
+
if (eventBus && !isLastLevelGeo) {
|
|
1056
|
+
eventBus.publish({
|
|
1057
|
+
type: 'widget:change',
|
|
1058
|
+
widgetId,
|
|
1059
|
+
value: newValue,
|
|
1060
|
+
timestamp: Date.now(),
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
}, [config, widgetId, dispatch, onValueChange, eventBus] // Removed 'values' to prevent stale closures
|
|
1064
|
+
);
|
|
861
1065
|
// Handle blur
|
|
862
1066
|
const handleBlur = useCallback(() => {
|
|
863
1067
|
dispatch(setTouched({ widgetId, touched: true }));
|
|
864
1068
|
// Validate on blur
|
|
865
1069
|
const validationErrors = validateWidget(currentValue, config['widget-data-validation'], config['widget-required']);
|
|
866
1070
|
dispatch(setError({ widgetId, errors: validationErrors }));
|
|
867
|
-
|
|
1071
|
+
// Publish widget:blur event
|
|
1072
|
+
if (eventBus) {
|
|
1073
|
+
eventBus.publish({
|
|
1074
|
+
type: 'widget:blur',
|
|
1075
|
+
widgetId,
|
|
1076
|
+
value: currentValue,
|
|
1077
|
+
timestamp: Date.now(),
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}, [currentValue, config, widgetId, dispatch, eventBus]);
|
|
868
1081
|
// Get field value helper
|
|
869
1082
|
const getFieldValue = useCallback((path) => {
|
|
870
1083
|
return getWidgetValue(values, path, '');
|
|
@@ -894,33 +1107,102 @@ const useBaseWidget = (options) => {
|
|
|
894
1107
|
}
|
|
895
1108
|
return formatValue(currentValue, config['widget-data-format'], config.widget);
|
|
896
1109
|
}, [currentValue, config]);
|
|
1110
|
+
// Track readonly state explicitly to detect changes
|
|
1111
|
+
// Use JSON.stringify to create a stable reference for the dependency array
|
|
1112
|
+
const isReadonly = config['widget-readonly'] ?? false;
|
|
1113
|
+
const dataSource = config['widget-data-source'];
|
|
1114
|
+
const geoConfig = config['widget-geo-config'];
|
|
1115
|
+
// Use ref to store handler to avoid stale closures
|
|
1116
|
+
const handlerRef = useRef(dataSourceRequestHandler);
|
|
1117
|
+
useEffect(() => {
|
|
1118
|
+
handlerRef.current = dataSourceRequestHandler;
|
|
1119
|
+
}, [dataSourceRequestHandler]);
|
|
1120
|
+
// Create a stable key for the config to detect changes
|
|
1121
|
+
// This ensures the effect runs when widget-readonly changes
|
|
1122
|
+
const apiService = dataSource?.type === 'api' ? dataSource.service : '';
|
|
1123
|
+
const apiEndpoint = dataSource?.type === 'api' ? dataSource.endpoint : '';
|
|
1124
|
+
const configKey = `${widgetId}-${isReadonly}-${dataSource?.type || 'none'}-${apiService}-${apiEndpoint}`;
|
|
897
1125
|
// Handle data source loading
|
|
898
1126
|
useEffect(() => {
|
|
899
|
-
const dataSource = config['widget-data-source'];
|
|
900
1127
|
if (!dataSource) {
|
|
901
1128
|
return;
|
|
902
1129
|
}
|
|
1130
|
+
// For API data sources, check if widget is readonly
|
|
1131
|
+
// According to PRD: "Level 1 geo widgets load on widget mount or when entering edit mode"
|
|
1132
|
+
// So we should only load API data sources when widget is NOT readonly
|
|
1133
|
+
if (dataSource.type === 'api' && isReadonly) {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
// For widgets with dependencies, check if dependency value exists
|
|
1137
|
+
if (dataSource.type === 'api' && dataSource.dependsOn) {
|
|
1138
|
+
// Check if dependency value exists
|
|
1139
|
+
let depValue = null;
|
|
1140
|
+
if (dataSource.dependsOn.includes('.')) {
|
|
1141
|
+
depValue = getWidgetValue(values, dataSource.dependsOn, '');
|
|
1142
|
+
}
|
|
1143
|
+
else {
|
|
1144
|
+
depValue = values[dataSource.dependsOn];
|
|
1145
|
+
}
|
|
1146
|
+
// If dependency is empty, don't load (will load when dependency has value)
|
|
1147
|
+
if (depValue === null || depValue === undefined || depValue === '') {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
903
1151
|
const loadDataSource = async () => {
|
|
1152
|
+
// Get current handler from ref to avoid stale closures
|
|
1153
|
+
// Also check prop directly as fallback (for initial render or when ref not updated yet)
|
|
1154
|
+
const currentHandler = handlerRef.current || dataSourceRequestHandler;
|
|
1155
|
+
// Only check for handler when we actually need it (inside the async function)
|
|
1156
|
+
// This avoids false errors during React Strict Mode double-invocation
|
|
1157
|
+
// If handler isn't available yet, silently skip - React will retry when it's ready
|
|
904
1158
|
try {
|
|
1159
|
+
if (dataSource.type === 'api' && !currentHandler) {
|
|
1160
|
+
// Silently skip if handler isn't available yet (common during React Strict Mode double-invocation)
|
|
1161
|
+
// React will call this effect again when the handler is ready
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
905
1164
|
dispatch(setLoading({ widgetId, loading: true }));
|
|
906
1165
|
let data = [];
|
|
907
1166
|
if (dataSource.type === 'static') {
|
|
908
1167
|
data = getStaticDataSource(dataSource);
|
|
909
1168
|
}
|
|
910
1169
|
else if (dataSource.type === 'api') {
|
|
911
|
-
|
|
1170
|
+
if (!currentHandler) {
|
|
1171
|
+
// Silently skip if handler isn't available yet
|
|
1172
|
+
dispatch(setLoading({ widgetId, loading: false }));
|
|
1173
|
+
dispatch(setDataSource({ widgetId, data: [] }));
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
// Extract level_id from widget-geo-config.level if available
|
|
1177
|
+
const levelId = geoConfig?.level;
|
|
1178
|
+
data = await getApiDataSource(dataSource, values, currentHandler, levelId);
|
|
912
1179
|
}
|
|
913
1180
|
else if (dataSource.type === 'schema') {
|
|
914
1181
|
data = getSchemaDataSource(dataSource, schemaData || {});
|
|
915
1182
|
}
|
|
916
1183
|
// Transform to { value, label } format
|
|
917
|
-
|
|
918
|
-
|
|
1184
|
+
// For geo widgets, default to level_value_id and level_value_mnemonic
|
|
1185
|
+
let valueKey;
|
|
1186
|
+
let labelKey;
|
|
1187
|
+
if (dataSource.type === 'static') {
|
|
1188
|
+
valueKey = undefined;
|
|
1189
|
+
labelKey = undefined;
|
|
1190
|
+
}
|
|
1191
|
+
else if (geoConfig) {
|
|
1192
|
+
// Geo widgets: default to level_value_id and level_value_mnemonic
|
|
1193
|
+
valueKey = dataSource.valueKey || 'level_value_id';
|
|
1194
|
+
labelKey = dataSource.labelKey || 'level_value_mnemonic';
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
// Non-geo widgets: use specified keys or undefined
|
|
1198
|
+
valueKey = dataSource.valueKey;
|
|
1199
|
+
labelKey = dataSource.labelKey;
|
|
1200
|
+
}
|
|
919
1201
|
const transformed = transformDataSourceOptions(data, valueKey, labelKey);
|
|
920
1202
|
dispatch(setDataSource({ widgetId, data: transformed }));
|
|
921
1203
|
}
|
|
922
1204
|
catch (error) {
|
|
923
|
-
console.error(
|
|
1205
|
+
console.error(`[useBaseWidget] ERROR loading data source for ${widgetId}:`, error);
|
|
924
1206
|
dispatch(setDataSource({ widgetId, data: [] }));
|
|
925
1207
|
}
|
|
926
1208
|
finally {
|
|
@@ -928,7 +1210,9 @@ const useBaseWidget = (options) => {
|
|
|
928
1210
|
}
|
|
929
1211
|
};
|
|
930
1212
|
loadDataSource();
|
|
931
|
-
|
|
1213
|
+
// Use configKey to ensure effect runs when readonly state changes
|
|
1214
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1215
|
+
}, [configKey, values, dataSourceRequestHandler, schemaData, widgetId, dispatch]);
|
|
932
1216
|
return {
|
|
933
1217
|
widgetId,
|
|
934
1218
|
value: currentValue,
|
|
@@ -947,6 +1231,357 @@ const useBaseWidget = (options) => {
|
|
|
947
1231
|
};
|
|
948
1232
|
};
|
|
949
1233
|
|
|
1234
|
+
/**
|
|
1235
|
+
* Hook for general widget cascade functionality
|
|
1236
|
+
* Handles listening to parent widget changes and reloading data sources
|
|
1237
|
+
*/
|
|
1238
|
+
const useWidgetCascade = (options) => {
|
|
1239
|
+
const { config, dataSourceRequestHandler, values } = options;
|
|
1240
|
+
const dispatch = useDispatch();
|
|
1241
|
+
const eventBus = useWidgetEventBus();
|
|
1242
|
+
const widgetId = config['widget-id'];
|
|
1243
|
+
const cascadeConfig = config['widget-cascade'];
|
|
1244
|
+
const dataSource = config['widget-data-source'];
|
|
1245
|
+
const valuesRef = useRef(values);
|
|
1246
|
+
const handlerRef = useRef(dataSourceRequestHandler);
|
|
1247
|
+
// Keep refs updated
|
|
1248
|
+
useEffect(() => {
|
|
1249
|
+
valuesRef.current = values;
|
|
1250
|
+
handlerRef.current = dataSourceRequestHandler;
|
|
1251
|
+
}, [values, dataSourceRequestHandler]);
|
|
1252
|
+
useEffect(() => {
|
|
1253
|
+
if (!cascadeConfig || !eventBus || !dataSource || dataSource.type !== 'api') {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const { listenTo, onEvent = 'widget:change', clearOnChange = true, reloadOnChange = true, debounce, throttle } = cascadeConfig;
|
|
1257
|
+
if (listenTo.length === 0) {
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const handleEvent = async (event) => {
|
|
1261
|
+
// Check if this event is from a parent we're listening to
|
|
1262
|
+
if (!listenTo.includes(event.widgetId)) {
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const currentValues = valuesRef.current;
|
|
1266
|
+
const currentHandler = handlerRef.current;
|
|
1267
|
+
// Clear value if configured
|
|
1268
|
+
if (clearOnChange) {
|
|
1269
|
+
dispatch(setValue({ widgetId, value: undefined }));
|
|
1270
|
+
}
|
|
1271
|
+
// Reload data source if configured
|
|
1272
|
+
if (reloadOnChange && currentHandler) {
|
|
1273
|
+
try {
|
|
1274
|
+
const data = await getApiDataSource(dataSource, currentValues, currentHandler);
|
|
1275
|
+
// Transform to { value, label } format
|
|
1276
|
+
const valueKey = dataSource.valueKey;
|
|
1277
|
+
const labelKey = dataSource.labelKey;
|
|
1278
|
+
const transformed = transformDataSourceOptions(data, valueKey, labelKey);
|
|
1279
|
+
dispatch(setDataSource({ widgetId, data: transformed }));
|
|
1280
|
+
}
|
|
1281
|
+
catch (error) {
|
|
1282
|
+
console.error('Error reloading data source in cascade:', error);
|
|
1283
|
+
dispatch(setDataSource({ widgetId, data: [] }));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
// Subscribe to events
|
|
1288
|
+
const unsubscribe = eventBus.subscribe(onEvent, handleEvent, { debounce, throttle });
|
|
1289
|
+
return () => {
|
|
1290
|
+
unsubscribe();
|
|
1291
|
+
};
|
|
1292
|
+
}, [cascadeConfig, eventBus, dataSource, widgetId, dispatch]);
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* Geo Hierarchy Builder
|
|
1297
|
+
* Manages geo hierarchy state and builds hierarchy JSON structure
|
|
1298
|
+
*/
|
|
1299
|
+
class GeoHierarchyBuilder {
|
|
1300
|
+
constructor() {
|
|
1301
|
+
this.hierarchies = new Map();
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Get or create hierarchy state for a group
|
|
1305
|
+
*/
|
|
1306
|
+
getHierarchy(groupId = 'default') {
|
|
1307
|
+
if (!this.hierarchies.has(groupId)) {
|
|
1308
|
+
this.hierarchies.set(groupId, {
|
|
1309
|
+
levels: new Map(),
|
|
1310
|
+
order: [],
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
return this.hierarchies.get(groupId);
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Add a level to the hierarchy
|
|
1317
|
+
*/
|
|
1318
|
+
addLevel(level, level_value_id, level_value_mnemonic, groupId = 'default') {
|
|
1319
|
+
const hierarchy = this.getHierarchy(groupId);
|
|
1320
|
+
// If level already exists, remove it and everything after it
|
|
1321
|
+
const existingIndex = hierarchy.order.indexOf(level);
|
|
1322
|
+
if (existingIndex >= 0) {
|
|
1323
|
+
// Remove this level and all subsequent levels
|
|
1324
|
+
const levelsToRemove = hierarchy.order.slice(existingIndex);
|
|
1325
|
+
levelsToRemove.forEach((l) => {
|
|
1326
|
+
hierarchy.levels.delete(l);
|
|
1327
|
+
hierarchy.order = hierarchy.order.filter((o) => o !== l);
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
// Add new level
|
|
1331
|
+
hierarchy.levels.set(level, {
|
|
1332
|
+
level,
|
|
1333
|
+
level_value_id,
|
|
1334
|
+
level_value_mnemonic,
|
|
1335
|
+
});
|
|
1336
|
+
hierarchy.order.push(level);
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Remove a level and all levels below it
|
|
1340
|
+
*/
|
|
1341
|
+
removeLevelAndBelow(level, groupId = 'default') {
|
|
1342
|
+
const hierarchy = this.getHierarchy(groupId);
|
|
1343
|
+
const index = hierarchy.order.indexOf(level);
|
|
1344
|
+
if (index >= 0) {
|
|
1345
|
+
// Remove this level and all subsequent levels
|
|
1346
|
+
const levelsToRemove = hierarchy.order.slice(index);
|
|
1347
|
+
levelsToRemove.forEach((l) => {
|
|
1348
|
+
hierarchy.levels.delete(l);
|
|
1349
|
+
hierarchy.order = hierarchy.order.filter((o) => o !== l);
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Build hierarchy JSON structure
|
|
1355
|
+
*/
|
|
1356
|
+
buildHierarchyJson(groupId = 'default') {
|
|
1357
|
+
const hierarchy = this.getHierarchy(groupId);
|
|
1358
|
+
if (hierarchy.order.length === 0) {
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
const hierarchyArray = hierarchy.order.map((level) => {
|
|
1362
|
+
const data = hierarchy.levels.get(level);
|
|
1363
|
+
return {
|
|
1364
|
+
level: data.level,
|
|
1365
|
+
level_value_id: data.level_value_id,
|
|
1366
|
+
level_value_mnemonic: data.level_value_mnemonic,
|
|
1367
|
+
};
|
|
1368
|
+
});
|
|
1369
|
+
const lowestLevel = hierarchy.order[hierarchy.order.length - 1];
|
|
1370
|
+
const lowestLevelData = hierarchy.levels.get(lowestLevel);
|
|
1371
|
+
return {
|
|
1372
|
+
geo_lowest_level_value_id: lowestLevelData.level_value_id,
|
|
1373
|
+
geo_code_hierarchy_json: {
|
|
1374
|
+
hierarchy: hierarchyArray,
|
|
1375
|
+
lowest_level_value_id: lowestLevelData.level_value_id,
|
|
1376
|
+
},
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Clear hierarchy for a group
|
|
1381
|
+
*/
|
|
1382
|
+
clear(groupId = 'default') {
|
|
1383
|
+
this.hierarchies.delete(groupId);
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Clear all hierarchies
|
|
1387
|
+
*/
|
|
1388
|
+
clearAll() {
|
|
1389
|
+
this.hierarchies.clear();
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Get current levels for a group
|
|
1393
|
+
*/
|
|
1394
|
+
getLevels(groupId = 'default') {
|
|
1395
|
+
const hierarchy = this.getHierarchy(groupId);
|
|
1396
|
+
return hierarchy.order.map((level) => hierarchy.levels.get(level));
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
// Singleton instance
|
|
1400
|
+
const geoHierarchyBuilder = new GeoHierarchyBuilder();
|
|
1401
|
+
|
|
1402
|
+
// Define stable empty array to avoid selector reference issues
|
|
1403
|
+
const EMPTY_DATA_SOURCE = [];
|
|
1404
|
+
/**
|
|
1405
|
+
* Hook for geo widget cascade functionality
|
|
1406
|
+
* Handles geo hierarchy building and cascade behavior
|
|
1407
|
+
*/
|
|
1408
|
+
const useGeoWidgetCascade = (options) => {
|
|
1409
|
+
const { config, dataSourceRequestHandler, values } = options;
|
|
1410
|
+
const dispatch = useDispatch();
|
|
1411
|
+
const eventBus = useWidgetEventBus();
|
|
1412
|
+
const widgetId = config['widget-id'];
|
|
1413
|
+
const geoConfig = config['widget-geo-config'];
|
|
1414
|
+
const dataSource = config['widget-data-source'];
|
|
1415
|
+
const dataPath = config['widget-data-path'];
|
|
1416
|
+
const valuesRef = useRef(values);
|
|
1417
|
+
const handlerRef = useRef(dataSourceRequestHandler);
|
|
1418
|
+
// Keep refs updated
|
|
1419
|
+
useEffect(() => {
|
|
1420
|
+
valuesRef.current = values;
|
|
1421
|
+
handlerRef.current = dataSourceRequestHandler;
|
|
1422
|
+
}, [values, dataSourceRequestHandler]);
|
|
1423
|
+
// Get current value and data source options
|
|
1424
|
+
const currentValue = useSelector((state) => {
|
|
1425
|
+
if (!dataPath) {
|
|
1426
|
+
return state.widget.values[widgetId];
|
|
1427
|
+
}
|
|
1428
|
+
return getWidgetValue(state.widget.values, dataPath, widgetId);
|
|
1429
|
+
});
|
|
1430
|
+
// Memoize selector to avoid returning new array reference
|
|
1431
|
+
const dataSourceOptions = useSelector((state) => state.widget.dataSources[widgetId] ?? EMPTY_DATA_SOURCE);
|
|
1432
|
+
useEffect(() => {
|
|
1433
|
+
if (!geoConfig || !eventBus || !dataSource || dataSource.type !== 'api') {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
const { level, isLastLevel, parentWidgetId } = geoConfig;
|
|
1437
|
+
// Listen to parent widget changes
|
|
1438
|
+
if (parentWidgetId) {
|
|
1439
|
+
const handleParentChange = async (event) => {
|
|
1440
|
+
if (event.widgetId !== parentWidgetId) {
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
// CRITICAL: Use a small delay to ensure Redux state has been updated
|
|
1444
|
+
// This prevents reading stale values from valuesRef
|
|
1445
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1446
|
+
const currentValues = valuesRef.current;
|
|
1447
|
+
const currentHandler = handlerRef.current;
|
|
1448
|
+
// CRITICAL: Get the parent value from Redux state, not from the event
|
|
1449
|
+
// The event.value might be stale, but Redux state is always current
|
|
1450
|
+
const parentValue = currentValues[parentWidgetId];
|
|
1451
|
+
// Remove this level and all below from hierarchy
|
|
1452
|
+
geoHierarchyBuilder.removeLevelAndBelow(level);
|
|
1453
|
+
// Clear this widget's value
|
|
1454
|
+
// CRITICAL: Only dispatch setValue for THIS widget, not for parent or other widgets
|
|
1455
|
+
// setWidgetValue returns the entire updated state, but we only want to update this widget
|
|
1456
|
+
if (dataPath) {
|
|
1457
|
+
const updatedValues = setWidgetValue(currentValues, dataPath, widgetId, undefined);
|
|
1458
|
+
// Only dispatch setValue for this widget's widgetId, not for parent or other widgets
|
|
1459
|
+
// This prevents accidentally overwriting the parent widget's value
|
|
1460
|
+
// The setWidgetValue function updates the nested structure, but we only want to
|
|
1461
|
+
// update the top-level widgetId key, not other keys that might be in updatedValues
|
|
1462
|
+
const newWidgetValue = updatedValues[widgetId];
|
|
1463
|
+
if (newWidgetValue !== undefined) {
|
|
1464
|
+
dispatch(setValue({ widgetId, value: newWidgetValue }));
|
|
1465
|
+
}
|
|
1466
|
+
else {
|
|
1467
|
+
// If widgetId is not in updatedValues, the value was set in a nested path
|
|
1468
|
+
// In this case, we need to use setValues to update the entire structure
|
|
1469
|
+
// But we need to be careful not to overwrite the parent widget's value
|
|
1470
|
+
// Only update keys that are related to this widget's dataPath
|
|
1471
|
+
const dataPathStr = typeof dataPath === 'string' ? dataPath : '';
|
|
1472
|
+
if (dataPathStr && !dataPathStr.startsWith(parentWidgetId + '.')) {
|
|
1473
|
+
// Only update if dataPath doesn't start with parentWidgetId
|
|
1474
|
+
// This ensures we don't accidentally overwrite the parent widget's value
|
|
1475
|
+
dispatch(setValue({ widgetId, value: undefined }));
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
else {
|
|
1480
|
+
dispatch(setValue({ widgetId, value: undefined }));
|
|
1481
|
+
}
|
|
1482
|
+
// Reload data source with new parent value
|
|
1483
|
+
// CRITICAL: Use parentValue from Redux, not event.value
|
|
1484
|
+
if (currentHandler && parentValue !== null && parentValue !== undefined) {
|
|
1485
|
+
try {
|
|
1486
|
+
// Merge the new parent value into current values for the API call
|
|
1487
|
+
// This ensures getApiDataSource can find the dependency value
|
|
1488
|
+
const updatedValues = {
|
|
1489
|
+
...currentValues,
|
|
1490
|
+
[parentWidgetId]: parentValue, // Use Redux value, not event.value
|
|
1491
|
+
};
|
|
1492
|
+
// Extract level_id from widget-geo-config.level
|
|
1493
|
+
const levelId = geoConfig.level;
|
|
1494
|
+
const data = await getApiDataSource(dataSource, updatedValues, currentHandler, levelId);
|
|
1495
|
+
// Transform to { value, label } format
|
|
1496
|
+
const valueKey = dataSource.valueKey || 'level_value_id';
|
|
1497
|
+
const labelKey = dataSource.labelKey || 'level_value_mnemonic';
|
|
1498
|
+
const transformed = transformDataSourceOptions(data, valueKey, labelKey);
|
|
1499
|
+
dispatch(setDataSource({ widgetId, data: transformed }));
|
|
1500
|
+
}
|
|
1501
|
+
catch (error) {
|
|
1502
|
+
console.error('Error reloading geo data source:', error);
|
|
1503
|
+
dispatch(setDataSource({ widgetId, data: [] }));
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
else {
|
|
1507
|
+
// If parent value is cleared, clear the data source
|
|
1508
|
+
dispatch(setDataSource({ widgetId, data: [] }));
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
const unsubscribe = eventBus.subscribe('widget:change', handleParentChange);
|
|
1512
|
+
return () => {
|
|
1513
|
+
unsubscribe();
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
}, [geoConfig, eventBus, dataSource, widgetId, dataPath, dispatch]);
|
|
1517
|
+
// Handle value changes to build hierarchy
|
|
1518
|
+
useEffect(() => {
|
|
1519
|
+
if (!geoConfig) {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
// Skip if value is empty/null (but allow 0 and false)
|
|
1523
|
+
if (currentValue === null || currentValue === undefined || currentValue === '') {
|
|
1524
|
+
// If value was cleared, remove this level and below from hierarchy
|
|
1525
|
+
const { level } = geoConfig;
|
|
1526
|
+
geoHierarchyBuilder.removeLevelAndBelow(level);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const { level, isLastLevel } = geoConfig;
|
|
1530
|
+
// For last level, check if hierarchy is already built to prevent endless loops
|
|
1531
|
+
if (isLastLevel && dataPath) {
|
|
1532
|
+
const currentHierarchy = getWidgetValue(valuesRef.current, dataPath, widgetId);
|
|
1533
|
+
// If hierarchy JSON is already set and matches current value, skip rebuilding
|
|
1534
|
+
if (currentHierarchy && typeof currentHierarchy === 'object' && currentHierarchy.geo_code_hierarchy_json) {
|
|
1535
|
+
// Check if the lowest level value matches
|
|
1536
|
+
const currentLevelValue = typeof currentValue === 'object'
|
|
1537
|
+
? (currentValue.level_value_id || currentValue.id || currentValue.value)
|
|
1538
|
+
: currentValue;
|
|
1539
|
+
if (currentHierarchy.geo_lowest_level_value_id === currentLevelValue) {
|
|
1540
|
+
return; // Hierarchy already built for this value, skip
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
// Extract level_value_id and level_value_mnemonic from current value
|
|
1545
|
+
// The value could be the ID itself or an object with id/name
|
|
1546
|
+
let level_value_id;
|
|
1547
|
+
let level_value_mnemonic;
|
|
1548
|
+
if (typeof currentValue === 'string' || typeof currentValue === 'number') {
|
|
1549
|
+
// Value is just the ID, need to find mnemonic from data source
|
|
1550
|
+
level_value_id = String(currentValue);
|
|
1551
|
+
// Try to get mnemonic from data source options
|
|
1552
|
+
const option = dataSourceOptions.find((opt) => opt.value === currentValue);
|
|
1553
|
+
level_value_mnemonic = option?.label || String(currentValue);
|
|
1554
|
+
}
|
|
1555
|
+
else if (currentValue && typeof currentValue === 'object') {
|
|
1556
|
+
level_value_id = currentValue.level_value_id || currentValue.id || currentValue.value;
|
|
1557
|
+
level_value_mnemonic = currentValue.level_value_mnemonic || currentValue.name || currentValue.label;
|
|
1558
|
+
}
|
|
1559
|
+
else {
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
// When a widget's own value changes, remove this level and all below from hierarchy first
|
|
1563
|
+
// This ensures that when level 1 changes, we clear the hierarchy and rebuild from scratch
|
|
1564
|
+
// The addLevel method already handles removing existing levels, but we explicitly clear to be safe
|
|
1565
|
+
geoHierarchyBuilder.removeLevelAndBelow(level);
|
|
1566
|
+
// Add level to hierarchy
|
|
1567
|
+
geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic);
|
|
1568
|
+
// If this is the last level, build and store hierarchy JSON
|
|
1569
|
+
if (isLastLevel && dataPath) {
|
|
1570
|
+
const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson();
|
|
1571
|
+
if (hierarchyJson) {
|
|
1572
|
+
// Store both geo_lowest_level_value_id and geo_code_hierarchy_json
|
|
1573
|
+
const updatedValues = setWidgetValue(valuesRef.current, dataPath, widgetId, {
|
|
1574
|
+
geo_lowest_level_value_id: hierarchyJson.geo_lowest_level_value_id,
|
|
1575
|
+
geo_code_hierarchy_json: hierarchyJson.geo_code_hierarchy_json,
|
|
1576
|
+
});
|
|
1577
|
+
Object.entries(updatedValues).forEach(([key, value]) => {
|
|
1578
|
+
dispatch(setValue({ widgetId: key, value }));
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}, [geoConfig, currentValue, widgetId, dataPath, dispatch, dataSourceOptions]);
|
|
1583
|
+
};
|
|
1584
|
+
|
|
950
1585
|
var jsxRuntime = {exports: {}};
|
|
951
1586
|
|
|
952
1587
|
var reactJsxRuntime_production = {};
|
|
@@ -1449,42 +2084,208 @@ class WidgetRegistry {
|
|
|
1449
2084
|
// Singleton instance
|
|
1450
2085
|
const widgetRegistry = new WidgetRegistry();
|
|
1451
2086
|
|
|
2087
|
+
/**
|
|
2088
|
+
* Widget Event Bus
|
|
2089
|
+
* Provides a publish/subscribe mechanism for widget communication
|
|
2090
|
+
* Supports debounce and throttle for performance optimization
|
|
2091
|
+
*/
|
|
2092
|
+
class WidgetEventBus {
|
|
2093
|
+
constructor() {
|
|
2094
|
+
this.subscriptions = new Map();
|
|
2095
|
+
this.throttleTimers = new Map();
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Subscribe to an event type
|
|
2099
|
+
* @param eventType The type of event to listen to
|
|
2100
|
+
* @param handler The handler function
|
|
2101
|
+
* @param options Debounce/throttle options
|
|
2102
|
+
* @returns Unsubscribe function
|
|
2103
|
+
*/
|
|
2104
|
+
subscribe(eventType, handler, options) {
|
|
2105
|
+
if (!this.subscriptions.has(eventType)) {
|
|
2106
|
+
this.subscriptions.set(eventType, new Map());
|
|
2107
|
+
}
|
|
2108
|
+
const eventSubscriptions = this.subscriptions.get(eventType);
|
|
2109
|
+
const subscriptionId = `${Date.now()}-${Math.random()}`;
|
|
2110
|
+
const subscription = {
|
|
2111
|
+
handler,
|
|
2112
|
+
debounce: options?.debounce,
|
|
2113
|
+
throttle: options?.throttle,
|
|
2114
|
+
};
|
|
2115
|
+
if (!eventSubscriptions.has(subscriptionId)) {
|
|
2116
|
+
eventSubscriptions.set(subscriptionId, []);
|
|
2117
|
+
}
|
|
2118
|
+
eventSubscriptions.get(subscriptionId).push(subscription);
|
|
2119
|
+
// Return unsubscribe function
|
|
2120
|
+
return () => {
|
|
2121
|
+
const subs = eventSubscriptions.get(subscriptionId);
|
|
2122
|
+
if (subs) {
|
|
2123
|
+
const index = subs.indexOf(subscription);
|
|
2124
|
+
if (index > -1) {
|
|
2125
|
+
subs.splice(index, 1);
|
|
2126
|
+
if (subs.length === 0) {
|
|
2127
|
+
eventSubscriptions.delete(subscriptionId);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Publish an event
|
|
2135
|
+
* @param event The event to publish
|
|
2136
|
+
*/
|
|
2137
|
+
publish(event) {
|
|
2138
|
+
const eventSubscriptions = this.subscriptions.get(event.type);
|
|
2139
|
+
if (!eventSubscriptions) {
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
eventSubscriptions.forEach((subscriptions) => {
|
|
2143
|
+
subscriptions.forEach((subscription) => {
|
|
2144
|
+
this.executeHandler(subscription, event);
|
|
2145
|
+
});
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Execute handler with debounce/throttle support
|
|
2150
|
+
*/
|
|
2151
|
+
executeHandler(subscription, event) {
|
|
2152
|
+
const { handler, debounce, throttle } = subscription;
|
|
2153
|
+
// Clear existing debounce timer if any
|
|
2154
|
+
if (subscription.debounceTimer) {
|
|
2155
|
+
clearTimeout(subscription.debounceTimer);
|
|
2156
|
+
}
|
|
2157
|
+
// Handle throttle
|
|
2158
|
+
if (throttle && throttle > 0) {
|
|
2159
|
+
const throttleKey = `${event.type}-${event.widgetId}`;
|
|
2160
|
+
const now = Date.now();
|
|
2161
|
+
const lastCallTime = subscription.lastCallTime || 0;
|
|
2162
|
+
if (now - lastCallTime < throttle) {
|
|
2163
|
+
// Still in throttle period, schedule for later
|
|
2164
|
+
if (this.throttleTimers.has(throttleKey)) {
|
|
2165
|
+
return; // Already scheduled
|
|
2166
|
+
}
|
|
2167
|
+
const remainingTime = throttle - (now - lastCallTime);
|
|
2168
|
+
const timer = setTimeout(() => {
|
|
2169
|
+
subscription.lastCallTime = Date.now();
|
|
2170
|
+
handler(event);
|
|
2171
|
+
this.throttleTimers.delete(throttleKey);
|
|
2172
|
+
}, remainingTime);
|
|
2173
|
+
this.throttleTimers.set(throttleKey, timer);
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
subscription.lastCallTime = now;
|
|
2177
|
+
}
|
|
2178
|
+
// Handle debounce
|
|
2179
|
+
if (debounce && debounce > 0) {
|
|
2180
|
+
subscription.debounceTimer = setTimeout(() => {
|
|
2181
|
+
handler(event);
|
|
2182
|
+
subscription.debounceTimer = undefined;
|
|
2183
|
+
}, debounce);
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
// Execute immediately if no debounce/throttle
|
|
2187
|
+
handler(event);
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Clear all subscriptions
|
|
2191
|
+
*/
|
|
2192
|
+
clear() {
|
|
2193
|
+
// Clear all timers
|
|
2194
|
+
this.subscriptions.forEach((eventSubscriptions) => {
|
|
2195
|
+
eventSubscriptions.forEach((subscriptions) => {
|
|
2196
|
+
subscriptions.forEach((sub) => {
|
|
2197
|
+
if (sub.debounceTimer) {
|
|
2198
|
+
clearTimeout(sub.debounceTimer);
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
2201
|
+
});
|
|
2202
|
+
});
|
|
2203
|
+
this.throttleTimers.forEach((timer) => clearTimeout(timer));
|
|
2204
|
+
this.throttleTimers.clear();
|
|
2205
|
+
this.subscriptions.clear();
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
1452
2209
|
const WidgetContext = createContext({
|
|
1453
|
-
|
|
2210
|
+
dataSourceRequestHandler: undefined,
|
|
1454
2211
|
schemaData: undefined,
|
|
1455
2212
|
translate: undefined,
|
|
1456
2213
|
});
|
|
1457
2214
|
const useWidgetContext = () => {
|
|
1458
2215
|
return useContext(WidgetContext);
|
|
1459
2216
|
};
|
|
1460
|
-
const WidgetProvider = ({ store,
|
|
2217
|
+
const WidgetProvider = ({ store, dataSourceRequestHandler, schemaData, translate, children, }) => {
|
|
1461
2218
|
const widgetStore = useMemo(() => store || createWidgetStore(), [store]);
|
|
1462
|
-
//
|
|
2219
|
+
// Create event bus instance (one per provider)
|
|
2220
|
+
const eventBus = useMemo(() => new WidgetEventBus(), []);
|
|
2221
|
+
// Memoize context value to prevent unnecessary re-renders
|
|
2222
|
+
const contextValue = useMemo(() => ({
|
|
2223
|
+
dataSourceRequestHandler,
|
|
2224
|
+
schemaData,
|
|
2225
|
+
translate,
|
|
2226
|
+
}), [dataSourceRequestHandler, schemaData, translate]);
|
|
2227
|
+
// Warn if dataSourceRequestHandler is missing (will cause issues with API data sources)
|
|
2228
|
+
useEffect(() => {
|
|
2229
|
+
if (!dataSourceRequestHandler) {
|
|
2230
|
+
console.warn('[WidgetProvider] dataSourceRequestHandler is not provided. ' +
|
|
2231
|
+
'Widgets with API data sources will not be able to load data. ' +
|
|
2232
|
+
'Please provide dataSourceRequestHandler prop to WidgetProvider.');
|
|
2233
|
+
}
|
|
2234
|
+
}, [dataSourceRequestHandler]);
|
|
2235
|
+
// Cleanup event bus on unmount
|
|
2236
|
+
useEffect(() => {
|
|
2237
|
+
return () => {
|
|
2238
|
+
eventBus.clear();
|
|
2239
|
+
};
|
|
2240
|
+
}, [eventBus]);
|
|
2241
|
+
// Initialize schemaData to Redux store (only on mount)
|
|
2242
|
+
// This prevents overwriting user changes when schemaData prop changes
|
|
1463
2243
|
useEffect(() => {
|
|
1464
2244
|
if (schemaData) {
|
|
1465
2245
|
widgetStore.dispatch(setValues(schemaData));
|
|
1466
2246
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
2247
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2248
|
+
}, []); // Only run on mount
|
|
2249
|
+
const content = (jsxRuntimeExports.jsx(Provider, { store: widgetStore, children: jsxRuntimeExports.jsx(WidgetContext.Provider, { value: contextValue, children: jsxRuntimeExports.jsx(WidgetEventBusContext.Provider, { value: eventBus, children: children }) }) }));
|
|
1469
2250
|
return content;
|
|
1470
2251
|
};
|
|
1471
2252
|
|
|
1472
|
-
const WidgetRenderer = ({ config,
|
|
2253
|
+
const WidgetRenderer = ({ config, dataSourceRequestHandler: propDataSourceRequestHandler, schemaData: propSchemaData, onValueChange, defaultComponent, }) => {
|
|
1473
2254
|
// Use context values as fallback
|
|
1474
2255
|
const context = useWidgetContext();
|
|
1475
|
-
const
|
|
2256
|
+
const dataSourceRequestHandler = propDataSourceRequestHandler || context.dataSourceRequestHandler;
|
|
1476
2257
|
const schemaData = propSchemaData || context.schemaData;
|
|
2258
|
+
if (!dataSourceRequestHandler && config['widget-data-source']?.type === 'api') {
|
|
2259
|
+
console.error(`[WidgetRenderer] dataSourceRequestHandler is required for widget ${config['widget-id']} with API data source`);
|
|
2260
|
+
}
|
|
2261
|
+
// Get values from Redux for cascade hooks
|
|
2262
|
+
const values = useSelector((state) => state.widget.values);
|
|
1477
2263
|
const widgetContext = useBaseWidget({
|
|
1478
2264
|
config,
|
|
1479
|
-
|
|
2265
|
+
dataSourceRequestHandler,
|
|
1480
2266
|
schemaData,
|
|
1481
2267
|
onValueChange,
|
|
1482
2268
|
});
|
|
2269
|
+
// Apply cascade hooks if configured (only if handler is available)
|
|
2270
|
+
if (dataSourceRequestHandler) {
|
|
2271
|
+
useWidgetCascade({
|
|
2272
|
+
config,
|
|
2273
|
+
dataSourceRequestHandler,
|
|
2274
|
+
values,
|
|
2275
|
+
});
|
|
2276
|
+
useGeoWidgetCascade({
|
|
2277
|
+
config,
|
|
2278
|
+
dataSourceRequestHandler,
|
|
2279
|
+
values,
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
1483
2282
|
// Don't render if not visible
|
|
1484
2283
|
if (!widgetContext.isVisible) {
|
|
1485
2284
|
return null;
|
|
1486
2285
|
}
|
|
1487
2286
|
// Render widget using registry
|
|
2287
|
+
// Don't use key based on readonly state - it causes remounting which resets userHasSetValueRef
|
|
2288
|
+
// The readonly state is already handled in the widget components themselves
|
|
1488
2289
|
return (jsxRuntimeExports.jsx("div", { className: "widget-container", "data-widget-id": widgetContext.widgetId, style: { marginBottom: 0 }, children: widgetRegistry.render(config, widgetContext, defaultComponent) }));
|
|
1489
2290
|
};
|
|
1490
2291
|
|
|
@@ -1587,7 +2388,7 @@ const useWidgetTranslation = () => {
|
|
|
1587
2388
|
* - Nested panels (for layout composition)
|
|
1588
2389
|
* - Widgets (for actual form inputs/controls)
|
|
1589
2390
|
*/
|
|
1590
|
-
const PanelRenderer = ({ panel,
|
|
2391
|
+
const PanelRenderer = ({ panel, dataSourceRequestHandler, schemaData, onValueChange, isEditMode = false, }) => {
|
|
1591
2392
|
const { translateConfig } = useWidgetTranslation();
|
|
1592
2393
|
const orientation = panel['panel-orientation'] || 'vertical';
|
|
1593
2394
|
const nestedPanels = panel.panels || [];
|
|
@@ -1679,7 +2480,7 @@ const PanelRenderer = ({ panel, apiAdapter, schemaData, onValueChange, isEditMod
|
|
|
1679
2480
|
}
|
|
1680
2481
|
};
|
|
1681
2482
|
const nestedPanelStyle = getNestedPanelStyle();
|
|
1682
|
-
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,
|
|
2483
|
+
return (jsxRuntimeExports.jsx(React.Fragment, { children: jsxRuntimeExports.jsxs("div", { className: orientation === 'horizontal' ? 'min-w-200 relative' : 'w-full', style: nestedPanelStyle, "data-panel-column-span": columnSpan || undefined, children: [jsxRuntimeExports.jsx(PanelRenderer, { panel: nestedPanel, dataSourceRequestHandler: dataSourceRequestHandler, schemaData: schemaData, onValueChange: onValueChange, isEditMode: isEditMode }), orientation === 'horizontal' && !isLastPanel && (jsxRuntimeExports.jsx("div", { style: {
|
|
1683
2484
|
position: 'absolute',
|
|
1684
2485
|
right: 0,
|
|
1685
2486
|
top: 0,
|
|
@@ -1687,7 +2488,11 @@ const PanelRenderer = ({ panel, apiAdapter, schemaData, onValueChange, isEditMod
|
|
|
1687
2488
|
width: '1px',
|
|
1688
2489
|
backgroundColor: isEditMode ? '#F2BA1A' : '#D1D5DB',
|
|
1689
2490
|
} }))] }) }, nestedPanel['panel-id'] || `panel-${index}`));
|
|
1690
|
-
}), widgets.map((widgetConfig, index) =>
|
|
2491
|
+
}), widgets.map((widgetConfig, index) => {
|
|
2492
|
+
// Don't use readonly state in key - it causes remounting which resets userHasSetValueRef
|
|
2493
|
+
// The readonly state is already handled in the widget components themselves
|
|
2494
|
+
return (jsxRuntimeExports.jsx(WidgetRenderer, { config: widgetConfig, dataSourceRequestHandler: dataSourceRequestHandler, schemaData: schemaData, onValueChange: onValueChange }, widgetConfig['widget-id'] || `widget-${index}`));
|
|
2495
|
+
})] }));
|
|
1691
2496
|
// Render panel without card styling (panel-type removed from schema)
|
|
1692
2497
|
// Vertical panels will have constrained width via CSS in SectionRenderer
|
|
1693
2498
|
// Horizontal panels take full width
|
|
@@ -2387,11 +3192,13 @@ const namespaceSectionConfig = (section, namespace) => {
|
|
|
2387
3192
|
* - Panels wrap when they exceed available width
|
|
2388
3193
|
* - Sections can sit side-by-side if there's space
|
|
2389
3194
|
*/
|
|
2390
|
-
const SectionRenderer = ({ section,
|
|
3195
|
+
const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequestHandler, schemaData, onValueChange, gridColumnSpan, onSectionSave, hideEditButton = false, mode = 'RegistryView', namespace, }) => {
|
|
2391
3196
|
const { translateConfig, translate } = useWidgetTranslation();
|
|
2392
|
-
const { schemaData: contextSchemaData } = useWidgetContext();
|
|
3197
|
+
const { schemaData: contextSchemaData, dataSourceRequestHandler: contextDataSourceRequestHandler } = useWidgetContext();
|
|
2393
3198
|
const store = useStore();
|
|
2394
3199
|
const dispatch = useDispatch();
|
|
3200
|
+
// Use prop handler if provided, otherwise fall back to context
|
|
3201
|
+
const dataSourceRequestHandler = propDataSourceRequestHandler || contextDataSourceRequestHandler;
|
|
2395
3202
|
// Get CRView data from schemaData (prefer prop over context, then Redux store)
|
|
2396
3203
|
const currentSchemaData = schemaData || contextSchemaData || {};
|
|
2397
3204
|
const storeValues = useSelector((state) => state.widget?.values || {});
|
|
@@ -2450,10 +3257,6 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
|
|
|
2450
3257
|
approvedBy: getValueByPath(dataSource, 'approvedBy') || getValueByPath(dataSource, 'approved_by'),
|
|
2451
3258
|
approvedDate: getValueByPath(dataSource, 'approvedDate') || getValueByPath(dataSource, 'approved_date'),
|
|
2452
3259
|
};
|
|
2453
|
-
// Debug logging (can be removed in production)
|
|
2454
|
-
if (mode === 'CRView') {
|
|
2455
|
-
console.log('CRView Data Source:', { dataSource, result, currentSchemaData, storeValues });
|
|
2456
|
-
}
|
|
2457
3260
|
return result;
|
|
2458
3261
|
}, [mode, currentSchemaData, storeValues]);
|
|
2459
3262
|
// Use namespaced section for rendering
|
|
@@ -2577,16 +3380,19 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
|
|
|
2577
3380
|
const modifiedPanel = {
|
|
2578
3381
|
...panel,
|
|
2579
3382
|
panels: panel.panels ? makePanelsEditable(panel.panels, editable) : undefined,
|
|
2580
|
-
widgets: panel.widgets?.map(widget =>
|
|
2581
|
-
...widget,
|
|
3383
|
+
widgets: panel.widgets?.map(widget => {
|
|
2582
3384
|
// When NOT in edit mode (editable = false), set all widgets to readonly
|
|
2583
3385
|
// When in edit mode (editable = true):
|
|
2584
3386
|
// - If section-editable is true, force widgets to be editable (override widget-readonly)
|
|
2585
3387
|
// - Otherwise, respect original readonly setting
|
|
2586
|
-
|
|
3388
|
+
const newReadonly = editable
|
|
2587
3389
|
? (sectionEditable ? false : (widget['widget-readonly'] || false))
|
|
2588
|
-
: true
|
|
2589
|
-
|
|
3390
|
+
: true;
|
|
3391
|
+
return {
|
|
3392
|
+
...widget,
|
|
3393
|
+
'widget-readonly': newReadonly,
|
|
3394
|
+
};
|
|
3395
|
+
}),
|
|
2590
3396
|
};
|
|
2591
3397
|
return modifiedPanel;
|
|
2592
3398
|
});
|
|
@@ -2667,7 +3473,7 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
|
|
|
2667
3473
|
overflowY: 'auto',
|
|
2668
3474
|
}, children: [sectionToRender['section-title'] && (jsxRuntimeExports.jsx("h2", { className: "text-xl font-semibold mb-4", style: { fontFamily: 'Roboto, sans-serif', marginTop: '35px' }, children: translateConfig(sectionToRender['section-title']) })), jsxRuntimeExports.jsxs("div", { id: editGridId, className: "section-panels", children: [editableSection.panels.map((panel, index) => {
|
|
2669
3475
|
const isLastPanel = index === editableSection.panels.length - 1;
|
|
2670
|
-
return (jsxRuntimeExports.jsx("div", { className: `panel-wrapper ${isLastPanel ? 'last-panel-wrapper' : ''}`, children: jsxRuntimeExports.jsx(PanelRenderer, { panel: panel,
|
|
3476
|
+
return (jsxRuntimeExports.jsx("div", { className: `panel-wrapper ${isLastPanel ? 'last-panel-wrapper' : ''}`, children: jsxRuntimeExports.jsx(PanelRenderer, { panel: panel, dataSourceRequestHandler: dataSourceRequestHandler, schemaData: namespacedSchemaData, onValueChange: onValueChange, isEditMode: true }) }, panel['panel-id'] || `section-panel-${index}`));
|
|
2671
3477
|
}), hasSupportingDocuments && (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("hr", { className: "my-4 w-full", style: { height: '1px', backgroundColor: '#F2BA1A', border: 'none' } }), jsxRuntimeExports.jsxs("div", { className: "supporting-documents-container", children: [jsxRuntimeExports.jsxs("button", { type: "button", onClick: () => setIsDocumentsExpanded(!isDocumentsExpanded), className: "supporting-documents-title-button w-full flex items-center text-left", children: [jsxRuntimeExports.jsx("span", { className: "font-semibold", style: { fontFamily: 'Roboto, sans-serif', fontSize: '16px' }, children: translate('common.supportedDocuments') || 'Supported Documents' }), jsxRuntimeExports.jsx("svg", { className: `w-5 h-5 text-[#ED7C22] transition-transform ml-2 ${isDocumentsExpanded ? 'rotate-180' : ''}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntimeExports.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }) })] }), isDocumentsExpanded && (jsxRuntimeExports.jsx("div", { className: "supporting-documents-grid mt-4", children: supportingDocuments.map((doc, index) => {
|
|
2672
3478
|
const docConfig = createDocumentWidgetConfig(doc, sectionId, index);
|
|
2673
3479
|
return (jsxRuntimeExports.jsx("div", { className: "supporting-document-item", children: jsxRuntimeExports.jsx(FileInputWidget, { config: docConfig }) }, `${sectionId}-doc-${index}`));
|
|
@@ -3017,7 +3823,7 @@ const SectionRenderer = ({ section, apiAdapter, schemaData, onValueChange, gridC
|
|
|
3017
3823
|
minHeight: 'auto',
|
|
3018
3824
|
height: 'auto'
|
|
3019
3825
|
}),
|
|
3020
|
-
}, 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,
|
|
3826
|
+
}, children: [sectionToRender['section-title'] && (jsxRuntimeExports.jsx("h2", { className: "text-xl font-semibold mb-4", style: { marginTop: '35px' }, children: translateConfig(sectionToRender['section-title']) })), jsxRuntimeExports.jsxs("div", { id: gridId, className: "section-panels", style: mode === 'RegistryView' && hideEditButton ? { paddingBottom: '40px' } : {}, children: [editableSection.panels.map((panel, index) => (jsxRuntimeExports.jsx("div", { className: "panel-wrapper", children: jsxRuntimeExports.jsx(PanelRenderer, { panel: panel, dataSourceRequestHandler: dataSourceRequestHandler, schemaData: namespacedSchemaData, onValueChange: onValueChange }) }, panel['panel-id'] || `section-panel-${index}`))), mode === 'CRView' && crViewData && (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("hr", { className: "border-gray-300 w-full", style: { height: '1px', marginTop: '20px', marginBottom: '0px' } }), jsxRuntimeExports.jsxs("div", { className: "cr-view-container", style: {
|
|
3021
3827
|
marginTop: '20px',
|
|
3022
3828
|
paddingBottom: '30px',
|
|
3023
3829
|
display: 'flex',
|
|
@@ -3153,7 +3959,18 @@ const countVerticalPanels = (panels) => {
|
|
|
3153
3959
|
* - All sections align to the same grid, ensuring right-side alignment
|
|
3154
3960
|
* - Handles nested structure: multiple horizontal panels, each with multiple vertical panels
|
|
3155
3961
|
*/
|
|
3156
|
-
const SectionsContainer = ({ sections,
|
|
3962
|
+
const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceRequestHandler, schemaData, onValueChange, className = '', onSectionSave, hideEditButton = false, mode = 'RegistryView', namespace, }) => {
|
|
3963
|
+
// Get dataSourceRequestHandler from context if not provided as prop
|
|
3964
|
+
const { dataSourceRequestHandler: contextDataSourceRequestHandler } = useWidgetContext();
|
|
3965
|
+
const dataSourceRequestHandler = propDataSourceRequestHandler || contextDataSourceRequestHandler;
|
|
3966
|
+
// Warn if dataSourceRequestHandler is missing
|
|
3967
|
+
useEffect(() => {
|
|
3968
|
+
if (!dataSourceRequestHandler) {
|
|
3969
|
+
console.warn('[SectionsContainer] ⚠️ dataSourceRequestHandler is not provided. ' +
|
|
3970
|
+
'Sections with widgets that have API data sources will not be able to load data. ' +
|
|
3971
|
+
'Please provide dataSourceRequestHandler prop to SectionsContainer or WidgetProvider.');
|
|
3972
|
+
}
|
|
3973
|
+
}, [dataSourceRequestHandler]);
|
|
3157
3974
|
// Find the maximum number of vertical panels across all sections
|
|
3158
3975
|
// This determines the grid size (minimum 3 columns)
|
|
3159
3976
|
// Also account for table widgets and their explicit column spans
|
|
@@ -3202,7 +4019,7 @@ const SectionsContainer = ({ sections, apiAdapter, schemaData, onValueChange, cl
|
|
|
3202
4019
|
: undefined;
|
|
3203
4020
|
// Check if section has explicit column span
|
|
3204
4021
|
if (section['section-column-span']) {
|
|
3205
|
-
return (jsxRuntimeExports.jsx(SectionRenderer, { section: section,
|
|
4022
|
+
return (jsxRuntimeExports.jsx(SectionRenderer, { section: section, dataSourceRequestHandler: dataSourceRequestHandler, schemaData: schemaData, onValueChange: onValueChange, gridColumnSpan: section['section-column-span'], onSectionSave: onSectionSave, hideEditButton: hideEditButton, mode: mode, namespace: sectionNamespace }, section['section-id']));
|
|
3206
4023
|
}
|
|
3207
4024
|
const verticalPanelsCount = countVerticalPanels(section.panels);
|
|
3208
4025
|
const tableWidgetColumnSpan = getTableWidgetColumnSpan(section.panels);
|
|
@@ -3213,16 +4030,1676 @@ const SectionsContainer = ({ sections, apiAdapter, schemaData, onValueChange, cl
|
|
|
3213
4030
|
const columnSpan = tableWidgetColumnSpan !== null
|
|
3214
4031
|
? tableWidgetColumnSpan
|
|
3215
4032
|
: (containsTable ? Math.max(verticalPanelsCount, 2) : verticalPanelsCount);
|
|
3216
|
-
return (jsxRuntimeExports.jsx(SectionRenderer, { section: section,
|
|
4033
|
+
return (jsxRuntimeExports.jsx(SectionRenderer, { section: section, dataSourceRequestHandler: dataSourceRequestHandler, schemaData: schemaData, onValueChange: onValueChange, gridColumnSpan: columnSpan, onSectionSave: onSectionSave, hideEditButton: hideEditButton, mode: mode, namespace: sectionNamespace }, section['section-id']));
|
|
3217
4034
|
}) })] }));
|
|
3218
4035
|
};
|
|
3219
4036
|
|
|
3220
4037
|
/**
|
|
3221
|
-
*
|
|
4038
|
+
* JSON Schema definitions for Section Builder validation
|
|
4039
|
+
* These schemas are used by json-edit-react to provide validation and constraints
|
|
3222
4040
|
*/
|
|
3223
|
-
const
|
|
3224
|
-
|
|
3225
|
-
|
|
4041
|
+
const WIDGET_TYPES = [
|
|
4042
|
+
'text',
|
|
4043
|
+
'textarea',
|
|
4044
|
+
'number',
|
|
4045
|
+
'boolean',
|
|
4046
|
+
'date',
|
|
4047
|
+
'datetime',
|
|
4048
|
+
'select',
|
|
4049
|
+
'radio',
|
|
4050
|
+
'checkbox',
|
|
4051
|
+
'file',
|
|
4052
|
+
'simple-table',
|
|
4053
|
+
'table',
|
|
4054
|
+
'array-widget',
|
|
4055
|
+
'iterable-accordion',
|
|
4056
|
+
'phone',
|
|
4057
|
+
'currency',
|
|
4058
|
+
'display',
|
|
4059
|
+
'profile',
|
|
4060
|
+
];
|
|
4061
|
+
const ORIENTATIONS = ['horizontal', 'vertical'];
|
|
4062
|
+
const CONDITION_OPERATORS = [
|
|
4063
|
+
'equals',
|
|
4064
|
+
'notEquals',
|
|
4065
|
+
'notEmpty',
|
|
4066
|
+
'empty',
|
|
4067
|
+
'greaterThan',
|
|
4068
|
+
'lessThan',
|
|
4069
|
+
'contains',
|
|
4070
|
+
'notContains',
|
|
4071
|
+
];
|
|
4072
|
+
const DATA_SOURCE_TYPES = ['static', 'api', 'schema'];
|
|
4073
|
+
const VALIDATION_TYPES = ['email', 'phone', 'url'];
|
|
4074
|
+
const CHARACTER_TYPES = [
|
|
4075
|
+
'any',
|
|
4076
|
+
'alphabetic',
|
|
4077
|
+
'alphanumeric',
|
|
4078
|
+
'numeric',
|
|
4079
|
+
'numeric-decimal',
|
|
4080
|
+
'custom',
|
|
4081
|
+
];
|
|
4082
|
+
const CASE_CONTROLS = ['none', 'lowercase', 'uppercase', 'capitalize'];
|
|
4083
|
+
const NUMERIC_TYPES = ['integer', 'decimal'];
|
|
4084
|
+
const BOOLEAN_REPRESENTATIONS = ['true-false', 'yes-no', 'on-off', 'custom'];
|
|
4085
|
+
const BOOLEAN_CONTROL_TYPES = ['checkbox', 'radio', 'toggle'];
|
|
4086
|
+
|
|
4087
|
+
// Inject styles to constrain json-edit-react container
|
|
4088
|
+
if (typeof document !== 'undefined') {
|
|
4089
|
+
const styleId = 'json-editor-constraints';
|
|
4090
|
+
if (!document.getElementById(styleId)) {
|
|
4091
|
+
const style = document.createElement('style');
|
|
4092
|
+
style.id = styleId;
|
|
4093
|
+
style.textContent = `
|
|
4094
|
+
.json-editor-scroll-container {
|
|
4095
|
+
display: flex !important;
|
|
4096
|
+
flex-direction: column !important;
|
|
4097
|
+
height: 100% !important;
|
|
4098
|
+
max-height: 100% !important;
|
|
4099
|
+
overflow: auto !important;
|
|
4100
|
+
}
|
|
4101
|
+
.json-editor-scroll-container .jer-editor-container {
|
|
4102
|
+
max-width: 100% !important;
|
|
4103
|
+
width: 100% !important;
|
|
4104
|
+
height: auto !important;
|
|
4105
|
+
max-height: none !important;
|
|
4106
|
+
flex-shrink: 0 !important;
|
|
4107
|
+
background-color: white !important;
|
|
4108
|
+
}
|
|
4109
|
+
.json-editor-scroll-container .jer-component {
|
|
4110
|
+
width: 100% !important;
|
|
4111
|
+
}
|
|
4112
|
+
`;
|
|
4113
|
+
document.head.appendChild(style);
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
/**
|
|
4117
|
+
* JSON Editor Panel - Left side of Section Builder
|
|
4118
|
+
*/
|
|
4119
|
+
const JSONEditorPanel = ({ section, onChange, onReset, onSelectNode, context = 'section', }) => {
|
|
4120
|
+
const [jsonData, setJsonData] = useState(section);
|
|
4121
|
+
const [validationErrors, setValidationErrors] = useState([]);
|
|
4122
|
+
const [rawJsonView, setRawJsonView] = useState(false);
|
|
4123
|
+
const [rawJsonText, setRawJsonText] = useState('');
|
|
4124
|
+
const [showPreview, setShowPreview] = useState(false);
|
|
4125
|
+
const [editorKey, setEditorKey] = useState(0); // Key to force JsonEditor re-render on reset
|
|
4126
|
+
// Store the original section when component mounts or section prop changes
|
|
4127
|
+
const originalSectionRef = useRef(section);
|
|
4128
|
+
// Get WidgetProvider context for preview modal (optional - may not be available)
|
|
4129
|
+
let widgetContext;
|
|
4130
|
+
try {
|
|
4131
|
+
widgetContext = useWidgetContext();
|
|
4132
|
+
}
|
|
4133
|
+
catch {
|
|
4134
|
+
widgetContext = {
|
|
4135
|
+
dataSourceRequestHandler: undefined,
|
|
4136
|
+
schemaData: undefined,
|
|
4137
|
+
translate: undefined,
|
|
4138
|
+
};
|
|
4139
|
+
}
|
|
4140
|
+
// Create a store for the preview modal if we're not in a Provider
|
|
4141
|
+
// This ensures SectionRenderer has access to Redux
|
|
4142
|
+
const previewStore = useMemo(() => createWidgetStore(), []);
|
|
4143
|
+
// Track if this is the initial mount
|
|
4144
|
+
const isInitialMount = useRef(true);
|
|
4145
|
+
useEffect(() => {
|
|
4146
|
+
// Only update original section on initial mount (when page loads)
|
|
4147
|
+
// This ensures reset works until save is clicked
|
|
4148
|
+
// Don't update original when user makes edits (those come through onChange)
|
|
4149
|
+
if (isInitialMount.current) {
|
|
4150
|
+
originalSectionRef.current = JSON.parse(JSON.stringify(section)); // Deep copy
|
|
4151
|
+
isInitialMount.current = false;
|
|
4152
|
+
}
|
|
4153
|
+
// Always sync the display with the section prop (for external updates like reset from parent)
|
|
4154
|
+
setJsonData(section);
|
|
4155
|
+
setRawJsonText(JSON.stringify(section, null, 2));
|
|
4156
|
+
// Force JsonEditor to update when section prop changes (e.g., from parent reset)
|
|
4157
|
+
setEditorKey(prev => prev + 1);
|
|
4158
|
+
}, [section]);
|
|
4159
|
+
// Reset to original section
|
|
4160
|
+
const handleReset = useCallback(() => {
|
|
4161
|
+
// If parent provides onReset, use it (this will reset both JSON editor and visual builder)
|
|
4162
|
+
if (onReset) {
|
|
4163
|
+
onReset();
|
|
4164
|
+
// Also force JsonEditor to remount to ensure it picks up the reset
|
|
4165
|
+
setEditorKey(prev => prev + 1);
|
|
4166
|
+
return;
|
|
4167
|
+
}
|
|
4168
|
+
// Fallback: reset only this panel (for standalone usage)
|
|
4169
|
+
const original = JSON.parse(JSON.stringify(originalSectionRef.current)); // Deep copy to ensure new reference
|
|
4170
|
+
// Update state immediately
|
|
4171
|
+
setJsonData(original);
|
|
4172
|
+
setRawJsonText(JSON.stringify(original, null, 2));
|
|
4173
|
+
// Force JsonEditor to completely remount by changing key
|
|
4174
|
+
// This is critical because json-edit-react maintains internal state that doesn't sync with props
|
|
4175
|
+
setEditorKey(prev => prev + 1);
|
|
4176
|
+
// Notify parent
|
|
4177
|
+
onChange(original);
|
|
4178
|
+
}, [onChange, onReset]);
|
|
4179
|
+
// Handle Escape key to close preview
|
|
4180
|
+
useEffect(() => {
|
|
4181
|
+
if (!showPreview)
|
|
4182
|
+
return;
|
|
4183
|
+
const handleEscape = (e) => {
|
|
4184
|
+
if (e.key === 'Escape') {
|
|
4185
|
+
setShowPreview(false);
|
|
4186
|
+
}
|
|
4187
|
+
};
|
|
4188
|
+
window.addEventListener('keydown', handleEscape);
|
|
4189
|
+
return () => window.removeEventListener('keydown', handleEscape);
|
|
4190
|
+
}, [showPreview]);
|
|
4191
|
+
// Handle clicks in JSON editor to select corresponding node in visual builder
|
|
4192
|
+
useEffect(() => {
|
|
4193
|
+
if (!onSelectNode || rawJsonView)
|
|
4194
|
+
return; // Only work in tree view, not raw JSON view
|
|
4195
|
+
const handleJsonEditorClick = (e) => {
|
|
4196
|
+
const mouseEvent = e;
|
|
4197
|
+
const target = mouseEvent.target;
|
|
4198
|
+
// Find the key text element (json-edit-react uses .jer-key-text class)
|
|
4199
|
+
const keyElement = target.closest('.jer-key-text') ||
|
|
4200
|
+
target.querySelector('.jer-key-text') ||
|
|
4201
|
+
(target.classList.contains('jer-key-text') ? target : null);
|
|
4202
|
+
if (!keyElement)
|
|
4203
|
+
return;
|
|
4204
|
+
const keyText = keyElement.textContent?.trim();
|
|
4205
|
+
if (!keyText)
|
|
4206
|
+
return;
|
|
4207
|
+
// Remove colon if present
|
|
4208
|
+
const keyName = keyText.replace(':', '').trim();
|
|
4209
|
+
// Map key names to node types and find the corresponding node
|
|
4210
|
+
let nodeId = null;
|
|
4211
|
+
let nodeType = null;
|
|
4212
|
+
if (keyName === 'section-id') {
|
|
4213
|
+
// Find the section-id value
|
|
4214
|
+
const keyRow = keyElement.closest('.jer-collection-header-row, .jer-value-row');
|
|
4215
|
+
if (keyRow) {
|
|
4216
|
+
const valueElement = keyRow.querySelector('.jer-value-text, .jer-string-value');
|
|
4217
|
+
if (valueElement) {
|
|
4218
|
+
nodeId = valueElement.textContent?.replace(/^"|"$/g, '').trim() || section['section-id'];
|
|
4219
|
+
nodeType = 'section';
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
else if (keyName === 'panel-id') {
|
|
4224
|
+
// Find the panel-id value in the current panel object
|
|
4225
|
+
const panelContainer = keyElement.closest('.jer-collection-component');
|
|
4226
|
+
if (panelContainer) {
|
|
4227
|
+
const valueElement = panelContainer.querySelector('.jer-value-text, .jer-string-value');
|
|
4228
|
+
if (valueElement) {
|
|
4229
|
+
// Try to find panel-id value in this panel
|
|
4230
|
+
const allKeys = panelContainer.querySelectorAll('.jer-key-text');
|
|
4231
|
+
for (const key of Array.from(allKeys)) {
|
|
4232
|
+
if (key.textContent?.includes('panel-id')) {
|
|
4233
|
+
const keyRow = key.closest('.jer-collection-header-row, .jer-value-row');
|
|
4234
|
+
if (keyRow) {
|
|
4235
|
+
const valElement = keyRow.querySelector('.jer-value-text, .jer-string-value');
|
|
4236
|
+
if (valElement) {
|
|
4237
|
+
nodeId = valElement.textContent?.replace(/^"|"$/g, '').trim() || null;
|
|
4238
|
+
nodeType = 'panel';
|
|
4239
|
+
break;
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
else if (keyName === 'widget-id') {
|
|
4248
|
+
// Find the widget-id value in the current widget object
|
|
4249
|
+
const widgetContainer = keyElement.closest('.jer-collection-component');
|
|
4250
|
+
if (widgetContainer) {
|
|
4251
|
+
const allKeys = widgetContainer.querySelectorAll('.jer-key-text');
|
|
4252
|
+
for (const key of Array.from(allKeys)) {
|
|
4253
|
+
if (key.textContent?.includes('widget-id')) {
|
|
4254
|
+
const keyRow = key.closest('.jer-collection-header-row, .jer-value-row');
|
|
4255
|
+
if (keyRow) {
|
|
4256
|
+
const valElement = keyRow.querySelector('.jer-value-text, .jer-string-value');
|
|
4257
|
+
if (valElement) {
|
|
4258
|
+
nodeId = valElement.textContent?.replace(/^"|"$/g, '').trim() || null;
|
|
4259
|
+
nodeType = 'widget';
|
|
4260
|
+
break;
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
// If we found a node, select it in the visual builder
|
|
4268
|
+
if (nodeId && nodeType) {
|
|
4269
|
+
onSelectNode(nodeId, nodeType);
|
|
4270
|
+
}
|
|
4271
|
+
};
|
|
4272
|
+
// Add click listener to the JSON editor container
|
|
4273
|
+
const editorContainer = document.querySelector('.json-editor-scroll-container');
|
|
4274
|
+
if (editorContainer) {
|
|
4275
|
+
editorContainer.addEventListener('click', handleJsonEditorClick);
|
|
4276
|
+
return () => {
|
|
4277
|
+
editorContainer.removeEventListener('click', handleJsonEditorClick);
|
|
4278
|
+
};
|
|
4279
|
+
}
|
|
4280
|
+
}, [onSelectNode, rawJsonView, section]);
|
|
4281
|
+
// Make section editable for preview - remove readonly flags from widgets
|
|
4282
|
+
const makeSectionEditable = useCallback((section) => {
|
|
4283
|
+
const processWidget = (widget) => {
|
|
4284
|
+
if (!widget || typeof widget !== 'object')
|
|
4285
|
+
return widget;
|
|
4286
|
+
const editableWidget = {
|
|
4287
|
+
...widget,
|
|
4288
|
+
'widget-readonly': false, // Make all widgets editable in preview
|
|
4289
|
+
};
|
|
4290
|
+
// Process nested widgets
|
|
4291
|
+
if (widget.widgets && Array.isArray(widget.widgets)) {
|
|
4292
|
+
editableWidget.widgets = widget.widgets.map(processWidget);
|
|
4293
|
+
}
|
|
4294
|
+
if (widget['widget-item']) {
|
|
4295
|
+
editableWidget['widget-item'] = processWidget(widget['widget-item']);
|
|
4296
|
+
}
|
|
4297
|
+
// Process table columns
|
|
4298
|
+
if (widget['widget-data-columns'] && Array.isArray(widget['widget-data-columns'])) {
|
|
4299
|
+
editableWidget['widget-data-columns'] = widget['widget-data-columns'].map((col) => {
|
|
4300
|
+
if (col && typeof col === 'object' && col.widget) {
|
|
4301
|
+
return processWidget(col);
|
|
4302
|
+
}
|
|
4303
|
+
return col;
|
|
4304
|
+
});
|
|
4305
|
+
}
|
|
4306
|
+
return editableWidget;
|
|
4307
|
+
};
|
|
4308
|
+
const processPanel = (panel) => {
|
|
4309
|
+
if (!panel || typeof panel !== 'object')
|
|
4310
|
+
return panel;
|
|
4311
|
+
const editablePanel = { ...panel };
|
|
4312
|
+
if (panel.widgets && Array.isArray(panel.widgets)) {
|
|
4313
|
+
editablePanel.widgets = panel.widgets.map(processWidget);
|
|
4314
|
+
}
|
|
4315
|
+
if (panel.panels && Array.isArray(panel.panels)) {
|
|
4316
|
+
editablePanel.panels = panel.panels.map(processPanel);
|
|
4317
|
+
}
|
|
4318
|
+
return editablePanel;
|
|
4319
|
+
};
|
|
4320
|
+
return {
|
|
4321
|
+
...section,
|
|
4322
|
+
'section-editable': true,
|
|
4323
|
+
panels: section.panels ? section.panels.map(processPanel) : [],
|
|
4324
|
+
};
|
|
4325
|
+
}, []);
|
|
4326
|
+
// Auto-populate widget-type based on widget selection
|
|
4327
|
+
const autoPopulateWidgetType = useCallback((data) => {
|
|
4328
|
+
if (!data || typeof data !== 'object')
|
|
4329
|
+
return data;
|
|
4330
|
+
const processWidget = (widget) => {
|
|
4331
|
+
if (!widget || typeof widget !== 'object')
|
|
4332
|
+
return widget;
|
|
4333
|
+
const widgetType = widget.widget;
|
|
4334
|
+
if (widgetType && !widget['widget-type']) {
|
|
4335
|
+
// Auto-determine widget-type based on widget name
|
|
4336
|
+
const widgetTypeMap = {
|
|
4337
|
+
'text': 'input',
|
|
4338
|
+
'textarea': 'input',
|
|
4339
|
+
'number': 'input',
|
|
4340
|
+
'boolean': 'input',
|
|
4341
|
+
'date': 'input',
|
|
4342
|
+
'datetime': 'input',
|
|
4343
|
+
'select': 'input',
|
|
4344
|
+
'radio': 'input',
|
|
4345
|
+
'checkbox': 'input',
|
|
4346
|
+
'file': 'input',
|
|
4347
|
+
'phone': 'input',
|
|
4348
|
+
'currency': 'input',
|
|
4349
|
+
'display': 'input',
|
|
4350
|
+
'table': 'table',
|
|
4351
|
+
'simple-table': 'table',
|
|
4352
|
+
'array-widget': 'group',
|
|
4353
|
+
'iterable-accordion': 'group',
|
|
4354
|
+
'profile': 'layout',
|
|
4355
|
+
};
|
|
4356
|
+
widget = {
|
|
4357
|
+
...widget,
|
|
4358
|
+
'widget-type': widgetTypeMap[widgetType] || 'input',
|
|
4359
|
+
};
|
|
4360
|
+
}
|
|
4361
|
+
// Process nested widgets
|
|
4362
|
+
if (widget.widgets && Array.isArray(widget.widgets)) {
|
|
4363
|
+
widget = {
|
|
4364
|
+
...widget,
|
|
4365
|
+
widgets: widget.widgets.map(processWidget),
|
|
4366
|
+
};
|
|
4367
|
+
}
|
|
4368
|
+
// Process widget-item
|
|
4369
|
+
if (widget['widget-item']) {
|
|
4370
|
+
widget = {
|
|
4371
|
+
...widget,
|
|
4372
|
+
'widget-item': processWidget(widget['widget-item']),
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
// Process table columns
|
|
4376
|
+
if (widget['widget-data-columns'] && Array.isArray(widget['widget-data-columns'])) {
|
|
4377
|
+
widget = {
|
|
4378
|
+
...widget,
|
|
4379
|
+
'widget-data-columns': widget['widget-data-columns'].map((col) => {
|
|
4380
|
+
if (col && typeof col === 'object' && col.widget && !col['widget-type']) {
|
|
4381
|
+
const widgetTypeMap = {
|
|
4382
|
+
'text': 'input',
|
|
4383
|
+
'number': 'input',
|
|
4384
|
+
'date': 'input',
|
|
4385
|
+
'select': 'input',
|
|
4386
|
+
'boolean': 'input',
|
|
4387
|
+
};
|
|
4388
|
+
return {
|
|
4389
|
+
...col,
|
|
4390
|
+
'widget-type': widgetTypeMap[col.widget] || 'input',
|
|
4391
|
+
};
|
|
4392
|
+
}
|
|
4393
|
+
return col;
|
|
4394
|
+
}),
|
|
4395
|
+
};
|
|
4396
|
+
}
|
|
4397
|
+
return widget;
|
|
4398
|
+
};
|
|
4399
|
+
const processPanel = (panel) => {
|
|
4400
|
+
if (!panel || typeof panel !== 'object')
|
|
4401
|
+
return panel;
|
|
4402
|
+
let processed = { ...panel };
|
|
4403
|
+
// Process widgets in panel
|
|
4404
|
+
if (processed.widgets && Array.isArray(processed.widgets)) {
|
|
4405
|
+
processed.widgets = processed.widgets.map(processWidget);
|
|
4406
|
+
}
|
|
4407
|
+
// Process nested panels
|
|
4408
|
+
if (processed.panels && Array.isArray(processed.panels)) {
|
|
4409
|
+
processed.panels = processed.panels.map(processPanel);
|
|
4410
|
+
}
|
|
4411
|
+
return processed;
|
|
4412
|
+
};
|
|
4413
|
+
// Process section
|
|
4414
|
+
if (data.panels && Array.isArray(data.panels)) {
|
|
4415
|
+
return {
|
|
4416
|
+
...data,
|
|
4417
|
+
panels: data.panels.map(processPanel),
|
|
4418
|
+
};
|
|
4419
|
+
}
|
|
4420
|
+
return data;
|
|
4421
|
+
}, []);
|
|
4422
|
+
const handleJsonChange = useCallback((data) => {
|
|
4423
|
+
// json-edit-react may wrap the data in a "root" key - unwrap it if present
|
|
4424
|
+
let unwrappedData = data?.root ? data.root : data;
|
|
4425
|
+
// Auto-populate widget-type for widgets that don't have it
|
|
4426
|
+
unwrappedData = autoPopulateWidgetType(unwrappedData);
|
|
4427
|
+
setJsonData(unwrappedData);
|
|
4428
|
+
setRawJsonText(JSON.stringify(unwrappedData, null, 2));
|
|
4429
|
+
// Basic validation
|
|
4430
|
+
const errors = [];
|
|
4431
|
+
if (!unwrappedData['section-id']) {
|
|
4432
|
+
errors.push('section-id is required');
|
|
4433
|
+
}
|
|
4434
|
+
if (!unwrappedData.panels || !Array.isArray(unwrappedData.panels)) {
|
|
4435
|
+
errors.push('panels must be an array');
|
|
4436
|
+
}
|
|
4437
|
+
setValidationErrors(errors);
|
|
4438
|
+
// Only update if valid
|
|
4439
|
+
if (errors.length === 0) {
|
|
4440
|
+
onChange(unwrappedData);
|
|
4441
|
+
}
|
|
4442
|
+
}, [onChange, autoPopulateWidgetType]);
|
|
4443
|
+
const handleRawJsonChange = useCallback((text) => {
|
|
4444
|
+
setRawJsonText(text);
|
|
4445
|
+
try {
|
|
4446
|
+
const parsed = JSON.parse(text);
|
|
4447
|
+
const errors = [];
|
|
4448
|
+
if (!parsed['section-id']) {
|
|
4449
|
+
errors.push('section-id is required');
|
|
4450
|
+
}
|
|
4451
|
+
if (!parsed.panels || !Array.isArray(parsed.panels)) {
|
|
4452
|
+
errors.push('panels must be an array');
|
|
4453
|
+
}
|
|
4454
|
+
setValidationErrors(errors);
|
|
4455
|
+
// Auto-populate widget-type
|
|
4456
|
+
const processed = autoPopulateWidgetType(parsed);
|
|
4457
|
+
if (errors.length === 0) {
|
|
4458
|
+
setJsonData(processed);
|
|
4459
|
+
onChange(processed);
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
catch (error) {
|
|
4463
|
+
setValidationErrors([`Invalid JSON: ${error instanceof Error ? error.message : 'Parse error'}`]);
|
|
4464
|
+
}
|
|
4465
|
+
}, [onChange, autoPopulateWidgetType]);
|
|
4466
|
+
const toggleRawJsonView = useCallback(() => {
|
|
4467
|
+
if (!rawJsonView) {
|
|
4468
|
+
// Switching to raw view - update text from current data
|
|
4469
|
+
setRawJsonText(JSON.stringify(jsonData, null, 2));
|
|
4470
|
+
}
|
|
4471
|
+
setRawJsonView(!rawJsonView);
|
|
4472
|
+
}, [rawJsonView, jsonData]);
|
|
4473
|
+
// Create enum configuration for json-edit-react
|
|
4474
|
+
// This maps field paths to their allowed enum values
|
|
4475
|
+
const enumConfig = useCallback(() => {
|
|
4476
|
+
return {
|
|
4477
|
+
// Section level
|
|
4478
|
+
'section-id': undefined, // string, no enum
|
|
4479
|
+
'section-title': undefined, // string, no enum
|
|
4480
|
+
'section-editable': undefined, // boolean, no enum
|
|
4481
|
+
'section-column-span': undefined, // number, no enum
|
|
4482
|
+
// Panel level - can be nested in panels array
|
|
4483
|
+
'panel-id': undefined, // string, no enum
|
|
4484
|
+
'panel-orientation': ORIENTATIONS, // enum: ['horizontal', 'vertical']
|
|
4485
|
+
'panel-column-span': undefined, // number, no enum
|
|
4486
|
+
// Widget level - can be nested in widgets array or widget-item
|
|
4487
|
+
'widget': WIDGET_TYPES, // enum: all widget types
|
|
4488
|
+
'widget-type': ['input', 'layout', 'table', 'group'], // enum
|
|
4489
|
+
'widget-id': undefined, // string, no enum
|
|
4490
|
+
'widget-label': undefined, // string, no enum
|
|
4491
|
+
'widget-orientation': ORIENTATIONS, // enum: ['horizontal', 'vertical']
|
|
4492
|
+
'widget-required': undefined, // boolean, no enum
|
|
4493
|
+
'widget-readonly': undefined, // boolean, no enum
|
|
4494
|
+
// Widget data source type
|
|
4495
|
+
'widget-data-source.type': DATA_SOURCE_TYPES, // enum: ['static', 'api', 'schema']
|
|
4496
|
+
'widget-data-source.method': ['GET', 'POST', 'PUT', 'DELETE'], // HTTP methods
|
|
4497
|
+
// Widget validation
|
|
4498
|
+
'widget-data-validation.validationType': VALIDATION_TYPES, // enum: ['email', 'phone', 'url']
|
|
4499
|
+
// Widget format options
|
|
4500
|
+
'widget-data-format.inputType': ['text', 'email', 'password', 'number', 'tel', 'url', 'search', 'file'],
|
|
4501
|
+
'widget-data-format.characterType': CHARACTER_TYPES,
|
|
4502
|
+
'widget-data-format.caseControl': CASE_CONTROLS,
|
|
4503
|
+
'widget-data-format.numericType': NUMERIC_TYPES,
|
|
4504
|
+
'widget-data-format.roundingMode': ['round', 'truncate'],
|
|
4505
|
+
'widget-data-format.textAlign': ['left', 'right'],
|
|
4506
|
+
'widget-data-format.booleanRepresentation': BOOLEAN_REPRESENTATIONS,
|
|
4507
|
+
'widget-data-format.booleanControlType': BOOLEAN_CONTROL_TYPES,
|
|
4508
|
+
'widget-data-format.layout': ['vertical', 'horizontal', 'grid'],
|
|
4509
|
+
'widget-data-format.inputMethod': ['picker', 'manual', 'hybrid'],
|
|
4510
|
+
'widget-data-format.dateConstraint': ['any', 'past-only', 'future-only'],
|
|
4511
|
+
'widget-data-format.dateTimeConstraint': ['any', 'past-only', 'future-only'],
|
|
4512
|
+
// Widget options
|
|
4513
|
+
'widget-data-options.action': ['show', 'hide', 'enable', 'disable'],
|
|
4514
|
+
'widget-data-options.condition.operator': CONDITION_OPERATORS,
|
|
4515
|
+
};
|
|
4516
|
+
}, []);
|
|
4517
|
+
return (jsxRuntimeExports.jsxs("div", { style: {
|
|
4518
|
+
display: 'flex',
|
|
4519
|
+
flexDirection: 'column',
|
|
4520
|
+
height: '100%',
|
|
4521
|
+
width: '100%',
|
|
4522
|
+
minHeight: 0,
|
|
4523
|
+
borderRight: '0px',
|
|
4524
|
+
}, children: [jsxRuntimeExports.jsxs("div", { style: {
|
|
4525
|
+
padding: '15px 20px',
|
|
4526
|
+
background: '#ffffff',
|
|
4527
|
+
display: 'flex',
|
|
4528
|
+
justifyContent: 'space-between',
|
|
4529
|
+
alignItems: 'center',
|
|
4530
|
+
}, children: [jsxRuntimeExports.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '12px' }, children: [jsxRuntimeExports.jsx("div", { style: { fontWeight: 600, fontSize: '16px', color: '#2c3e50' }, children: "JSON Editor" }), validationErrors.length === 0 ? (jsxRuntimeExports.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '6px', color: '#28a745' }, children: [jsxRuntimeExports.jsx("div", { style: {
|
|
4531
|
+
width: '12px',
|
|
4532
|
+
height: '12px',
|
|
4533
|
+
borderRadius: '50%',
|
|
4534
|
+
background: '#28a745',
|
|
4535
|
+
} }), jsxRuntimeExports.jsx("span", { style: { fontSize: '12px' }, children: "Valid JSON Schema" })] })) : (jsxRuntimeExports.jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '6px', color: '#e74c3c' }, children: [jsxRuntimeExports.jsx("div", { style: {
|
|
4536
|
+
width: '12px',
|
|
4537
|
+
height: '12px',
|
|
4538
|
+
borderRadius: '50%',
|
|
4539
|
+
background: '#e74c3c',
|
|
4540
|
+
} }), jsxRuntimeExports.jsx("span", { style: { fontSize: '12px' }, children: "Validation Errors" })] }))] }), jsxRuntimeExports.jsxs("div", { style: {
|
|
4541
|
+
display: 'flex',
|
|
4542
|
+
alignItems: 'center',
|
|
4543
|
+
gap: '8px',
|
|
4544
|
+
}, children: [jsxRuntimeExports.jsxs("button", { onClick: handleReset, style: {
|
|
4545
|
+
padding: '6px 12px',
|
|
4546
|
+
border: '1px solid #ddd',
|
|
4547
|
+
borderRadius: '4px',
|
|
4548
|
+
background: 'white',
|
|
4549
|
+
color: '#666',
|
|
4550
|
+
cursor: 'pointer',
|
|
4551
|
+
display: 'flex',
|
|
4552
|
+
alignItems: 'center',
|
|
4553
|
+
gap: '6px',
|
|
4554
|
+
fontSize: '12px',
|
|
4555
|
+
fontWeight: 500,
|
|
4556
|
+
transition: 'all 0.2s',
|
|
4557
|
+
}, onMouseEnter: (e) => {
|
|
4558
|
+
e.currentTarget.style.background = '#f8f9fa';
|
|
4559
|
+
e.currentTarget.style.borderColor = '#999';
|
|
4560
|
+
}, onMouseLeave: (e) => {
|
|
4561
|
+
e.currentTarget.style.background = 'white';
|
|
4562
|
+
e.currentTarget.style.borderColor = '#ddd';
|
|
4563
|
+
}, title: "Reset to original JSON", children: [jsxRuntimeExports.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntimeExports.jsx("path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }), jsxRuntimeExports.jsx("path", { d: "M21 3v5h-5" }), jsxRuntimeExports.jsx("path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }), jsxRuntimeExports.jsx("path", { d: "M3 21v-5h5" })] }), "Reset"] }), jsxRuntimeExports.jsxs("button", { onClick: () => setShowPreview(true), style: {
|
|
4564
|
+
padding: '6px 12px',
|
|
4565
|
+
border: '1px solid #ddd',
|
|
4566
|
+
borderRadius: '4px',
|
|
4567
|
+
background: 'white',
|
|
4568
|
+
color: '#666',
|
|
4569
|
+
cursor: 'pointer',
|
|
4570
|
+
display: 'flex',
|
|
4571
|
+
alignItems: 'center',
|
|
4572
|
+
gap: '6px',
|
|
4573
|
+
fontSize: '12px',
|
|
4574
|
+
fontWeight: 500,
|
|
4575
|
+
}, title: "Preview Section", children: [jsxRuntimeExports.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntimeExports.jsx("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }), jsxRuntimeExports.jsx("circle", { cx: "12", cy: "12", r: "3" })] }), "Preview"] }), jsxRuntimeExports.jsx("span", { style: {
|
|
4576
|
+
fontSize: '12px',
|
|
4577
|
+
color: !rawJsonView ? '#007bff' : '#6c757d',
|
|
4578
|
+
fontWeight: !rawJsonView ? 600 : 400,
|
|
4579
|
+
transition: 'color 0.2s',
|
|
4580
|
+
}, children: "Tree" }), jsxRuntimeExports.jsx("div", { onClick: toggleRawJsonView, style: {
|
|
4581
|
+
position: 'relative',
|
|
4582
|
+
width: '44px',
|
|
4583
|
+
height: '24px',
|
|
4584
|
+
background: rawJsonView ? '#007bff' : '#ccc',
|
|
4585
|
+
borderRadius: '12px',
|
|
4586
|
+
cursor: 'pointer',
|
|
4587
|
+
transition: 'background 0.2s',
|
|
4588
|
+
}, children: jsxRuntimeExports.jsx("div", { style: {
|
|
4589
|
+
position: 'absolute',
|
|
4590
|
+
top: '2px',
|
|
4591
|
+
left: rawJsonView ? '22px' : '2px',
|
|
4592
|
+
width: '20px',
|
|
4593
|
+
height: '20px',
|
|
4594
|
+
background: 'white',
|
|
4595
|
+
borderRadius: '50%',
|
|
4596
|
+
transition: 'left 0.2s',
|
|
4597
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
|
4598
|
+
} }) }), jsxRuntimeExports.jsx("span", { style: {
|
|
4599
|
+
fontSize: '12px',
|
|
4600
|
+
color: rawJsonView ? '#007bff' : '#6c757d',
|
|
4601
|
+
fontWeight: rawJsonView ? 600 : 400,
|
|
4602
|
+
transition: 'color 0.2s',
|
|
4603
|
+
}, children: "Raw" })] })] }), jsxRuntimeExports.jsx("div", { style: {
|
|
4604
|
+
flex: 1,
|
|
4605
|
+
minHeight: 0,
|
|
4606
|
+
maxHeight: '100%',
|
|
4607
|
+
overflow: 'hidden',
|
|
4608
|
+
background: 'white',
|
|
4609
|
+
border: '1px solid #E1E1E1',
|
|
4610
|
+
borderRadius: '10px',
|
|
4611
|
+
padding: rawJsonView ? '0' : '20px',
|
|
4612
|
+
position: 'relative',
|
|
4613
|
+
display: 'flex',
|
|
4614
|
+
flexDirection: 'column',
|
|
4615
|
+
}, children: rawJsonView ? (jsxRuntimeExports.jsx("textarea", { value: rawJsonText, onChange: (e) => handleRawJsonChange(e.target.value), style: {
|
|
4616
|
+
width: '100%',
|
|
4617
|
+
height: '100%',
|
|
4618
|
+
background: 'white',
|
|
4619
|
+
color: '#333',
|
|
4620
|
+
border: 'none',
|
|
4621
|
+
padding: '20px',
|
|
4622
|
+
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, "source-code-pro", monospace',
|
|
4623
|
+
fontSize: '13px',
|
|
4624
|
+
lineHeight: '1.5',
|
|
4625
|
+
resize: 'none',
|
|
4626
|
+
outline: 'none',
|
|
4627
|
+
boxSizing: 'border-box',
|
|
4628
|
+
borderRadius: '10px',
|
|
4629
|
+
}, spellCheck: false })) : (jsxRuntimeExports.jsx("div", { className: "json-editor-scroll-container", style: {
|
|
4630
|
+
width: '100%',
|
|
4631
|
+
height: '100%',
|
|
4632
|
+
minHeight: 0,
|
|
4633
|
+
maxHeight: '100%',
|
|
4634
|
+
overflow: 'auto',
|
|
4635
|
+
position: 'relative',
|
|
4636
|
+
display: 'flex',
|
|
4637
|
+
flexDirection: 'column',
|
|
4638
|
+
flex: '1 1 0',
|
|
4639
|
+
}, children: jsxRuntimeExports.jsx(JsonEditor, { data: jsonData, setData: handleJsonChange, ...{ enumOptions: enumConfig() } }, `editor-${editorKey}`) })) }), showPreview && createPortal(jsxRuntimeExports.jsx("div", { className: "section-builder-preview-backdrop", style: {
|
|
4640
|
+
position: 'fixed',
|
|
4641
|
+
top: 0,
|
|
4642
|
+
left: 0,
|
|
4643
|
+
right: 0,
|
|
4644
|
+
bottom: 0,
|
|
4645
|
+
background: 'rgba(0, 0, 0, 0.5)',
|
|
4646
|
+
zIndex: 10000,
|
|
4647
|
+
display: 'flex',
|
|
4648
|
+
alignItems: 'center',
|
|
4649
|
+
justifyContent: 'center',
|
|
4650
|
+
padding: '20px',
|
|
4651
|
+
}, onClick: () => setShowPreview(false), children: jsxRuntimeExports.jsxs("div", { className: "section-builder-preview-modal", style: {
|
|
4652
|
+
background: 'white',
|
|
4653
|
+
borderRadius: '8px',
|
|
4654
|
+
width: '100%',
|
|
4655
|
+
minWidth: '700px', // Ensure enough width for 600px content + padding
|
|
4656
|
+
maxWidth: '90vw',
|
|
4657
|
+
height: '90vh',
|
|
4658
|
+
maxHeight: '90vh',
|
|
4659
|
+
display: 'flex',
|
|
4660
|
+
flexDirection: 'column',
|
|
4661
|
+
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
|
|
4662
|
+
}, onClick: (e) => e.stopPropagation(), children: [jsxRuntimeExports.jsxs("div", { className: "section-builder-preview-header", style: {
|
|
4663
|
+
padding: '15px 20px',
|
|
4664
|
+
borderBottom: '1px solid #ddd',
|
|
4665
|
+
display: 'flex',
|
|
4666
|
+
justifyContent: 'space-between',
|
|
4667
|
+
alignItems: 'center',
|
|
4668
|
+
background: '#f8f9fa',
|
|
4669
|
+
}, children: [jsxRuntimeExports.jsx("h2", { className: "section-builder-preview-title", style: { margin: 0, fontSize: '18px', fontWeight: 600, color: '#2c3e50' }, children: "Section Preview" }), jsxRuntimeExports.jsx("button", { className: "section-builder-preview-close", onClick: () => setShowPreview(false), style: {
|
|
4670
|
+
padding: '6px 12px',
|
|
4671
|
+
border: 'none',
|
|
4672
|
+
borderRadius: '4px',
|
|
4673
|
+
background: '#e74c3c',
|
|
4674
|
+
color: 'white',
|
|
4675
|
+
cursor: 'pointer',
|
|
4676
|
+
fontSize: '14px',
|
|
4677
|
+
fontWeight: 600,
|
|
4678
|
+
}, children: "Close" })] }), jsxRuntimeExports.jsx("div", { className: "section-builder-preview-content", style: {
|
|
4679
|
+
flex: 1,
|
|
4680
|
+
overflow: 'auto',
|
|
4681
|
+
padding: '20px',
|
|
4682
|
+
}, children: jsxRuntimeExports.jsx(Provider, { store: previewStore, children: jsxRuntimeExports.jsx(WidgetProvider, { store: previewStore, dataSourceRequestHandler: widgetContext.dataSourceRequestHandler, schemaData: widgetContext.schemaData, translate: widgetContext.translate, children: jsxRuntimeExports.jsx("div", { style: { position: 'relative', width: '100%', height: '100%' }, children: jsxRuntimeExports.jsx(SectionRenderer, { section: makeSectionEditable(jsonData), hideEditButton: true, onValueChange: (widgetId, value) => {
|
|
4683
|
+
// Handle value changes in preview (optional - for tracking)
|
|
4684
|
+
console.log('Preview value changed:', widgetId, value);
|
|
4685
|
+
} }) }) }) }) })] }) }), document.body)] }));
|
|
4686
|
+
};
|
|
4687
|
+
|
|
4688
|
+
/**
|
|
4689
|
+
* Recursively build tree nodes from panel structure
|
|
4690
|
+
*/
|
|
4691
|
+
function buildTreeNodesFromPanels(panels, parent) {
|
|
4692
|
+
const nodes = [];
|
|
4693
|
+
panels.forEach((panel) => {
|
|
4694
|
+
const panelNode = {
|
|
4695
|
+
type: 'panel',
|
|
4696
|
+
id: panel['panel-id'],
|
|
4697
|
+
label: `Panel: ${panel['panel-id']}`,
|
|
4698
|
+
data: panel,
|
|
4699
|
+
parent: parent,
|
|
4700
|
+
children: [],
|
|
4701
|
+
};
|
|
4702
|
+
// Add nested panels recursively
|
|
4703
|
+
if (panel.panels && panel.panels.length > 0) {
|
|
4704
|
+
panelNode.children = panelNode.children || [];
|
|
4705
|
+
panelNode.children.push(...buildTreeNodesFromPanels(panel.panels, panelNode));
|
|
4706
|
+
}
|
|
4707
|
+
// Add widgets
|
|
4708
|
+
if (panel.widgets && panel.widgets.length > 0) {
|
|
4709
|
+
panelNode.children = panelNode.children || [];
|
|
4710
|
+
panel.widgets.forEach((widget) => {
|
|
4711
|
+
panelNode.children.push({
|
|
4712
|
+
type: 'widget',
|
|
4713
|
+
id: widget['widget-id'],
|
|
4714
|
+
label: `Widget: ${widget['widget-id']} (${widget.widget})`,
|
|
4715
|
+
data: widget,
|
|
4716
|
+
parent: panelNode,
|
|
4717
|
+
});
|
|
4718
|
+
});
|
|
4719
|
+
}
|
|
4720
|
+
nodes.push(panelNode);
|
|
4721
|
+
});
|
|
4722
|
+
return nodes;
|
|
4723
|
+
}
|
|
4724
|
+
/**
|
|
4725
|
+
* Tree view component for section structure
|
|
4726
|
+
*/
|
|
4727
|
+
const SectionTree = ({ section, selectedNode, onSelectNode, onAddPanel, onAddWidget, onDeleteNode, onDuplicateNode, }) => {
|
|
4728
|
+
const treeNodes = React.useMemo(() => {
|
|
4729
|
+
if (section.panels && section.panels.length > 0) {
|
|
4730
|
+
return buildTreeNodesFromPanels(section.panels);
|
|
4731
|
+
}
|
|
4732
|
+
return [];
|
|
4733
|
+
}, [section]);
|
|
4734
|
+
const renderTreeNode = (node, level = 0) => {
|
|
4735
|
+
const isSelected = selectedNode?.id === node.id && selectedNode?.type === node.type;
|
|
4736
|
+
const indent = level * 20;
|
|
4737
|
+
const getNodeStyles = () => {
|
|
4738
|
+
const baseStyles = {
|
|
4739
|
+
marginLeft: `${indent}px`,
|
|
4740
|
+
marginTop: '4px',
|
|
4741
|
+
padding: '8px 12px',
|
|
4742
|
+
borderRadius: '4px',
|
|
4743
|
+
cursor: 'pointer',
|
|
4744
|
+
position: 'relative',
|
|
4745
|
+
transition: 'background 0.2s',
|
|
4746
|
+
display: 'flex',
|
|
4747
|
+
alignItems: 'center',
|
|
4748
|
+
justifyContent: 'space-between',
|
|
4749
|
+
};
|
|
4750
|
+
if (isSelected) {
|
|
4751
|
+
switch (node.type) {
|
|
4752
|
+
case 'section':
|
|
4753
|
+
return { ...baseStyles, background: '#e3f2fd', border: '1px solid #2196f3' };
|
|
4754
|
+
case 'panel':
|
|
4755
|
+
return { ...baseStyles, background: '#fff3e0', border: '1px solid #ff9800' };
|
|
4756
|
+
case 'widget':
|
|
4757
|
+
return { ...baseStyles, background: '#e8f5e9', border: '1px solid #4caf50' };
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
switch (node.type) {
|
|
4761
|
+
case 'section':
|
|
4762
|
+
return { ...baseStyles, background: '#e3f2fd', border: '1px solid #2196f3' };
|
|
4763
|
+
case 'panel':
|
|
4764
|
+
return { ...baseStyles, background: '#fff3e0', border: '1px solid #ff9800' };
|
|
4765
|
+
case 'widget':
|
|
4766
|
+
return { ...baseStyles, background: '#e8f5e9', border: '1px solid #4caf50' };
|
|
4767
|
+
}
|
|
4768
|
+
return baseStyles;
|
|
4769
|
+
};
|
|
4770
|
+
const getIcon = () => {
|
|
4771
|
+
switch (node.type) {
|
|
4772
|
+
case 'section':
|
|
4773
|
+
return '📁';
|
|
4774
|
+
case 'panel':
|
|
4775
|
+
return '📦';
|
|
4776
|
+
case 'widget':
|
|
4777
|
+
return '🔧';
|
|
4778
|
+
}
|
|
4779
|
+
};
|
|
4780
|
+
const getActionButtonColor = () => {
|
|
4781
|
+
switch (node.type) {
|
|
4782
|
+
case 'section':
|
|
4783
|
+
return '#2196f3';
|
|
4784
|
+
case 'panel':
|
|
4785
|
+
return '#ff9800';
|
|
4786
|
+
case 'widget':
|
|
4787
|
+
return '#4caf50';
|
|
4788
|
+
}
|
|
4789
|
+
};
|
|
4790
|
+
return (jsxRuntimeExports.jsxs("div", { children: [jsxRuntimeExports.jsxs("div", { style: getNodeStyles(), onClick: (e) => {
|
|
4791
|
+
e.stopPropagation();
|
|
4792
|
+
onSelectNode(node);
|
|
4793
|
+
}, onMouseEnter: (e) => {
|
|
4794
|
+
const target = e.currentTarget;
|
|
4795
|
+
const actionBtn = target.querySelector('.tree-action-btn');
|
|
4796
|
+
if (actionBtn)
|
|
4797
|
+
actionBtn.style.opacity = '1';
|
|
4798
|
+
}, onMouseLeave: (e) => {
|
|
4799
|
+
const target = e.currentTarget;
|
|
4800
|
+
const actionBtn = target.querySelector('.tree-action-btn');
|
|
4801
|
+
if (actionBtn)
|
|
4802
|
+
actionBtn.style.opacity = '0';
|
|
4803
|
+
}, children: [jsxRuntimeExports.jsxs("span", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [jsxRuntimeExports.jsx("span", { children: getIcon() }), jsxRuntimeExports.jsx("span", { style: { fontSize: '13px' }, children: node.label })] }), jsxRuntimeExports.jsx("button", { className: "tree-action-btn", onClick: (e) => {
|
|
4804
|
+
e.stopPropagation();
|
|
4805
|
+
onSelectNode(node);
|
|
4806
|
+
}, style: {
|
|
4807
|
+
width: '24px',
|
|
4808
|
+
height: '24px',
|
|
4809
|
+
borderRadius: '50%',
|
|
4810
|
+
border: 'none',
|
|
4811
|
+
background: getActionButtonColor(),
|
|
4812
|
+
color: 'white',
|
|
4813
|
+
cursor: 'pointer',
|
|
4814
|
+
fontSize: '12px',
|
|
4815
|
+
opacity: isSelected ? '1' : '0',
|
|
4816
|
+
transition: 'opacity 0.2s',
|
|
4817
|
+
display: 'flex',
|
|
4818
|
+
alignItems: 'center',
|
|
4819
|
+
justifyContent: 'center',
|
|
4820
|
+
}, children: "\u2699" })] }), node.children && node.children.length > 0 && (jsxRuntimeExports.jsx("div", { style: { marginLeft: `${indent + 20}px` }, children: node.children.map((child) => renderTreeNode(child, level + 1)) }))] }, node.id));
|
|
4821
|
+
};
|
|
4822
|
+
return (jsxRuntimeExports.jsxs("div", { style: {
|
|
4823
|
+
padding: '15px',
|
|
4824
|
+
overflowY: 'auto',
|
|
4825
|
+
background: '#f8f9fa',
|
|
4826
|
+
height: '100%',
|
|
4827
|
+
}, children: [jsxRuntimeExports.jsx("h3", { style: { fontSize: '14px', marginBottom: '15px', color: '#2c3e50' }, children: "Section Structure" }), jsxRuntimeExports.jsxs("div", { style: {
|
|
4828
|
+
padding: '8px 12px',
|
|
4829
|
+
borderRadius: '4px',
|
|
4830
|
+
background: '#e3f2fd',
|
|
4831
|
+
border: '1px solid #2196f3',
|
|
4832
|
+
marginBottom: '10px',
|
|
4833
|
+
cursor: 'pointer',
|
|
4834
|
+
display: 'flex',
|
|
4835
|
+
alignItems: 'center',
|
|
4836
|
+
justifyContent: 'space-between',
|
|
4837
|
+
}, onClick: () => {
|
|
4838
|
+
const sectionNode = {
|
|
4839
|
+
type: 'section',
|
|
4840
|
+
id: section['section-id'],
|
|
4841
|
+
label: `Section: ${section['section-id']}`,
|
|
4842
|
+
data: section,
|
|
4843
|
+
children: treeNodes,
|
|
4844
|
+
};
|
|
4845
|
+
onSelectNode(sectionNode);
|
|
4846
|
+
}, children: [jsxRuntimeExports.jsxs("span", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [jsxRuntimeExports.jsx("span", { children: "\uD83D\uDCC1" }), jsxRuntimeExports.jsxs("span", { style: { fontSize: '13px', fontWeight: 600 }, children: ["Section: ", section['section-id']] })] }), jsxRuntimeExports.jsx("button", { onClick: (e) => {
|
|
4847
|
+
e.stopPropagation();
|
|
4848
|
+
const sectionNode = {
|
|
4849
|
+
type: 'section',
|
|
4850
|
+
id: section['section-id'],
|
|
4851
|
+
label: `Section: ${section['section-id']}`,
|
|
4852
|
+
data: section,
|
|
4853
|
+
children: treeNodes,
|
|
4854
|
+
};
|
|
4855
|
+
onSelectNode(sectionNode);
|
|
4856
|
+
}, style: {
|
|
4857
|
+
width: '24px',
|
|
4858
|
+
height: '24px',
|
|
4859
|
+
borderRadius: '50%',
|
|
4860
|
+
border: 'none',
|
|
4861
|
+
background: '#2196f3',
|
|
4862
|
+
color: 'white',
|
|
4863
|
+
cursor: 'pointer',
|
|
4864
|
+
fontSize: '12px',
|
|
4865
|
+
}, children: "\u2699" })] }), treeNodes.map((node) => renderTreeNode(node, 0))] }));
|
|
4866
|
+
};
|
|
4867
|
+
|
|
4868
|
+
/**
|
|
4869
|
+
* Property editor component for editing selected node properties
|
|
4870
|
+
*/
|
|
4871
|
+
const PropertyEditor = ({ node, onChange, onDelete, onDuplicate, }) => {
|
|
4872
|
+
const [localData, setLocalData] = useState(null);
|
|
4873
|
+
useEffect(() => {
|
|
4874
|
+
if (node) {
|
|
4875
|
+
setLocalData({ ...node.data });
|
|
4876
|
+
}
|
|
4877
|
+
}, [node]);
|
|
4878
|
+
if (!node || !localData) {
|
|
4879
|
+
return (jsxRuntimeExports.jsx("div", { style: {
|
|
4880
|
+
padding: '15px',
|
|
4881
|
+
background: '#f8f9fa',
|
|
4882
|
+
height: '100%',
|
|
4883
|
+
display: 'flex',
|
|
4884
|
+
alignItems: 'center',
|
|
4885
|
+
justifyContent: 'center',
|
|
4886
|
+
color: '#666',
|
|
4887
|
+
}, children: "Select an item to edit properties" }));
|
|
4888
|
+
}
|
|
4889
|
+
const handleChange = (field, value) => {
|
|
4890
|
+
const updated = { ...localData, [field]: value };
|
|
4891
|
+
setLocalData(updated);
|
|
4892
|
+
onChange(node, updated);
|
|
4893
|
+
};
|
|
4894
|
+
const handleNestedChange = (field, nestedField, value) => {
|
|
4895
|
+
const updated = {
|
|
4896
|
+
...localData,
|
|
4897
|
+
[field]: {
|
|
4898
|
+
...(localData[field] || {}),
|
|
4899
|
+
[nestedField]: value,
|
|
4900
|
+
},
|
|
4901
|
+
};
|
|
4902
|
+
setLocalData(updated);
|
|
4903
|
+
onChange(node, updated);
|
|
4904
|
+
};
|
|
4905
|
+
const ensureNestedObject = (field) => {
|
|
4906
|
+
if (!localData[field]) {
|
|
4907
|
+
const updated = {
|
|
4908
|
+
...localData,
|
|
4909
|
+
[field]: {},
|
|
4910
|
+
};
|
|
4911
|
+
setLocalData(updated);
|
|
4912
|
+
onChange(node, updated);
|
|
4913
|
+
}
|
|
4914
|
+
};
|
|
4915
|
+
const renderSectionProperties = () => {
|
|
4916
|
+
const section = localData;
|
|
4917
|
+
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsxs("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: ["Section ID ", jsxRuntimeExports.jsx("span", { style: { color: '#e74c3c' }, children: "*" })] }), jsxRuntimeExports.jsx("input", { type: "text", value: section['section-id'] || '', onChange: (e) => handleChange('section-id', e.target.value), style: {
|
|
4918
|
+
width: '100%',
|
|
4919
|
+
padding: '8px 12px',
|
|
4920
|
+
border: '1px solid #ccc',
|
|
4921
|
+
borderRadius: '4px',
|
|
4922
|
+
fontSize: '12px',
|
|
4923
|
+
} })] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: "Section Title" }), jsxRuntimeExports.jsx("input", { type: "text", value: section['section-title'] || '', onChange: (e) => handleChange('section-title', e.target.value), style: {
|
|
4924
|
+
width: '100%',
|
|
4925
|
+
padding: '8px 12px',
|
|
4926
|
+
border: '1px solid #ccc',
|
|
4927
|
+
borderRadius: '4px',
|
|
4928
|
+
fontSize: '12px',
|
|
4929
|
+
} })] }), jsxRuntimeExports.jsx("div", { style: { marginBottom: '20px' }, children: jsxRuntimeExports.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px' }, children: [jsxRuntimeExports.jsx("input", { type: "checkbox", checked: section['section-editable'] || false, onChange: (e) => handleChange('section-editable', e.target.checked) }), jsxRuntimeExports.jsx("span", { children: "Editable" })] }) }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: "Column Span" }), jsxRuntimeExports.jsx("input", { type: "number", value: section['section-column-span'] || 1, onChange: (e) => handleChange('section-column-span', parseInt(e.target.value) || 1), min: "1", style: {
|
|
4930
|
+
width: '100%',
|
|
4931
|
+
padding: '8px 12px',
|
|
4932
|
+
border: '1px solid #ccc',
|
|
4933
|
+
borderRadius: '4px',
|
|
4934
|
+
fontSize: '12px',
|
|
4935
|
+
} })] })] }));
|
|
4936
|
+
};
|
|
4937
|
+
const renderPanelProperties = () => {
|
|
4938
|
+
const panel = localData;
|
|
4939
|
+
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsxs("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: ["Panel ID ", jsxRuntimeExports.jsx("span", { style: { color: '#e74c3c' }, children: "*" })] }), jsxRuntimeExports.jsx("input", { type: "text", value: panel['panel-id'] || '', onChange: (e) => handleChange('panel-id', e.target.value), style: {
|
|
4940
|
+
width: '100%',
|
|
4941
|
+
padding: '8px 12px',
|
|
4942
|
+
border: '1px solid #ccc',
|
|
4943
|
+
borderRadius: '4px',
|
|
4944
|
+
fontSize: '12px',
|
|
4945
|
+
} })] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: "Orientation" }), jsxRuntimeExports.jsx("select", { value: panel['panel-orientation'] || 'vertical', onChange: (e) => handleChange('panel-orientation', e.target.value), style: {
|
|
4946
|
+
width: '100%',
|
|
4947
|
+
padding: '8px 12px',
|
|
4948
|
+
border: '1px solid #ccc',
|
|
4949
|
+
borderRadius: '4px',
|
|
4950
|
+
fontSize: '12px',
|
|
4951
|
+
background: 'white',
|
|
4952
|
+
}, children: ORIENTATIONS.map((opt) => (jsxRuntimeExports.jsx("option", { value: opt, children: opt }, opt))) })] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: "Column Span" }), jsxRuntimeExports.jsx("input", { type: "number", value: panel['panel-column-span'] || 1, onChange: (e) => handleChange('panel-column-span', parseInt(e.target.value) || 1), min: "1", style: {
|
|
4953
|
+
width: '100%',
|
|
4954
|
+
padding: '8px 12px',
|
|
4955
|
+
border: '1px solid #ccc',
|
|
4956
|
+
borderRadius: '4px',
|
|
4957
|
+
fontSize: '12px',
|
|
4958
|
+
} })] })] }));
|
|
4959
|
+
};
|
|
4960
|
+
const renderWidgetProperties = () => {
|
|
4961
|
+
const widget = localData;
|
|
4962
|
+
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsxs("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: ["Widget Type ", jsxRuntimeExports.jsx("span", { style: { color: '#e74c3c' }, children: "*" })] }), jsxRuntimeExports.jsx("select", { value: widget.widget || '', onChange: (e) => {
|
|
4963
|
+
const newWidgetType = e.target.value;
|
|
4964
|
+
// When widget type changes, preserve common fields but clear widget-specific ones
|
|
4965
|
+
const updated = {
|
|
4966
|
+
...widget,
|
|
4967
|
+
widget: newWidgetType,
|
|
4968
|
+
};
|
|
4969
|
+
// Clear widget-specific fields that don't apply to the new type
|
|
4970
|
+
if (!['select', 'radio', 'checkbox'].includes(newWidgetType)) {
|
|
4971
|
+
delete updated['widget-data-source'];
|
|
4972
|
+
}
|
|
4973
|
+
if (newWidgetType !== 'table') {
|
|
4974
|
+
delete updated['widget-data-columns'];
|
|
4975
|
+
}
|
|
4976
|
+
setLocalData(updated);
|
|
4977
|
+
onChange(node, updated);
|
|
4978
|
+
}, style: {
|
|
4979
|
+
width: '100%',
|
|
4980
|
+
padding: '8px 12px',
|
|
4981
|
+
border: '1px solid #ccc',
|
|
4982
|
+
borderRadius: '4px',
|
|
4983
|
+
fontSize: '12px',
|
|
4984
|
+
background: 'white',
|
|
4985
|
+
}, children: WIDGET_TYPES.map((type) => (jsxRuntimeExports.jsx("option", { value: type, children: type }, type))) })] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsxs("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: ["Widget ID ", jsxRuntimeExports.jsx("span", { style: { color: '#e74c3c' }, children: "*" })] }), jsxRuntimeExports.jsx("input", { type: "text", value: widget['widget-id'] || '', onChange: (e) => handleChange('widget-id', e.target.value), style: {
|
|
4986
|
+
width: '100%',
|
|
4987
|
+
padding: '8px 12px',
|
|
4988
|
+
border: '1px solid #ccc',
|
|
4989
|
+
borderRadius: '4px',
|
|
4990
|
+
fontSize: '12px',
|
|
4991
|
+
} })] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: "Widget Label" }), jsxRuntimeExports.jsx("input", { type: "text", value: widget['widget-label'] || '', onChange: (e) => handleChange('widget-label', e.target.value), style: {
|
|
4992
|
+
width: '100%',
|
|
4993
|
+
padding: '8px 12px',
|
|
4994
|
+
border: '1px solid #ccc',
|
|
4995
|
+
borderRadius: '4px',
|
|
4996
|
+
fontSize: '12px',
|
|
4997
|
+
}, placeholder: "Enter label..." })] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: "Data Path" }), jsxRuntimeExports.jsx("input", { type: "text", value: typeof widget['widget-data-path'] === 'string' ? widget['widget-data-path'] : '', onChange: (e) => handleChange('widget-data-path', e.target.value), style: {
|
|
4998
|
+
width: '100%',
|
|
4999
|
+
padding: '8px 12px',
|
|
5000
|
+
border: '1px solid #ccc',
|
|
5001
|
+
borderRadius: '4px',
|
|
5002
|
+
fontSize: '12px',
|
|
5003
|
+
}, placeholder: "person.name" })] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '12px', fontWeight: 500 }, children: "Placeholder" }), jsxRuntimeExports.jsx("input", { type: "text", value: widget['widget-data-placeholder'] || '', onChange: (e) => handleChange('widget-data-placeholder', e.target.value), style: {
|
|
5004
|
+
width: '100%',
|
|
5005
|
+
padding: '8px 12px',
|
|
5006
|
+
border: '1px solid #ccc',
|
|
5007
|
+
borderRadius: '4px',
|
|
5008
|
+
fontSize: '12px',
|
|
5009
|
+
}, placeholder: "Enter placeholder..." })] }), jsxRuntimeExports.jsx("div", { style: { marginBottom: '20px' }, children: jsxRuntimeExports.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px' }, children: [jsxRuntimeExports.jsx("input", { type: "checkbox", checked: widget['widget-required'] || false, onChange: (e) => handleChange('widget-required', e.target.checked) }), jsxRuntimeExports.jsx("span", { children: "Required" })] }) }), jsxRuntimeExports.jsx("div", { style: { marginBottom: '20px' }, children: jsxRuntimeExports.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px' }, children: [jsxRuntimeExports.jsx("input", { type: "checkbox", checked: widget['widget-readonly'] || false, onChange: (e) => handleChange('widget-readonly', e.target.checked) }), jsxRuntimeExports.jsx("span", { children: "Readonly" })] }) }), ['select', 'radio', 'checkbox'].includes(widget.widget) && (jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px', padding: '10px', background: '#e3f2fd', borderRadius: '4px' }, children: [jsxRuntimeExports.jsxs("label", { style: { display: 'block', marginBottom: '8px', fontSize: '12px', fontWeight: 600 }, children: ["Data Source (Required for ", widget.widget, ")"] }), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '10px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Source Type" }), jsxRuntimeExports.jsxs("select", { value: widget['widget-data-source']?.type || 'static', onChange: (e) => {
|
|
5010
|
+
ensureNestedObject('widget-data-source');
|
|
5011
|
+
handleNestedChange('widget-data-source', 'type', e.target.value);
|
|
5012
|
+
}, style: {
|
|
5013
|
+
width: '100%',
|
|
5014
|
+
padding: '6px 8px',
|
|
5015
|
+
border: '1px solid #ccc',
|
|
5016
|
+
borderRadius: '4px',
|
|
5017
|
+
fontSize: '11px',
|
|
5018
|
+
background: 'white',
|
|
5019
|
+
}, children: [jsxRuntimeExports.jsx("option", { value: "static", children: "Static" }), jsxRuntimeExports.jsx("option", { value: "api", children: "API" }), jsxRuntimeExports.jsx("option", { value: "schema", children: "Schema" })] })] }), widget['widget-data-source']?.type === 'static' && (jsxRuntimeExports.jsxs("div", { children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Options (JSON array format: see placeholder below)" }), jsxRuntimeExports.jsx("textarea", { value: JSON.stringify(widget['widget-data-source']?.options || [], null, 2), onChange: (e) => {
|
|
5020
|
+
try {
|
|
5021
|
+
const parsed = JSON.parse(e.target.value);
|
|
5022
|
+
handleNestedChange('widget-data-source', 'options', parsed);
|
|
5023
|
+
}
|
|
5024
|
+
catch {
|
|
5025
|
+
// Invalid JSON, ignore
|
|
5026
|
+
}
|
|
5027
|
+
}, style: {
|
|
5028
|
+
width: '100%',
|
|
5029
|
+
padding: '6px 8px',
|
|
5030
|
+
border: '1px solid #ccc',
|
|
5031
|
+
borderRadius: '4px',
|
|
5032
|
+
fontSize: '11px',
|
|
5033
|
+
fontFamily: 'monospace',
|
|
5034
|
+
minHeight: '80px',
|
|
5035
|
+
}, placeholder: '[{"value": "opt1", "label": "Option 1"}]' })] })), widget['widget-data-source']?.type === 'api' && (() => {
|
|
5036
|
+
const apiSource = widget['widget-data-source'];
|
|
5037
|
+
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("div", { style: { marginBottom: '10px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "API URL" }), jsxRuntimeExports.jsx("input", { type: "text", value: apiSource.url || '', onChange: (e) => handleNestedChange('widget-data-source', 'url', e.target.value), style: {
|
|
5038
|
+
width: '100%',
|
|
5039
|
+
padding: '6px 8px',
|
|
5040
|
+
border: '1px solid #ccc',
|
|
5041
|
+
borderRadius: '4px',
|
|
5042
|
+
fontSize: '11px',
|
|
5043
|
+
}, placeholder: "https://api.example.com/options" })] }), jsxRuntimeExports.jsxs("div", { children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Value/Label Keys (e.g., \"id\"/\"name\")" }), jsxRuntimeExports.jsxs("div", { style: { display: 'flex', gap: '8px' }, children: [jsxRuntimeExports.jsx("input", { type: "text", value: apiSource.valueKey || '', onChange: (e) => handleNestedChange('widget-data-source', 'valueKey', e.target.value), style: {
|
|
5044
|
+
flex: 1,
|
|
5045
|
+
padding: '6px 8px',
|
|
5046
|
+
border: '1px solid #ccc',
|
|
5047
|
+
borderRadius: '4px',
|
|
5048
|
+
fontSize: '11px',
|
|
5049
|
+
}, placeholder: "value key" }), jsxRuntimeExports.jsx("input", { type: "text", value: apiSource.labelKey || '', onChange: (e) => handleNestedChange('widget-data-source', 'labelKey', e.target.value), style: {
|
|
5050
|
+
flex: 1,
|
|
5051
|
+
padding: '6px 8px',
|
|
5052
|
+
border: '1px solid #ccc',
|
|
5053
|
+
borderRadius: '4px',
|
|
5054
|
+
fontSize: '11px',
|
|
5055
|
+
}, placeholder: "label key" })] })] })] }));
|
|
5056
|
+
})()] })), widget.widget === 'table' && (jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px', padding: '10px', background: '#fff3e0', borderRadius: '4px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '8px', fontSize: '12px', fontWeight: 600 }, children: "Table Columns" }), jsxRuntimeExports.jsx("div", { style: { fontSize: '11px', color: '#666', marginBottom: '10px' }, children: "Configure columns in JSON editor or add via code" }), jsxRuntimeExports.jsx("textarea", { value: JSON.stringify(widget['widget-data-columns'] || [], null, 2), onChange: (e) => {
|
|
5057
|
+
try {
|
|
5058
|
+
const parsed = JSON.parse(e.target.value);
|
|
5059
|
+
handleChange('widget-data-columns', parsed);
|
|
5060
|
+
}
|
|
5061
|
+
catch {
|
|
5062
|
+
// Invalid JSON, ignore
|
|
5063
|
+
}
|
|
5064
|
+
}, style: {
|
|
5065
|
+
width: '100%',
|
|
5066
|
+
padding: '6px 8px',
|
|
5067
|
+
border: '1px solid #ccc',
|
|
5068
|
+
borderRadius: '4px',
|
|
5069
|
+
fontSize: '11px',
|
|
5070
|
+
fontFamily: 'monospace',
|
|
5071
|
+
minHeight: '100px',
|
|
5072
|
+
}, placeholder: '[{"column-key": "col1", "widget-label": "Column 1", "widget": "text"}]' })] })), widget.widget === 'number' && (jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px', padding: '10px', background: '#f3e5f5', borderRadius: '4px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '8px', fontSize: '12px', fontWeight: 600 }, children: "Number Validation" }), jsxRuntimeExports.jsxs("div", { style: { display: 'flex', gap: '10px', marginBottom: '10px' }, children: [jsxRuntimeExports.jsxs("div", { style: { flex: 1 }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Min Value" }), jsxRuntimeExports.jsx("input", { type: "number", value: widget['widget-data-validation']?.min ?? '', onChange: (e) => handleNestedChange('widget-data-validation', 'min', e.target.value ? parseFloat(e.target.value) : undefined), style: {
|
|
5073
|
+
width: '100%',
|
|
5074
|
+
padding: '6px 8px',
|
|
5075
|
+
border: '1px solid #ccc',
|
|
5076
|
+
borderRadius: '4px',
|
|
5077
|
+
fontSize: '11px',
|
|
5078
|
+
} })] }), jsxRuntimeExports.jsxs("div", { style: { flex: 1 }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Max Value" }), jsxRuntimeExports.jsx("input", { type: "number", value: widget['widget-data-validation']?.max ?? '', onChange: (e) => handleNestedChange('widget-data-validation', 'max', e.target.value ? parseFloat(e.target.value) : undefined), style: {
|
|
5079
|
+
width: '100%',
|
|
5080
|
+
padding: '6px 8px',
|
|
5081
|
+
border: '1px solid #ccc',
|
|
5082
|
+
borderRadius: '4px',
|
|
5083
|
+
fontSize: '11px',
|
|
5084
|
+
} })] })] })] })), ['date', 'datetime'].includes(widget.widget) && (jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px', padding: '10px', background: '#e8f5e9', borderRadius: '4px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '8px', fontSize: '12px', fontWeight: 600 }, children: "Date Range Options" }), jsxRuntimeExports.jsxs("div", { style: { display: 'flex', gap: '10px', marginBottom: '10px' }, children: [jsxRuntimeExports.jsxs("div", { style: { flex: 1 }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Min Date" }), jsxRuntimeExports.jsx("input", { type: "date", value: widget['widget-data-options']?.minDate || '', onChange: (e) => handleNestedChange('widget-data-options', 'minDate', e.target.value || undefined), style: {
|
|
5085
|
+
width: '100%',
|
|
5086
|
+
padding: '6px 8px',
|
|
5087
|
+
border: '1px solid #ccc',
|
|
5088
|
+
borderRadius: '4px',
|
|
5089
|
+
fontSize: '11px',
|
|
5090
|
+
} })] }), jsxRuntimeExports.jsxs("div", { style: { flex: 1 }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Max Date" }), jsxRuntimeExports.jsx("input", { type: "date", value: widget['widget-data-options']?.maxDate || '', onChange: (e) => handleNestedChange('widget-data-options', 'maxDate', e.target.value || undefined), style: {
|
|
5091
|
+
width: '100%',
|
|
5092
|
+
padding: '6px 8px',
|
|
5093
|
+
border: '1px solid #ccc',
|
|
5094
|
+
borderRadius: '4px',
|
|
5095
|
+
fontSize: '11px',
|
|
5096
|
+
} })] })] }), jsxRuntimeExports.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '11px' }, children: [jsxRuntimeExports.jsx("input", { type: "checkbox", checked: widget['widget-data-options']?.showCalendar || false, onChange: (e) => handleNestedChange('widget-data-options', 'showCalendar', e.target.checked) }), jsxRuntimeExports.jsx("span", { children: "Show Calendar" })] })] })), jsxRuntimeExports.jsxs("div", { style: { marginBottom: '20px', padding: '10px', background: '#f0f0f0', borderRadius: '4px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '8px', fontSize: '12px', fontWeight: 600 }, children: "Validation" }), jsxRuntimeExports.jsx("div", { style: { marginBottom: '10px' }, children: jsxRuntimeExports.jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px' }, children: [jsxRuntimeExports.jsx("input", { type: "checkbox", checked: widget['widget-data-validation']?.required || false, onChange: (e) => {
|
|
5097
|
+
if (!widget['widget-data-validation']) {
|
|
5098
|
+
handleChange('widget-data-validation', { required: e.target.checked });
|
|
5099
|
+
}
|
|
5100
|
+
else {
|
|
5101
|
+
handleNestedChange('widget-data-validation', 'required', e.target.checked);
|
|
5102
|
+
}
|
|
5103
|
+
} }), jsxRuntimeExports.jsx("span", { children: "Required" })] }) }), ['text', 'textarea'].includes(widget.widget) && (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("div", { style: { marginBottom: '10px' }, children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Min Length" }), jsxRuntimeExports.jsx("input", { type: "number", value: widget['widget-data-validation']?.minLength || '', onChange: (e) => {
|
|
5104
|
+
const validation = widget['widget-data-validation'] || {};
|
|
5105
|
+
handleChange('widget-data-validation', {
|
|
5106
|
+
...validation,
|
|
5107
|
+
minLength: e.target.value ? parseInt(e.target.value) : undefined,
|
|
5108
|
+
});
|
|
5109
|
+
}, style: {
|
|
5110
|
+
width: '100%',
|
|
5111
|
+
padding: '6px 8px',
|
|
5112
|
+
border: '1px solid #ccc',
|
|
5113
|
+
borderRadius: '4px',
|
|
5114
|
+
fontSize: '11px',
|
|
5115
|
+
} })] }), jsxRuntimeExports.jsxs("div", { children: [jsxRuntimeExports.jsx("label", { style: { display: 'block', marginBottom: '5px', fontSize: '11px' }, children: "Max Length" }), jsxRuntimeExports.jsx("input", { type: "number", value: widget['widget-data-validation']?.maxLength || '', onChange: (e) => {
|
|
5116
|
+
const validation = widget['widget-data-validation'] || {};
|
|
5117
|
+
handleChange('widget-data-validation', {
|
|
5118
|
+
...validation,
|
|
5119
|
+
maxLength: e.target.value ? parseInt(e.target.value) : undefined,
|
|
5120
|
+
});
|
|
5121
|
+
}, style: {
|
|
5122
|
+
width: '100%',
|
|
5123
|
+
padding: '6px 8px',
|
|
5124
|
+
border: '1px solid #ccc',
|
|
5125
|
+
borderRadius: '4px',
|
|
5126
|
+
fontSize: '11px',
|
|
5127
|
+
} })] })] }))] })] }));
|
|
5128
|
+
};
|
|
5129
|
+
const getHeaderColor = () => {
|
|
5130
|
+
switch (node.type) {
|
|
5131
|
+
case 'section':
|
|
5132
|
+
return '#2196f3';
|
|
5133
|
+
case 'panel':
|
|
5134
|
+
return '#ff9800';
|
|
5135
|
+
case 'widget':
|
|
5136
|
+
return '#4caf50';
|
|
5137
|
+
}
|
|
5138
|
+
};
|
|
5139
|
+
return (jsxRuntimeExports.jsxs("div", { style: {
|
|
5140
|
+
padding: '15px',
|
|
5141
|
+
overflowY: 'auto',
|
|
5142
|
+
background: '#f8f9fa',
|
|
5143
|
+
height: '100%',
|
|
5144
|
+
}, children: [jsxRuntimeExports.jsx("h3", { style: { fontSize: '14px', marginBottom: '15px', color: '#2c3e50' }, children: "Properties" }), jsxRuntimeExports.jsxs("div", { style: {
|
|
5145
|
+
background: getHeaderColor(),
|
|
5146
|
+
color: 'white',
|
|
5147
|
+
padding: '12px',
|
|
5148
|
+
borderRadius: '4px',
|
|
5149
|
+
marginBottom: '20px',
|
|
5150
|
+
fontWeight: 600,
|
|
5151
|
+
fontSize: '13px',
|
|
5152
|
+
}, children: [node.type === 'section' && `Section: ${node.data['section-id']}`, node.type === 'panel' && `Panel: ${node.data['panel-id']}`, node.type === 'widget' && `Widget: ${node.data['widget-id']}`] }), node.type === 'section' && renderSectionProperties(), node.type === 'panel' && renderPanelProperties(), node.type === 'widget' && renderWidgetProperties(), jsxRuntimeExports.jsxs("div", { style: { display: 'flex', gap: '10px', marginTop: '30px' }, children: [jsxRuntimeExports.jsx("button", { onClick: () => onDelete(node), style: {
|
|
5153
|
+
flex: 1,
|
|
5154
|
+
padding: '10px',
|
|
5155
|
+
border: 'none',
|
|
5156
|
+
borderRadius: '4px',
|
|
5157
|
+
background: '#f44336',
|
|
5158
|
+
color: 'white',
|
|
5159
|
+
fontWeight: 600,
|
|
5160
|
+
cursor: 'pointer',
|
|
5161
|
+
fontSize: '12px',
|
|
5162
|
+
}, children: "Delete" }), jsxRuntimeExports.jsx("button", { onClick: () => onDuplicate(node), style: {
|
|
5163
|
+
flex: 1,
|
|
5164
|
+
padding: '10px',
|
|
5165
|
+
border: 'none',
|
|
5166
|
+
borderRadius: '4px',
|
|
5167
|
+
background: '#ff9800',
|
|
5168
|
+
color: 'white',
|
|
5169
|
+
fontWeight: 600,
|
|
5170
|
+
cursor: 'pointer',
|
|
5171
|
+
fontSize: '12px',
|
|
5172
|
+
}, children: "Duplicate" })] })] }));
|
|
5173
|
+
};
|
|
5174
|
+
|
|
5175
|
+
/**
|
|
5176
|
+
* Visual Builder Panel - Right side of Section Builder
|
|
5177
|
+
*/
|
|
5178
|
+
const VisualBuilderPanel = ({ section, selectedNode, onSelectNode, onSectionChange, onAddPanel, onAddWidget, onDeleteNode, onDuplicateNode, onSave, isMaximized = false, onToggleMaximize, }) => {
|
|
5179
|
+
// Validate section before saving
|
|
5180
|
+
const validateSection = (sectionToValidate) => {
|
|
5181
|
+
const errors = [];
|
|
5182
|
+
if (!sectionToValidate['section-id']) {
|
|
5183
|
+
errors.push('Section ID is required');
|
|
5184
|
+
}
|
|
5185
|
+
if (!sectionToValidate.panels || sectionToValidate.panels.length === 0) {
|
|
5186
|
+
errors.push('Section must have at least one panel');
|
|
5187
|
+
}
|
|
5188
|
+
// Validate panels
|
|
5189
|
+
const validatePanels = (panels) => {
|
|
5190
|
+
panels.forEach((panel, index) => {
|
|
5191
|
+
if (!panel['panel-id']) {
|
|
5192
|
+
errors.push(`Panel at index ${index} is missing panel-id`);
|
|
5193
|
+
}
|
|
5194
|
+
if (panel.panels) {
|
|
5195
|
+
validatePanels(panel.panels);
|
|
5196
|
+
}
|
|
5197
|
+
if (panel.widgets) {
|
|
5198
|
+
panel.widgets.forEach((widget, widgetIndex) => {
|
|
5199
|
+
if (!widget['widget-id']) {
|
|
5200
|
+
errors.push(`Widget at panel ${panel['panel-id'] || index}, index ${widgetIndex} is missing widget-id`);
|
|
5201
|
+
}
|
|
5202
|
+
if (!widget.widget) {
|
|
5203
|
+
errors.push(`Widget ${widget['widget-id'] || widgetIndex} is missing widget type`);
|
|
5204
|
+
}
|
|
5205
|
+
});
|
|
5206
|
+
}
|
|
5207
|
+
});
|
|
5208
|
+
};
|
|
5209
|
+
if (sectionToValidate.panels) {
|
|
5210
|
+
validatePanels(sectionToValidate.panels);
|
|
5211
|
+
}
|
|
5212
|
+
return {
|
|
5213
|
+
isValid: errors.length === 0,
|
|
5214
|
+
errors,
|
|
5215
|
+
};
|
|
5216
|
+
};
|
|
5217
|
+
const handleSave = () => {
|
|
5218
|
+
const validation = validateSection(section);
|
|
5219
|
+
if (!validation.isValid) {
|
|
5220
|
+
console.error('Section validation failed:', validation.errors);
|
|
5221
|
+
alert(`Cannot save section. Please fix the following errors:\n\n${validation.errors.join('\n')}`);
|
|
5222
|
+
return;
|
|
5223
|
+
}
|
|
5224
|
+
if (onSave) {
|
|
5225
|
+
try {
|
|
5226
|
+
onSave(section);
|
|
5227
|
+
}
|
|
5228
|
+
catch (error) {
|
|
5229
|
+
console.error('Error saving section:', error);
|
|
5230
|
+
alert('An error occurred while saving the section. Please check the console for details.');
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
5233
|
+
};
|
|
5234
|
+
const handleNodeChange = (node, updates) => {
|
|
5235
|
+
// Create a deep copy of the section
|
|
5236
|
+
const updatedSection = JSON.parse(JSON.stringify(section));
|
|
5237
|
+
// Find and update the node in the section structure
|
|
5238
|
+
const updateInSection = (current, targetId, targetType) => {
|
|
5239
|
+
if (targetType === 'section' && current['section-id'] === targetId) {
|
|
5240
|
+
Object.assign(current, updates);
|
|
5241
|
+
return true;
|
|
5242
|
+
}
|
|
5243
|
+
if (current.panels) {
|
|
5244
|
+
for (const panel of current.panels) {
|
|
5245
|
+
if (targetType === 'panel' && panel['panel-id'] === targetId) {
|
|
5246
|
+
Object.assign(panel, updates);
|
|
5247
|
+
return true;
|
|
5248
|
+
}
|
|
5249
|
+
if (updateInSection(panel, targetId, targetType)) {
|
|
5250
|
+
return true;
|
|
5251
|
+
}
|
|
5252
|
+
if (panel.widgets) {
|
|
5253
|
+
for (const widget of panel.widgets) {
|
|
5254
|
+
if (targetType === 'widget' && widget['widget-id'] === targetId) {
|
|
5255
|
+
Object.assign(widget, updates);
|
|
5256
|
+
return true;
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
if (current.widgets) {
|
|
5263
|
+
for (const widget of current.widgets) {
|
|
5264
|
+
if (targetType === 'widget' && widget['widget-id'] === targetId) {
|
|
5265
|
+
Object.assign(widget, updates);
|
|
5266
|
+
return true;
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5270
|
+
return false;
|
|
5271
|
+
};
|
|
5272
|
+
updateInSection(updatedSection, node.id, node.type);
|
|
5273
|
+
onSectionChange(updatedSection);
|
|
5274
|
+
};
|
|
5275
|
+
const handleAddPanel = () => {
|
|
5276
|
+
if (selectedNode) {
|
|
5277
|
+
if (selectedNode.type === 'section' || selectedNode.type === 'panel') {
|
|
5278
|
+
onAddPanel(selectedNode.id, selectedNode.type);
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
else {
|
|
5282
|
+
// Add to root section
|
|
5283
|
+
onAddPanel(section['section-id'], 'section');
|
|
5284
|
+
}
|
|
5285
|
+
};
|
|
5286
|
+
const handleAddWidget = () => {
|
|
5287
|
+
if (selectedNode) {
|
|
5288
|
+
if (selectedNode.type === 'panel') {
|
|
5289
|
+
onAddWidget(selectedNode.id);
|
|
5290
|
+
}
|
|
5291
|
+
else if (selectedNode.type === 'section') {
|
|
5292
|
+
// Find first panel or create one
|
|
5293
|
+
if (section.panels && section.panels.length > 0) {
|
|
5294
|
+
onAddWidget(section.panels[0]['panel-id']);
|
|
5295
|
+
}
|
|
5296
|
+
else {
|
|
5297
|
+
// Create a panel first, then add widget
|
|
5298
|
+
const newPanel = {
|
|
5299
|
+
'panel-id': `panel-${Date.now()}`,
|
|
5300
|
+
'panel-orientation': 'vertical',
|
|
5301
|
+
widgets: [],
|
|
5302
|
+
};
|
|
5303
|
+
const updatedSection = {
|
|
5304
|
+
...section,
|
|
5305
|
+
panels: [...(section.panels || []), newPanel],
|
|
5306
|
+
};
|
|
5307
|
+
onSectionChange(updatedSection);
|
|
5308
|
+
onAddWidget(newPanel['panel-id']);
|
|
5309
|
+
}
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
else {
|
|
5313
|
+
// Add to first panel or create one
|
|
5314
|
+
if (section.panels && section.panels.length > 0) {
|
|
5315
|
+
onAddWidget(section.panels[0]['panel-id']);
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5318
|
+
};
|
|
5319
|
+
return (jsxRuntimeExports.jsxs("div", { style: {
|
|
5320
|
+
display: 'flex',
|
|
5321
|
+
flexDirection: 'column',
|
|
5322
|
+
height: '100%',
|
|
5323
|
+
width: '100%',
|
|
5324
|
+
minHeight: 0,
|
|
5325
|
+
}, children: [jsxRuntimeExports.jsxs("div", { style: {
|
|
5326
|
+
padding: '15px 20px',
|
|
5327
|
+
background: '#ffffff',
|
|
5328
|
+
borderBottom: '1px solid #ddd',
|
|
5329
|
+
display: 'flex',
|
|
5330
|
+
justifyContent: 'space-between',
|
|
5331
|
+
alignItems: 'center',
|
|
5332
|
+
}, children: [jsxRuntimeExports.jsx("div", { style: { fontWeight: 600, fontSize: '16px', color: '#2c3e50' }, children: "Visual Builder" }), jsxRuntimeExports.jsxs("div", { style: { display: 'flex', gap: '10px' }, children: [jsxRuntimeExports.jsx("button", { onClick: handleAddPanel, style: {
|
|
5333
|
+
padding: '8px 16px',
|
|
5334
|
+
border: 'none',
|
|
5335
|
+
borderRadius: '4px',
|
|
5336
|
+
background: '#2196f3',
|
|
5337
|
+
color: 'white',
|
|
5338
|
+
fontWeight: 600,
|
|
5339
|
+
cursor: 'pointer',
|
|
5340
|
+
fontSize: '12px',
|
|
5341
|
+
whiteSpace: 'nowrap',
|
|
5342
|
+
}, children: "+ Add Panel" }), jsxRuntimeExports.jsx("button", { onClick: handleAddWidget, style: {
|
|
5343
|
+
padding: '8px 16px',
|
|
5344
|
+
border: 'none',
|
|
5345
|
+
borderRadius: '4px',
|
|
5346
|
+
background: '#4caf50',
|
|
5347
|
+
color: 'white',
|
|
5348
|
+
fontWeight: 600,
|
|
5349
|
+
cursor: 'pointer',
|
|
5350
|
+
fontSize: '12px',
|
|
5351
|
+
whiteSpace: 'nowrap',
|
|
5352
|
+
}, children: "+ Add Widget" }), onSave && (jsxRuntimeExports.jsx("button", { onClick: handleSave, style: {
|
|
5353
|
+
padding: '8px 16px',
|
|
5354
|
+
border: 'none',
|
|
5355
|
+
borderRadius: '4px',
|
|
5356
|
+
background: '#ff9800',
|
|
5357
|
+
color: 'white',
|
|
5358
|
+
fontWeight: 600,
|
|
5359
|
+
cursor: 'pointer',
|
|
5360
|
+
fontSize: '12px',
|
|
5361
|
+
whiteSpace: 'nowrap',
|
|
5362
|
+
}, children: "Save" })), onToggleMaximize && (jsxRuntimeExports.jsx("button", { onClick: onToggleMaximize, style: {
|
|
5363
|
+
padding: '8px',
|
|
5364
|
+
border: 'none',
|
|
5365
|
+
borderRadius: '4px',
|
|
5366
|
+
background: 'transparent',
|
|
5367
|
+
color: '#666',
|
|
5368
|
+
cursor: 'pointer',
|
|
5369
|
+
display: 'flex',
|
|
5370
|
+
alignItems: 'center',
|
|
5371
|
+
justifyContent: 'center',
|
|
5372
|
+
width: '32px',
|
|
5373
|
+
height: '32px',
|
|
5374
|
+
}, title: isMaximized ? 'Minimize' : 'Maximize', children: isMaximized ? (jsxRuntimeExports.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsxRuntimeExports.jsx("path", { d: "M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3" }) })) : (jsxRuntimeExports.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsxRuntimeExports.jsx("path", { d: "M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" }) })) }))] })] }), jsxRuntimeExports.jsxs("div", { style: {
|
|
5375
|
+
flex: 1,
|
|
5376
|
+
display: 'flex',
|
|
5377
|
+
overflow: 'hidden',
|
|
5378
|
+
minHeight: 0, // Important for flex children to respect overflow
|
|
5379
|
+
}, children: [jsxRuntimeExports.jsx("div", { style: {
|
|
5380
|
+
width: '45%',
|
|
5381
|
+
borderRight: '1px solid #ddd',
|
|
5382
|
+
display: 'flex',
|
|
5383
|
+
flexDirection: 'column',
|
|
5384
|
+
minHeight: 0, // Important for flex children to respect overflow
|
|
5385
|
+
overflow: 'hidden',
|
|
5386
|
+
}, children: jsxRuntimeExports.jsx(SectionTree, { section: section, selectedNode: selectedNode, onSelectNode: onSelectNode, onAddPanel: onAddPanel, onAddWidget: onAddWidget, onDeleteNode: onDeleteNode, onDuplicateNode: onDuplicateNode }) }), jsxRuntimeExports.jsx("div", { style: {
|
|
5387
|
+
width: '55%',
|
|
5388
|
+
display: 'flex',
|
|
5389
|
+
flexDirection: 'column',
|
|
5390
|
+
minHeight: 0, // Important for flex children to respect overflow
|
|
5391
|
+
overflow: 'hidden',
|
|
5392
|
+
}, children: jsxRuntimeExports.jsx(PropertyEditor, { node: selectedNode, onChange: handleNodeChange, onDelete: onDeleteNode, onDuplicate: onDuplicateNode }) })] })] }));
|
|
5393
|
+
};
|
|
5394
|
+
|
|
5395
|
+
/**
|
|
5396
|
+
* Main Section Builder Component
|
|
5397
|
+
* Provides dual-panel interface for editing section JSON
|
|
5398
|
+
*/
|
|
5399
|
+
const SectionBuilder = ({ initialSection, onChange, onSave, }) => {
|
|
5400
|
+
const defaultSection = {
|
|
5401
|
+
'section-id': 'new-section',
|
|
5402
|
+
'section-title': '',
|
|
5403
|
+
'section-editable': false,
|
|
5404
|
+
panels: [],
|
|
5405
|
+
};
|
|
5406
|
+
const [section, setSection] = useState(initialSection || defaultSection);
|
|
5407
|
+
const [selectedNode, setSelectedNode] = useState(null);
|
|
5408
|
+
const [isMaximized, setIsMaximized] = useState(false);
|
|
5409
|
+
// Store the original section for reset functionality
|
|
5410
|
+
const originalSectionRef = useRef(initialSection ? JSON.parse(JSON.stringify(initialSection)) : defaultSection);
|
|
5411
|
+
const isInitialMount = useRef(true);
|
|
5412
|
+
useEffect(() => {
|
|
5413
|
+
if (initialSection) {
|
|
5414
|
+
// Only update original on initial mount
|
|
5415
|
+
if (isInitialMount.current) {
|
|
5416
|
+
originalSectionRef.current = JSON.parse(JSON.stringify(initialSection));
|
|
5417
|
+
isInitialMount.current = false;
|
|
5418
|
+
}
|
|
5419
|
+
setSection(initialSection);
|
|
5420
|
+
}
|
|
5421
|
+
}, [initialSection]);
|
|
5422
|
+
const handleSectionChange = useCallback((updatedSection) => {
|
|
5423
|
+
setSection(updatedSection);
|
|
5424
|
+
if (onChange) {
|
|
5425
|
+
onChange(updatedSection);
|
|
5426
|
+
}
|
|
5427
|
+
}, [onChange]);
|
|
5428
|
+
// Reset to original section - resets both JSON editor and visual builder
|
|
5429
|
+
const handleReset = useCallback(() => {
|
|
5430
|
+
const original = JSON.parse(JSON.stringify(originalSectionRef.current));
|
|
5431
|
+
setSection(original);
|
|
5432
|
+
setSelectedNode(null); // Clear selection on reset
|
|
5433
|
+
if (onChange) {
|
|
5434
|
+
onChange(original);
|
|
5435
|
+
}
|
|
5436
|
+
}, [onChange]);
|
|
5437
|
+
// Handle node selection from JSON editor
|
|
5438
|
+
const handleSelectNodeFromJson = useCallback((nodeId, nodeType) => {
|
|
5439
|
+
// Find the corresponding node in the section structure
|
|
5440
|
+
const findNode = () => {
|
|
5441
|
+
if (nodeType === 'section') {
|
|
5442
|
+
return {
|
|
5443
|
+
type: 'section',
|
|
5444
|
+
id: section['section-id'],
|
|
5445
|
+
label: `Section: ${section['section-id']}`,
|
|
5446
|
+
data: section,
|
|
5447
|
+
};
|
|
5448
|
+
}
|
|
5449
|
+
// Recursively search for panel or widget
|
|
5450
|
+
const searchInPanels = (panels, parent) => {
|
|
5451
|
+
for (const panel of panels) {
|
|
5452
|
+
if (nodeType === 'panel' && panel['panel-id'] === nodeId) {
|
|
5453
|
+
return {
|
|
5454
|
+
type: 'panel',
|
|
5455
|
+
id: panel['panel-id'],
|
|
5456
|
+
label: `Panel: ${panel['panel-id']}`,
|
|
5457
|
+
data: panel,
|
|
5458
|
+
parent: parent,
|
|
5459
|
+
};
|
|
5460
|
+
}
|
|
5461
|
+
// Check nested panels
|
|
5462
|
+
if (panel.panels) {
|
|
5463
|
+
const panelNode = {
|
|
5464
|
+
type: 'panel',
|
|
5465
|
+
id: panel['panel-id'],
|
|
5466
|
+
label: `Panel: ${panel['panel-id']}`,
|
|
5467
|
+
data: panel,
|
|
5468
|
+
parent: parent,
|
|
5469
|
+
};
|
|
5470
|
+
const found = searchInPanels(panel.panels, panelNode);
|
|
5471
|
+
if (found)
|
|
5472
|
+
return found;
|
|
5473
|
+
}
|
|
5474
|
+
// Check widgets
|
|
5475
|
+
if (panel.widgets) {
|
|
5476
|
+
const panelNode = {
|
|
5477
|
+
type: 'panel',
|
|
5478
|
+
id: panel['panel-id'],
|
|
5479
|
+
label: `Panel: ${panel['panel-id']}`,
|
|
5480
|
+
data: panel,
|
|
5481
|
+
parent: parent,
|
|
5482
|
+
};
|
|
5483
|
+
for (const widget of panel.widgets) {
|
|
5484
|
+
if (nodeType === 'widget' && widget['widget-id'] === nodeId) {
|
|
5485
|
+
return {
|
|
5486
|
+
type: 'widget',
|
|
5487
|
+
id: widget['widget-id'],
|
|
5488
|
+
label: `Widget: ${widget['widget-id']} (${widget.widget})`,
|
|
5489
|
+
data: widget,
|
|
5490
|
+
parent: panelNode,
|
|
5491
|
+
};
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
return null;
|
|
5497
|
+
};
|
|
5498
|
+
if (section.panels) {
|
|
5499
|
+
return searchInPanels(section.panels);
|
|
5500
|
+
}
|
|
5501
|
+
return null;
|
|
5502
|
+
};
|
|
5503
|
+
const node = findNode();
|
|
5504
|
+
if (node) {
|
|
5505
|
+
setSelectedNode(node);
|
|
5506
|
+
}
|
|
5507
|
+
}, [section]);
|
|
5508
|
+
const handleAddPanel = useCallback((parentId, parentType) => {
|
|
5509
|
+
const updatedSection = JSON.parse(JSON.stringify(section));
|
|
5510
|
+
const newPanel = {
|
|
5511
|
+
'panel-id': `panel-${Date.now()}`,
|
|
5512
|
+
'panel-orientation': 'vertical',
|
|
5513
|
+
widgets: [],
|
|
5514
|
+
};
|
|
5515
|
+
if (parentType === 'section') {
|
|
5516
|
+
updatedSection.panels = [...(updatedSection.panels || []), newPanel];
|
|
5517
|
+
}
|
|
5518
|
+
else if (parentType === 'panel') {
|
|
5519
|
+
const addPanelToParent = (panels) => {
|
|
5520
|
+
for (const panel of panels) {
|
|
5521
|
+
if (panel['panel-id'] === parentId) {
|
|
5522
|
+
panel.panels = [...(panel.panels || []), newPanel];
|
|
5523
|
+
return true;
|
|
5524
|
+
}
|
|
5525
|
+
if (panel.panels && addPanelToParent(panel.panels)) {
|
|
5526
|
+
return true;
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
return false;
|
|
5530
|
+
};
|
|
5531
|
+
if (updatedSection.panels) {
|
|
5532
|
+
addPanelToParent(updatedSection.panels);
|
|
5533
|
+
}
|
|
5534
|
+
}
|
|
5535
|
+
handleSectionChange(updatedSection);
|
|
5536
|
+
}, [section, handleSectionChange]);
|
|
5537
|
+
const handleAddWidget = useCallback((parentId) => {
|
|
5538
|
+
const updatedSection = JSON.parse(JSON.stringify(section));
|
|
5539
|
+
const newWidget = {
|
|
5540
|
+
widget: 'text',
|
|
5541
|
+
'widget-id': `widget-${Date.now()}`,
|
|
5542
|
+
'widget-label': 'New Widget',
|
|
5543
|
+
'widget-data-path': '',
|
|
5544
|
+
};
|
|
5545
|
+
const addWidgetToPanel = (panels) => {
|
|
5546
|
+
for (const panel of panels) {
|
|
5547
|
+
if (panel['panel-id'] === parentId) {
|
|
5548
|
+
panel.widgets = [...(panel.widgets || []), newWidget];
|
|
5549
|
+
return true;
|
|
5550
|
+
}
|
|
5551
|
+
if (panel.panels && addWidgetToPanel(panel.panels)) {
|
|
5552
|
+
return true;
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
return false;
|
|
5556
|
+
};
|
|
5557
|
+
if (updatedSection.panels) {
|
|
5558
|
+
addWidgetToPanel(updatedSection.panels);
|
|
5559
|
+
}
|
|
5560
|
+
handleSectionChange(updatedSection);
|
|
5561
|
+
}, [section, handleSectionChange]);
|
|
5562
|
+
const handleDeleteNode = useCallback((node) => {
|
|
5563
|
+
const updatedSection = JSON.parse(JSON.stringify(section));
|
|
5564
|
+
if (node.type === 'section') {
|
|
5565
|
+
// Can't delete section, but can reset it
|
|
5566
|
+
return;
|
|
5567
|
+
}
|
|
5568
|
+
const deleteFromSection = (current) => {
|
|
5569
|
+
if (current.panels) {
|
|
5570
|
+
const panelIndex = current.panels.findIndex((p) => p['panel-id'] === node.id);
|
|
5571
|
+
if (panelIndex !== -1 && node.type === 'panel') {
|
|
5572
|
+
current.panels.splice(panelIndex, 1);
|
|
5573
|
+
return true;
|
|
5574
|
+
}
|
|
5575
|
+
for (const panel of current.panels) {
|
|
5576
|
+
if (panel['panel-id'] === node.id && node.type === 'panel') {
|
|
5577
|
+
// This shouldn't happen due to findIndex above, but handle nested case
|
|
5578
|
+
const index = current.panels.indexOf(panel);
|
|
5579
|
+
if (index !== -1) {
|
|
5580
|
+
current.panels.splice(index, 1);
|
|
5581
|
+
return true;
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
if (panel.widgets) {
|
|
5585
|
+
const widgetIndex = panel.widgets.findIndex((w) => w['widget-id'] === node.id);
|
|
5586
|
+
if (widgetIndex !== -1 && node.type === 'widget') {
|
|
5587
|
+
panel.widgets.splice(widgetIndex, 1);
|
|
5588
|
+
return true;
|
|
5589
|
+
}
|
|
5590
|
+
}
|
|
5591
|
+
if (panel.panels && deleteFromSection(panel)) {
|
|
5592
|
+
return true;
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
return false;
|
|
5597
|
+
};
|
|
5598
|
+
deleteFromSection(updatedSection);
|
|
5599
|
+
handleSectionChange(updatedSection);
|
|
5600
|
+
setSelectedNode(null);
|
|
5601
|
+
}, [section, handleSectionChange]);
|
|
5602
|
+
const handleDuplicateNode = useCallback((node) => {
|
|
5603
|
+
const updatedSection = JSON.parse(JSON.stringify(section));
|
|
5604
|
+
if (node.type === 'section') {
|
|
5605
|
+
return;
|
|
5606
|
+
}
|
|
5607
|
+
const duplicateInSection = (current) => {
|
|
5608
|
+
if (current.panels) {
|
|
5609
|
+
for (const panel of current.panels) {
|
|
5610
|
+
if (panel['panel-id'] === node.id && node.type === 'panel') {
|
|
5611
|
+
const duplicated = {
|
|
5612
|
+
...panel,
|
|
5613
|
+
'panel-id': `${panel['panel-id']}-copy-${Date.now()}`,
|
|
5614
|
+
};
|
|
5615
|
+
const index = current.panels.indexOf(panel);
|
|
5616
|
+
current.panels.splice(index + 1, 0, duplicated);
|
|
5617
|
+
return true;
|
|
5618
|
+
}
|
|
5619
|
+
if (panel.widgets) {
|
|
5620
|
+
for (const widget of panel.widgets) {
|
|
5621
|
+
if (widget['widget-id'] === node.id && node.type === 'widget') {
|
|
5622
|
+
const duplicated = {
|
|
5623
|
+
...widget,
|
|
5624
|
+
'widget-id': `${widget['widget-id']}-copy-${Date.now()}`,
|
|
5625
|
+
};
|
|
5626
|
+
const index = panel.widgets.indexOf(widget);
|
|
5627
|
+
panel.widgets.splice(index + 1, 0, duplicated);
|
|
5628
|
+
return true;
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
}
|
|
5632
|
+
if (panel.panels && duplicateInSection(panel)) {
|
|
5633
|
+
return true;
|
|
5634
|
+
}
|
|
5635
|
+
}
|
|
5636
|
+
}
|
|
5637
|
+
return false;
|
|
5638
|
+
};
|
|
5639
|
+
duplicateInSection(updatedSection);
|
|
5640
|
+
handleSectionChange(updatedSection);
|
|
5641
|
+
}, [section, handleSectionChange]);
|
|
5642
|
+
const toggleMaximize = useCallback(() => {
|
|
5643
|
+
setIsMaximized((prev) => !prev);
|
|
5644
|
+
}, []);
|
|
5645
|
+
// Handle Escape key to exit fullscreen
|
|
5646
|
+
useEffect(() => {
|
|
5647
|
+
if (!isMaximized)
|
|
5648
|
+
return;
|
|
5649
|
+
const handleEscape = (e) => {
|
|
5650
|
+
if (e.key === 'Escape') {
|
|
5651
|
+
setIsMaximized(false);
|
|
5652
|
+
}
|
|
5653
|
+
};
|
|
5654
|
+
window.addEventListener('keydown', handleEscape);
|
|
5655
|
+
return () => window.removeEventListener('keydown', handleEscape);
|
|
5656
|
+
}, [isMaximized]);
|
|
5657
|
+
return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [isMaximized && (jsxRuntimeExports.jsx("div", { style: {
|
|
5658
|
+
position: 'fixed',
|
|
5659
|
+
top: 0,
|
|
5660
|
+
left: 0,
|
|
5661
|
+
right: 0,
|
|
5662
|
+
bottom: 0,
|
|
5663
|
+
background: 'rgba(0, 0, 0, 0.5)',
|
|
5664
|
+
zIndex: 9998,
|
|
5665
|
+
}, onClick: toggleMaximize })), jsxRuntimeExports.jsxs("div", { style: {
|
|
5666
|
+
display: 'flex',
|
|
5667
|
+
height: isMaximized ? '100vh' : '100%',
|
|
5668
|
+
width: isMaximized ? '100vw' : '100%',
|
|
5669
|
+
minHeight: 0,
|
|
5670
|
+
background: '#FFFFFF',
|
|
5671
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
5672
|
+
overflow: 'hidden',
|
|
5673
|
+
border: 'none',
|
|
5674
|
+
position: isMaximized ? 'fixed' : 'relative',
|
|
5675
|
+
top: isMaximized ? 0 : 'auto',
|
|
5676
|
+
left: isMaximized ? 0 : 'auto',
|
|
5677
|
+
zIndex: isMaximized ? 9999 : 'auto',
|
|
5678
|
+
}, children: [jsxRuntimeExports.jsx("div", { style: {
|
|
5679
|
+
width: '50%',
|
|
5680
|
+
height: '100%',
|
|
5681
|
+
minHeight: 0,
|
|
5682
|
+
overflow: 'hidden',
|
|
5683
|
+
display: 'flex',
|
|
5684
|
+
padding: '10px 10px 10px 10px',
|
|
5685
|
+
flexDirection: 'column',
|
|
5686
|
+
borderRight: '0px',
|
|
5687
|
+
}, children: jsxRuntimeExports.jsx(JSONEditorPanel, { section: section, onChange: handleSectionChange, onReset: handleReset, onSelectNode: handleSelectNodeFromJson }) }), jsxRuntimeExports.jsx("div", { style: {
|
|
5688
|
+
width: '50%',
|
|
5689
|
+
height: '100%',
|
|
5690
|
+
minHeight: 0,
|
|
5691
|
+
overflow: 'hidden',
|
|
5692
|
+
display: 'flex',
|
|
5693
|
+
flexDirection: 'column',
|
|
5694
|
+
}, children: jsxRuntimeExports.jsx(VisualBuilderPanel, { section: section, selectedNode: selectedNode, onSelectNode: setSelectedNode, onSectionChange: handleSectionChange, onAddPanel: handleAddPanel, onAddWidget: handleAddWidget, onDeleteNode: handleDeleteNode, onDuplicateNode: handleDuplicateNode, onSave: onSave, isMaximized: isMaximized, onToggleMaximize: toggleMaximize }) })] })] }));
|
|
5695
|
+
};
|
|
5696
|
+
|
|
5697
|
+
/**
|
|
5698
|
+
* Filter input value based on allowed character type
|
|
5699
|
+
*/
|
|
5700
|
+
const filterByCharacterType = (value, characterType = 'any', customCharset) => {
|
|
5701
|
+
if (characterType === 'any') {
|
|
5702
|
+
return value;
|
|
3226
5703
|
}
|
|
3227
5704
|
let regex;
|
|
3228
5705
|
switch (characterType) {
|
|
@@ -4680,87 +7157,54 @@ const RadioWidget = ({ config }) => {
|
|
|
4680
7157
|
}
|
|
4681
7158
|
return options;
|
|
4682
7159
|
}, [dataSourceOptions, sortOptions]);
|
|
4683
|
-
// Handle
|
|
4684
|
-
useCallback((
|
|
4685
|
-
onChange(
|
|
7160
|
+
// Handle value change
|
|
7161
|
+
const handleChange = useCallback((optionValue) => {
|
|
7162
|
+
onChange(optionValue);
|
|
4686
7163
|
}, [onChange]);
|
|
4687
|
-
//
|
|
7164
|
+
// Handle unset (clear selection) - only if optional
|
|
7165
|
+
const handleUnset = useCallback(() => {
|
|
7166
|
+
if (allowUnset) {
|
|
7167
|
+
onChange(null);
|
|
7168
|
+
}
|
|
7169
|
+
}, [allowUnset, onChange]);
|
|
7170
|
+
// Determine current value (handle null/undefined for optional fields)
|
|
4688
7171
|
const currentValue = useMemo(() => {
|
|
4689
|
-
console.log('[RadioWidget] Value from useBaseWidget:', {
|
|
4690
|
-
value,
|
|
4691
|
-
type: typeof value,
|
|
4692
|
-
widgetId: widgetConfig['widget-id'],
|
|
4693
|
-
dataPath: widgetConfig['widget-data-path']
|
|
4694
|
-
});
|
|
4695
7172
|
if (value === null || value === undefined) {
|
|
4696
7173
|
return null;
|
|
4697
7174
|
}
|
|
4698
7175
|
return value;
|
|
4699
|
-
}, [value
|
|
4700
|
-
//
|
|
4701
|
-
const
|
|
4702
|
-
// Handle null/undefined cases
|
|
4703
|
-
if (currentValue === null || currentValue === undefined) {
|
|
4704
|
-
return optionValue === null || optionValue === undefined;
|
|
4705
|
-
}
|
|
4706
|
-
if (optionValue === null || optionValue === undefined) {
|
|
4707
|
-
return false;
|
|
4708
|
-
}
|
|
4709
|
-
// Convert both to strings and compare (handles any type mismatch)
|
|
4710
|
-
const currentStr = String(currentValue).trim();
|
|
4711
|
-
const optionStr = String(optionValue).trim();
|
|
4712
|
-
return currentStr === optionStr;
|
|
4713
|
-
}, [currentValue]);
|
|
4714
|
-
// Get layout classes
|
|
4715
|
-
const getLayoutClass = useMemo(() => {
|
|
7176
|
+
}, [value]);
|
|
7177
|
+
// Get layout classes and styles
|
|
7178
|
+
const layoutConfig = useMemo(() => {
|
|
4716
7179
|
switch (layout) {
|
|
4717
7180
|
case 'horizontal':
|
|
4718
|
-
return
|
|
7181
|
+
return {
|
|
7182
|
+
className: 'flex flex-row flex-wrap gap-4',
|
|
7183
|
+
style: undefined,
|
|
7184
|
+
};
|
|
4719
7185
|
case 'grid':
|
|
4720
|
-
|
|
4721
|
-
|
|
7186
|
+
// Calculate grid columns based on option count (max 4 columns, min 2)
|
|
7187
|
+
const cols = Math.max(2, Math.min(processedOptions.length, 4));
|
|
7188
|
+
return {
|
|
7189
|
+
className: 'grid gap-3',
|
|
7190
|
+
style: { gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` },
|
|
7191
|
+
};
|
|
4722
7192
|
case 'vertical':
|
|
4723
7193
|
default:
|
|
4724
|
-
return
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
if (layout === 'grid') {
|
|
4729
|
-
const cols = Math.max(2, Math.min(processedOptions.length, 4));
|
|
4730
|
-
return { gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` };
|
|
7194
|
+
return {
|
|
7195
|
+
className: 'flex flex-col space-y-2',
|
|
7196
|
+
style: undefined,
|
|
7197
|
+
};
|
|
4731
7198
|
}
|
|
4732
|
-
return undefined;
|
|
4733
7199
|
}, [layout, processedOptions.length]);
|
|
4734
7200
|
// For readonly mode, render as display text
|
|
4735
7201
|
if (widgetConfig['widget-readonly']) {
|
|
4736
7202
|
const label = translateConfig(widgetConfig['widget-label']);
|
|
4737
|
-
const selectedOption = processedOptions.find(opt =>
|
|
7203
|
+
const selectedOption = processedOptions.find(opt => opt.value === currentValue);
|
|
4738
7204
|
const displayValue = selectedOption ? selectedOption.label : (allowUnset && currentValue === null ? '-' : '');
|
|
4739
7205
|
return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] RadioDisplayWidget 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 }) })] }));
|
|
4740
7206
|
}
|
|
4741
|
-
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.jsx("div", { className:
|
|
4742
|
-
// onChange={() => handleRadioChange(null)}
|
|
4743
|
-
disabled: !isEnabled || widgetConfig['widget-readonly'], className: "mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" }), jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-700", children: "-" })] })), processedOptions.map((option, index) => {
|
|
4744
|
-
const isChecked = isValueSelected(option.value);
|
|
4745
|
-
// Debug log (remove after testing)
|
|
4746
|
-
if (index === 0) {
|
|
4747
|
-
console.log('[RadioWidget] Value check:', {
|
|
4748
|
-
currentValue,
|
|
4749
|
-
optionValue: option.value,
|
|
4750
|
-
currentType: typeof currentValue,
|
|
4751
|
-
optionType: typeof option.value,
|
|
4752
|
-
isChecked,
|
|
4753
|
-
currentStr: String(currentValue),
|
|
4754
|
-
optionStr: String(option.value)
|
|
4755
|
-
});
|
|
4756
|
-
}
|
|
4757
|
-
return (jsxRuntimeExports.jsxs("label", { className: `flex items-center cursor-pointer ${!isEnabled || widgetConfig['widget-readonly'] ? 'opacity-50 cursor-not-allowed' : ''}`, children: [jsxRuntimeExports.jsx("input", { type: "radio", name: widgetConfig['widget-id'], value: String(option.value), checked: isChecked,
|
|
4758
|
-
// onChange={() => {
|
|
4759
|
-
// console.log('[RadioWidget] onChange called:', option.value);
|
|
4760
|
-
// handleRadioChange(option.value);
|
|
4761
|
-
// }}
|
|
4762
|
-
disabled: !isEnabled || widgetConfig['widget-readonly'], className: "mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" }), jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-700", children: translateConfig(option.label) })] }, `${widgetConfig['widget-id']}-${option.value}-${index}`));
|
|
4763
|
-
})] })) }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-sm mt-1", children: error[0] }))] })] }) }));
|
|
7207
|
+
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.jsx("div", { className: layoutConfig.className, style: layoutConfig.style, onBlur: onBlur, children: loading ? (jsxRuntimeExports.jsx("p", { className: "text-sm text-gray-500", children: translate('common.loading') })) : (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [allowUnset && (jsxRuntimeExports.jsxs("label", { className: `flex items-center cursor-pointer ${!isEnabled || widgetConfig['widget-readonly'] ? 'opacity-50 cursor-not-allowed' : ''}`, children: [jsxRuntimeExports.jsx("input", { type: "radio", name: widgetConfig['widget-id'], checked: currentValue === null, onChange: handleUnset, disabled: !isEnabled || widgetConfig['widget-readonly'], className: "mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" }), jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-700", children: "-" })] })), processedOptions.map((option) => (jsxRuntimeExports.jsxs("label", { className: `flex items-center cursor-pointer ${!isEnabled || widgetConfig['widget-readonly'] ? 'opacity-50 cursor-not-allowed' : ''}`, children: [jsxRuntimeExports.jsx("input", { type: "radio", name: widgetConfig['widget-id'], value: option.value, checked: currentValue === option.value, onChange: (e) => handleChange(option.value), disabled: !isEnabled || widgetConfig['widget-readonly'], className: "mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" }), jsxRuntimeExports.jsx("span", { className: "text-sm text-gray-700", children: translateConfig(option.label) })] }, option.value)))] })) }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-red-500 text-sm mt-1", children: error[0] }))] })] }) }));
|
|
4764
7208
|
};
|
|
4765
7209
|
|
|
4766
7210
|
const CheckboxWidget = ({ config }) => {
|
|
@@ -5133,7 +7577,7 @@ const TableCellNumber = ({ config, value, onValueChange }) => {
|
|
|
5133
7577
|
const TableWidget = ({ config }) => {
|
|
5134
7578
|
const { value, error, touched, isEnabled, onChange, config: widgetConfig, } = useBaseWidget({ config });
|
|
5135
7579
|
const { translate, translateConfig } = useWidgetTranslation();
|
|
5136
|
-
const {
|
|
7580
|
+
const { dataSourceRequestHandler } = useWidgetContext();
|
|
5137
7581
|
const dispatch = useDispatch();
|
|
5138
7582
|
const storeValues = useSelector((state) => state.widget?.values || {});
|
|
5139
7583
|
const rows = Array.isArray(value) ? value : [];
|
|
@@ -5248,18 +7692,12 @@ const TableWidget = ({ config }) => {
|
|
|
5248
7692
|
setLoadingRowIndex(rowIndex);
|
|
5249
7693
|
try {
|
|
5250
7694
|
// If API config exists, make API call
|
|
5251
|
-
|
|
7695
|
+
// TODO: Update to use dataSourceRequestHandler pattern
|
|
7696
|
+
if (dataSourceRequestHandler && apiConfig.edit) {
|
|
5252
7697
|
const editConfig = apiConfig.edit;
|
|
5253
|
-
|
|
5254
|
-
//
|
|
5255
|
-
|
|
5256
|
-
url = url.replace('{id}', rowData.id);
|
|
5257
|
-
}
|
|
5258
|
-
await apiAdapter(url, {
|
|
5259
|
-
method: editConfig.method || 'PUT',
|
|
5260
|
-
headers: editConfig.headers || { 'Content-Type': 'application/json' },
|
|
5261
|
-
body: rowData,
|
|
5262
|
-
});
|
|
7698
|
+
// Extract service and endpoint from URL if possible, or use config
|
|
7699
|
+
// For now, API operations in TableWidget are disabled
|
|
7700
|
+
console.warn('[TableWidget] API edit operations require migration to dataSourceRequestHandler pattern');
|
|
5263
7701
|
}
|
|
5264
7702
|
// Update local state
|
|
5265
7703
|
const newRows = [...rows];
|
|
@@ -5320,7 +7758,7 @@ const TableWidget = ({ config }) => {
|
|
|
5320
7758
|
finally {
|
|
5321
7759
|
setLoadingRowIndex(null);
|
|
5322
7760
|
}
|
|
5323
|
-
}, [editingState, rows, onChange,
|
|
7761
|
+
}, [editingState, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode, originalRows, columns, widgetConfig, dispatch]);
|
|
5324
7762
|
// Add new row
|
|
5325
7763
|
const startAdd = useCallback(() => {
|
|
5326
7764
|
// If there's an unsaved edit, cancel it first (no confirmation needed)
|
|
@@ -5342,14 +7780,14 @@ const TableWidget = ({ config }) => {
|
|
|
5342
7780
|
try {
|
|
5343
7781
|
let savedRow = { ...newRowData };
|
|
5344
7782
|
// If API config exists, make API call
|
|
5345
|
-
|
|
7783
|
+
// TODO: Update to use dataSourceRequestHandler pattern
|
|
7784
|
+
if (dataSourceRequestHandler && apiConfig.add) {
|
|
5346
7785
|
const addConfig = apiConfig.add;
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
// Use response data if available (might contain generated ID)
|
|
7786
|
+
// Extract service and endpoint from URL if possible, or use config
|
|
7787
|
+
// For now, API operations in TableWidget are disabled
|
|
7788
|
+
console.warn('[TableWidget] API add operations require migration to dataSourceRequestHandler pattern');
|
|
7789
|
+
// Use newRowData as response for now
|
|
7790
|
+
const response = newRowData;
|
|
5353
7791
|
if (response && typeof response === 'object') {
|
|
5354
7792
|
savedRow = { ...savedRow, ...response };
|
|
5355
7793
|
}
|
|
@@ -5368,7 +7806,7 @@ const TableWidget = ({ config }) => {
|
|
|
5368
7806
|
finally {
|
|
5369
7807
|
setLoadingRowIndex(null);
|
|
5370
7808
|
}
|
|
5371
|
-
}, [isAdding, newRowData, rows, onChange,
|
|
7809
|
+
}, [isAdding, newRowData, rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode]);
|
|
5372
7810
|
// Delete row
|
|
5373
7811
|
const deleteRow = useCallback(async (rowIndex) => {
|
|
5374
7812
|
if (isAnyRowEditing) {
|
|
@@ -5384,21 +7822,16 @@ const TableWidget = ({ config }) => {
|
|
|
5384
7822
|
}
|
|
5385
7823
|
}, [isAnyRowEditing, showConfirmation, cancelEdit, translate]);
|
|
5386
7824
|
const performDelete = useCallback(async (rowIndex) => {
|
|
5387
|
-
|
|
7825
|
+
rows[rowIndex];
|
|
5388
7826
|
setLoadingRowIndex(rowIndex);
|
|
5389
7827
|
try {
|
|
5390
7828
|
// If API config exists, make API call
|
|
5391
|
-
|
|
7829
|
+
// TODO: Update to use dataSourceRequestHandler pattern
|
|
7830
|
+
if (dataSourceRequestHandler && apiConfig.delete) {
|
|
5392
7831
|
const deleteConfig = apiConfig.delete;
|
|
5393
|
-
|
|
5394
|
-
//
|
|
5395
|
-
|
|
5396
|
-
url = url.replace('{id}', row.id);
|
|
5397
|
-
}
|
|
5398
|
-
await apiAdapter(url, {
|
|
5399
|
-
method: deleteConfig.method || 'DELETE',
|
|
5400
|
-
headers: deleteConfig.headers || {},
|
|
5401
|
-
});
|
|
7832
|
+
// Extract service and endpoint from URL if possible, or use config
|
|
7833
|
+
// For now, API operations in TableWidget are disabled
|
|
7834
|
+
console.warn('[TableWidget] API delete operations require migration to dataSourceRequestHandler pattern');
|
|
5402
7835
|
}
|
|
5403
7836
|
// In section edit mode, mark row as deleted instead of removing it
|
|
5404
7837
|
if (isSectionEditMode) {
|
|
@@ -5422,7 +7855,7 @@ const TableWidget = ({ config }) => {
|
|
|
5422
7855
|
finally {
|
|
5423
7856
|
setLoadingRowIndex(null);
|
|
5424
7857
|
}
|
|
5425
|
-
}, [rows, onChange,
|
|
7858
|
+
}, [rows, onChange, dataSourceRequestHandler, apiConfig, translate, isSectionEditMode]);
|
|
5426
7859
|
// Get cell value (from editing state or row data)
|
|
5427
7860
|
const getCellValue = useCallback((rowIndex, columnKey) => {
|
|
5428
7861
|
// When a specific row is being edited (either in section edit mode or normal mode)
|
|
@@ -6311,5 +8744,5 @@ const translateUISchema = (schema, translate) => {
|
|
|
6311
8744
|
};
|
|
6312
8745
|
};
|
|
6313
8746
|
|
|
6314
|
-
export { ArrayWidget, BooleanWidget, CheckboxWidget, CurrencyInputWidget, DateInputWidget, DateTimeInputWidget, DisplayWidget, FileInputWidget, IterableAccordionWidget, NumberInputWidget, PanelRenderer, PhoneInputWidget, ProfileWidget, RadioWidget, SectionRenderer, SectionsContainer, SelectWidget, SimpleTableWidget, TableWidget, TextAreaWidget, TextInputWidget, WidgetProvider, WidgetRenderer, applyCaseControl, applyDecimalPrecision, applyMask, createWidgetStore, createZodSchema, evaluateCondition, filterByCharacterType, formatCurrency, formatDate, formatNumber, formatPhone, formatValue, getApiDataSource, getFormattedNumberLength, getSchemaDataSource, getStaticDataSource, getValueByPath, getWidgetValue, initI18n, isAllowedKey, parseDataPath, parseNumber, registerDefaultWidgets, removeMask, resetAll, resetWidget, setDataSource, setError, setLoading, setTouched, setValue, setValueByPath, setValues, setWidgetValue, shouldEnableWidget, shouldShowWidget, transformDataSourceOptions, translatePanelConfig, translateUISchema, translateWidgetConfig, useBaseWidget, useWidgetContext, useWidgetTranslation, validateNumericValue, validateWidget, widgetRegistry };
|
|
8747
|
+
export { ArrayWidget, BooleanWidget, CheckboxWidget, CurrencyInputWidget, DateInputWidget, DateTimeInputWidget, DisplayWidget, FileInputWidget, IterableAccordionWidget, JSONEditorPanel, NumberInputWidget, PanelRenderer, PhoneInputWidget, ProfileWidget, PropertyEditor, RadioWidget, SectionBuilder, SectionRenderer, SectionTree, SectionsContainer, SelectWidget, SimpleTableWidget, TableWidget, TextAreaWidget, TextInputWidget, VisualBuilderPanel, WidgetEventBus, WidgetProvider, WidgetRenderer, applyCaseControl, applyDecimalPrecision, applyMask, createWidgetStore, createZodSchema, evaluateCondition, filterByCharacterType, formatCurrency, formatDate, formatNumber, formatPhone, formatValue, geoHierarchyBuilder, getApiDataSource, getFormattedNumberLength, getSchemaDataSource, getStaticDataSource, getValueByPath, getWidgetValue, initI18n, isAllowedKey, parseDataPath, parseNumber, registerDefaultWidgets, removeMask, resetAll, resetWidget, setDataSource, setError, setLoading, setTouched, setValue, setValueByPath, setValues, setWidgetValue, shouldEnableWidget, shouldShowWidget, transformDataSourceOptions, translatePanelConfig, translateUISchema, translateWidgetConfig, useBaseWidget, useGeoWidgetCascade, useWidgetCascade, useWidgetContext, useWidgetEventBus, useWidgetTranslation, validateNumericValue, validateWidget, widgetRegistry };
|
|
6315
8748
|
//# sourceMappingURL=index.esm.js.map
|