@syncfusion/ej2-react-form-builder 0.1.0-beta
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/Common/CheckboxGroup.d.ts +13 -0
- package/Common/CheckboxGroup.js +50 -0
- package/Common/ConditionalRuleEngine.d.ts +296 -0
- package/Common/ConditionalRuleEngine.js +566 -0
- package/Common/CustomTabComponent.d.ts +17 -0
- package/Common/CustomTabComponent.js +30 -0
- package/Common/CustomValidationEngine.d.ts +58 -0
- package/Common/CustomValidationEngine.js +146 -0
- package/Common/DataGridComponent.d.ts +20 -0
- package/Common/DataGridComponent.js +273 -0
- package/Common/FormValidator.d.ts +485 -0
- package/Common/FormValidator.js +575 -0
- package/Common/MinMaxRuleEngine.d.ts +105 -0
- package/Common/MinMaxRuleEngine.js +365 -0
- package/Common/StaticHtmlRenderer.d.ts +14 -0
- package/Common/StaticHtmlRenderer.js +24 -0
- package/Common/expressions/DependencyTracker.d.ts +30 -0
- package/Common/expressions/DependencyTracker.js +128 -0
- package/Common/expressions/ExpressionEngine.d.ts +95 -0
- package/Common/expressions/ExpressionEngine.js +506 -0
- package/Common/expressions/ExpressionParser.d.ts +41 -0
- package/Common/expressions/ExpressionParser.js +269 -0
- package/Common/expressions/useGridExpressions.d.ts +12 -0
- package/Common/expressions/useGridExpressions.js +71 -0
- package/Common/globalFormId.d.ts +26 -0
- package/Common/globalFormId.js +45 -0
- package/Common/json-converter.d.ts +444 -0
- package/Common/json-converter.js +1184 -0
- package/Common/parser.d.ts +98 -0
- package/Common/parser.js +753 -0
- package/Common/utils.d.ts +130 -0
- package/Common/utils.js +574 -0
- package/README.md +53 -0
- package/index.d.ts +6 -0
- package/index.js +6 -0
- package/license +13 -0
- package/package.json +54 -0
- package/src/FormBuilder.d.ts +211 -0
- package/src/FormBuilder.js +1299 -0
- package/src/components/Database/ConnectionForm.d.ts +8 -0
- package/src/components/Database/ConnectionForm.js +28 -0
- package/src/components/TopBar.d.ts +10 -0
- package/src/components/TopBar.js +7 -0
- package/src/components/icons/ComponentIcons.d.ts +44 -0
- package/src/components/icons/ComponentIcons.js +40 -0
- package/src/components/layout/FormFieldWrapper.d.ts +41 -0
- package/src/components/layout/FormFieldWrapper.js +112 -0
- package/src/components/layout/LeftPane.d.ts +23 -0
- package/src/components/layout/LeftPane.js +310 -0
- package/src/components/layout/ListBoxPane.d.ts +24 -0
- package/src/components/layout/ListBoxPane.js +94 -0
- package/src/components/layout/MainSection.d.ts +39 -0
- package/src/components/layout/MainSection.js +1766 -0
- package/src/components/layout/Preview.d.ts +12 -0
- package/src/components/layout/Preview.js +337 -0
- package/src/components/layout/RightPane.d.ts +22 -0
- package/src/components/layout/RightPane.js +370 -0
- package/src/components/theme/ThemeSwitcher.d.ts +14 -0
- package/src/components/theme/ThemeSwitcher.js +24 -0
- package/src/components/theme/theme-constant.d.ts +37 -0
- package/src/components/theme/theme-constant.js +23 -0
- package/src/config/component-properties.json +4929 -0
- package/src/config/form-setting.json +56 -0
- package/src/config/locale.d.ts +449 -0
- package/src/config/locale.js +449 -0
- package/src/context/FormSchemaContext.d.ts +32 -0
- package/src/context/FormSchemaContext.js +155 -0
- package/src/context/LocalizationContext.d.ts +27 -0
- package/src/context/LocalizationContext.js +43 -0
- package/src/context/ThemeContext.d.ts +23 -0
- package/src/context/ThemeContext.js +161 -0
- package/src/context/contexts.d.ts +6 -0
- package/src/context/contexts.js +6 -0
- package/src/context/defaultFormSchema.d.ts +6 -0
- package/src/context/defaultFormSchema.js +20 -0
- package/src/defaultPalette.d.ts +9 -0
- package/src/defaultPalette.js +45 -0
- package/src/defaultTemplate.d.ts +3 -0
- package/src/defaultTemplate.js +185 -0
- package/src/import/schemaImportUtils.d.ts +24 -0
- package/src/import/schemaImportUtils.js +121 -0
- package/src/index.d.ts +16 -0
- package/src/index.js +6 -0
- package/src/types/ActionButtons.d.ts +44 -0
- package/src/types/ActionButtons.js +27 -0
- package/src/types/ComponentProperties.d.ts +33 -0
- package/src/types/ComponentProperties.js +2 -0
- package/src/types/FormSchema.d.ts +585 -0
- package/src/types/FormSchema.js +147 -0
- package/src/types/connection.d.ts +19 -0
- package/src/types/connection.js +9 -0
- package/src/utils/AttributesEditor.d.ts +13 -0
- package/src/utils/AttributesEditor.js +117 -0
- package/src/utils/ButtonGroupEditor.d.ts +15 -0
- package/src/utils/ButtonGroupEditor.js +141 -0
- package/src/utils/ChoiceBasedFieldEditor.d.ts +16 -0
- package/src/utils/ChoiceBasedFieldEditor.js +87 -0
- package/src/utils/ClipboardHelper.d.ts +26 -0
- package/src/utils/ClipboardHelper.js +83 -0
- package/src/utils/ColumnBuilder.d.ts +11 -0
- package/src/utils/ColumnBuilder.js +153 -0
- package/src/utils/ColumnSpanEditor.d.ts +8 -0
- package/src/utils/ColumnSpanEditor.js +98 -0
- package/src/utils/ConditionDataEditor.d.ts +29 -0
- package/src/utils/ConditionDataEditor.js +177 -0
- package/src/utils/ConditionalEditorUtils.d.ts +27 -0
- package/src/utils/ConditionalEditorUtils.js +310 -0
- package/src/utils/ConditionsEditor.d.ts +20 -0
- package/src/utils/ConditionsEditor.js +571 -0
- package/src/utils/CustomAccordion.d.ts +13 -0
- package/src/utils/CustomAccordion.js +38 -0
- package/src/utils/CustomValidationEditor.d.ts +11 -0
- package/src/utils/CustomValidationEditor.js +208 -0
- package/src/utils/DataSourceCollector.d.ts +3 -0
- package/src/utils/DataSourceCollector.js +252 -0
- package/src/utils/ExpressionEditor.d.ts +13 -0
- package/src/utils/ExpressionEditor.js +99 -0
- package/src/utils/FormSettingsPanel.d.ts +9 -0
- package/src/utils/FormSettingsPanel.js +60 -0
- package/src/utils/LookupConfig.d.ts +11 -0
- package/src/utils/LookupConfig.js +159 -0
- package/src/utils/MinMaxValueEditor.d.ts +25 -0
- package/src/utils/MinMaxValueEditor.js +188 -0
- package/src/utils/OptionsEditor.d.ts +8 -0
- package/src/utils/OptionsEditor.js +79 -0
- package/src/utils/PropertyRenderer.d.ts +22 -0
- package/src/utils/PropertyRenderer.js +210 -0
- package/src/utils/ValueRenderer.d.ts +2 -0
- package/src/utils/ValueRenderer.js +91 -0
- package/src/utils/ej2DbAdaptor.d.ts +6 -0
- package/src/utils/ej2DbAdaptor.js +38 -0
- package/src/utils/mappingHeuristics.d.ts +10 -0
- package/src/utils/mappingHeuristics.js +33 -0
- package/src/utils/renderComponent.d.ts +18 -0
- package/src/utils/renderComponent.js +300 -0
- package/styles/tailwind3.css +67 -0
- package/styles/themes/action-buttons.css +110 -0
- package/styles/themes/datagrid.css +380 -0
- package/styles/themes/drag-reorder.css +248 -0
- package/styles/themes/import-dialog.css +292 -0
- package/styles/themes/index.css +658 -0
- package/styles/themes/left-pane.css +253 -0
- package/styles/themes/options-editor.css +63 -0
- package/styles/themes/preview.css +702 -0
- package/styles/themes/property-dialog.css +371 -0
|
@@ -0,0 +1,1299 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
|
|
3
|
+
import LeftPane from './components/layout/LeftPane';
|
|
4
|
+
import MainSection, { isDefaultFormSchema } from './components/layout/MainSection';
|
|
5
|
+
import formSettingsConfig from './config/form-setting.json';
|
|
6
|
+
import RightPane from './components/layout/RightPane';
|
|
7
|
+
import componentProperties from './config/component-properties.json';
|
|
8
|
+
import { ClipboardHelper } from './utils/ClipboardHelper';
|
|
9
|
+
import { useFormSchema, FormSchemaProvider, LocalizationProvider, useLocalization } from './context/contexts';
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { DragDrop } from '@syncfusion/react-base';
|
|
12
|
+
import defaultTemplates from './defaultTemplate';
|
|
13
|
+
import { generateId, generateName, splitSchema } from '../Common/json-converter';
|
|
14
|
+
import { importSplitSchemas } from './import/schemaImportUtils';
|
|
15
|
+
import { defaultFormSchema } from './context/defaultFormSchema';
|
|
16
|
+
import { ButtonComponent } from '@syncfusion/ej2-react-buttons';
|
|
17
|
+
import { InPlaceEditorComponent } from '@syncfusion/ej2-react-inplace-editor';
|
|
18
|
+
import { DropDownButtonComponent } from '@syncfusion/ej2-react-splitbuttons';
|
|
19
|
+
import { enableRtl as Rtl } from '@syncfusion/ej2-base';
|
|
20
|
+
import { navigateTo } from '../Common/utils';
|
|
21
|
+
/**
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
export let formBuilderMode = 'component';
|
|
25
|
+
/**
|
|
26
|
+
* @private
|
|
27
|
+
*/
|
|
28
|
+
export const extractTemplateComponents = (template) => {
|
|
29
|
+
try {
|
|
30
|
+
if (!template.schema) {
|
|
31
|
+
console.warn(`Template "${template.title}" missing schema`);
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
// Convert template schema to JSON string
|
|
35
|
+
const dataSchemaJson = JSON.stringify(template.schema);
|
|
36
|
+
// Use importSplitSchemas to parse and merge template into empty schema
|
|
37
|
+
const emptyFormSchema = { version: '0.1.0', theme: 'tailwind3', components: [] };
|
|
38
|
+
const mergedSchema = importSplitSchemas(dataSchemaJson, emptyFormSchema);
|
|
39
|
+
// Extract components and regenerate IDs to ensure uniqueness
|
|
40
|
+
const regenerateIds = (comp) => {
|
|
41
|
+
const updated = { ...comp, id: generateId(comp.type) };
|
|
42
|
+
if (comp.children) {
|
|
43
|
+
updated.children = comp.children.map(regenerateIds);
|
|
44
|
+
}
|
|
45
|
+
if (comp.tableCells) {
|
|
46
|
+
updated.tableCells = comp.tableCells.map(row => row.map(cell => cell.map(regenerateIds)));
|
|
47
|
+
}
|
|
48
|
+
if (comp.tabItems) {
|
|
49
|
+
updated.tabItems = comp.tabItems.map(tab => ({
|
|
50
|
+
...tab,
|
|
51
|
+
content: tab.content?.map(regenerateIds)
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
return updated;
|
|
55
|
+
};
|
|
56
|
+
// Filter out submit buttons from templates to prevent duplicates
|
|
57
|
+
const filterOutSubmitButtons = (comp) => {
|
|
58
|
+
// Remove submit buttons
|
|
59
|
+
if (comp.type === 'button' && comp.buttonType === 'submit') {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
};
|
|
64
|
+
const componentsWithIds = (mergedSchema?.components || []).map(regenerateIds);
|
|
65
|
+
return componentsWithIds.filter(filterOutSubmitButtons);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error('Error extracting template components:', template.title, error);
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const FormBuilderComponent = ({ allowExport, enablePreview, onFieldDragStarted, onFieldDraggingOver, onFieldDropped, onPropertyChanged, enableRtl = false, apiRef, mode, domRef, locale, enablePersistence, formTemplates = defaultTemplates, paletteConfig }) => {
|
|
73
|
+
// Use FormSchema context instead of local state
|
|
74
|
+
const { formSchema, setFormSchema } = useFormSchema();
|
|
75
|
+
const { l10n } = useLocalization();
|
|
76
|
+
Rtl(enableRtl);
|
|
77
|
+
// Expose imperative API methods via ref
|
|
78
|
+
useImperativeHandle(apiRef, () => ({
|
|
79
|
+
element: domRef?.current ?? null,
|
|
80
|
+
locale,
|
|
81
|
+
allowExport,
|
|
82
|
+
enablePreview,
|
|
83
|
+
enablePersistence,
|
|
84
|
+
getFormSettings: () => {
|
|
85
|
+
// formSchema may sometimes be missing a top-level `settings` (imported/legacy shapes).
|
|
86
|
+
// Read defensively: prefer top-level `settings`, then try common alternate locations,
|
|
87
|
+
// and fall back to an empty object so defaults are applied below.
|
|
88
|
+
const fsAny = formSchema;
|
|
89
|
+
const settings = fsAny?.settings ?? fsAny?.schema?.settings ?? fsAny?.properties?.settings ?? {};
|
|
90
|
+
const unified = splitSchema(formSchema);
|
|
91
|
+
const output = {
|
|
92
|
+
name: settings?.name ?? '',
|
|
93
|
+
width: settings?.width ?? '',
|
|
94
|
+
layout: {
|
|
95
|
+
hideLabel: settings?.hideLabel ?? false,
|
|
96
|
+
size: settings?.size ?? '',
|
|
97
|
+
floatLabelType: settings?.floatLabelType ?? ''
|
|
98
|
+
},
|
|
99
|
+
schema: unified
|
|
100
|
+
};
|
|
101
|
+
return output;
|
|
102
|
+
}
|
|
103
|
+
}), [formSchema, domRef, locale, allowExport, enablePreview, enablePersistence]);
|
|
104
|
+
const [pendingComponent, setPendingComponent] = useState(null);
|
|
105
|
+
const [pendingDropContext, setPendingDropContext] = useState(null);
|
|
106
|
+
// Widget switch state: holds context when user initiates a widget switch
|
|
107
|
+
const [pendingWidgetSwitch, setPendingWidgetSwitch] = useState(null);
|
|
108
|
+
// Edit mode state
|
|
109
|
+
const [editingField, setEditingField] = useState(null);
|
|
110
|
+
// Left pane collapsed state for toggle functionality
|
|
111
|
+
const [isLeftPaneCollapsed, setIsLeftPaneCollapsed] = useState(false);
|
|
112
|
+
const [isRightPaneCollapsed, setIsRightPaneCollapsed] = useState(false);
|
|
113
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
114
|
+
const [isRightPaneHovered, setIsRightPaneHovered] = useState(false);
|
|
115
|
+
const [isLeftPaneHovered, setIsLeftPaneHovered] = useState(false);
|
|
116
|
+
const leftPaneHoverTimeoutRef = useRef(null);
|
|
117
|
+
const rightPaneHoverTimeoutRef = useRef(null);
|
|
118
|
+
const contentSectionRef = useRef(null);
|
|
119
|
+
const isReorderingRef = useRef(false);
|
|
120
|
+
const builderContainerRef = useRef(null);
|
|
121
|
+
const [containerWidth, setContainerWidth] = useState(1024);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (pendingWidgetSwitch) {
|
|
124
|
+
handlePropertyDialogSave(pendingWidgetSwitch.originalComponent);
|
|
125
|
+
}
|
|
126
|
+
if (pendingDropContext) {
|
|
127
|
+
handlePropertyDialogSave(pendingComponent);
|
|
128
|
+
}
|
|
129
|
+
}, [pendingWidgetSwitch, pendingDropContext, pendingComponent]);
|
|
130
|
+
const handleFormSettingsSave = useCallback((values, key) => {
|
|
131
|
+
const settings = values;
|
|
132
|
+
// Apply form settings to all existing components
|
|
133
|
+
const updateComponentsWithSettings = (components) => {
|
|
134
|
+
return components.map(component => {
|
|
135
|
+
// Skip components that have explicitly set these properties
|
|
136
|
+
const updatedComponent = { ...component };
|
|
137
|
+
// Only update the specific property if key is provided, otherwise update all
|
|
138
|
+
if (key === 'floatLabelType') {
|
|
139
|
+
if (settings.floatLabelType !== undefined) {
|
|
140
|
+
updatedComponent.floatLabelType = settings.floatLabelType;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
updatedComponent.floatLabelType = 'Never';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (key === 'size') {
|
|
147
|
+
if (settings.size !== undefined) {
|
|
148
|
+
updatedComponent.size = settings.size;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
updatedComponent.size = 'Default';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (key === 'hideLabel') {
|
|
155
|
+
if (settings.hideLabel !== undefined) {
|
|
156
|
+
updatedComponent.hideLabel = settings.hideLabel;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
updatedComponent.hideLabel = false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Handle nested components (panels, tables, tabs)
|
|
163
|
+
if (component.children?.length) {
|
|
164
|
+
updatedComponent.children = updateComponentsWithSettings(component.children);
|
|
165
|
+
}
|
|
166
|
+
if (component.tableCells?.length) {
|
|
167
|
+
updatedComponent.tableCells = component.tableCells.map(row => row.map(cell => updateComponentsWithSettings(cell)));
|
|
168
|
+
}
|
|
169
|
+
if (component.tabItems?.length) {
|
|
170
|
+
updatedComponent.tabItems = component.tabItems.map(tab => ({
|
|
171
|
+
...tab,
|
|
172
|
+
content: tab.content ? updateComponentsWithSettings(tab.content) : []
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
return updatedComponent;
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
setFormSchema(prev => ({
|
|
179
|
+
...prev,
|
|
180
|
+
settings: settings,
|
|
181
|
+
components: updateComponentsWithSettings(prev.components)
|
|
182
|
+
}));
|
|
183
|
+
}, [setFormSchema]);
|
|
184
|
+
const handleComponentDrop = (component, dropTargetIndex, dropPosition) => {
|
|
185
|
+
// Check if this component type has property configuration and hasn't been configured yet
|
|
186
|
+
if (componentProperties[component.type] && !component._configured) {
|
|
187
|
+
// Show property dialog
|
|
188
|
+
setPendingComponent(component);
|
|
189
|
+
setEditingField(component);
|
|
190
|
+
setPendingDropContext({
|
|
191
|
+
isNested: false,
|
|
192
|
+
targetIndex: dropTargetIndex,
|
|
193
|
+
targetPosition: dropPosition
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
setPendingComponent(component);
|
|
197
|
+
setEditingField(component);
|
|
198
|
+
// Calculate insert index
|
|
199
|
+
let insertIndex;
|
|
200
|
+
if (dropTargetIndex !== null && dropTargetIndex !== undefined && dropPosition) {
|
|
201
|
+
insertIndex = dropPosition === 'before' ? dropTargetIndex : dropTargetIndex + 1;
|
|
202
|
+
}
|
|
203
|
+
// Add at specified index or with smart positioning
|
|
204
|
+
setFormSchema(prev => {
|
|
205
|
+
const submitIndex = prev.components.findIndex(c => c.type === 'button' && c.buttonType === 'submit');
|
|
206
|
+
let newComponents;
|
|
207
|
+
if (insertIndex !== undefined) {
|
|
208
|
+
// Insert at specified index
|
|
209
|
+
newComponents = [...prev.components];
|
|
210
|
+
newComponents.splice(insertIndex, 0, component);
|
|
211
|
+
}
|
|
212
|
+
else if (submitIndex !== -1 && submitIndex === prev.components.length - 1) {
|
|
213
|
+
// If submit exists and is at the last position, insert before it
|
|
214
|
+
newComponents = [...prev.components];
|
|
215
|
+
newComponents.splice(submitIndex, 0, component);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// Otherwise (submit moved or doesn't exist), add at end
|
|
219
|
+
newComponents = [...prev.components, component];
|
|
220
|
+
}
|
|
221
|
+
return { ...prev, components: newComponents };
|
|
222
|
+
});
|
|
223
|
+
if (isMobile && !isLeftPaneCollapsed) {
|
|
224
|
+
setIsLeftPaneCollapsed(true);
|
|
225
|
+
setIsRightPaneCollapsed(false);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* Deep search for a component by id across panels (children), tables (tableCells) and tabs (tabItems).
|
|
230
|
+
*/
|
|
231
|
+
function findComponentById(components = [], id) {
|
|
232
|
+
const stack = [...components];
|
|
233
|
+
while (stack.length) {
|
|
234
|
+
const comp = stack.pop();
|
|
235
|
+
if (comp.id === id)
|
|
236
|
+
return comp;
|
|
237
|
+
// children (e.g. panel)
|
|
238
|
+
if (comp.children?.length)
|
|
239
|
+
stack.push(...comp.children);
|
|
240
|
+
// tabs -> tabItems[].content
|
|
241
|
+
if (comp.tabItems?.length) {
|
|
242
|
+
for (const tab of comp.tabItems) {
|
|
243
|
+
if (tab.content?.length)
|
|
244
|
+
stack.push(...tab.content);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// table -> tableCells: row[] -> cell[] -> components[]
|
|
248
|
+
if (comp.tableCells?.length) {
|
|
249
|
+
for (const row of comp.tableCells) {
|
|
250
|
+
for (const cell of row) {
|
|
251
|
+
if (cell?.length)
|
|
252
|
+
stack.push(...cell);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
// T009: Handle Edit action
|
|
260
|
+
const handleEdit = React.useCallback((fieldId) => {
|
|
261
|
+
const field = findComponentById(formSchema.components, fieldId);
|
|
262
|
+
if (!field)
|
|
263
|
+
return;
|
|
264
|
+
// Check if this component type has property configuration
|
|
265
|
+
if (!componentProperties[field.type]) {
|
|
266
|
+
console.warn(`No property configuration found for component type: ${field.type}`);
|
|
267
|
+
alert(`Edit functionality is not yet configured for ${field.type} components.`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
setEditingField(field);
|
|
271
|
+
setPendingComponent(field);
|
|
272
|
+
}, [formSchema.components]);
|
|
273
|
+
// Handle user selecting a new widget type from the Switch Widget dropdown
|
|
274
|
+
const handleSwitchWidget = React.useCallback((fieldId, newWidgetTypePascal) => {
|
|
275
|
+
const field = findComponentById(formSchema.components, fieldId);
|
|
276
|
+
if (!field)
|
|
277
|
+
return;
|
|
278
|
+
// Verify property configuration exists for the new widget type
|
|
279
|
+
if (!componentProperties[newWidgetTypePascal]) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
// Prepare a component copy with the new type and open the PropertyDialog
|
|
283
|
+
const componentWithNewType = { ...field, type: newWidgetTypePascal };
|
|
284
|
+
setPendingWidgetSwitch({ fieldId, originalComponent: field, newWidgetType: newWidgetTypePascal });
|
|
285
|
+
setPendingComponent(componentWithNewType);
|
|
286
|
+
setEditingField(null);
|
|
287
|
+
}, [formSchema.components]);
|
|
288
|
+
// Filter component to only include properties valid for its type and only those that differ from defaults (validation at root)
|
|
289
|
+
const filterComponentProperties = (component) => {
|
|
290
|
+
const componentType = component.type;
|
|
291
|
+
const configData = componentProperties[componentType];
|
|
292
|
+
if (!configData)
|
|
293
|
+
return component;
|
|
294
|
+
const validProps = new Set();
|
|
295
|
+
configData.groups?.forEach((group) => {
|
|
296
|
+
group.properties?.forEach((prop) => {
|
|
297
|
+
if (prop.key) {
|
|
298
|
+
validProps.add(prop.key);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
const coreProps = ['id', 'type', 'label', 'name', '_configured'];
|
|
303
|
+
if (componentType === 'autoNumber') {
|
|
304
|
+
coreProps.push('visible', 'size');
|
|
305
|
+
}
|
|
306
|
+
const structuralProps = ['children', 'tableCells', 'tabItems'];
|
|
307
|
+
// Get defaults from configData
|
|
308
|
+
const defaults = {};
|
|
309
|
+
configData.groups?.forEach((group) => {
|
|
310
|
+
group.properties?.forEach((prop) => {
|
|
311
|
+
if (prop.key && prop.default !== undefined) {
|
|
312
|
+
defaults[prop.key] = prop.default;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
const filtered = {};
|
|
317
|
+
Object.keys(component).forEach(key => {
|
|
318
|
+
if (coreProps.includes(key) || structuralProps.includes(key)) {
|
|
319
|
+
filtered[key] = component[key];
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Only include if valid and different from default
|
|
323
|
+
if (validProps.has(key)) {
|
|
324
|
+
if (key === 'dataSource' &&
|
|
325
|
+
Array.isArray(component[key]) &&
|
|
326
|
+
component[key].length === 0) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (component[key] !== defaults[key]) {
|
|
330
|
+
filtered[key] = component[key];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
// Ensure structural properties exist for container components
|
|
335
|
+
if (componentType === 'panel' && !filtered.children) {
|
|
336
|
+
filtered.children = [];
|
|
337
|
+
}
|
|
338
|
+
if (componentType === 'button' && !filtered.buttonType && !(component.buttonsGroups?.length > 0)) {
|
|
339
|
+
filtered.buttonType = 'button';
|
|
340
|
+
}
|
|
341
|
+
if (componentType === 'button' && !filtered.buttonType && (component.buttonsGroups.length > 0)) {
|
|
342
|
+
const hasSubmitButton = component.buttonsGroups.some((btn) => btn.type === 'submit');
|
|
343
|
+
if (hasSubmitButton) {
|
|
344
|
+
filtered.buttonType = 'submit';
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (componentType === 'card' && !filtered.children) {
|
|
348
|
+
filtered.children = [];
|
|
349
|
+
}
|
|
350
|
+
if (componentType === 'table' && !filtered.tableCells) {
|
|
351
|
+
filtered.tableCells = component.tableCells || [];
|
|
352
|
+
}
|
|
353
|
+
if (componentType === 'tabs' && !filtered.tabItems) {
|
|
354
|
+
filtered.tabItems = component.tabItems || [];
|
|
355
|
+
}
|
|
356
|
+
return filtered;
|
|
357
|
+
};
|
|
358
|
+
const handlePropertyDialogSave = useCallback((properties) => {
|
|
359
|
+
if (!pendingComponent)
|
|
360
|
+
return;
|
|
361
|
+
// Map collected properties to FormComponent fields
|
|
362
|
+
const mapPropertiesToComponent = (baseComponent, props) => {
|
|
363
|
+
const updatedComponent = {
|
|
364
|
+
...baseComponent,
|
|
365
|
+
// Display properties - use properties from dialog or keep existing
|
|
366
|
+
label: props.label !== undefined ? props.label : baseComponent.label,
|
|
367
|
+
name: props.name !== undefined ? props.name : baseComponent.name,
|
|
368
|
+
description: (props.description !== undefined || props.description !== '') ? props.description : undefined,
|
|
369
|
+
prefix: (props.prefix !== undefined || props.prefix !== '') ? props.prefix : undefined,
|
|
370
|
+
suffix: (props.suffix !== undefined || props.suffix !== '') ? props.suffix : undefined,
|
|
371
|
+
conditions: props.conditions !== undefined ? props.conditions : baseComponent.conditions,
|
|
372
|
+
customValidation: props.customValidation !== undefined ? props.customValidation : baseComponent.customValidation,
|
|
373
|
+
pageSize: props.pageSize !== undefined ? props.pageSize : baseComponent.pageSize,
|
|
374
|
+
size: formSchema.settings?.size !== undefined ? formSchema.settings.size : (props.size !== undefined ? props.size : baseComponent.size),
|
|
375
|
+
allowPaging: props.allowPaging !== undefined ? props.allowPaging : baseComponent.allowPaging,
|
|
376
|
+
allowAdding: props.allowAdding !== undefined ? props.allowAdding : baseComponent.allowAdding,
|
|
377
|
+
allowFiltering: props.allowFiltering !== undefined ? props.allowFiltering : baseComponent.allowFiltering,
|
|
378
|
+
allowEditing: props.allowEditing !== undefined ? props.allowEditing : baseComponent.allowEditing,
|
|
379
|
+
allowDeleting: props.allowDeleting !== undefined ? props.allowDeleting : baseComponent.allowDeleting,
|
|
380
|
+
dataSource: props.dataSource !== undefined ? props.dataSource : baseComponent.dataSource || [],
|
|
381
|
+
cardTitle: props.cardTitle !== undefined ? props.cardTitle : baseComponent.cardTitle,
|
|
382
|
+
cardSubtitle: props.cardSubtitle !== undefined ? props.cardSubtitle : baseComponent.cardSubtitle,
|
|
383
|
+
textboxType: props.textboxType !== undefined ? props.textboxType.toLowerCase() : baseComponent.textboxType?.toLowerCase(),
|
|
384
|
+
hideBorders: props.hideBorders !== undefined ? props.hideBorders : baseComponent.hideBorders,
|
|
385
|
+
placeholder: props.placeholder !== undefined ? props.placeholder : baseComponent.placeholder,
|
|
386
|
+
defaultValue: props.defaultValue !== undefined ? props.defaultValue : baseComponent.defaultValue,
|
|
387
|
+
expressionValue: props.expressionValue !== undefined ? props.expressionValue : baseComponent.expressionValue,
|
|
388
|
+
disabled: props.disabled !== undefined ? props.disabled : (baseComponent.disabled || false),
|
|
389
|
+
visible: props.visible !== undefined ? props.visible : baseComponent.visible || false,
|
|
390
|
+
tooltip: props.tooltip !== undefined ? props.tooltip : baseComponent.tooltip,
|
|
391
|
+
cssClass: props.cssClass !== undefined ? props.cssClass : baseComponent.cssClass,
|
|
392
|
+
hideLabel: formSchema.settings?.hideLabel !== undefined ? formSchema.settings.hideLabel : (props.hideLabel !== undefined ? props.hideLabel : (baseComponent.hideLabel || false)),
|
|
393
|
+
labelPosition: props.labelPosition !== undefined ? props.labelPosition : (baseComponent.labelPosition || 'top'),
|
|
394
|
+
checkboxLabelPosition: props.checkboxLabelPosition !== undefined ? props.checkboxLabelPosition : (baseComponent.checkboxLabelPosition || 'After'),
|
|
395
|
+
layoutDirection: props.layoutDirection !== undefined ? props.layoutDirection.toLowerCase() : (baseComponent.layoutDirection?.toLowerCase() || 'column'),
|
|
396
|
+
enableHtmlSanitizer: props.enableHtmlSanitizer !== undefined ? props.enableHtmlSanitizer : (baseComponent.enableHtmlSanitizer || 'true'),
|
|
397
|
+
position: props.position !== undefined ? props.position : (baseComponent.position || 'left'),
|
|
398
|
+
labelWidth: props.labelWidth !== undefined ? props.labelWidth.toLowerCase() : (baseComponent.labelWidth?.toLowerCase() || 'auto'),
|
|
399
|
+
iconCss: props.iconCss !== undefined ? props.iconCss : baseComponent.iconCss,
|
|
400
|
+
iconPosition: props.iconPosition !== undefined ? props.iconPosition : baseComponent.iconPosition,
|
|
401
|
+
startIndex: props.startIndex !== undefined ? props.startIndex : baseComponent.startIndex,
|
|
402
|
+
style: props.style !== undefined ? props.style.toLowerCase() : baseComponent.style?.toLowerCase(),
|
|
403
|
+
buttonType: props.buttonType !== undefined ? props.buttonType.toLowerCase() : baseComponent.buttonType?.toLowerCase(),
|
|
404
|
+
// DatePicker-specific properties
|
|
405
|
+
calendarMode: props.calendarMode !== undefined ? props.calendarMode : baseComponent.calendarMode,
|
|
406
|
+
dayHeaderFormat: props.dayHeaderFormat !== undefined ? props.dayHeaderFormat : baseComponent.dayHeaderFormat,
|
|
407
|
+
depth: props.depth !== undefined ? props.depth : baseComponent.depth,
|
|
408
|
+
firstDayOfWeek: props.firstDayOfWeek !== undefined ? props.firstDayOfWeek : baseComponent.firstDayOfWeek,
|
|
409
|
+
start: props.start !== undefined ? props.start : baseComponent.start,
|
|
410
|
+
// DateTimePicker-specific properties
|
|
411
|
+
allowEdit: props.allowEdit !== undefined ? props.allowEdit : baseComponent.allowEdit,
|
|
412
|
+
enabled: props.enabled !== undefined ? props.enabled : baseComponent.enabled,
|
|
413
|
+
timeFormat: props.timeFormat !== undefined ? props.timeFormat : baseComponent.timeFormat,
|
|
414
|
+
showTodayButton: props.showTodayButton !== undefined ? props.showTodayButton : baseComponent.showTodayButton,
|
|
415
|
+
floatLabelType: formSchema.settings?.floatLabelType !== undefined ? formSchema.settings.floatLabelType : (props.floatLabelType !== undefined ? props.floatLabelType : (baseComponent.floatLabelType || 'Never')),
|
|
416
|
+
fieldLabelPosition: props.fieldLabelPosition !== undefined ? props.fieldLabelPosition : (baseComponent.fieldLabelPosition || 'After'),
|
|
417
|
+
readOnly: props.readOnly !== undefined ? props.readOnly : (baseComponent.readOnly || false),
|
|
418
|
+
autocomplete: props.autocomplete !== undefined ? props.autocomplete : (baseComponent.autocomplete !== false),
|
|
419
|
+
multiline: props.multiline !== undefined ? props.multiline : baseComponent.multiline,
|
|
420
|
+
showClearButton: props.showClearButton !== undefined ? props.showClearButton : (baseComponent.showClearButton || false),
|
|
421
|
+
checked: props.checked !== undefined ? props.checked : (baseComponent.checked || false),
|
|
422
|
+
indeterminate: props.indeterminate !== undefined ? props.indeterminate : (baseComponent.indeterminate || false),
|
|
423
|
+
options: props.options !== undefined ? props.options : (baseComponent.options || ['Option 1']),
|
|
424
|
+
tabOptions: props.tabOptions !== undefined ? props.tabOptions : (baseComponent.tabOptions || ['Tab 1']),
|
|
425
|
+
decimals: props.decimals !== undefined ? props.decimals : baseComponent.decimals,
|
|
426
|
+
currency: props.currency !== undefined ? props.currency : baseComponent.currency,
|
|
427
|
+
format: props.format !== undefined ? props.format : baseComponent.format,
|
|
428
|
+
numberFormat: props.numberFormat !== undefined ? props.numberFormat : baseComponent.numberFormat,
|
|
429
|
+
step: props.step !== undefined ? props.step : baseComponent.step,
|
|
430
|
+
enableVirtualization: props.enableVirtualization !== undefined ? props.enableVirtualization : baseComponent.enableVirtualization,
|
|
431
|
+
popupHeight: props.popupHeight !== undefined ? props.popupHeight : baseComponent.popupHeight,
|
|
432
|
+
popupWidth: props.popupWidth !== undefined ? props.popupWidth : baseComponent.popupWidth,
|
|
433
|
+
htmlAttributes: props.htmlAttributes !== undefined ? props.htmlAttributes : baseComponent.htmlAttributes,
|
|
434
|
+
buttonsGroups: props.buttonsGroups !== undefined ? props.buttonsGroups : baseComponent.buttonsGroups,
|
|
435
|
+
gap: props.gap !== undefined ? props.gap : baseComponent.gap,
|
|
436
|
+
cols: props.cols !== undefined ? props.cols : baseComponent.cols,
|
|
437
|
+
textareaRows: props.textareaRows !== undefined ? props.textareaRows : baseComponent.textareaRows,
|
|
438
|
+
offLabel: props.offLabel !== undefined ? props.offLabel : baseComponent.offLabel,
|
|
439
|
+
onLabel: props.onLabel !== undefined ? props.onLabel : baseComponent.onLabel,
|
|
440
|
+
startDate: props.startDate !== undefined ? props.startDate : baseComponent.startDate,
|
|
441
|
+
endDate: props.endDate !== undefined ? props.endDate : baseComponent.endDate,
|
|
442
|
+
separator: props.separator !== undefined ? props.separator : baseComponent.separator,
|
|
443
|
+
mask: props.mask !== undefined ? props.mask : baseComponent.mask,
|
|
444
|
+
customCharacters: props.customCharacters !== undefined ? JSON.parse(props.customCharacters) : baseComponent.customCharacters,
|
|
445
|
+
promptChar: props.promptChar !== undefined ? props.promptChar : baseComponent.promptChar,
|
|
446
|
+
allowCustomValue: props.allowCustomValue !== undefined ? props.allowCustomValue : baseComponent.allowCustomValue,
|
|
447
|
+
addTagOnBlur: props.addTagOnBlur !== undefined ? props.addTagOnBlur : baseComponent.addTagOnBlur,
|
|
448
|
+
delimiterChar: props.delimiterChar !== undefined ? props.delimiterChar : baseComponent.delimiterChar,
|
|
449
|
+
visualMode: props.visualMode !== undefined ? props.visualMode : baseComponent.visualMode,
|
|
450
|
+
showSelectAll: props.showSelectAll !== undefined ? props.showSelectAll : baseComponent.showSelectAll,
|
|
451
|
+
selectAllText: props.selectAllText !== undefined ? props.selectAllText : baseComponent.selectAllText,
|
|
452
|
+
unSelectAllText: props.unSelectAllText !== undefined ? props.unSelectAllText : baseComponent.unSelectAllText,
|
|
453
|
+
showDropDownIcon: props.showDropDownIcon !== undefined ? props.showDropDownIcon : baseComponent.showDropDownIcon,
|
|
454
|
+
showCloseIcon: props.showCloseIcon !== undefined ? props.showCloseIcon : (baseComponent.showCloseIcon || false),
|
|
455
|
+
showIcon: props.showIcon !== undefined ? props.showIcon : (baseComponent.showIcon || false),
|
|
456
|
+
variant: props.variant !== undefined ? props.variant : baseComponent.variant,
|
|
457
|
+
severity: props.severity !== undefined ? props.severity : baseComponent.severity,
|
|
458
|
+
content: props.content !== undefined ? props.content : baseComponent.content,
|
|
459
|
+
// Rating-specific display properties
|
|
460
|
+
allowReset: props.allowReset !== undefined ? props.allowReset : baseComponent.allowReset,
|
|
461
|
+
itemsCount: props.itemsCount !== undefined ? props.itemsCount : baseComponent.itemsCount,
|
|
462
|
+
precision: props.precision !== undefined ? props.precision : baseComponent.precision,
|
|
463
|
+
showRatingTooltip: props.showRatingTooltip !== undefined ? props.showRatingTooltip : baseComponent.showRatingTooltip,
|
|
464
|
+
showRatingLabel: props.showRatingLabel !== undefined ? props.showRatingLabel : baseComponent.showRatingLabel,
|
|
465
|
+
ratingLabelPosition: props.ratingLabelPosition !== undefined ? props.ratingLabelPosition : baseComponent.ratingLabelPosition,
|
|
466
|
+
// ImageEditor-specific properties
|
|
467
|
+
height: props.height !== undefined ? props.height : baseComponent.height,
|
|
468
|
+
width: props.width !== undefined ? props.width : baseComponent.width,
|
|
469
|
+
// ColorPicker-specific properties
|
|
470
|
+
enableOpacity: props.enableOpacity !== undefined ? props.enableOpacity : baseComponent.enableOpacity,
|
|
471
|
+
showNoColor: props.showNoColor !== undefined ? props.showNoColor : baseComponent.showNoColor,
|
|
472
|
+
showButtons: props.showButtons !== undefined ? props.showButtons : baseComponent.showButtons,
|
|
473
|
+
showRecentColors: props.showRecentColors !== undefined ? props.showRecentColors : baseComponent.showRecentColors,
|
|
474
|
+
// Signature-specific properties
|
|
475
|
+
backgroundColor: props.backgroundColor !== undefined ? props.backgroundColor : baseComponent.backgroundColor,
|
|
476
|
+
strokeColor: props.strokeColor !== undefined ? props.strokeColor : baseComponent.strokeColor,
|
|
477
|
+
maximumStrokeWidth: props.maximumStrokeWidth !== undefined ? props.maximumStrokeWidth : baseComponent.maximumStrokeWidth,
|
|
478
|
+
minimumStrokeWidth: props.minimumStrokeWidth !== undefined ? props.minimumStrokeWidth : baseComponent.minimumStrokeWidth,
|
|
479
|
+
velocity: props.velocity !== undefined ? props.velocity : baseComponent.velocity,
|
|
480
|
+
// Uploader-specific properties
|
|
481
|
+
allowExtensions: props.allowExtensions !== undefined ? props.allowExtensions : baseComponent.allowExtensions,
|
|
482
|
+
saveUrl: props.saveUrl !== undefined ? props.saveUrl : baseComponent.saveUrl,
|
|
483
|
+
removeUrl: props.removeUrl !== undefined ? props.removeUrl : baseComponent.removeUrl,
|
|
484
|
+
buttons: props.buttons !== undefined ? props.buttons : baseComponent.buttons,
|
|
485
|
+
allowMultiple: props.allowMultiple !== undefined ? props.allowMultiple : baseComponent.allowMultiple,
|
|
486
|
+
showFileList: props.showFileList !== undefined ? props.showFileList : baseComponent.showFileList,
|
|
487
|
+
// Slider-specific properties
|
|
488
|
+
orientation: props.orientation !== undefined ? props.orientation : baseComponent.orientation,
|
|
489
|
+
rows: props.rows !== undefined ? props.rows : baseComponent.rows,
|
|
490
|
+
columns: props.columns !== undefined ? props.columns : baseComponent.columns,
|
|
491
|
+
lookUpConfig: props.lookUpConfig !== undefined ? props.lookUpConfig : baseComponent.lookUpConfig,
|
|
492
|
+
enableImportFromWord: props.enableImportFromWord !== undefined ? props.enableImportFromWord : baseComponent.enableImportFromWord,
|
|
493
|
+
enableExportToWord: props.enableExportToWord !== undefined ? props.enableExportToWord : baseComponent.enableExportToWord,
|
|
494
|
+
enableExportToPdf: props.enableExportToPdf !== undefined ? props.enableExportToPdf : baseComponent.enableExportToPdf,
|
|
495
|
+
tableCells: baseComponent.type === 'table'
|
|
496
|
+
? (() => {
|
|
497
|
+
const newRows = props.rows !== undefined ? props.rows : (baseComponent.rows || 2);
|
|
498
|
+
const newCols = props.columns !== undefined ? props.columns : (baseComponent.columns || 2);
|
|
499
|
+
const existingCells = baseComponent.tableCells || [];
|
|
500
|
+
return Array.from({ length: newRows }, (_, rowIndex) => Array.from({ length: newCols }, (_, colIndex) => {
|
|
501
|
+
return existingCells[rowIndex]?.[colIndex] || [];
|
|
502
|
+
}));
|
|
503
|
+
})()
|
|
504
|
+
: undefined,
|
|
505
|
+
tabItems: baseComponent.type === 'tabs'
|
|
506
|
+
? (() => {
|
|
507
|
+
const tabHeaders = props.tabOptions !== undefined
|
|
508
|
+
? props.tabOptions
|
|
509
|
+
: (baseComponent.tabItems?.map(tab => tab.header) || ['Tab 1']);
|
|
510
|
+
const existingTabs = baseComponent.tabItems || [];
|
|
511
|
+
return tabHeaders.map((header, index) => {
|
|
512
|
+
// ✅ CORRECT: Match by index position instead of header text to preserve children
|
|
513
|
+
const existingTab = existingTabs[index];
|
|
514
|
+
return {
|
|
515
|
+
header: header,
|
|
516
|
+
content: existingTab?.content || []
|
|
517
|
+
};
|
|
518
|
+
});
|
|
519
|
+
})()
|
|
520
|
+
: undefined,
|
|
521
|
+
columnWidths: (props.columnWidths?.widths ?? baseComponent.columnWidths?.widths)?.every((w) => w === null)
|
|
522
|
+
? undefined
|
|
523
|
+
: (props.columnWidths ?? baseComponent.columnWidths),
|
|
524
|
+
// Validation properties - keep at root level for FormComponent interface
|
|
525
|
+
required: props.required !== undefined ? props.required : (baseComponent?.required || false),
|
|
526
|
+
validateOn: props.validateOn !== undefined ? props.validateOn : (baseComponent?.validateOn || 'Change'),
|
|
527
|
+
min: props.minValue !== undefined ? props.minValue : baseComponent?.min,
|
|
528
|
+
max: props.maxValue !== undefined ? props.maxValue : baseComponent?.max,
|
|
529
|
+
minValue: props.minValue !== undefined ? props.minValue : baseComponent?.minValue,
|
|
530
|
+
maxValue: props.maxValue !== undefined ? props.maxValue : baseComponent?.maxValue,
|
|
531
|
+
minMaxRange: props.minMaxRange !== undefined ? props.minMaxRange : baseComponent?.minMaxRange,
|
|
532
|
+
minMaxTimeRange: props.minMaxTimeRange !== undefined ? props.minMaxTimeRange : baseComponent?.minMaxTimeRange,
|
|
533
|
+
minLength: props.minLength !== undefined ? props.minLength : baseComponent?.minLength,
|
|
534
|
+
maxLength: props.maxLength !== undefined ? props.maxLength : baseComponent?.maxLength,
|
|
535
|
+
minDate: props.minDate !== undefined ? props.minDate : baseComponent?.minDate,
|
|
536
|
+
maxDate: props.maxDate !== undefined ? props.maxDate : baseComponent?.maxDate,
|
|
537
|
+
regex: props.regex !== undefined ? props.regex : baseComponent?.regex,
|
|
538
|
+
minTime: props.minTime !== undefined ? props.minTime : baseComponent?.minTime,
|
|
539
|
+
maxTime: props.maxTime !== undefined ? props.maxTime : baseComponent?.maxTime,
|
|
540
|
+
validationMessage: props.validationMessage !== undefined ? props.validationMessage : baseComponent?.validationMessage,
|
|
541
|
+
// Mark as configured to prevent dialog from opening again (internal flag, not stored in output)
|
|
542
|
+
_configured: true
|
|
543
|
+
};
|
|
544
|
+
return updatedComponent;
|
|
545
|
+
};
|
|
546
|
+
if (editingField) {
|
|
547
|
+
// Update existing field with new properties
|
|
548
|
+
const mappedField = mapPropertiesToComponent(editingField, properties);
|
|
549
|
+
const updatedField = filterComponentProperties(mappedField);
|
|
550
|
+
const updateComponentById = (components, targetId, replacement) => {
|
|
551
|
+
return components.map(comp => {
|
|
552
|
+
if (comp.id === targetId)
|
|
553
|
+
return replacement;
|
|
554
|
+
const updatedComp = { ...comp };
|
|
555
|
+
if (comp.children) {
|
|
556
|
+
updatedComp.children = updateComponentById(comp.children, targetId, replacement);
|
|
557
|
+
}
|
|
558
|
+
if (comp.tabItems) {
|
|
559
|
+
updatedComp.tabItems = comp.tabItems.map(tab => ({
|
|
560
|
+
...tab,
|
|
561
|
+
content: tab.content ? updateComponentById(tab.content, targetId, replacement) : tab.content
|
|
562
|
+
}));
|
|
563
|
+
}
|
|
564
|
+
if (comp.tableCells) {
|
|
565
|
+
updatedComp.tableCells = comp.tableCells.map(row => row.map(cell => updateComponentById(cell, targetId, replacement)));
|
|
566
|
+
}
|
|
567
|
+
return updatedComp;
|
|
568
|
+
});
|
|
569
|
+
};
|
|
570
|
+
// Update form schema immutably
|
|
571
|
+
setFormSchema(prev => ({
|
|
572
|
+
...prev,
|
|
573
|
+
components: updateComponentById(prev.components, editingField.id, updatedField)
|
|
574
|
+
}));
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
// If this was a widget switch flow, update the existing component in-place
|
|
578
|
+
if (pendingWidgetSwitch) {
|
|
579
|
+
const mappedField = mapPropertiesToComponent(pendingComponent, properties);
|
|
580
|
+
const updatedField = filterComponentProperties(mappedField);
|
|
581
|
+
const updateComponentById = (components) => {
|
|
582
|
+
return components.map(comp => {
|
|
583
|
+
if (comp.id === pendingWidgetSwitch.fieldId)
|
|
584
|
+
return updatedField;
|
|
585
|
+
const updatedComp = { ...comp };
|
|
586
|
+
if (comp.children) {
|
|
587
|
+
updatedComp.children = updateComponentById(comp.children);
|
|
588
|
+
}
|
|
589
|
+
if (comp.tabItems) {
|
|
590
|
+
updatedComp.tabItems = comp.tabItems.map(tab => ({
|
|
591
|
+
...tab,
|
|
592
|
+
content: tab.content ? updateComponentById(tab.content) : []
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
595
|
+
if (comp.tableCells) {
|
|
596
|
+
updatedComp.tableCells = comp.tableCells.map(row => row.map(cell => updateComponentById(cell)));
|
|
597
|
+
}
|
|
598
|
+
return updatedComp;
|
|
599
|
+
});
|
|
600
|
+
};
|
|
601
|
+
setTimeout(() => {
|
|
602
|
+
setFormSchema(prev => ({
|
|
603
|
+
...prev,
|
|
604
|
+
components: updateComponentById(prev.components)
|
|
605
|
+
}));
|
|
606
|
+
}, 100);
|
|
607
|
+
setPendingWidgetSwitch(null);
|
|
608
|
+
setEditingField(null);
|
|
609
|
+
setPendingDropContext(null);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
// Map collected properties to FormComponent fields for new component
|
|
613
|
+
const mappedComponent = mapPropertiesToComponent(pendingComponent, properties);
|
|
614
|
+
const componentWithProperties = filterComponentProperties(mappedComponent);
|
|
615
|
+
// Add to form based on drop context
|
|
616
|
+
if (pendingDropContext?.isNested && pendingDropContext.parentId) {
|
|
617
|
+
handleNestedComponentDrop(pendingDropContext.parentId, componentWithProperties, pendingDropContext.cellInfo, pendingDropContext.parentPath);
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
// Insert before submit button only if submit is at the last position (not moved by user)
|
|
621
|
+
setFormSchema(prev => {
|
|
622
|
+
const submitIndex = prev.components.findIndex(c => c.type === 'button' && c.buttonType === 'submit');
|
|
623
|
+
// If submit exists and is at the last position, insert before it
|
|
624
|
+
if (submitIndex !== -1 && submitIndex === prev.components.length - 1) {
|
|
625
|
+
const newComponents = [...prev.components];
|
|
626
|
+
newComponents.splice(submitIndex, 0, componentWithProperties);
|
|
627
|
+
return { ...prev, components: newComponents };
|
|
628
|
+
}
|
|
629
|
+
// Otherwise (submit moved or doesn't exist), add at end
|
|
630
|
+
return { ...prev, components: [...prev.components, componentWithProperties] };
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
setPendingDropContext(null);
|
|
634
|
+
}, [pendingComponent, pendingDropContext, editingField, pendingWidgetSwitch, setFormSchema]);
|
|
635
|
+
const handleNestedComponentDrop = (parentId, component, cellInfo, parentPath, dropTargetIndex, dropPosition) => {
|
|
636
|
+
// Check if this component type has property configuration and hasn't been configured yet
|
|
637
|
+
if (componentProperties[component.type] && !component._configured) {
|
|
638
|
+
// Show property dialog in nested context
|
|
639
|
+
setPendingComponent(component);
|
|
640
|
+
setEditingField(component);
|
|
641
|
+
setPendingDropContext({
|
|
642
|
+
isNested: true,
|
|
643
|
+
parentId,
|
|
644
|
+
cellInfo,
|
|
645
|
+
parentPath,
|
|
646
|
+
targetIndex: dropTargetIndex,
|
|
647
|
+
targetPosition: dropPosition
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
setPendingComponent(component);
|
|
651
|
+
setEditingField(component);
|
|
652
|
+
// Calculate insert index for drop position
|
|
653
|
+
let insertIndex;
|
|
654
|
+
if (dropTargetIndex !== null && dropTargetIndex !== undefined && dropPosition) {
|
|
655
|
+
insertIndex = dropPosition === 'before' ? dropTargetIndex : dropTargetIndex + 1;
|
|
656
|
+
}
|
|
657
|
+
const updateNestedComponent = (components, targetId, newComp, info, idx) => {
|
|
658
|
+
return components.map(comp => {
|
|
659
|
+
if (comp.id === targetId) {
|
|
660
|
+
// Handle panel/tabs with children array
|
|
661
|
+
if (comp.children && !info) {
|
|
662
|
+
if (idx !== undefined) {
|
|
663
|
+
const newChildren = [...comp.children];
|
|
664
|
+
newChildren.splice(idx, 0, newComp);
|
|
665
|
+
return { ...comp, children: newChildren };
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
...comp,
|
|
669
|
+
children: [...comp.children, newComp]
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
// Handle table with cell-specific drops
|
|
673
|
+
if (comp.tableCells && info && info.row !== undefined && info.col !== undefined && info.tabIndex === undefined) {
|
|
674
|
+
const newTableCells = comp.tableCells.map((row, rowIndex) => row.map((cell, colIndex) => {
|
|
675
|
+
if (rowIndex === info.row && colIndex === info.col) {
|
|
676
|
+
if (idx !== undefined) {
|
|
677
|
+
const newCell = [...cell];
|
|
678
|
+
newCell.splice(idx, 0, newComp);
|
|
679
|
+
return newCell;
|
|
680
|
+
}
|
|
681
|
+
return [...cell, newComp];
|
|
682
|
+
}
|
|
683
|
+
return cell;
|
|
684
|
+
}));
|
|
685
|
+
return {
|
|
686
|
+
...comp,
|
|
687
|
+
tableCells: newTableCells
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
// Handle tabs with tab-specific drops
|
|
691
|
+
if (comp.tabItems && info?.tabIndex !== undefined) {
|
|
692
|
+
const newTabItems = comp.tabItems.map((tab, index) => {
|
|
693
|
+
if (index === info.tabIndex) {
|
|
694
|
+
if (idx !== undefined) {
|
|
695
|
+
const newContent = [...(tab.content || [])];
|
|
696
|
+
newContent.splice(idx, 0, newComp);
|
|
697
|
+
return {
|
|
698
|
+
...tab,
|
|
699
|
+
content: newContent
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
return {
|
|
703
|
+
...tab,
|
|
704
|
+
content: [...(tab.content || []), newComp]
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
return tab;
|
|
708
|
+
});
|
|
709
|
+
return {
|
|
710
|
+
...comp,
|
|
711
|
+
tabItems: newTabItems
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (comp.tabItems) {
|
|
716
|
+
const newTabItems = comp.tabItems.map(tab => ({
|
|
717
|
+
...tab,
|
|
718
|
+
content: tab.content ? updateNestedComponent(tab.content, targetId, newComp, info, idx) : []
|
|
719
|
+
}));
|
|
720
|
+
return {
|
|
721
|
+
...comp,
|
|
722
|
+
tabItems: newTabItems
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
// Recursively search in nested components
|
|
726
|
+
if (comp.children) {
|
|
727
|
+
return {
|
|
728
|
+
...comp,
|
|
729
|
+
children: updateNestedComponent(comp.children, targetId, newComp, info, idx)
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
if (comp.tableCells) {
|
|
733
|
+
const newTableCells = comp.tableCells.map(row => row.map(cell => updateNestedComponent(cell, targetId, newComp, info, idx)));
|
|
734
|
+
return {
|
|
735
|
+
...comp,
|
|
736
|
+
tableCells: newTableCells
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
return comp;
|
|
740
|
+
});
|
|
741
|
+
};
|
|
742
|
+
setFormSchema(prev => ({
|
|
743
|
+
...prev,
|
|
744
|
+
components: updateNestedComponent(prev.components, parentId, component, cellInfo, insertIndex)
|
|
745
|
+
}));
|
|
746
|
+
};
|
|
747
|
+
const handleReorder = (newComponents) => {
|
|
748
|
+
setFormSchema(prev => ({
|
|
749
|
+
...prev,
|
|
750
|
+
components: newComponents
|
|
751
|
+
}));
|
|
752
|
+
};
|
|
753
|
+
const handleNestedReorder = (parentId, parentPath, newChildren, cellInfo) => {
|
|
754
|
+
const updateNestedChildren = (components, targetPath, updatedChildren, info) => {
|
|
755
|
+
return components.map(comp => {
|
|
756
|
+
const currentPath = comp.parentPath ? `${comp.parentPath}.${comp.id}` : comp.id;
|
|
757
|
+
// Check if this is the target component
|
|
758
|
+
if (currentPath === targetPath || comp.id === parentId) {
|
|
759
|
+
// Handle panel children
|
|
760
|
+
if (comp.children && !info) {
|
|
761
|
+
return {
|
|
762
|
+
...comp,
|
|
763
|
+
children: updatedChildren
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
// Handle table cell children
|
|
767
|
+
if (comp.tableCells && info && info.row !== undefined && info.col !== undefined && info.tabIndex === undefined) {
|
|
768
|
+
const newTableCells = comp.tableCells.map((row, rowIndex) => row.map((cell, colIndex) => {
|
|
769
|
+
if (rowIndex === info.row && colIndex === info.col) {
|
|
770
|
+
return updatedChildren;
|
|
771
|
+
}
|
|
772
|
+
return cell;
|
|
773
|
+
}));
|
|
774
|
+
return {
|
|
775
|
+
...comp,
|
|
776
|
+
tableCells: newTableCells
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
// Handle tab content children
|
|
780
|
+
if (comp.tabItems && info && info.tabIndex !== undefined) {
|
|
781
|
+
const newTabItems = comp.tabItems.map((tab, index) => {
|
|
782
|
+
if (index === info.tabIndex) {
|
|
783
|
+
return {
|
|
784
|
+
...tab,
|
|
785
|
+
content: updatedChildren
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
return tab;
|
|
789
|
+
});
|
|
790
|
+
return {
|
|
791
|
+
...comp,
|
|
792
|
+
tabItems: newTabItems
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (comp.tableCells) {
|
|
797
|
+
const newTableCells = comp.tableCells.map(row => row.map(cell => updateNestedChildren(cell, targetPath, updatedChildren, info)));
|
|
798
|
+
return {
|
|
799
|
+
...comp,
|
|
800
|
+
tableCells: newTableCells
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
if (comp.tabItems) {
|
|
804
|
+
const newTabItems = comp.tabItems.map(tab => ({
|
|
805
|
+
...tab,
|
|
806
|
+
content: tab.content ? updateNestedChildren(tab.content, targetPath, updatedChildren, info) : []
|
|
807
|
+
}));
|
|
808
|
+
return {
|
|
809
|
+
...comp,
|
|
810
|
+
tabItems: newTabItems
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
// Recursively search in nested structures
|
|
814
|
+
if (comp.children) {
|
|
815
|
+
return {
|
|
816
|
+
...comp,
|
|
817
|
+
children: updateNestedChildren(comp.children, targetPath, updatedChildren, info)
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
return comp;
|
|
821
|
+
});
|
|
822
|
+
};
|
|
823
|
+
setFormSchema(prev => ({
|
|
824
|
+
...prev,
|
|
825
|
+
components: updateNestedChildren(prev.components, parentPath, newChildren, cellInfo)
|
|
826
|
+
}));
|
|
827
|
+
};
|
|
828
|
+
// T024: Handle Copy action - copy to system clipboard
|
|
829
|
+
const [hasClipboardData, setHasClipboardData] = React.useState(false);
|
|
830
|
+
// In-memory cache of the copied field so paste never needs to read the
|
|
831
|
+
// system clipboard (avoids triggering the browser's native paste UI in
|
|
832
|
+
// Firefox, which was firing on the first drag-drop operation).
|
|
833
|
+
const copiedFieldRef = React.useRef(null);
|
|
834
|
+
const handleCopy = React.useCallback(async (field) => {
|
|
835
|
+
try {
|
|
836
|
+
await ClipboardHelper.copyToClipboard(field);
|
|
837
|
+
console.log('Field copied to system clipboard:', field.id);
|
|
838
|
+
copiedFieldRef.current = field;
|
|
839
|
+
setHasClipboardData(true);
|
|
840
|
+
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
console.error('Copy failed:', error);
|
|
843
|
+
}
|
|
844
|
+
}, []);
|
|
845
|
+
// T025-T026: Handle Paste action - paste from system clipboard AFTER target field
|
|
846
|
+
const handlePaste = React.useCallback(async (targetFieldId) => {
|
|
847
|
+
try {
|
|
848
|
+
// Use the in-memory copy instead of reading the system clipboard.
|
|
849
|
+
// Reading navigator.clipboard.readText() at runtime in Firefox triggers
|
|
850
|
+
// the browser's native paste UI prompt (esp. on first invocation after
|
|
851
|
+
// drag-drop), so we avoid it entirely.
|
|
852
|
+
const copiedField = copiedFieldRef.current;
|
|
853
|
+
if (!copiedField) {
|
|
854
|
+
setHasClipboardData(false);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
// Remap IDs for layout components and all their descendants
|
|
858
|
+
const remapIds = (comp) => {
|
|
859
|
+
const newId = generateId(comp.type);
|
|
860
|
+
const newName = generateName(comp.type);
|
|
861
|
+
const remapped = {
|
|
862
|
+
...comp,
|
|
863
|
+
id: newId,
|
|
864
|
+
name: newName
|
|
865
|
+
};
|
|
866
|
+
if (comp.children) {
|
|
867
|
+
remapped.children = comp.children.map(child => remapIds(child));
|
|
868
|
+
}
|
|
869
|
+
if (comp.tabItems) {
|
|
870
|
+
remapped.tabItems = comp.tabItems.map(tab => ({
|
|
871
|
+
...tab,
|
|
872
|
+
content: tab.content ? tab.content.map(child => remapIds(child)) : []
|
|
873
|
+
}));
|
|
874
|
+
}
|
|
875
|
+
if (comp.tableCells) {
|
|
876
|
+
remapped.tableCells = comp.tableCells.map(row => row.map(cell => cell.map(child => remapIds(child))));
|
|
877
|
+
}
|
|
878
|
+
return remapped;
|
|
879
|
+
};
|
|
880
|
+
// Create a unique field to insert (remap nested ids if layout)
|
|
881
|
+
let newField = ClipboardHelper.createUniqueField(copiedField);
|
|
882
|
+
// Always generate new ID and name (for both layout and regular components)
|
|
883
|
+
newField = {
|
|
884
|
+
...newField,
|
|
885
|
+
id: generateId(newField.type),
|
|
886
|
+
name: generateName(newField.type)
|
|
887
|
+
};
|
|
888
|
+
// For layout components, also remap all nested children
|
|
889
|
+
if (['panel', 'tabs', 'table', 'card'].includes(newField.type)) {
|
|
890
|
+
newField = remapIds(newField);
|
|
891
|
+
}
|
|
892
|
+
// Recursive inserter: inserts newComp AFTER the component with targetId
|
|
893
|
+
const insertAfterTarget = (components, targetId) => {
|
|
894
|
+
const out = [];
|
|
895
|
+
for (let i = 0; i < components.length; i++) {
|
|
896
|
+
const comp = components[i];
|
|
897
|
+
// Direct match at this level
|
|
898
|
+
out.push(comp);
|
|
899
|
+
if (comp.id === targetId) {
|
|
900
|
+
out.push(newField);
|
|
901
|
+
// append remaining items
|
|
902
|
+
out.push(...components.slice(i + 1));
|
|
903
|
+
return { components: out, inserted: true };
|
|
904
|
+
}
|
|
905
|
+
// Search in children (panel)
|
|
906
|
+
if (comp.children) {
|
|
907
|
+
const { components: updatedChildren, inserted } = insertAfterTarget(comp.children, targetId);
|
|
908
|
+
if (inserted) {
|
|
909
|
+
const updatedComp = { ...comp, children: updatedChildren };
|
|
910
|
+
// replace last pushed comp with updatedComp
|
|
911
|
+
out[out.length - 1] = updatedComp;
|
|
912
|
+
out.push(...components.slice(i + 1));
|
|
913
|
+
return { components: out, inserted: true };
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// Search in tabItems content
|
|
917
|
+
if (comp.tabItems) {
|
|
918
|
+
let tabInserted = false;
|
|
919
|
+
const newTabItems = comp.tabItems.map(tab => {
|
|
920
|
+
if (!tab.content)
|
|
921
|
+
return tab;
|
|
922
|
+
const { components: updatedContent, inserted } = insertAfterTarget(tab.content, targetId);
|
|
923
|
+
if (inserted)
|
|
924
|
+
tabInserted = true;
|
|
925
|
+
return { ...tab, content: updatedContent };
|
|
926
|
+
});
|
|
927
|
+
if (tabInserted) {
|
|
928
|
+
const updatedComp = { ...comp, tabItems: newTabItems };
|
|
929
|
+
out[out.length - 1] = updatedComp;
|
|
930
|
+
out.push(...components.slice(i + 1));
|
|
931
|
+
return { components: out, inserted: true };
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
// Search in tableCells (rows -> cells -> component arrays)
|
|
935
|
+
if (comp.tableCells) {
|
|
936
|
+
let tableInserted = false;
|
|
937
|
+
const newTableCells = comp.tableCells.map((row) => row.map((cell) => {
|
|
938
|
+
const { components: updatedCell, inserted } = insertAfterTarget(cell, targetId);
|
|
939
|
+
if (inserted)
|
|
940
|
+
tableInserted = true;
|
|
941
|
+
return updatedCell;
|
|
942
|
+
}));
|
|
943
|
+
if (tableInserted) {
|
|
944
|
+
const updatedComp = { ...comp, tableCells: newTableCells };
|
|
945
|
+
out[out.length - 1] = updatedComp;
|
|
946
|
+
out.push(...components.slice(i + 1));
|
|
947
|
+
return { components: out, inserted: true };
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return { components: out, inserted: false };
|
|
952
|
+
};
|
|
953
|
+
setFormSchema(prev => {
|
|
954
|
+
const { components: updated, inserted } = insertAfterTarget(prev.components, targetFieldId);
|
|
955
|
+
if (!inserted) {
|
|
956
|
+
// fallback: try to insert at root after target if present
|
|
957
|
+
const rootIndex = prev.components.findIndex(c => c.id === targetFieldId);
|
|
958
|
+
if (rootIndex !== -1) {
|
|
959
|
+
const newComponents = [...prev.components];
|
|
960
|
+
newComponents.splice(rootIndex + 1, 0, newField);
|
|
961
|
+
return { ...prev, components: newComponents };
|
|
962
|
+
}
|
|
963
|
+
console.warn('Paste target not found:', targetFieldId);
|
|
964
|
+
return prev;
|
|
965
|
+
}
|
|
966
|
+
return { ...prev, components: updated };
|
|
967
|
+
});
|
|
968
|
+
// visual feedback (unchanged)
|
|
969
|
+
setTimeout(() => {
|
|
970
|
+
const wrapper = document.querySelector(`[data-field-id="${newField.id}"]`);
|
|
971
|
+
if (wrapper) {
|
|
972
|
+
wrapper.classList.add('field-inserted');
|
|
973
|
+
setTimeout(() => wrapper.classList.remove('field-inserted'), 2000);
|
|
974
|
+
}
|
|
975
|
+
}, 50);
|
|
976
|
+
// Clear the system clipboard and in-memory copy so the paste button
|
|
977
|
+
// is hidden. This signals to the user that a fresh copy is needed
|
|
978
|
+
// before pasting again.
|
|
979
|
+
copiedFieldRef.current = null;
|
|
980
|
+
setHasClipboardData(false);
|
|
981
|
+
ClipboardHelper.clearData().catch(() => { });
|
|
982
|
+
}
|
|
983
|
+
catch (error) {
|
|
984
|
+
console.error('Paste failed:', error);
|
|
985
|
+
}
|
|
986
|
+
}, []);
|
|
987
|
+
// T017-T018: Handle Delete action with focus management
|
|
988
|
+
const handleDeleteAction = React.useCallback((fieldId) => {
|
|
989
|
+
setFormSchema(prev => ({
|
|
990
|
+
...prev,
|
|
991
|
+
components: prev.components.filter(c => c.id !== fieldId)
|
|
992
|
+
}));
|
|
993
|
+
// Move focus to canvas container after delete
|
|
994
|
+
setTimeout(() => {
|
|
995
|
+
const canvas = document.querySelector('.form-preview-area');
|
|
996
|
+
canvas?.focus();
|
|
997
|
+
}, 0);
|
|
998
|
+
}, []);
|
|
999
|
+
const isDescendantOf = (parentId, childId, components) => {
|
|
1000
|
+
const stack = [...components];
|
|
1001
|
+
while (stack.length) {
|
|
1002
|
+
const comp = stack.pop();
|
|
1003
|
+
if (comp.id === parentId) {
|
|
1004
|
+
// Found parent, now search for child within it
|
|
1005
|
+
const searchInChildren = (items) => {
|
|
1006
|
+
for (const item of items) {
|
|
1007
|
+
if (item.id === childId)
|
|
1008
|
+
return true;
|
|
1009
|
+
if (item.children && searchInChildren(item.children))
|
|
1010
|
+
return true;
|
|
1011
|
+
if (item.tabItems) {
|
|
1012
|
+
for (const tab of item.tabItems) {
|
|
1013
|
+
if (tab.content && searchInChildren(tab.content))
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (item.tableCells) {
|
|
1018
|
+
for (const row of item.tableCells) {
|
|
1019
|
+
for (const cell of row) {
|
|
1020
|
+
if (searchInChildren(cell))
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return false;
|
|
1027
|
+
};
|
|
1028
|
+
return searchInChildren(comp.children || []) ||
|
|
1029
|
+
(comp.tabItems?.some(t => searchInChildren(t.content || [])) || false) ||
|
|
1030
|
+
(comp.tableCells?.some(row => row.some(cell => searchInChildren(cell))) || false);
|
|
1031
|
+
}
|
|
1032
|
+
if (comp.children?.length)
|
|
1033
|
+
stack.push(...comp.children);
|
|
1034
|
+
if (comp.tabItems?.length) {
|
|
1035
|
+
for (const tab of comp.tabItems) {
|
|
1036
|
+
if (tab.content?.length)
|
|
1037
|
+
stack.push(...tab.content);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (comp.tableCells?.length) {
|
|
1041
|
+
for (const row of comp.tableCells) {
|
|
1042
|
+
for (const cell of row) {
|
|
1043
|
+
if (cell?.length)
|
|
1044
|
+
stack.push(...cell);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return false;
|
|
1050
|
+
};
|
|
1051
|
+
const handleDelete = React.useCallback((fieldId, isReorering) => {
|
|
1052
|
+
isReorderingRef.current = isReorering || false;
|
|
1053
|
+
// Recursive function to remove component from nested structures
|
|
1054
|
+
const removeComponentById = (components) => {
|
|
1055
|
+
return components
|
|
1056
|
+
.filter(c => c.id !== fieldId)
|
|
1057
|
+
.map(c => {
|
|
1058
|
+
// If component has tabs, recursively remove from tab content
|
|
1059
|
+
if (c.tabItems) {
|
|
1060
|
+
return {
|
|
1061
|
+
...c,
|
|
1062
|
+
tabItems: c.tabItems.map(tab => ({
|
|
1063
|
+
...tab,
|
|
1064
|
+
content: tab.content ? removeComponentById(tab.content) : []
|
|
1065
|
+
}))
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
// If component has children (panel), recursively remove from children
|
|
1069
|
+
if (c.children) {
|
|
1070
|
+
return { ...c, children: removeComponentById(c.children) };
|
|
1071
|
+
}
|
|
1072
|
+
// If component has table cells, recursively remove from cells
|
|
1073
|
+
if (c.tableCells) {
|
|
1074
|
+
return {
|
|
1075
|
+
...c,
|
|
1076
|
+
tableCells: c.tableCells.map(row => row.map(cell => removeComponentById(cell)))
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
return c;
|
|
1080
|
+
});
|
|
1081
|
+
};
|
|
1082
|
+
setFormSchema(prev => ({
|
|
1083
|
+
...prev,
|
|
1084
|
+
components: removeComponentById(prev.components)
|
|
1085
|
+
}));
|
|
1086
|
+
setPendingComponent(prev => (prev && (prev.id === fieldId || isDescendantOf(fieldId, prev.id, formSchema.components)) && !isReorering)
|
|
1087
|
+
? null
|
|
1088
|
+
: prev);
|
|
1089
|
+
setEditingField(prev => (prev && (prev.id === fieldId || isDescendantOf(fieldId, prev.id, formSchema.components)) && !isReorering)
|
|
1090
|
+
? null
|
|
1091
|
+
: prev);
|
|
1092
|
+
// Move focus to canvas container after delete
|
|
1093
|
+
setTimeout(() => {
|
|
1094
|
+
const canvas = document.querySelector('.form-preview-area');
|
|
1095
|
+
canvas?.focus();
|
|
1096
|
+
}, 0);
|
|
1097
|
+
setTimeout(() => {
|
|
1098
|
+
isReorderingRef.current = false;
|
|
1099
|
+
}, 500);
|
|
1100
|
+
}, [pendingComponent, editingField, setFormSchema]);
|
|
1101
|
+
const handleLeftPaneToggle = useCallback(() => {
|
|
1102
|
+
if (isMobile && !isRightPaneCollapsed) {
|
|
1103
|
+
setIsRightPaneCollapsed(true);
|
|
1104
|
+
}
|
|
1105
|
+
setIsLeftPaneCollapsed(prev => !prev);
|
|
1106
|
+
}, [isMobile, isRightPaneCollapsed]);
|
|
1107
|
+
const handleSidebarOverlayClick = useCallback(() => {
|
|
1108
|
+
if (!isLeftPaneCollapsed) {
|
|
1109
|
+
setIsLeftPaneCollapsed(true);
|
|
1110
|
+
}
|
|
1111
|
+
if (!isRightPaneCollapsed) {
|
|
1112
|
+
setIsRightPaneCollapsed(true);
|
|
1113
|
+
}
|
|
1114
|
+
}, [isLeftPaneCollapsed, isRightPaneCollapsed]);
|
|
1115
|
+
const handleRightPaneToggle = useCallback(() => {
|
|
1116
|
+
if (isMobile && !isLeftPaneCollapsed) {
|
|
1117
|
+
setIsLeftPaneCollapsed(true);
|
|
1118
|
+
}
|
|
1119
|
+
setIsRightPaneCollapsed(prev => !prev);
|
|
1120
|
+
}, [isMobile, isLeftPaneCollapsed]);
|
|
1121
|
+
const handleLeftPaneMouseEnter = useCallback(() => {
|
|
1122
|
+
if (leftPaneHoverTimeoutRef.current) {
|
|
1123
|
+
clearTimeout(leftPaneHoverTimeoutRef.current);
|
|
1124
|
+
}
|
|
1125
|
+
if (isLeftPaneCollapsed) {
|
|
1126
|
+
leftPaneHoverTimeoutRef.current = setTimeout(() => {
|
|
1127
|
+
if (!isMobile) {
|
|
1128
|
+
setIsLeftPaneHovered(true);
|
|
1129
|
+
}
|
|
1130
|
+
}, 500);
|
|
1131
|
+
}
|
|
1132
|
+
}, [isLeftPaneCollapsed]);
|
|
1133
|
+
const handleLeftPaneMouseLeave = useCallback(() => {
|
|
1134
|
+
if (leftPaneHoverTimeoutRef.current) {
|
|
1135
|
+
clearTimeout(leftPaneHoverTimeoutRef.current);
|
|
1136
|
+
}
|
|
1137
|
+
setIsLeftPaneHovered(false);
|
|
1138
|
+
}, []);
|
|
1139
|
+
const handleRightPaneMouseEnter = useCallback(() => {
|
|
1140
|
+
if (rightPaneHoverTimeoutRef.current) {
|
|
1141
|
+
clearTimeout(rightPaneHoverTimeoutRef.current);
|
|
1142
|
+
}
|
|
1143
|
+
if (isRightPaneCollapsed) {
|
|
1144
|
+
rightPaneHoverTimeoutRef.current = setTimeout(() => {
|
|
1145
|
+
if (!isMobile) {
|
|
1146
|
+
setIsRightPaneHovered(true);
|
|
1147
|
+
}
|
|
1148
|
+
}, 500);
|
|
1149
|
+
}
|
|
1150
|
+
}, [isRightPaneCollapsed, isMobile]);
|
|
1151
|
+
const handleRightPaneMouseLeave = useCallback(() => {
|
|
1152
|
+
if (rightPaneHoverTimeoutRef.current) {
|
|
1153
|
+
clearTimeout(rightPaneHoverTimeoutRef.current);
|
|
1154
|
+
}
|
|
1155
|
+
setIsRightPaneHovered(false);
|
|
1156
|
+
}, []);
|
|
1157
|
+
const splitData = React.useMemo(() => splitSchema(formSchema), [formSchema]);
|
|
1158
|
+
const downloadCombinedSchema = async () => {
|
|
1159
|
+
try {
|
|
1160
|
+
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
|
|
1161
|
+
const blob = new Blob([JSON.stringify(splitData, null, 2)], { type: 'application/json' });
|
|
1162
|
+
const url = URL.createObjectURL(blob);
|
|
1163
|
+
const link = document.createElement('a');
|
|
1164
|
+
link.href = url;
|
|
1165
|
+
link.download = `form-schema-${timestamp}.json`;
|
|
1166
|
+
document.body.appendChild(link);
|
|
1167
|
+
link.click();
|
|
1168
|
+
link.remove();
|
|
1169
|
+
URL.revokeObjectURL(url);
|
|
1170
|
+
}
|
|
1171
|
+
catch (err) {
|
|
1172
|
+
console.error('Download failed', err);
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
const modeItems = [
|
|
1176
|
+
{ text: l10n.getConstant('simple'), id: 'simple' },
|
|
1177
|
+
{ text: l10n.getConstant('developer'), id: 'developer' }
|
|
1178
|
+
];
|
|
1179
|
+
const handleModeSelect = (args) => {
|
|
1180
|
+
const selectedMode = args.item.id === 'simple' ? 'Simple' : 'Developer';
|
|
1181
|
+
const url = new URL(window.location.href);
|
|
1182
|
+
const currentUrlMode = url.searchParams.get('mode');
|
|
1183
|
+
if (currentUrlMode === selectedMode.toLowerCase()) {
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
url.searchParams.set('mode', selectedMode.toLowerCase());
|
|
1187
|
+
window.location.href = url.toString();
|
|
1188
|
+
};
|
|
1189
|
+
useEffect(() => {
|
|
1190
|
+
const element = builderContainerRef.current;
|
|
1191
|
+
if (!element)
|
|
1192
|
+
return;
|
|
1193
|
+
const updateWidth = () => {
|
|
1194
|
+
setContainerWidth(element.getBoundingClientRect().width);
|
|
1195
|
+
};
|
|
1196
|
+
updateWidth();
|
|
1197
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
1198
|
+
updateWidth();
|
|
1199
|
+
});
|
|
1200
|
+
resizeObserver.observe(element);
|
|
1201
|
+
return () => {
|
|
1202
|
+
resizeObserver.disconnect();
|
|
1203
|
+
};
|
|
1204
|
+
}, []);
|
|
1205
|
+
useEffect(() => {
|
|
1206
|
+
setIsLeftPaneCollapsed(containerWidth <= 820);
|
|
1207
|
+
setIsRightPaneCollapsed(containerWidth <= 1024);
|
|
1208
|
+
setIsMobile(containerWidth <= 1024);
|
|
1209
|
+
}, [containerWidth]);
|
|
1210
|
+
useEffect(() => {
|
|
1211
|
+
if (isMobile) {
|
|
1212
|
+
if (pendingComponent && isRightPaneCollapsed && !isReorderingRef.current) {
|
|
1213
|
+
handleRightPaneToggle();
|
|
1214
|
+
}
|
|
1215
|
+
if (!pendingComponent && !isRightPaneCollapsed) {
|
|
1216
|
+
handleRightPaneToggle();
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}, [isMobile, pendingComponent]);
|
|
1220
|
+
useEffect(() => {
|
|
1221
|
+
const element = contentSectionRef.current;
|
|
1222
|
+
const preventTouchScroll = (e) => {
|
|
1223
|
+
e.preventDefault();
|
|
1224
|
+
};
|
|
1225
|
+
element?.addEventListener("touchmove", preventTouchScroll, {
|
|
1226
|
+
passive: false,
|
|
1227
|
+
});
|
|
1228
|
+
return () => {
|
|
1229
|
+
element?.removeEventListener("touchmove", preventTouchScroll);
|
|
1230
|
+
};
|
|
1231
|
+
}, []);
|
|
1232
|
+
return (_jsxs("div", { "aria-label": "Form Builder", ref: builderContainerRef, className: `builder-container ${enableRtl ? 'e-rtl' : ''} ${formBuilderMode === 'component' ? 'e-component' : ''} ${containerWidth <= 820 ? 'small-screen' : ''}`, children: [_jsxs("div", { className: `main-header`, children: [_jsxs("div", { className: "left-section", children: [isMobile && _jsx(ButtonComponent, { cssClass: 'e-flat', iconCss: 'e-icons e-show-hide-panel', onClick: handleLeftPaneToggle, "aria-label": "Open component palette" }), formBuilderMode === 'sass' && !isMobile &&
|
|
1233
|
+
_jsx(ButtonComponent, { cssClass: 'action-btn e-flat', iconCss: 'e-icons e-arrow-head', onClick: () => { navigateTo('/dashboard'); }, "aria-label": "Back to dashboard" }), !isMobile && _jsx(InPlaceEditorComponent, { type: 'Text', mode: 'Inline', value: formSchema.settings?.name || 'Untitled Form', change: (e) => {
|
|
1234
|
+
if (formSchema.settings) {
|
|
1235
|
+
formSchema.settings.name = e.value;
|
|
1236
|
+
setFormSchema({ ...formSchema });
|
|
1237
|
+
}
|
|
1238
|
+
} })] }), _jsxs("div", { className: "right-section", children: [_jsx(DropDownButtonComponent, { items: modeItems, select: handleModeSelect, cssClass: "e-outline mode-button", popupWidth: "100%", "aria-label": l10n.getConstant(`${mode?.toLowerCase()}Mode`), children: l10n.getConstant(`${mode?.toLowerCase()}${!isMobile ? 'Mode' : ''}`) }), allowExport && _jsxs(_Fragment, { children: [_jsx("div", { className: 'separator' }), _jsx(ButtonComponent, { cssClass: "action-btn e-flat e-primary", onClick: downloadCombinedSchema, disabled: isDefaultFormSchema(formSchema), "aria-label": "Export json schema", children: l10n.getConstant('export') })] }), isMobile && _jsx(ButtonComponent, { cssClass: 'e-flat', iconCss: 'e-icons e-adjustment', onClick: handleRightPaneToggle, "aria-label": "Open properties panel" })] })] }), _jsxs("div", { ref: contentSectionRef, role: "main", className: `contentSection${editingField || pendingWidgetSwitch || pendingComponent ? ' sidebar-open' : ' sidebar-closed'}`, children: [containerWidth <= 820 && (!isLeftPaneCollapsed || !isRightPaneCollapsed) && _jsx("div", { className: "sidebar-overlay", "aria-hidden": "true", onClick: handleSidebarOverlayClick }), _jsxs(DragDrop, { children: [_jsx(LeftPane, { className: `${isLeftPaneCollapsed ? 'collapsed' : 'expanded'}`, templates: formTemplates ?? defaultTemplates, paletteConfig: paletteConfig, mode: mode, onFieldDragStarted: onFieldDragStarted, onDrop: handleComponentDrop, containerWidth: containerWidth }), !isMobile &&
|
|
1239
|
+
_jsxs("button", { className: `e-btn left-pane-toggle ${isLeftPaneCollapsed ? 'collapsed' : 'expanded'}`, onClick: handleLeftPaneToggle, onMouseEnter: handleLeftPaneMouseEnter, onMouseLeave: handleLeftPaneMouseLeave, "aria-label": isLeftPaneCollapsed ? 'Expand component palette' : 'Collapse component palette', "aria-expanded": !isLeftPaneCollapsed, children: [_jsx("span", { "aria-hidden": "true", className: `e-btn-icon e-icons ${enableRtl
|
|
1240
|
+
? (isLeftPaneCollapsed ? 'e-chevron-left' : 'e-chevron-right')
|
|
1241
|
+
: (isLeftPaneCollapsed ? 'e-chevron-right' : 'e-chevron-left')}` }), isLeftPaneCollapsed && isLeftPaneHovered && _jsx("span", { style: { marginLeft: '8px' }, children: "Expand" })] }, "left-pane-toggle"), _jsx(MainSection, { className: `${isRightPaneCollapsed ? 'right-collapsed' : ''} ${isLeftPaneCollapsed ? 'left-collapsed' : ''}`, formSchema: formSchema, onDrop: handleComponentDrop, activeFieldId: pendingComponent?.id, onNestedDrop: handleNestedComponentDrop, onReorder: handleReorder, onNestedReorder: handleNestedReorder, onEdit: handleEdit, mode: mode, onCopy: handleCopy, onPaste: handlePaste, onDelete: handleDelete, onSwitchWidget: handleSwitchWidget, onDeleteAction: handleDeleteAction, enablePreview: enablePreview, onFieldDragStarted: onFieldDragStarted, onFieldDraggingOver: onFieldDraggingOver, onFieldDropped: onFieldDropped, hasClipboardData: hasClipboardData })] }), !isMobile &&
|
|
1242
|
+
_jsxs("button", { className: `e-btn right-pane-toggle ${isRightPaneCollapsed ? 'collapsed' : 'expanded'}`, onClick: handleRightPaneToggle, onMouseEnter: handleRightPaneMouseEnter, onMouseLeave: handleRightPaneMouseLeave, "aria-label": isRightPaneCollapsed ? 'Expand properties panel' : 'Collapse properties panel', "aria-expanded": !isRightPaneCollapsed, children: [_jsx("span", { "aria-hidden": "true", className: `e-btn-icon e-icons ${enableRtl
|
|
1243
|
+
? (isRightPaneCollapsed ? 'e-chevron-right' : 'e-chevron-left')
|
|
1244
|
+
: (isRightPaneCollapsed ? 'e-chevron-left' : 'e-chevron-right')}` }), isRightPaneCollapsed && isRightPaneHovered && _jsx("span", { style: { marginLeft: '8px' }, children: "Expand" })] }, "right-pane-toggle"), _jsxs("aside", { role: "complementary", "aria-label": pendingComponent && !isDefaultFormSchema(formSchema)
|
|
1245
|
+
? 'Component Properties'
|
|
1246
|
+
: 'Form Settings', className: `right-sidebar ${isRightPaneCollapsed ? 'sidebar-closed' : 'expanded'}`, children: [pendingComponent && componentProperties[pendingComponent.type] && !isDefaultFormSchema(formSchema) && (_jsx(RightPane, { mode: 'component', componentType: pendingComponent.type, configData: componentProperties, existingValues: editingField || pendingComponent, onSave: handlePropertyDialogSave, onPropertyChanged: onPropertyChanged, onClose: () => {
|
|
1247
|
+
setPendingComponent(null);
|
|
1248
|
+
setEditingField(null);
|
|
1249
|
+
setPendingWidgetSwitch(null);
|
|
1250
|
+
setPendingDropContext(null);
|
|
1251
|
+
}, builderMode: mode }, `sidebar-${(editingField || pendingComponent).id}`)), (!pendingComponent || isDefaultFormSchema(formSchema)) && (_jsx(RightPane, { componentType: 'form', mode: "formSettings", configData: componentProperties, formSettingsConfig: formSettingsConfig, formSettings: formSchema.settings, onSave: handleFormSettingsSave, onClose: () => { } }, "form-settings"))] })] })] }));
|
|
1252
|
+
};
|
|
1253
|
+
/**
|
|
1254
|
+
* The FormBuilder component provides a visual, drag-and-drop interface for authoring form schemas. It supports palette customization, template insertion, dual modes (Simple/Developer), and emits structural form schemas (FormSchema) that can be consumed by FormRenderer.
|
|
1255
|
+
*
|
|
1256
|
+
* ```typescript
|
|
1257
|
+
* import { FormBuilder } from '@syncfusion/ej2-react-form-builder';
|
|
1258
|
+
*
|
|
1259
|
+
* <FormBuilder />
|
|
1260
|
+
* ```
|
|
1261
|
+
*/
|
|
1262
|
+
export const FormBuilder = forwardRef((props, ref) => {
|
|
1263
|
+
const formBuilderRef = useRef(null);
|
|
1264
|
+
const { locale = 'en-US', allowExport = true, enablePreview = true, enablePersistence = false, enableRtl = false, schema, mode = 'Developer', formTemplates, paletteConfig, onFieldDragStarted, onFieldDraggingOver, onFieldDropped, onPropertyChanged, style: userStyle, ...domProps } = props;
|
|
1265
|
+
// Determine mode synchronously from the URL on first render (no extra render cycle)
|
|
1266
|
+
const [effectiveMode] = useState(() => {
|
|
1267
|
+
if (typeof window === 'undefined')
|
|
1268
|
+
return undefined;
|
|
1269
|
+
const urlMode = new URLSearchParams(window.location.search).get('mode');
|
|
1270
|
+
if (urlMode === 'developer')
|
|
1271
|
+
return 'Developer';
|
|
1272
|
+
if (urlMode === 'simple')
|
|
1273
|
+
return 'Simple';
|
|
1274
|
+
return undefined;
|
|
1275
|
+
});
|
|
1276
|
+
useEffect(() => {
|
|
1277
|
+
if (typeof window === 'undefined')
|
|
1278
|
+
return;
|
|
1279
|
+
const params = new URLSearchParams(window.location.search);
|
|
1280
|
+
// Only set mode in URL if a mode is provided via props and URL doesn't have one
|
|
1281
|
+
if (params.get('mode') !== 'developer' && params.get('mode') !== 'simple' && mode) {
|
|
1282
|
+
const url = new URL(window.location.href);
|
|
1283
|
+
url.searchParams.set('mode', mode.toLowerCase());
|
|
1284
|
+
window.history.replaceState({}, '', url.toString());
|
|
1285
|
+
}
|
|
1286
|
+
}, [mode]);
|
|
1287
|
+
const finalMode = effectiveMode || mode;
|
|
1288
|
+
const computedStyle = {
|
|
1289
|
+
...(formBuilderMode === 'component' ? { height: '700px' } : {}),
|
|
1290
|
+
...userStyle
|
|
1291
|
+
};
|
|
1292
|
+
const computedSchema = schema
|
|
1293
|
+
? schema.components !== undefined
|
|
1294
|
+
? schema
|
|
1295
|
+
: importSplitSchemas(typeof schema === 'string' ? schema : JSON.stringify(schema), defaultFormSchema)
|
|
1296
|
+
: schema;
|
|
1297
|
+
return (_jsx(FormSchemaProvider, { enablePersistence: enablePersistence, initialSchema: computedSchema || defaultFormSchema, mode: finalMode, children: _jsx(LocalizationProvider, { locale: locale, children: _jsx("div", { id: "form-builder", ref: formBuilderRef, style: computedStyle, ...domProps, children: _jsx(FormBuilderComponent, { apiRef: ref, domRef: formBuilderRef, enableRtl: enableRtl, locale: locale, mode: finalMode, enablePersistence: enablePersistence, onFieldDragStarted: onFieldDragStarted, onFieldDraggingOver: onFieldDraggingOver, onFieldDropped: onFieldDropped, onPropertyChanged: onPropertyChanged, allowExport: allowExport, enablePreview: enablePreview, formTemplates: formTemplates, paletteConfig: paletteConfig }) }) }) }));
|
|
1298
|
+
});
|
|
1299
|
+
export default React.memo(FormBuilder);
|