feat: add React metabox island for coupon editor
- Create CouponMetabox React component with WPCFTO design system - Add MetaboxLayout with vertical tabs (Rules, Restrictions) - Implement Rules tab: active toggle, type radio, amount fields, multi-currency support - Implement Restrictions tab: usage limit, date limit, autocomplete for forms/products/customers - Add metabox registration in Coupon.php for formipay-coupon post type - Update ReactAdmin to load assets on post.php edit screens - Add autocomplete AJAX handler for relation fields - Disable old WPCFTO metabox in favor of React island
This commit is contained in:
146
src/admin/components/coupons/CouponMetabox.css
Normal file
146
src/admin/components/coupons/CouponMetabox.css
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Coupon Metabox Styles
|
||||
* WPCFTO-inspired design for React metabox island
|
||||
*/
|
||||
|
||||
.formipay-coupon-metabox {
|
||||
margin: -12px;
|
||||
}
|
||||
|
||||
.formipay-metabox-actions {
|
||||
padding: 20px 30px;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #f0f0f1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Radio Group */
|
||||
.formipay-radio-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.formipay-radio {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border: 2px solid var(--formipay-color-border-dark);
|
||||
border-radius: var(--formipay-radius-lg);
|
||||
transition: all var(--formipay-transition-fast);
|
||||
}
|
||||
|
||||
.formipay-radio input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.formipay-radio span {
|
||||
font-weight: var(--formipay-font-weight-medium);
|
||||
color: var(--formipay-color-text-muted);
|
||||
}
|
||||
|
||||
.formipay-radio:hover {
|
||||
border-color: var(--formipay-color-primary);
|
||||
}
|
||||
|
||||
.formipay-radio.active {
|
||||
background-color: var(--formipay-color-primary);
|
||||
border-color: var(--formipay-color-primary);
|
||||
}
|
||||
|
||||
.formipay-radio.active span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Autocomplete Field */
|
||||
.formipay-autocomplete {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.formipay-autocomplete-selected {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.formipay-autocomplete-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
background-color: var(--formipay-color-primary);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: var(--formipay-font-size-sm);
|
||||
}
|
||||
|
||||
.formipay-autocomplete-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.formipay-autocomplete-remove:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.formipay-autocomplete-input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.formipay-autocomplete-loading {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--formipay-color-text-muted);
|
||||
}
|
||||
|
||||
.formipay-autocomplete-results {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background-color: white;
|
||||
border: 1px solid var(--formipay-color-border-dark);
|
||||
border-radius: var(--formipay-radius-sm);
|
||||
box-shadow: var(--formipay-shadow-md);
|
||||
z-index: 100;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.formipay-autocomplete-result {
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--formipay-transition-fast);
|
||||
}
|
||||
|
||||
.formipay-autocomplete-result:hover {
|
||||
background-color: var(--formipay-color-content-bg);
|
||||
}
|
||||
|
||||
.formipay-autocomplete-result:first-child {
|
||||
border-radius: var(--formipay-radius-sm) var(--formipay-radius-sm) 0 0;
|
||||
}
|
||||
|
||||
.formipay-autocomplete-result:last-child {
|
||||
border-radius: 0 0 var(--formipay-radius-sm) var(--formipay-radius-sm);
|
||||
}
|
||||
545
src/admin/components/coupons/CouponMetabox.js
Normal file
545
src/admin/components/coupons/CouponMetabox.js
Normal file
@@ -0,0 +1,545 @@
|
||||
/**
|
||||
* Coupon Metabox - React metabox island for post.php editor
|
||||
* Uses WPCFTO design system components
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { MetaboxLayout, TabNav, TabPanel, Field, Input, Checkbox, Textarea, Button, Notice, GroupTitle } from '../../design-system';
|
||||
|
||||
// Currency helper functions
|
||||
const getGlobalCurrencies = () => {
|
||||
if (window.formipayGlobalCurrencies) {
|
||||
return window.formipayGlobalCurrencies;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const getCurrencySymbol = (currencyRaw) => {
|
||||
const parts = currencyRaw.split(':::');
|
||||
return parts[1] || currencyRaw;
|
||||
};
|
||||
|
||||
const getCurrencyCode = (currencyRaw) => {
|
||||
const parts = currencyRaw.split(':::');
|
||||
return parts[0] || currencyRaw;
|
||||
};
|
||||
|
||||
const getFlagByCurrency = (currencyRaw) => {
|
||||
if (window.formipayGetFlag) {
|
||||
return window.formipayGetFlag(currencyRaw);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export default function CouponMetabox({ postId }) {
|
||||
const [activeTab, setActiveTab] = useState('rules');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [message, setMessage] = useState(null);
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({
|
||||
active: 'on',
|
||||
type: 'percentage',
|
||||
amount_percentage: '',
|
||||
case_sensitive: '',
|
||||
free_shipping: '',
|
||||
quantity_active: '',
|
||||
use_limit: '',
|
||||
date_limit: '',
|
||||
amounts_fixed: {},
|
||||
max_amounts: {},
|
||||
forms: [],
|
||||
products: [],
|
||||
customers: [],
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{ id: 'rules', label: __('Rules', 'formipay'), icon: 'fa fa-cog' },
|
||||
{ id: 'restriction', label: __('Restrictions', 'formipay'), icon: 'fa fa-lock' },
|
||||
];
|
||||
|
||||
// Load coupon data
|
||||
useEffect(() => {
|
||||
if (postId > 0) {
|
||||
loadCouponData();
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [postId]);
|
||||
|
||||
const loadCouponData = async () => {
|
||||
try {
|
||||
const response = await fetch(`${window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}?action=formipay-get-coupon`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
id: postId,
|
||||
_wpnonce: window.formipayAdmin?.nonce || '',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const data = result.data;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
active: data.active || 'on',
|
||||
type: data.type || 'percentage',
|
||||
amount_percentage: data.amount_percentage || '',
|
||||
case_sensitive: data.case_sensitive || '',
|
||||
free_shipping: data.free_shipping || '',
|
||||
quantity_active: data.quantity_active || '',
|
||||
use_limit: data.use_limit || '',
|
||||
date_limit: data.date_limit || '',
|
||||
amounts_fixed: data.amounts_fixed?.reduce((acc, item) => {
|
||||
acc[item.symbol] = item.amount;
|
||||
return acc;
|
||||
}, {}) || {},
|
||||
max_amounts: data.max_amounts?.reduce((acc, item) => {
|
||||
acc[item.symbol] = item.amount;
|
||||
return acc;
|
||||
}, {}) || {},
|
||||
forms: data.forms || [],
|
||||
products: data.products || [],
|
||||
customers: data.customers || [],
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load coupon:', error);
|
||||
setMessage({ type: 'error', text: __('Failed to load coupon data.', 'formipay') });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
setMessage(null);
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
id: postId,
|
||||
title: document.querySelector('#title input')?.value || '',
|
||||
_wpnonce: window.formipayAdmin?.nonce || '',
|
||||
...formData,
|
||||
});
|
||||
|
||||
// Add fixed amounts
|
||||
Object.entries(formData.amounts_fixed).forEach(([symbol, amount]) => {
|
||||
params.append(`amount_fixed_${symbol}`, amount);
|
||||
});
|
||||
|
||||
// Add max amounts
|
||||
Object.entries(formData.max_amounts).forEach(([symbol, amount]) => {
|
||||
params.append(`max_amount_${symbol}`, amount);
|
||||
});
|
||||
|
||||
const response = await fetch(`${window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}?action=formipay-save-coupon`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: params,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setMessage({ type: 'success', text: result.data.message || __('Coupon saved successfully.', 'formipay') });
|
||||
} else {
|
||||
setMessage({ type: 'error', text: result.data?.message || __('Failed to save coupon.', 'formipay') });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save coupon:', error);
|
||||
setMessage({ type: 'error', text: __('Failed to save coupon.', 'formipay') });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isPercentage = formData.type === 'percentage';
|
||||
const isFixed = formData.type === 'fixed';
|
||||
|
||||
if (loading) {
|
||||
return <div className="formipay-loading">{__('Loading...', 'formipay')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="formipay-coupon-metabox">
|
||||
{message && (
|
||||
<Notice
|
||||
type={message.type}
|
||||
onDismiss={() => setMessage(null)}
|
||||
>
|
||||
{message.text}
|
||||
</Notice>
|
||||
)}
|
||||
|
||||
<MetaboxLayout
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
>
|
||||
<TabPanel tabs={tabs} activeTab={activeTab}>
|
||||
{(tab) => {
|
||||
if (tab.id === 'rules') {
|
||||
return (
|
||||
<div className="formipay-tab-content">
|
||||
{/* General Settings */}
|
||||
<GroupTitle title={__('General', 'formipay')} icon="fa fa-cog" />
|
||||
|
||||
<Field
|
||||
label={__('Active', 'formipay')}
|
||||
description={__('Enable this coupon.', 'formipay')}
|
||||
>
|
||||
<Checkbox
|
||||
checked={formData.active === 'on'}
|
||||
onChange={(e) => handleChange('active', e.target.checked ? 'on' : '')}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={__('Type', 'formipay')}
|
||||
description={__('Choose discount type.', 'formipay')}
|
||||
required
|
||||
>
|
||||
<div className="formipay-radio-group">
|
||||
<label className={`formipay-radio ${isFixed ? 'active' : ''}`}>
|
||||
<input
|
||||
type="radio"
|
||||
name="type"
|
||||
value="fixed"
|
||||
checked={isFixed}
|
||||
onChange={(e) => handleChange('type', e.target.value)}
|
||||
/>
|
||||
<span>{__('Fixed', 'formipay')}</span>
|
||||
</label>
|
||||
<label className={`formipay-radio ${isPercentage ? 'active' : ''}`}>
|
||||
<input
|
||||
type="radio"
|
||||
name="type"
|
||||
value="percentage"
|
||||
checked={isPercentage}
|
||||
onChange={(e) => handleChange('type', e.target.value)}
|
||||
/>
|
||||
<span>{__('Percentage', 'formipay')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
{isPercentage && (
|
||||
<Field
|
||||
label={__('Amount', 'formipay')}
|
||||
description={__('Discount percentage.', 'formipay')}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.01"
|
||||
value={formData.amount_percentage}
|
||||
onChange={(e) => handleChange('amount_percentage', e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{/* Fixed Amount Section */}
|
||||
{isFixed && (
|
||||
<>
|
||||
<GroupTitle title={__('Discount Amount', 'formipay')} icon="fa fa-money" />
|
||||
|
||||
{getGlobalCurrencies().map((currency) => {
|
||||
const symbol = getCurrencySymbol(currency.currency);
|
||||
const currencyCode = getCurrencyCode(currency.currency);
|
||||
const flag = getFlagByCurrency(currency.currency);
|
||||
const step = currency.decimal_digits > 0 ? 1 / (currency.decimal_digits * 10) : 1;
|
||||
|
||||
return (
|
||||
<div key={currencyCode} className="formipay-generic-field">
|
||||
<div className="formipay-field-aside">
|
||||
<div className="formipay-field-label required">
|
||||
<span className="formipay-field-label-text">
|
||||
{flag && <img src={flag} alt="" width="18" style={{ verticalAlign: 'middle', marginRight: '4px' }} />}
|
||||
{__('Amount in', 'formipay')} {symbol}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="formipay-field-content">
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step={step}
|
||||
placeholder={__('Enter Amount...', 'formipay')}
|
||||
value={formData.amounts_fixed[symbol] || ''}
|
||||
onChange={(e) => handleChange('amounts_fixed', {
|
||||
...formData.amounts_fixed,
|
||||
[symbol]: e.target.value
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Max Discount Section */}
|
||||
<GroupTitle
|
||||
title={__('Max Discount Amount', 'formipay')}
|
||||
icon="fa fa-calculator"
|
||||
/>
|
||||
|
||||
{getGlobalCurrencies().map((currency) => {
|
||||
const symbol = getCurrencySymbol(currency.currency);
|
||||
const currencyCode = getCurrencyCode(currency.currency);
|
||||
const flag = getFlagByCurrency(currency.currency);
|
||||
const step = currency.decimal_digits > 0 ? 1 / (currency.decimal_digits * 10) : 1;
|
||||
|
||||
return (
|
||||
<Field
|
||||
key={`max_${currencyCode}`}
|
||||
label={
|
||||
<span>
|
||||
{flag && <img src={flag} alt="" width="18" style={{ verticalAlign: 'middle', marginRight: '4px' }} />}
|
||||
{__('Max Amount in', 'formipay')} {symbol}
|
||||
</span>
|
||||
}
|
||||
description={__('Leave empty to not limit the max discount amount.', 'formipay')}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step={step}
|
||||
placeholder={__('Enter Max Amount...', 'formipay')}
|
||||
value={formData.max_amounts[symbol] || ''}
|
||||
onChange={(e) => handleChange('max_amounts', {
|
||||
...formData.max_amounts,
|
||||
[symbol]: e.target.value
|
||||
})}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Rules Section */}
|
||||
<GroupTitle title={__('Rules', 'formipay')} icon="fa fa-list" />
|
||||
|
||||
<Field
|
||||
label={__('Case Sensitive', 'formipay')}
|
||||
description={__('If activated, coupon codes must be entered with the exact capitalization.', 'formipay')}
|
||||
>
|
||||
<Checkbox
|
||||
checked={formData.case_sensitive === 'on'}
|
||||
onChange={(e) => handleChange('case_sensitive', e.target.checked ? 'on' : '')}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={__('Free Shipping', 'formipay')}
|
||||
description={__('Shipping cost will be free when this coupon is applied.', 'formipay')}
|
||||
>
|
||||
<Checkbox
|
||||
checked={formData.free_shipping === 'on'}
|
||||
onChange={(e) => handleChange('free_shipping', e.target.checked ? 'on' : '')}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{isFixed && (
|
||||
<Field
|
||||
label={__('Influenced by Quantity', 'formipay')}
|
||||
description={__('Example: when buyer buys 4 items, 4 × discount amount will be applied.', 'formipay')}
|
||||
>
|
||||
<Checkbox
|
||||
checked={formData.quantity_active === 'on'}
|
||||
onChange={(e) => handleChange('quantity_active', e.target.checked ? 'on' : '')}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tab.id === 'restriction') {
|
||||
return (
|
||||
<div className="formipay-tab-content">
|
||||
<GroupTitle title={__('Restrictions', 'formipay')} icon="fa fa-lock" />
|
||||
|
||||
<Field
|
||||
label={__('Usage Limit', 'formipay')}
|
||||
description={__('Leave empty or 0 (zero) for unlimited usage.', 'formipay')}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
value={formData.use_limit}
|
||||
onChange={(e) => handleChange('use_limit', e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={__('Date Limit', 'formipay')}
|
||||
description={__('Last day the coupon can be used. Leave empty for no limit.', 'formipay')}
|
||||
>
|
||||
<Input
|
||||
type="date"
|
||||
value={formData.date_limit}
|
||||
onChange={(e) => handleChange('date_limit', e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={__('Forms', 'formipay')}
|
||||
description={__('Only selected form(s) can use the coupon. Leave empty to apply to all forms.', 'formipay')}
|
||||
>
|
||||
<AutocompleteField
|
||||
postType="formipay-form"
|
||||
value={formData.forms}
|
||||
onChange={(values) => handleChange('forms', values)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={__('Products', 'formipay')}
|
||||
description={__('Only selected product(s) can use the coupon. Leave empty to apply to all products.', 'formipay')}
|
||||
>
|
||||
<AutocompleteField
|
||||
postType="formipay-product"
|
||||
value={formData.products}
|
||||
onChange={(values) => handleChange('products', values)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={__('Customers', 'formipay')}
|
||||
description={__('Only selected customer(s) can use the coupon. Leave empty to apply to all customers.', 'formipay')}
|
||||
>
|
||||
<AutocompleteField
|
||||
postType="formipay-customer"
|
||||
value={formData.customers}
|
||||
onChange={(values) => handleChange('customers', values)}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}}
|
||||
</TabPanel>
|
||||
|
||||
<div className="formipay-metabox-actions">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? __('Saving...', 'formipay') : __('Save Coupon', 'formipay')}
|
||||
</Button>
|
||||
</div>
|
||||
</MetaboxLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Simple Autocomplete Field Component
|
||||
function AutocompleteField({ postType, value, onChange }) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [results, setResults] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const selectedItems = value || [];
|
||||
|
||||
const handleSearch = async (query) => {
|
||||
setSearch(query);
|
||||
if (query.length < 2) {
|
||||
setResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}?action=formipay-autocomplete-search`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
post_type: postType,
|
||||
search: query,
|
||||
_wpnonce: window.formipayAdmin?.nonce || '',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
setResults(result.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Autocomplete search failed:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (item) => {
|
||||
if (!selectedItems.includes(item.value)) {
|
||||
onChange([...selectedItems, item.value]);
|
||||
}
|
||||
setSearch('');
|
||||
setResults([]);
|
||||
};
|
||||
|
||||
const handleRemove = (valueToRemove) => {
|
||||
onChange(selectedItems.filter(v => v !== valueToRemove));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="formipay-autocomplete">
|
||||
<div className="formipay-autocomplete-selected">
|
||||
{selectedItems.map(val => (
|
||||
<span key={val} className="formipay-autocomplete-tag">
|
||||
{val}
|
||||
<button
|
||||
type="button"
|
||||
className="formipay-autocomplete-remove"
|
||||
onClick={() => handleRemove(val)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="formipay-autocomplete-input-wrapper">
|
||||
<Input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
onFocus={() => setOpen(true)}
|
||||
placeholder={__('Search...', 'formipay')}
|
||||
/>
|
||||
{loading && <span className="formipay-autocomplete-loading">...</span>}
|
||||
</div>
|
||||
{open && results.length > 0 && (
|
||||
<div className="formipay-autocomplete-results">
|
||||
{results.map(item => (
|
||||
<div
|
||||
key={item.value}
|
||||
className="formipay-autocomplete-result"
|
||||
onClick={() => handleSelect(item)}
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,9 +4,12 @@
|
||||
|
||||
import { render } from '@wordpress/element';
|
||||
import App from './components/App';
|
||||
import CouponMetabox from './components/coupons/CouponMetabox';
|
||||
import './components/coupons/CouponMetabox.css';
|
||||
|
||||
// Mount the React app to all available mount points
|
||||
const mountApps = () => {
|
||||
// Mount main admin app pages
|
||||
const mountPoints = document.querySelectorAll('[data-formipay-mount]');
|
||||
|
||||
console.log('[Formipay] Mount points found:', mountPoints.length);
|
||||
@@ -28,6 +31,30 @@ const mountApps = () => {
|
||||
console.error('[Formipay] Failed to mount:', page, error);
|
||||
}
|
||||
});
|
||||
|
||||
// Mount metabox islands
|
||||
const metaboxPoints = document.querySelectorAll('[data-formipay-metabox]');
|
||||
|
||||
console.log('[Formipay] Metabox points found:', metaboxPoints.length);
|
||||
|
||||
metaboxPoints.forEach((mountPoint) => {
|
||||
const metaboxType = mountPoint.dataset.formipayMetabox;
|
||||
const postId = parseInt(mountPoint.dataset.postId || '0');
|
||||
|
||||
console.log('[Formipay] Mounting metabox:', metaboxType, 'for post:', postId);
|
||||
|
||||
try {
|
||||
if (metaboxType === 'coupon') {
|
||||
render(
|
||||
<CouponMetabox postId={postId} />,
|
||||
mountPoint
|
||||
);
|
||||
console.log('[Formipay] Successfully mounted coupon metabox');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Formipay] Failed to mount metabox:', metaboxType, error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize when DOM is ready
|
||||
|
||||
Reference in New Issue
Block a user