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:
@@ -42,6 +42,10 @@ class Coupon {
|
||||
// React Metabox
|
||||
add_action( 'add_meta_boxes', [$this, 'add_react_metabox'] );
|
||||
add_action( 'admin_footer-post.php', [$this, 'render_react_metabox_template'] );
|
||||
add_action( 'admin_footer-post-new.php', [$this, 'render_react_metabox_template'] );
|
||||
|
||||
// Save coupon data via WordPress save_post hook (regular hook with post type check inside)
|
||||
add_action( 'save_post', [$this, 'save_coupon_on_post_update'], 10, 2 );
|
||||
|
||||
// Order
|
||||
add_filter( 'formipay/order/order-details', [$this, 'order_details'], 99, 3 );
|
||||
@@ -112,13 +116,6 @@ class Coupon {
|
||||
if ($screen && $screen->post_type === 'formipay-coupon' && $screen->base === 'post') {
|
||||
wp_enqueue_style('sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.css', [], '11.14.4', 'all');
|
||||
wp_enqueue_script('sweetalert2', FORMIPAY_URL . 'vendor/SweetAlert2/sweetalert2.min.js', ['jquery'], '11.14.4', true);
|
||||
|
||||
// Localize admin data
|
||||
wp_localize_script('formipay-admin', 'formipayAdmin', [
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'siteUrl' => site_url(),
|
||||
'nonce' => wp_create_nonce('formipay-admin'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +151,7 @@ class Coupon {
|
||||
$rules_group_1 = array(
|
||||
'rules_general_group' => array(
|
||||
'type' => 'group_title',
|
||||
'label' => __( 'General', 'formipay' ),
|
||||
'group' => 'started'
|
||||
),
|
||||
'active' => array(
|
||||
@@ -169,6 +167,7 @@ class Coupon {
|
||||
'fixed' => __( 'Fixed', 'formipay' ),
|
||||
'percentage' => __( 'Percentage', 'formipay' )
|
||||
),
|
||||
'value' => 'fixed'
|
||||
),
|
||||
'amount_percentage' => array(
|
||||
'type' => 'number',
|
||||
@@ -209,8 +208,7 @@ class Coupon {
|
||||
$currency_symbol = formipay_get_currency_data_by_value($currency['currency'], 'symbol');
|
||||
$currency_title = ucwords(formipay_get_currency_data_by_value($currency['currency'], 'title'));
|
||||
$decimal_digits = intval($currency['decimal_digits']);
|
||||
$step = $decimal_digits * 10;
|
||||
$step = $step > 0 ? 1 / $step : 1;
|
||||
$step = $decimal_digits > 0 ? pow(10, -$decimal_digits) : 1;
|
||||
|
||||
$rules_group_2['amount_fixed_'.$currency_symbol] = array(
|
||||
'type' => 'number',
|
||||
@@ -238,7 +236,11 @@ class Coupon {
|
||||
),
|
||||
'step' => $step,
|
||||
'min' => 0,
|
||||
'placeholder' => __( 'Enter Max Amount...', 'formipay' )
|
||||
'placeholder' => __( 'Enter Max Amount...', 'formipay' ),
|
||||
'dependency' => array(
|
||||
'key' => 'type',
|
||||
'value' => 'percentage'
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
@@ -324,7 +326,8 @@ class Coupon {
|
||||
'use_limit' => array(
|
||||
'type' => 'number',
|
||||
'label' => __( 'Usage Limit', 'formipay' ),
|
||||
'description' => __( 'Leave it empty or 0 (zero) to set it as unlimited usage.', 'formipay' )
|
||||
'description' => __( 'Leave it empty or 0 (zero) to set it as unlimited usage.', 'formipay' ),
|
||||
'placeholder' => __( 'Set limit...', 'formipay' )
|
||||
),
|
||||
'date_limit' => array(
|
||||
'type' => 'date',
|
||||
@@ -343,11 +346,11 @@ class Coupon {
|
||||
'label' => __( 'Products', 'formipay' ),
|
||||
'description' => __( 'Only selected product(s) can use the coupon. Leave empty to apply to all products.', 'formipay' )
|
||||
),
|
||||
'customers' => array(
|
||||
'users' => array(
|
||||
'type' => 'autocomplete',
|
||||
'post_type' => array('formipay-product'),
|
||||
'object_type' => 'user',
|
||||
'label' => __( 'Customers', 'formipay' ),
|
||||
'description' => __( 'Only selected customer(s) can use the coupon. Leave empty to apply to all customers.', 'formipay' )
|
||||
'description' => __( 'Only selected registered customer(s) can use this coupon. Leave empty to apply to all customers.', 'formipay' )
|
||||
)
|
||||
);
|
||||
|
||||
@@ -588,6 +591,14 @@ class Coupon {
|
||||
}
|
||||
|
||||
$date_limit = formipay_get_post_meta($coupon->ID, 'date_limit');
|
||||
$date_limit_display = 'none';
|
||||
if ($date_limit && $date_limit !== '') {
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}/', $date_limit)) {
|
||||
$date_limit_display = $date_limit;
|
||||
} elseif (is_numeric($date_limit)) {
|
||||
$date_limit_display = formipay_date('Y-m-d', intval($date_limit) / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
$type = formipay_get_post_meta($coupon->ID, 'type');
|
||||
$amount_meta_key = "amount_$type";
|
||||
@@ -622,7 +633,7 @@ class Coupon {
|
||||
'case_sensitive' => formipay_get_post_meta($coupon->ID, 'case_sensitive'),
|
||||
'usage_count' => $this->count_order_by_coupon_code(get_the_title($coupon->ID)),
|
||||
'usages' => $this->count_order_by_coupon_code(get_the_title($coupon->ID)),
|
||||
'date_limit' => false !== $date_limit ? formipay_date('Y-m-d', intval(formipay_get_post_meta($coupon->ID, 'date_limit')) / 1000) : 'none',
|
||||
'date_limit' => $date_limit_display,
|
||||
'active' => $is_active ? 'on' : 'off',
|
||||
'post_status' => $is_active ? 'active' : 'inactive',
|
||||
'status' => $is_active ? 'active' : 'inactive'
|
||||
@@ -885,7 +896,7 @@ class Coupon {
|
||||
'max_amounts' => [],
|
||||
'forms' => formipay_get_post_meta($post_id, 'forms') ?: [],
|
||||
'products' => formipay_get_post_meta($post_id, 'products') ?: [],
|
||||
'customers' => formipay_get_post_meta($post_id, 'customers') ?: [],
|
||||
'users' => formipay_get_post_meta($post_id, 'users') ?: [],
|
||||
];
|
||||
|
||||
// Get fixed amounts for each currency
|
||||
@@ -987,7 +998,7 @@ class Coupon {
|
||||
}
|
||||
|
||||
// Save relation fields
|
||||
$relation_fields = ['forms', 'products', 'customers'];
|
||||
$relation_fields = ['forms', 'products', 'users'];
|
||||
foreach ($relation_fields as $field) {
|
||||
if ( isset($_REQUEST[$field]) ) {
|
||||
$values = is_array($_REQUEST[$field]) ? array_map('intval', $_REQUEST[$field]) : [];
|
||||
@@ -1006,7 +1017,7 @@ class Coupon {
|
||||
*/
|
||||
public function add_react_metabox() {
|
||||
add_meta_box(
|
||||
'formipay_coupon_reactor_metabox',
|
||||
'formipay_coupon_settings',
|
||||
__( 'Coupon Settings', 'formipay' ),
|
||||
[$this, 'render_react_metabox'],
|
||||
'formipay-coupon',
|
||||
@@ -1021,9 +1032,9 @@ class Coupon {
|
||||
public function render_react_metabox($post) {
|
||||
?>
|
||||
<div
|
||||
data-formipay-metabox="coupon"
|
||||
data-formipay-field-renderer="coupon"
|
||||
data-post-id="<?php echo esc_attr($post->ID); ?>"
|
||||
class="formipay-react-metabox-container"
|
||||
class="formipay-field-renderer-container"
|
||||
>
|
||||
<div class="formipay-loading">
|
||||
<div class="formipay-spinner"></div>
|
||||
@@ -1042,9 +1053,39 @@ class Coupon {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get global currencies
|
||||
// Add Toaster container for toast notifications
|
||||
echo '<div id="formipay-toaster-container" class="formipay-design-system" style="position:fixed;bottom:20px;right:20px;z-index:99999;"></div>';
|
||||
|
||||
// Get field configuration for this post
|
||||
$config = \Formipay\Admin\FieldConfigBridge::get_config_for_post($post->ID, $post->post_type);
|
||||
|
||||
// Pass config to JavaScript
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
window.formipayFieldConfig = <?php echo wp_json_encode($config); ?>;
|
||||
</script>
|
||||
<?php
|
||||
|
||||
// Keep the legacy global currencies for backward compatibility during transition
|
||||
$global_currencies = get_global_currency_array();
|
||||
|
||||
// Fallback: if no multicurrencies configured, use default currency
|
||||
if (empty($global_currencies)) {
|
||||
$formipay_settings = get_option('formipay_settings');
|
||||
$default_currency_raw = formipay_default_currency('raw');
|
||||
|
||||
if (!empty($default_currency_raw)) {
|
||||
$global_currencies = [
|
||||
[
|
||||
'currency' => $default_currency_raw,
|
||||
'decimal_digits' => isset($formipay_settings['default_currency_decimal_digits']) ? intval($formipay_settings['default_currency_decimal_digits']) : 2,
|
||||
'decimal_symbol' => isset($formipay_settings['default_currency_decimal_symbol']) ? $formipay_settings['default_currency_decimal_symbol'] : '.',
|
||||
'thousand_separator' => isset($formipay_settings['default_currency_thousand_separator']) ? $formipay_settings['default_currency_thousand_separator'] : ',',
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
window.formipayGlobalCurrencies = <?php echo wp_json_encode($global_currencies); ?>;
|
||||
@@ -1062,6 +1103,7 @@ class Coupon {
|
||||
|
||||
/**
|
||||
* Autocomplete search for relation fields
|
||||
* Supports CPTs (post_type) and WP Users (object_type=user)
|
||||
*/
|
||||
public function formipay_autocomplete_search() {
|
||||
|
||||
@@ -1072,9 +1114,56 @@ class Coupon {
|
||||
}
|
||||
|
||||
$post_type = isset($_REQUEST['post_type']) ? sanitize_text_field(wp_unslash($_REQUEST['post_type'])) : '';
|
||||
$object_type = isset($_REQUEST['object_type']) ? sanitize_text_field(wp_unslash($_REQUEST['object_type'])) : '';
|
||||
$search = isset($_REQUEST['search']) ? sanitize_text_field(wp_unslash($_REQUEST['search'])) : '';
|
||||
$include = isset($_REQUEST['include']) ? array_map('intval', (array) $_REQUEST['include']) : [];
|
||||
|
||||
// Handle WP Users
|
||||
if ($object_type === 'user') {
|
||||
// Resolve labels for specific IDs (pre-selected items)
|
||||
if (!empty($include)) {
|
||||
$users = get_users(['include' => $include, 'fields' => ['ID', 'display_name', 'user_email']]);
|
||||
|
||||
$results = [];
|
||||
if (!empty($users)) {
|
||||
foreach ($users as $user) {
|
||||
$results[] = [
|
||||
'value' => $user->ID,
|
||||
'label' => $user->display_name . ' (' . $user->user_email . ')',
|
||||
];
|
||||
}
|
||||
}
|
||||
wp_send_json_success($results);
|
||||
return;
|
||||
}
|
||||
|
||||
// Search by keyword
|
||||
if (strlen($search) < 2) {
|
||||
wp_send_json_error( [ 'message' => 'Invalid request' ] );
|
||||
}
|
||||
|
||||
$users = get_users([
|
||||
'search' => '*' . $search . '*',
|
||||
'search_columns' => ['display_name', 'user_email', 'user_login'],
|
||||
'number' => 20,
|
||||
'fields' => ['ID', 'display_name', 'user_email'],
|
||||
]);
|
||||
|
||||
$results = [];
|
||||
if (!empty($users)) {
|
||||
foreach ($users as $user) {
|
||||
$results[] = [
|
||||
'value' => $user->ID,
|
||||
'label' => $user->display_name . ' (' . $user->user_email . ')',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_success($results);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle CPTs (posts)
|
||||
if (empty($post_type)) {
|
||||
wp_send_json_error( [ 'message' => 'Invalid request' ] );
|
||||
}
|
||||
@@ -1125,4 +1214,78 @@ class Coupon {
|
||||
wp_send_json_success($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save coupon data via WordPress save_post hook
|
||||
* Called when user clicks WordPress Update button
|
||||
*/
|
||||
public function save_coupon_on_post_update($post_id, $post) {
|
||||
// Check if this is an autosave
|
||||
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
if (!current_user_can('manage_options')) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
// Verify nonce
|
||||
if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'update-post_' . $post_id)) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
// Only save for our coupon post type
|
||||
if ($post->post_type !== 'formipay-coupon') {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
// Get global currencies for currency field processing
|
||||
$global_currencies = get_global_currency_array();
|
||||
|
||||
// Save basic meta fields
|
||||
$meta_fields = [
|
||||
'active',
|
||||
'type',
|
||||
'amount_percentage',
|
||||
'case_sensitive',
|
||||
'free_shipping',
|
||||
'quantity_active',
|
||||
'use_limit',
|
||||
'date_limit',
|
||||
];
|
||||
|
||||
foreach ($meta_fields as $field) {
|
||||
if (isset($_POST[$field])) {
|
||||
$value = wp_unslash($_POST[$field]);
|
||||
update_post_meta($post_id, $field, $value);
|
||||
}
|
||||
}
|
||||
|
||||
// Save fixed amounts for each currency
|
||||
foreach ($global_currencies as $currency) {
|
||||
$symbol = formipay_get_currency_data_by_value($currency['currency'], 'symbol');
|
||||
|
||||
if (isset($_POST['amount_fixed_' . $symbol])) {
|
||||
update_post_meta($post_id, 'amount_fixed_' . $symbol, wp_unslash($_POST['amount_fixed_' . $symbol]));
|
||||
}
|
||||
|
||||
if (isset($_POST['max_amount_' . $symbol])) {
|
||||
update_post_meta($post_id, 'max_amount_' . $symbol, wp_unslash($_POST['max_amount_' . $symbol]));
|
||||
}
|
||||
}
|
||||
|
||||
// Save relation fields
|
||||
$relation_fields = ['forms', 'products', 'users'];
|
||||
foreach ($relation_fields as $field) {
|
||||
if (isset($_POST[$field])) {
|
||||
$values = is_array($_POST[$field]) ? array_map('intval', $_POST[$field]) : [];
|
||||
update_post_meta($post_id, $field, $values);
|
||||
} else {
|
||||
delete_post_meta($post_id, $field);
|
||||
}
|
||||
}
|
||||
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user