feat: add React FieldRenderer system for settings and metaboxes
Complete React-based field rendering system that replaces WPCFTO Vue.js layer while maintaining PHP field configuration compatibility. Components: - FieldRenderer: Main renderer with tabs support (metabox) and direct mode (settings) - FieldTypes: 15+ field types (Text, Number, Select, Radio, Date, etc.) - RepeaterField: Collapsible repeater with currency label parsing - DependencyEngine: Show/hide fields based on conditions - ValidationEngine: Client-side validation with error messages - SettingsRenderer: Settings page with AJAX save to wp_options Features: - Repeater rows collapsed by default with readable currency titles - Searchable dropdowns using Popover + Command pattern - Proper label resolution for pre-selected values - Hidden input sync for WordPress form submission Also includes: - FieldConfigBridge: Transform PHP configs to React format - Updated Settings.php for React-based settings page - Radio-group UI component - wp-admin-restore.css for admin panel isolation
This commit is contained in:
460
includes/Admin/FieldConfigBridge.php
Normal file
460
includes/Admin/FieldConfigBridge.php
Normal file
@@ -0,0 +1,460 @@
|
||||
<?php
|
||||
namespace Formipay\Admin;
|
||||
|
||||
use Formipay\Traits\SingletonTrait;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
/**
|
||||
* FieldConfigBridge - Transforms WPCFTO field config arrays to React-ready JSON
|
||||
*
|
||||
* Bridges the gap between PHP field configurations and the React FieldRenderer:
|
||||
* - Collects field configs from filter system
|
||||
* - Loads saved post meta / option values
|
||||
* - Transforms PHP config format to React-ready structure
|
||||
* - Handles dynamic currency field expansion
|
||||
* - Handles dependency transformation
|
||||
*/
|
||||
class FieldConfigBridge {
|
||||
|
||||
use SingletonTrait;
|
||||
|
||||
/**
|
||||
* Map CPT types to their filter names
|
||||
*/
|
||||
private static $cpt_filter_map = [
|
||||
'formipay-coupon' => 'formipay/coupon-config',
|
||||
'formipay-product' => 'formipay/product-config',
|
||||
'formipay-form' => 'formipay/form-config',
|
||||
'formipay-access' => 'formipay/access-config',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get complete field configuration for a post with saved values
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
* @param string $post_type Post type
|
||||
* @return array React-ready field configuration
|
||||
*/
|
||||
public static function get_config_for_post($post_id, $post_type) {
|
||||
$filter_name = self::$cpt_filter_map[$post_type] ?? null;
|
||||
|
||||
if (!$filter_name) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get field configuration from filters
|
||||
$fields = apply_filters($filter_name, []);
|
||||
|
||||
// Load saved values from post meta
|
||||
$values = self::load_post_meta_values($post_id, $fields);
|
||||
|
||||
// Transform to React-ready format
|
||||
return self::transform_fields_config($fields, $values, $post_id, $post_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration for settings page (wp_options based)
|
||||
*
|
||||
* @param string $option_name Option name
|
||||
* @return array React-ready field configuration
|
||||
*/
|
||||
public static function get_config_for_settings($option_name = 'formipay_settings') {
|
||||
// Get settings values
|
||||
$settings = get_option($option_name, []);
|
||||
|
||||
// Get field definitions directly from Settings class
|
||||
$settings_instance = \Formipay\Settings::get_instance();
|
||||
$tabs_config = $settings_instance->get_settings_fields();
|
||||
|
||||
// Transform to React-ready format
|
||||
return self::transform_settings_config($settings, $tabs_config, $option_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform settings config to React-ready format
|
||||
*
|
||||
* @param array $settings Settings values
|
||||
* @param array $tabs_config Tab configuration from Settings class
|
||||
* @param string $option_name Option name
|
||||
* @return array React-ready configuration
|
||||
*/
|
||||
private static function transform_settings_config($settings, $tabs_config, $option_name) {
|
||||
$tabs = [];
|
||||
|
||||
foreach ($tabs_config as $tab_key => $tab_data) {
|
||||
if (!isset($tab_data['name']) || !isset($tab_data['fields'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tab = [
|
||||
'id' => self::slugify($tab_data['name']),
|
||||
'label' => $tab_data['name'],
|
||||
'fields' => [],
|
||||
];
|
||||
|
||||
$current_group = null;
|
||||
|
||||
foreach ($tab_data['fields'] as $field_name => $field) {
|
||||
// Handle group_title (section headers)
|
||||
if (isset($field['type']) && $field['type'] === 'group_title') {
|
||||
$group_value = $field['group'] ?? '';
|
||||
|
||||
if ($group_value === 'started') {
|
||||
$current_group = [
|
||||
'type' => 'section',
|
||||
'label' => $field['label'] ?? '',
|
||||
'description' => $field['description'] ?? '',
|
||||
];
|
||||
} elseif ($group_value === 'ended') {
|
||||
$current_group = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add section if we have one
|
||||
if ($current_group !== null) {
|
||||
$tab['fields'][] = $current_group;
|
||||
$current_group = null;
|
||||
}
|
||||
|
||||
// Transform field configuration
|
||||
$transformed_field = self::transform_settings_field($field_name, $field, $settings);
|
||||
|
||||
// Add to fields
|
||||
$tab['fields'][] = $transformed_field;
|
||||
}
|
||||
|
||||
$tabs[] = $tab;
|
||||
}
|
||||
|
||||
return [
|
||||
'tabs' => $tabs,
|
||||
'optionName' => $option_name,
|
||||
'globalCurrencies' => self::get_global_currencies_data(),
|
||||
'nonce' => wp_create_nonce('formipay-field-config'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load saved post meta values for all fields
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
* @param array $fields Field configuration array
|
||||
* @return array Field values keyed by field name
|
||||
*/
|
||||
private static function load_post_meta_values($post_id, $fields) {
|
||||
$values = [];
|
||||
|
||||
// The config structure is: $fields['formipay_{cpt}_settings'][tab_key] = ['name' => ..., 'fields' => [...]]
|
||||
foreach ($fields as $container_key => $container_data) {
|
||||
if (!is_array($container_data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($container_data as $tab_key => $tab_data) {
|
||||
if (!isset($tab_data['fields']) || !is_array($tab_data['fields'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($tab_data['fields'] as $field_name => $field) {
|
||||
// Skip group_title fields
|
||||
if (isset($field['type']) && $field['type'] === 'group_title') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$meta_value = get_post_meta($post_id, $field_name, true);
|
||||
$values[$field_name] = $meta_value !== '' ? $meta_value : ($field['value'] ?? null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform fields config to React-ready format
|
||||
*
|
||||
* @param array $fields Raw field configuration from PHP
|
||||
* @param array $values Saved field values
|
||||
* @param int $post_id Post ID
|
||||
* @param string $post_type Post type
|
||||
* @return array React-ready configuration
|
||||
*/
|
||||
private static function transform_fields_config($fields, $values, $post_id, $post_type) {
|
||||
$tabs = [];
|
||||
|
||||
// The config structure is: $fields['formipay_{cpt}_settings'][tab_key] = ['name' => ..., 'fields' => [...]]
|
||||
foreach ($fields as $container_key => $container_data) {
|
||||
if (!is_array($container_data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($container_data as $tab_key => $tab_data) {
|
||||
if (!isset($tab_data['name']) || !isset($tab_data['fields'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tab = [
|
||||
'id' => $tab_key,
|
||||
'label' => $tab_data['name'],
|
||||
'fields' => [],
|
||||
];
|
||||
|
||||
$current_group = null;
|
||||
|
||||
foreach ($tab_data['fields'] as $field_name => $field) {
|
||||
// Handle group_title (section headers)
|
||||
if (isset($field['type']) && $field['type'] === 'group_title') {
|
||||
$group_value = $field['group'] ?? '';
|
||||
|
||||
if ($group_value === 'started') {
|
||||
$current_group = [
|
||||
'type' => 'section',
|
||||
'label' => $field['label'] ?? '',
|
||||
'description' => $field['description'] ?? '',
|
||||
];
|
||||
} elseif ($group_value === 'ended') {
|
||||
$current_group = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add section if we have one
|
||||
if ($current_group !== null) {
|
||||
$tab['fields'][] = $current_group;
|
||||
$current_group = null;
|
||||
}
|
||||
|
||||
// Transform field configuration
|
||||
$transformed_field = self::transform_single_field($field_name, $field, $values);
|
||||
|
||||
// Add to fields
|
||||
$tab['fields'][] = $transformed_field;
|
||||
}
|
||||
|
||||
$tabs[] = $tab;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'tabs' => $tabs,
|
||||
'postId' => $post_id,
|
||||
'postType' => $post_type,
|
||||
'globalCurrencies' => self::get_global_currencies_data(),
|
||||
'nonce' => wp_create_nonce('formipay-field-config'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a single field configuration
|
||||
*
|
||||
* @param string $field_name Field name
|
||||
* @param array $field Field configuration
|
||||
* @param array $values Saved values
|
||||
* @return array Transformed field config
|
||||
*/
|
||||
private static function transform_single_field($field_name, $field, $values) {
|
||||
$transformed = [
|
||||
'name' => $field_name,
|
||||
'type' => self::transform_field_type($field['type'] ?? 'text'),
|
||||
'label' => $field['label'] ?? '',
|
||||
'description' => $field['description'] ?? '',
|
||||
'value' => $values[$field_name] ?? $field['value'] ?? '',
|
||||
'required' => !empty($field['required']),
|
||||
'dependency' => isset($field['dependency']) ? self::transform_dependency($field['dependency']) : null,
|
||||
];
|
||||
|
||||
// Add options for select/radio fields
|
||||
if (isset($field['options'])) {
|
||||
$transformed['options'] = $field['options'];
|
||||
}
|
||||
|
||||
// Add subtype info for currency fields
|
||||
if (isset($field['step'])) {
|
||||
$transformed['step'] = $field['step'];
|
||||
}
|
||||
if (isset($field['min'])) {
|
||||
$transformed['min'] = $field['min'];
|
||||
}
|
||||
if (isset($field['placeholder'])) {
|
||||
$transformed['placeholder'] = $field['placeholder'];
|
||||
}
|
||||
|
||||
// Handle special field types
|
||||
if (isset($field['repeater_fields'])) {
|
||||
$transformed['fields'] = $field['repeater_fields'];
|
||||
}
|
||||
|
||||
// Pass post_type for autocomplete fields (CPTs)
|
||||
if (isset($field['post_type'])) {
|
||||
$transformed['post_type'] = $field['post_type'];
|
||||
}
|
||||
|
||||
// Pass object_type for autocomplete fields (e.g., 'user' for WP Users)
|
||||
if (isset($field['object_type'])) {
|
||||
$transformed['object_type'] = $field['object_type'];
|
||||
}
|
||||
|
||||
return $transformed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform field type from PHP to React format
|
||||
*
|
||||
* @param string $type PHP field type
|
||||
* @return string React field type
|
||||
*/
|
||||
private static function transform_field_type($type) {
|
||||
$type_map = [
|
||||
'checkbox' => 'switch', // Use toggle switch UI
|
||||
'tinymce' => 'editor', // Rich text editor
|
||||
'editor' => 'editor', // Map 'editor' type to tinymce field
|
||||
'image' => 'image', // WordPress Media Library picker
|
||||
'notification_message' => 'notification', // Notification banner component
|
||||
];
|
||||
|
||||
return $type_map[$type] ?? $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform dependency from PHP to React format
|
||||
*
|
||||
* @param array $dependency PHP dependency config
|
||||
* @return array React dependency config
|
||||
*/
|
||||
private static function transform_dependency($dependency) {
|
||||
// Handle multiple dependencies (AND logic)
|
||||
if (isset($dependency[0]) && is_array($dependency[0])) {
|
||||
return [
|
||||
'mode' => 'and',
|
||||
'rules' => array_map([self::class, 'transform_single_dependency'], $dependency),
|
||||
];
|
||||
}
|
||||
|
||||
return self::transform_single_dependency($dependency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a single dependency rule
|
||||
*
|
||||
* @param array $dependency PHP dependency
|
||||
* @return array React dependency rule
|
||||
*/
|
||||
private static function transform_single_dependency($dependency) {
|
||||
$rule = [
|
||||
'field' => $dependency['key'] ?? '',
|
||||
];
|
||||
|
||||
$value = $dependency['value'] ?? '';
|
||||
|
||||
// Handle special values
|
||||
if ($value === 'not_empty') {
|
||||
$rule['operator'] = 'not_empty';
|
||||
} elseif ($value === 'empty') {
|
||||
$rule['operator'] = 'empty';
|
||||
} else {
|
||||
$rule['value'] = $value;
|
||||
$rule['operator'] = 'eq';
|
||||
}
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get global currencies data for React
|
||||
*
|
||||
* @return array Currencies data
|
||||
*/
|
||||
private static function get_global_currencies_data() {
|
||||
$currencies = get_global_currency_array();
|
||||
|
||||
return array_map(function($currency) {
|
||||
$parts = explode(':::', $currency['currency']);
|
||||
|
||||
return [
|
||||
'currency' => $currency['currency'],
|
||||
'code' => $parts[0] ?? '',
|
||||
'title' => $parts[1] ?? '',
|
||||
'symbol' => $parts[2] ?? $parts[1] ?? '',
|
||||
'decimalDigits' => intval($currency['decimal_digits'] ?? 2),
|
||||
'decimalSymbol' => $currency['decimal_symbol'] ?? '.',
|
||||
'thousandSeparator' => $currency['thousand_separator'] ?? ',',
|
||||
'flag' => formipay_get_flag_by_currency($currency['currency']),
|
||||
];
|
||||
}, $currencies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a single settings field configuration
|
||||
*
|
||||
* @param string $field_name Field name
|
||||
* @param array $field Field configuration
|
||||
* @param array $settings Saved settings values
|
||||
* @return array Transformed field config
|
||||
*/
|
||||
private static function transform_settings_field($field_name, $field, $settings) {
|
||||
$transformed = [
|
||||
'name' => $field_name,
|
||||
'type' => self::transform_field_type($field['type'] ?? 'text'),
|
||||
'label' => $field['label'] ?? '',
|
||||
'description' => $field['description'] ?? '',
|
||||
'value' => $settings[$field_name] ?? $field['value'] ?? '',
|
||||
'required' => !empty($field['required']),
|
||||
'dependency' => isset($field['dependency']) ? self::transform_dependency($field['dependency']) : null,
|
||||
];
|
||||
|
||||
// Add options for select/radio/multi_checkbox fields
|
||||
if (isset($field['options'])) {
|
||||
$transformed['options'] = $field['options'];
|
||||
}
|
||||
|
||||
// Add options for image_select
|
||||
if (isset($field['options']) && $field['type'] === 'image_select') {
|
||||
$transformed['width'] = $field['width'] ?? 100;
|
||||
$transformed['height'] = $field['height'] ?? 100;
|
||||
}
|
||||
|
||||
// Add hints for hint_textarea
|
||||
if (isset($field['hints'])) {
|
||||
$transformed['hints'] = $field['hints'];
|
||||
}
|
||||
|
||||
// Add step/min for number fields
|
||||
if (isset($field['step'])) {
|
||||
$transformed['step'] = $field['step'];
|
||||
}
|
||||
if (isset($field['min'])) {
|
||||
$transformed['min'] = $field['min'];
|
||||
}
|
||||
if (isset($field['placeholder'])) {
|
||||
$transformed['placeholder'] = $field['placeholder'];
|
||||
}
|
||||
if (isset($field['rows'])) {
|
||||
$transformed['rows'] = $field['rows'];
|
||||
}
|
||||
|
||||
// Handle repeater fields
|
||||
if (isset($field['fields'])) {
|
||||
// Transform sub-fields to include name property
|
||||
$sub_fields = [];
|
||||
foreach ($field['fields'] as $sub_field_name => $sub_field) {
|
||||
$sub_field['name'] = $sub_field_name;
|
||||
$sub_fields[] = $sub_field;
|
||||
}
|
||||
$transformed['fields'] = $sub_fields;
|
||||
}
|
||||
|
||||
return $transformed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string to URL-safe slug
|
||||
*
|
||||
* @param string $text Text to slugify
|
||||
* @return string URL-safe slug
|
||||
*/
|
||||
private static function slugify($text) {
|
||||
return strtolower(sanitize_title($text));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user