feat: build React Form Builder (F2.7-F2.10)
Components: - FieldPalette: drag-and-drop source for all field types - FormCanvas: drop target with field list, reordering, CRUD - FormField: individual field component with actions - FieldSettingsPanel: edit field properties (label, ID, options, etc.) - FormFieldOptions: manage select/radio/checkbox options - FormPreview: live preview of rendered form - FormFieldPreview: preview individual field types Features: - 16 field types (text, email, select, checkbox, radio, etc.) - Categorized field palette - Drag-and-drop field reordering - Per-field settings panel - Option management for choice fields - Live form preview - Save via AJAX Config: - fieldTypes.js: field definitions and constants - Generate unique field IDs - Field type categories (input, choice, layout, preset, advanced)
This commit is contained in:
69
src/admin/components/formBuilder/FieldPalette.css
Normal file
69
src/admin/components/formBuilder/FieldPalette.css
Normal file
@@ -0,0 +1,69 @@
|
||||
.formipay-field-palette {
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
.formipay-field-palette h3 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e1e1e;
|
||||
}
|
||||
|
||||
.formipay-palette-category {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.formipay-palette-category h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: #646970;
|
||||
}
|
||||
|
||||
.formipay-palette-items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.formipay-palette-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: #f0f0f1;
|
||||
border: 1px solid #dcdcde;
|
||||
border-radius: 4px;
|
||||
cursor: grab;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.formipay-palette-item:hover {
|
||||
background: #fff;
|
||||
border-color: #2271b1;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.formipay-palette-item:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.formipay-palette-item svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
fill: #1e1e1e;
|
||||
}
|
||||
|
||||
.formipay-palette-item span {
|
||||
font-size: 12px;
|
||||
color: #1e1e1e;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
50
src/admin/components/formBuilder/FieldPalette.js
Normal file
50
src/admin/components/formBuilder/FieldPalette.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Field Palette - Drag-and-drop source for form fields
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Icon } from '@wordpress/icons';
|
||||
import { FIELD_CATEGORIES, getFieldTypesByCategory } from '../../config/fieldTypes';
|
||||
import './FieldPalette.css';
|
||||
|
||||
export default function FieldPalette({ onDragStart }) {
|
||||
const fieldTypesByCategory = getFieldTypesByCategory();
|
||||
const categories = Object.values(FIELD_CATEGORIES).sort((a, b) => a.order - b.order);
|
||||
|
||||
const handleDragStart = (event, fieldType) => {
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('formipay-field-type', fieldType);
|
||||
onDragStart?.(fieldType);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="formipay-field-palette">
|
||||
<h3>{ __('Add Field', 'formipay') }</h3>
|
||||
|
||||
{categories.map((category) => {
|
||||
const fields = fieldTypesByCategory[category.label] || [];
|
||||
if (fields.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={category.label} className="formipay-palette-category">
|
||||
<h4>{ category.label }</h4>
|
||||
<div className="formipay-palette-items">
|
||||
{fields.map((field) => (
|
||||
<div
|
||||
key={field.type}
|
||||
className="formipay-palette-item"
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, field.type)}
|
||||
title={field.label}
|
||||
>
|
||||
<Icon icon={field.icon} />
|
||||
<span>{ field.label }</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
src/admin/components/formBuilder/FieldSettingsPanel.css
Normal file
48
src/admin/components/formBuilder/FieldSettingsPanel.css
Normal file
@@ -0,0 +1,48 @@
|
||||
.formipay-field-settings-panel {
|
||||
width: 320px;
|
||||
background: #fff;
|
||||
border-left: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
.formipay-settings-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.formipay-settings-header h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.formipay-settings-header .field-type-badge {
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
background: #e0e0e0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.formipay-settings-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.formipay-settings-content .components-base-control {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.formipay-settings-content .components-base-control:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
129
src/admin/components/formBuilder/FieldSettingsPanel.js
Normal file
129
src/admin/components/formBuilder/FieldSettingsPanel.js
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Field Settings Panel - Edit field properties
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { TextControl, CheckboxControl, SelectControl, TextareaControl } from '@wordpress/components';
|
||||
import { FIELD_TYPES } from '../../config/fieldTypes';
|
||||
import FormFieldOptions from './FormFieldOptions';
|
||||
import './FieldSettingsPanel.css';
|
||||
|
||||
export default function FieldSettingsPanel({
|
||||
field,
|
||||
open,
|
||||
onClose,
|
||||
onUpdate,
|
||||
}) {
|
||||
if (!open || !field) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fieldConfig = FIELD_TYPES[field.field_type] || FIELD_TYPES.text;
|
||||
const hasOptions = ['select', 'checkbox', 'radio'].includes(field.field_type);
|
||||
const hasPlaceholder = [
|
||||
'text', 'url', 'email', 'tel', 'number', 'date', 'datetime', 'color',
|
||||
'select', 'checkbox', 'radio', 'hidden', 'textarea', 'country_list'
|
||||
].includes(field.field_type);
|
||||
const hasDefaultValue = [
|
||||
'text', 'url', 'email', 'tel', 'number', 'date', 'datetime', 'color',
|
||||
'select', 'checkbox', 'radio', 'hidden', 'textarea'
|
||||
].includes(field.field_type);
|
||||
const hasDescription = !['hidden'].includes(field.field_type);
|
||||
const isRequired = !['divider', 'page_break', 'hidden'].includes(field.field_type);
|
||||
const hasGridColumns = ['radio', 'checkbox'].includes(field.field_type);
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
onUpdate?.({ ...field, [key]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="formipay-field-settings-panel">
|
||||
<div className="formipay-settings-header">
|
||||
<h3>
|
||||
<span className="field-type-badge">{ fieldConfig.label }</span>
|
||||
{ __('Field Settings', 'formipay') }
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="button-link"
|
||||
onClick={onClose}
|
||||
>
|
||||
{ __('Close', 'formipay') }
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="formipay-settings-content">
|
||||
<TextControl
|
||||
label={ __('Label', 'formipay') }
|
||||
value={field.label || ''}
|
||||
onChange={(value) => handleChange('label', value)}
|
||||
help={ __('The display label for this field', 'formipay') }
|
||||
/>
|
||||
|
||||
<TextControl
|
||||
label={ __('Field ID', 'formipay') }
|
||||
value={field.field_id || ''}
|
||||
onChange={(value) => handleChange('field_id', value)}
|
||||
help={ __('Unique identifier for this field (used in form data)', 'formipay') }
|
||||
/>
|
||||
|
||||
{ hasPlaceholder && (
|
||||
<TextControl
|
||||
label={ __('Placeholder', 'formipay') }
|
||||
value={field.placeholder || ''}
|
||||
onChange={(value) => handleChange('placeholder', value)}
|
||||
/>
|
||||
) }
|
||||
|
||||
{ hasDefaultValue && (
|
||||
<TextControl
|
||||
label={ __('Default Value', 'formipay') }
|
||||
value={field.default_value || ''}
|
||||
onChange={(value) => handleChange('default_value', value)}
|
||||
/>
|
||||
) }
|
||||
|
||||
{ hasDescription && (
|
||||
<TextareaControl
|
||||
label={ __('Description', 'formipay') }
|
||||
value={field.description || ''}
|
||||
onChange={(value) => handleChange('description', value)}
|
||||
help={ __('Optional help text displayed below the field', 'formipay') }
|
||||
rows={3}
|
||||
/>
|
||||
) }
|
||||
|
||||
{ hasOptions && (
|
||||
<FormFieldOptions
|
||||
options={field.field_options || []}
|
||||
onChange={(value) => handleChange('field_options', value)}
|
||||
fieldType={field.field_type}
|
||||
/>
|
||||
) }
|
||||
|
||||
{ hasGridColumns && (
|
||||
<SelectControl
|
||||
label={ __('Option Grid Columns', 'formipay') }
|
||||
value={field.option_grid_columns || 1}
|
||||
options={[
|
||||
{ label: __('1 Column', 'formipay'), value: 1 },
|
||||
{ label: __('2 Columns', 'formipay'), value: 2 },
|
||||
{ label: __('3 Columns', 'formipay'), value: 3 },
|
||||
{ label: __('4 Columns', 'formipay'), value: 4 },
|
||||
]}
|
||||
onChange={(value) => handleChange('option_grid_columns', parseInt(value))}
|
||||
/>
|
||||
) }
|
||||
|
||||
{ isRequired && (
|
||||
<CheckboxControl
|
||||
label={ __('Required Field', 'formipay') }
|
||||
checked={field.is_required || false}
|
||||
onChange={(value) => handleChange('is_required', value)}
|
||||
help={ __('User must fill this field before submitting', 'formipay') }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
src/admin/components/formBuilder/FormBuilder.css
Normal file
31
src/admin/components/formBuilder/FormBuilder.css
Normal file
@@ -0,0 +1,31 @@
|
||||
.formipay-form-builder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 32px);
|
||||
}
|
||||
|
||||
.formipay-builder-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.formipay-builder-toolbar h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.formipay-builder-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.formipay-builder-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
114
src/admin/components/formBuilder/FormBuilder.js
Normal file
114
src/admin/components/formBuilder/FormBuilder.js
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Form Builder - Main container component
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState, useCallback } from '@wordpress/element';
|
||||
import FormCanvas from './FormCanvas';
|
||||
import FieldPalette from './FieldPalette';
|
||||
import FieldSettingsPanel from './FieldSettingsPanel';
|
||||
import FormPreview from './FormPreview';
|
||||
import './FormBuilder.css';
|
||||
|
||||
export default function FormBuilder({ formId, initialData = {} }) {
|
||||
const [fields, setFields] = useState(initialData.fields || []);
|
||||
const [selectedFieldId, setSelectedFieldId] = useState(null);
|
||||
|
||||
const selectedField = fields.find(f => f.field_id === selectedFieldId) || null;
|
||||
|
||||
const handleDrop = useCallback((newField) => {
|
||||
setFields([...fields, newField]);
|
||||
setSelectedFieldId(newField.field_id);
|
||||
}, [fields]);
|
||||
|
||||
const handleSelectField = useCallback((fieldId) => {
|
||||
setSelectedFieldId(fieldId);
|
||||
}, []);
|
||||
|
||||
const handleUpdateField = useCallback((fieldId, updates) => {
|
||||
setFields(fields.map(f => f.field_id === fieldId ? { ...f, ...updates } : f));
|
||||
}, [fields]);
|
||||
|
||||
const handleMoveField = useCallback((newFields) => {
|
||||
setFields(newFields);
|
||||
}, []);
|
||||
|
||||
const handleDeleteField = useCallback((fieldId) => {
|
||||
const newFields = fields.filter(f => f.field_id !== fieldId);
|
||||
setFields(newFields);
|
||||
if (selectedFieldId === fieldId) {
|
||||
setSelectedFieldId(null);
|
||||
}
|
||||
}, [fields, selectedFieldId]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
// Save form fields via AJAX
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'formipay_save_form_fields');
|
||||
formData.append('post_id', formId);
|
||||
formData.append('fields', JSON.stringify(fields));
|
||||
formData.append('_wpnonce', window.formipayAdmin?.nonce || '');
|
||||
|
||||
fetch(window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: formData,
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
// Show success message
|
||||
console.log('Form saved successfully');
|
||||
} else {
|
||||
console.error('Failed to save form:', result.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Save error:', error);
|
||||
});
|
||||
}, [fields, formId]);
|
||||
|
||||
return (
|
||||
<div className="formipay-form-builder">
|
||||
<div className="formipay-builder-toolbar">
|
||||
<h2>{ __('Form Builder', 'formipay') }</h2>
|
||||
<div className="formipay-builder-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="button button-secondary"
|
||||
onClick={() => setFields([])}
|
||||
>
|
||||
{ __('Clear All', 'formipay') }
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="button button-primary"
|
||||
onClick={handleSave}
|
||||
>
|
||||
{ __('Save Form', 'formipay') }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formipay-builder-content">
|
||||
<FieldPalette onDragStart={() => {}} />
|
||||
<FormCanvas
|
||||
fields={fields}
|
||||
selectedFieldId={selectedFieldId}
|
||||
onDrop={handleDrop}
|
||||
onSelectField={handleSelectField}
|
||||
onUpdateField={handleUpdateField}
|
||||
onMoveField={handleMoveField}
|
||||
onDeleteField={handleDeleteField}
|
||||
/>
|
||||
<FormPreview fields={fields} />
|
||||
<FieldSettingsPanel
|
||||
field={selectedField}
|
||||
open={!!selectedField}
|
||||
onClose={() => setSelectedFieldId(null)}
|
||||
onUpdate={handleUpdateField}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
62
src/admin/components/formBuilder/FormCanvas.css
Normal file
62
src/admin/components/formBuilder/FormCanvas.css
Normal file
@@ -0,0 +1,62 @@
|
||||
.formipay-form-canvas {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f6f7f7;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.formipay-canvas-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.formipay-canvas-header h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e1e1e;
|
||||
}
|
||||
|
||||
.field-count {
|
||||
font-size: 12px;
|
||||
color: #646970;
|
||||
}
|
||||
|
||||
.formipay-canvas-area {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.formipay-canvas-area.is-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.formipay-empty-state {
|
||||
text-align: center;
|
||||
color: #646970;
|
||||
}
|
||||
|
||||
.formipay-empty-state svg {
|
||||
display: block;
|
||||
margin: 0 auto 16px;
|
||||
fill: #c3c4c7;
|
||||
}
|
||||
|
||||
.formipay-empty-state p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.formipay-fields-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
103
src/admin/components/formBuilder/FormCanvas.js
Normal file
103
src/admin/components/formBuilder/FormCanvas.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Form Canvas - Drop target for form fields
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Icon, plus } from '@wordpress/icons';
|
||||
import { DEFAULT_FIELD_CONFIG, generateFieldId } from '../../config/fieldTypes';
|
||||
import FormField from './FormField';
|
||||
import './FormCanvas.css';
|
||||
|
||||
export default function FormCanvas({
|
||||
fields = [],
|
||||
selectedFieldId,
|
||||
onDrop,
|
||||
onSelectField,
|
||||
onUpdateField,
|
||||
onMoveField,
|
||||
onDeleteField,
|
||||
}) {
|
||||
const handleDragOver = (event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
};
|
||||
|
||||
const handleDrop = (event) => {
|
||||
event.preventDefault();
|
||||
const fieldType = event.dataTransfer.getData('formipay-field-type');
|
||||
if (!fieldType) return;
|
||||
|
||||
const newField = {
|
||||
...DEFAULT_FIELD_CONFIG,
|
||||
field_type: fieldType,
|
||||
field_id: generateFieldId(fieldType.replace('_', ' ')),
|
||||
label: fieldType.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
||||
};
|
||||
|
||||
onDrop?.(newField);
|
||||
};
|
||||
|
||||
const handleReorder = (dragIndex, dropIndex) => {
|
||||
const newFields = [...fields];
|
||||
const [draggedField] = newFields.splice(dragIndex, 1);
|
||||
newFields.splice(dropIndex, 0, draggedField);
|
||||
onMoveField?.(newFields);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="formipay-form-canvas">
|
||||
<div className="formipay-canvas-header">
|
||||
<h3>{ __('Form Fields', 'formipay') }</h3>
|
||||
<span className="field-count">
|
||||
{ fields.length } { __('fields', 'formipay') }
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`formipay-canvas-area ${fields.length === 0 ? 'is-empty' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{fields.length === 0 ? (
|
||||
<div className="formipay-empty-state">
|
||||
<Icon icon={plus} size={48} />
|
||||
<p>
|
||||
{ __('Drag fields from the palette to build your form', 'formipay') }
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="formipay-fields-list">
|
||||
{fields.map((field, index) => (
|
||||
<FormField
|
||||
key={field.field_id}
|
||||
field={field}
|
||||
index={index}
|
||||
isSelected={selectedFieldId === field.field_id}
|
||||
onSelect={() => onSelectField?.(field.field_id)}
|
||||
onUpdate={(updates) => onUpdateField?.(field.field_id, updates)}
|
||||
onMoveUp={index > 0 ? () => handleReorder(index, index - 1) : null}
|
||||
onMoveDown={index < fields.length - 1 ? () => handleReorder(index, index + 1) : null}
|
||||
onDelete={() => onDeleteField?.(field.field_id)}
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('formipay-field-index', index.toString());
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const fromIndex = parseInt(e.dataTransfer.getData('formipay-field-index'));
|
||||
if (!isNaN(fromIndex) && fromIndex !== index) {
|
||||
handleReorder(fromIndex, index);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
148
src/admin/components/formBuilder/FormField.css
Normal file
148
src/admin/components/formBuilder/FormField.css
Normal file
@@ -0,0 +1,148 @@
|
||||
.formipay-field-item {
|
||||
background: #fff;
|
||||
border: 1px solid #dcdcde;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.formipay-field-item:hover {
|
||||
border-color: #a7aaad;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.formipay-field-item.is-selected {
|
||||
border-color: #2271b1;
|
||||
box-shadow: 0 0 0 2px rgba(34, 113, 177, 0.2);
|
||||
}
|
||||
|
||||
.formipay-field-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
background: #f6f7f7;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.formipay-field-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.field-type-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
background: #e0e0e0;
|
||||
color: #1e1e1e;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.field-id {
|
||||
font-size: 11px;
|
||||
color: #646970;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.formipay-field-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.formipay-field-actions .button-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.formipay-field-actions .button-icon:hover:not(:disabled) {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.formipay-field-actions .button-icon:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.formipay-field-actions .button-icon svg {
|
||||
fill: #1e1e1e;
|
||||
}
|
||||
|
||||
.formipay-field-actions .button-danger:hover svg {
|
||||
fill: #d63638;
|
||||
}
|
||||
|
||||
.formipay-field-content {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.field-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.field-label-row strong {
|
||||
font-size: 14px;
|
||||
color: #1e1e1e;
|
||||
}
|
||||
|
||||
.field-label-row em {
|
||||
color: #a7aaad;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.required-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #d63638;
|
||||
background: #f6f7f7;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.field-description {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
color: #646970;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.field-options-preview {
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
background: #f6f7f7;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.field-options-preview small {
|
||||
font-size: 11px;
|
||||
color: #646970;
|
||||
}
|
||||
|
||||
.field-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.field-meta .meta-item {
|
||||
font-size: 11px;
|
||||
color: #646970;
|
||||
}
|
||||
113
src/admin/components/formBuilder/FormField.js
Normal file
113
src/admin/components/formBuilder/FormField.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Form Field - Individual field component in canvas
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Icon, chevronUp, chevronDown, trash } from '@wordpress/icons';
|
||||
import { FIELD_TYPES } from '../../config/fieldTypes';
|
||||
import './FormField.css';
|
||||
|
||||
export default function FormField({
|
||||
field,
|
||||
index,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onUpdate,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onDelete,
|
||||
onDragStart,
|
||||
onDragOver,
|
||||
onDrop,
|
||||
}) {
|
||||
const fieldConfig = FIELD_TYPES[field.field_type] || FIELD_TYPES.text;
|
||||
const hasOptions = ['select', 'checkbox', 'radio'].includes(field.field_type);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`formipay-field-item ${isSelected ? 'is-selected' : ''}`}
|
||||
draggable
|
||||
onDragStart={onDragStart}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<div className="formipay-field-header">
|
||||
<div className="formipay-field-info">
|
||||
<span className="field-type-badge">
|
||||
{ fieldConfig.label }
|
||||
</span>
|
||||
<span className="field-id">
|
||||
#{field.field_id || index}
|
||||
</span>
|
||||
</div>
|
||||
<div className="formipay-field-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="button-icon"
|
||||
onClick={(e) => { e.stopPropagation(); onMoveUp?.(); }}
|
||||
disabled={!onMoveUp}
|
||||
title={ __('Move Up', 'formipay') }
|
||||
>
|
||||
<Icon icon={chevronUp} size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="button-icon"
|
||||
onClick={(e) => { e.stopPropagation(); onMoveDown?.(); }}
|
||||
disabled={!onMoveDown}
|
||||
title={ __('Move Down', 'formipay') }
|
||||
>
|
||||
<Icon icon={chevronDown} size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="button-icon button-danger"
|
||||
onClick={(e) => { e.stopPropagation(); onDelete?.(); }}
|
||||
title={ __('Delete', 'formipay') }
|
||||
>
|
||||
<Icon icon={trash} size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formipay-field-content">
|
||||
<div className="field-label-row">
|
||||
<strong>{ field.label || <em>Untitled</em> }</strong>
|
||||
{ field.is_required && (
|
||||
<span className="required-badge">
|
||||
{ __('Required', 'formipay') }
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{ field.description && (
|
||||
<p className="field-description">
|
||||
{ field.description }
|
||||
</p>
|
||||
)}
|
||||
|
||||
{ hasOptions && field.field_options && field.field_options.length > 0 && (
|
||||
<div className="field-options-preview">
|
||||
<small>
|
||||
{ field.field_options.length } { __('options', 'formipay') }
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="field-meta">
|
||||
{ field.placeholder && (
|
||||
<span className="meta-item">
|
||||
Placeholder: "{ field.placeholder }"
|
||||
</span>
|
||||
)}
|
||||
{ field.default_value && (
|
||||
<span className="meta-item">
|
||||
Default: "{ field.default_value }"
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
65
src/admin/components/formBuilder/FormFieldOptions.css
Normal file
65
src/admin/components/formBuilder/FormFieldOptions.css
Normal file
@@ -0,0 +1,65 @@
|
||||
.formipay-field-options {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: #f6f7f7;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.formipay-options-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.formipay-options-header label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e1e1e;
|
||||
}
|
||||
|
||||
.formipay-options-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.formipay-option-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.formipay-option-fields {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.formipay-option-fields .components-base-control {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.formipay-option-fields .components-base-control__label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.formipay-option-actions {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.formipay-no-options {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border: 1px dashed #dcdcde;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.formipay-no-options p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #646970;
|
||||
}
|
||||
105
src/admin/components/formBuilder/FormFieldOptions.js
Normal file
105
src/admin/components/formBuilder/FormFieldOptions.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Form Field Options - Manage select/radio/checkbox options
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { TextControl, Button } from '@wordpress/components';
|
||||
import { Icon as WPIcon, plus } from '@wordpress/icons';
|
||||
|
||||
export default function FormFieldOptions({ options = [], onChange, fieldType }) {
|
||||
const handleAddOption = () => {
|
||||
const newOption = {
|
||||
label: '',
|
||||
value: '',
|
||||
amount: '',
|
||||
weight: '',
|
||||
quantity: false,
|
||||
};
|
||||
onChange([...options, newOption]);
|
||||
};
|
||||
|
||||
const handleUpdateOption = (index, updates) => {
|
||||
const newOptions = [...options];
|
||||
newOptions[index] = { ...newOptions[index], ...updates };
|
||||
onChange(newOptions);
|
||||
};
|
||||
|
||||
const handleDeleteOption = (index) => {
|
||||
const newOptions = options.filter((_, i) => i !== index);
|
||||
onChange(newOptions);
|
||||
};
|
||||
|
||||
const isMultiSelect = fieldType === 'checkbox';
|
||||
|
||||
return (
|
||||
<div className="formipay-field-options">
|
||||
<div className="formipay-options-header">
|
||||
<label>
|
||||
{ isMultiSelect
|
||||
? __('Checkbox Options', 'formipay')
|
||||
: fieldType === 'radio'
|
||||
? __('Radio Options', 'formipay')
|
||||
: __('Select Options', 'formipay')
|
||||
}
|
||||
</label>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="compact"
|
||||
onClick={handleAddOption}
|
||||
icon={<WPIcon icon={plus} size={16} />}
|
||||
>
|
||||
{ __('Add Option', 'formipay') }
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="formipay-options-list">
|
||||
{options.map((option, index) => (
|
||||
<div key={index} className="formipay-option-item">
|
||||
<div className="formipay-option-fields">
|
||||
<TextControl
|
||||
label={__('Label', 'formipay')}
|
||||
value={option.label || ''}
|
||||
onChange={(value) => handleUpdateOption(index, { label: value })}
|
||||
placeholder={__('Option label', 'formipay')}
|
||||
/>
|
||||
|
||||
<TextControl
|
||||
label={__('Value', 'formipay')}
|
||||
value={option.value || ''}
|
||||
onChange={(value) => handleUpdateOption(index, { value: value })}
|
||||
placeholder={__('Optional custom value', 'formipay')}
|
||||
/>
|
||||
|
||||
<TextControl
|
||||
label={__('Amount', 'formipay')}
|
||||
type="number"
|
||||
value={option.amount || ''}
|
||||
onChange={(value) => handleUpdateOption(index, { amount: value })}
|
||||
placeholder={__('0', 'formipay')}
|
||||
step="0.01"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="formipay-option-actions">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="compact"
|
||||
onClick={() => handleDeleteOption(index)}
|
||||
icon={<WPIcon icon={trash} size={16} />}
|
||||
label={__('Delete Option', 'formipay')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{options.length === 0 && (
|
||||
<div className="formipay-no-options">
|
||||
<p>
|
||||
{ __('No options added yet. Click "Add Option" to create one.', 'formipay') }
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
87
src/admin/components/formBuilder/FormFieldPreview.css
Normal file
87
src/admin/components/formBuilder/FormFieldPreview.css
Normal file
@@ -0,0 +1,87 @@
|
||||
.formipay-preview-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.formipay-field-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.formipay-field-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #1e1e1e;
|
||||
}
|
||||
|
||||
.formipay-field-label .required {
|
||||
color: #d63638;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.formipay-input,
|
||||
.formipay-textarea,
|
||||
.formipay-select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
color: #1e1e1e;
|
||||
background: #fff;
|
||||
border: 1px solid #8c8f94;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.formipay-input:focus,
|
||||
.formipay-textarea:focus,
|
||||
.formipay-select:focus {
|
||||
outline: none;
|
||||
border-color: #2271b1;
|
||||
box-shadow: 0 0 0 1px #2271b1;
|
||||
}
|
||||
|
||||
.formipay-textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.formipay-radio-group,
|
||||
.formipay-checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.formipay-radio-label,
|
||||
.formipay-checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #1e1e1e;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.formipay-field-description {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #646970;
|
||||
}
|
||||
|
||||
.formipay-divider {
|
||||
border: none;
|
||||
border-top: 1px solid #c3c4c7;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.formipay-page-break {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
background: #f6f7f7;
|
||||
border: 1px dashed #c3c4c7;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
color: #646970;
|
||||
}
|
||||
142
src/admin/components/formBuilder/FormFieldPreview.js
Normal file
142
src/admin/components/formBuilder/FormFieldPreview.js
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Form Field Preview - Preview individual form fields
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import './FormFieldPreview.css';
|
||||
|
||||
export default function FormFieldPreview({ field }) {
|
||||
const fieldType = field.field_type || 'text';
|
||||
const label = field.label || '';
|
||||
const placeholder = field.placeholder || '';
|
||||
const defaultValue = field.default_value || '';
|
||||
const description = field.description || '';
|
||||
const isRequired = field.is_required || false;
|
||||
const fieldId = field.field_id || `field_${Math.random().toString(36).slice(2, 11)}`;
|
||||
|
||||
const renderFieldInput = () => {
|
||||
const commonProps = {
|
||||
id: fieldId,
|
||||
name: fieldId,
|
||||
placeholder,
|
||||
defaultValue,
|
||||
required: isRequired,
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
switch (fieldType) {
|
||||
case 'text':
|
||||
case 'url':
|
||||
case 'email':
|
||||
case 'tel':
|
||||
case 'number':
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
case 'color':
|
||||
return (
|
||||
<input
|
||||
type={fieldType}
|
||||
className="formipay-input"
|
||||
{...commonProps}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'textarea':
|
||||
return (
|
||||
<textarea
|
||||
className="formipay-textarea"
|
||||
rows={4}
|
||||
{...commonProps}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<select className="formipay-select" {...commonProps}>
|
||||
<option value="">{ __('Select an option', 'formipay') }</option>
|
||||
{field.field_options?.map((option, index) => (
|
||||
<option
|
||||
key={index}
|
||||
value={option.value || option.label}
|
||||
>
|
||||
{ option.label }
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
|
||||
case 'radio':
|
||||
return (
|
||||
<div className="formipay-radio-group">
|
||||
{field.field_options?.map((option, index) => (
|
||||
<label key={index} className="formipay-radio-label">
|
||||
<input
|
||||
type="radio"
|
||||
name={fieldId}
|
||||
value={option.value || option.label}
|
||||
disabled={true}
|
||||
/>
|
||||
<span>{ option.label }</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'checkbox':
|
||||
return (
|
||||
<div className="formipay-checkbox-group">
|
||||
{field.field_options?.map((option, index) => (
|
||||
<label key={index} className="formipay-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
name={`${fieldId}_${index}`}
|
||||
value={option.value || option.label}
|
||||
disabled={true}
|
||||
/>
|
||||
<span>{ option.label }</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'divider':
|
||||
return <hr className="formipay-divider" />;
|
||||
|
||||
case 'page_break':
|
||||
return <div className="formipay-page-break">{ __('Page Break', 'formipay') }</div>;
|
||||
|
||||
case 'country_list':
|
||||
return (
|
||||
<select className="formipay-select" {...commonProps}>
|
||||
<option value="">{ __('Select country', 'formipay') }</option>
|
||||
<option value="United States">United States</option>
|
||||
<option value="Indonesia">Indonesia</option>
|
||||
</select>
|
||||
);
|
||||
|
||||
default:
|
||||
return <input type="text" className="formipay-input" {...commonProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
const isLayout = ['divider', 'page_break'].includes(fieldType);
|
||||
|
||||
return (
|
||||
<div className={`formipay-field-preview formipay-field-preview--${fieldType}`}>
|
||||
{!isLayout && label && (
|
||||
<label htmlFor={fieldId} className="formipay-field-label">
|
||||
{ label }
|
||||
{ isRequired && <span className="required">*</span> }
|
||||
</label>
|
||||
)}
|
||||
|
||||
{ renderFieldInput() }
|
||||
|
||||
{ description && !isLayout && (
|
||||
<p className="formipay-field-description">
|
||||
{ description }
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
src/admin/components/formBuilder/FormPreview.css
Normal file
39
src/admin/components/formBuilder/FormPreview.css
Normal file
@@ -0,0 +1,39 @@
|
||||
.formipay-form-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.formipay-preview-header {
|
||||
padding: 12px 16px;
|
||||
background: #f6f7f7;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.formipay-preview-header h4 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e1e1e;
|
||||
}
|
||||
|
||||
.formipay-preview-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.formipay-preview-content.is-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.formipay-preview-content.is-empty p {
|
||||
color: #646970;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
40
src/admin/components/formBuilder/FormPreview.js
Normal file
40
src/admin/components/formBuilder/FormPreview.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Form Preview - Live preview of form rendering
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import FormFieldPreview from './FormFieldPreview';
|
||||
import './FormPreview.css';
|
||||
|
||||
export default function FormPreview({ fields }) {
|
||||
if (!fields || fields.length === 0) {
|
||||
return (
|
||||
<div className="formipay-form-preview">
|
||||
<div className="formipay-preview-header">
|
||||
<h4>{ __('Live Preview', 'formipay') }</h4>
|
||||
</div>
|
||||
<div className="formipay-preview-content is-empty">
|
||||
<p>{ __('Add fields to see the preview', 'formipay') }</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="formipay-form-preview">
|
||||
<div className="formipay-preview-header">
|
||||
<h4>{ __('Live Preview', 'formipay') }</h4>
|
||||
</div>
|
||||
<div className="formipay-preview-content">
|
||||
<form className="formipay-preview-form">
|
||||
{fields.map((field, index) => (
|
||||
<FormFieldPreview
|
||||
key={field.field_id || index}
|
||||
field={field}
|
||||
/>
|
||||
))}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
src/admin/config/fieldTypes.js
Normal file
67
src/admin/config/fieldTypes.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Form Field Type Definitions
|
||||
*/
|
||||
|
||||
export const FIELD_TYPES = {
|
||||
text: { label: 'Text', icon: 'text', category: 'input' },
|
||||
url: { label: 'URL', icon: 'link', category: 'input' },
|
||||
email: { label: 'Email', icon: 'email', category: 'input' },
|
||||
tel: { label: 'Telephone', icon: 'phone', category: 'input' },
|
||||
number: { label: 'Number', icon: 'number', category: 'input' },
|
||||
date: { label: 'Date', icon: 'calendar', category: 'input' },
|
||||
datetime: { label: 'Date & Time', icon: 'calendar-alt', category: 'input' },
|
||||
color: { label: 'Color', icon: 'art', category: 'input' },
|
||||
select: { label: 'Select Dropdown', icon: 'list-view', category: 'choice' },
|
||||
checkbox: { label: 'Checkbox', icon: 'checkbox', category: 'choice' },
|
||||
radio: { label: 'Radio', icon: 'radio', category: 'choice' },
|
||||
hidden: { label: 'Hidden', icon: 'hidden', category: 'advanced' },
|
||||
textarea: { label: 'Textarea', icon: 'document', category: 'input' },
|
||||
divider: { label: 'Divider', icon: 'minus', category: 'layout' },
|
||||
page_break: { label: 'Page Break', icon: 'page-break', category: 'layout' },
|
||||
country_list: { label: 'Preset: Country List', icon: 'globe', category: 'preset' },
|
||||
};
|
||||
|
||||
export const FIELD_CATEGORIES = {
|
||||
input: { label: 'Input Fields', order: 1 },
|
||||
choice: { label: 'Choice Fields', order: 2 },
|
||||
layout: { label: 'Layout', order: 3 },
|
||||
preset: { label: 'Presets', order: 4 },
|
||||
advanced: { label: 'Advanced', order: 5 },
|
||||
};
|
||||
|
||||
export const DEFAULT_FIELD_CONFIG = {
|
||||
field_type: 'text',
|
||||
label: '',
|
||||
field_id: '',
|
||||
placeholder: '',
|
||||
default_value: '',
|
||||
description: '',
|
||||
is_required: false,
|
||||
field_options: [],
|
||||
option_grid_columns: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get field types grouped by category
|
||||
*/
|
||||
export function getFieldTypesByCategory() {
|
||||
const grouped = {};
|
||||
|
||||
Object.entries(FIELD_TYPES).forEach(([type, config]) => {
|
||||
if (!grouped[config.category]) {
|
||||
grouped[config.category] = [];
|
||||
}
|
||||
grouped[config.category].push({ type, ...config });
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique field ID
|
||||
*/
|
||||
export function generateFieldId(label) {
|
||||
const base = label.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
||||
const timestamp = Date.now().toString(36);
|
||||
return `${base}_${timestamp}`;
|
||||
}
|
||||
@@ -1,14 +1,32 @@
|
||||
/**
|
||||
* Forms Page - Placeholder
|
||||
* Forms Page - List view and Form Builder
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
import FormBuilder from '../components/formBuilder/FormBuilder';
|
||||
|
||||
export default function FormsPage({ initialData }) {
|
||||
const [isBuilder, setIsBuilder] = useState(false);
|
||||
const [selectedFormId, setSelectedFormId] = useState(null);
|
||||
|
||||
if (isBuilder) {
|
||||
return (
|
||||
<div className="formipay-page-forms">
|
||||
<FormBuilder
|
||||
formId={selectedFormId}
|
||||
initialData={initialData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="formipay-page-formipay-page">
|
||||
<h1>{ __('Forms', 'formipay') }</h1>
|
||||
<p>{ __('Page content coming soon...', 'formipay') }</p>
|
||||
<div className="formipay-page-forms">
|
||||
<div className="formipay-forms-list">
|
||||
<h1>{ __('Forms', 'formipay') }</h1>
|
||||
<p>{ __('Forms list coming soon. Use the classic editor for now.', 'formipay') }</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user