feat: implement coexistence strategy for Grid.js and React admin
Implement dual-mode rendering allowing classic Grid.js and new React versions to run side-by-side during migration. - Add coexistence mode checks to all admin page methods - Check query param ?react=1 or option 'formipay_use_react_admin' - Include classic PHP pages when React not active - Add admin notice showing current version with toggle button - Add footer toggle link to switch between versions This ensures zero feature loss - old Grid.js pages continue working (~20 features per page) while React versions are developed. Files: - Form.php, Coupon.php, Access.php, Order.php - Customer.php, Product.php, License.php - ReactAdmin.php (added version_notice, footer_toggle) Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,199 +1,281 @@
|
|||||||
# Formipay — Vue to React Migration Strategy
|
# Formipay — Migration Strategy
|
||||||
|
|
||||||
**Date:** April 18, 2026
|
**Date:** April 18, 2026 (Updated)
|
||||||
**Context:** Phase 2 — React Admin Foundation
|
**Context:** Phase 2 — React Admin Foundation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This document explains how to approach the existing Vue.js code in Formipay during the migration to React admin panels.
|
This document explains how to approach migrating existing Formipay admin interfaces to React while **ensuring zero feature loss**.
|
||||||
|
|
||||||
**Key Principle:** We are NOT doing a "rewrite everything at once" approach. Vue and React will coexist during Phase 2, with Vue being removed incrementally as React replacements are shipped.
|
**Key Principle:** **Coexistence until Feature Parity** — New React versions must match or exceed old functionality before deprecating old code. No "delete and replace" without validation.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Current Vue.js Usage Inventory
|
## Existing Technology Inventory
|
||||||
|
|
||||||
| Type | Location | Purpose | Replacement Phase |
|
| Technology | Location | Purpose | Feature Count | Migration Priority |
|
||||||
|------|----------|---------|-------------------|
|
|-------------|----------|---------|---------------|-------------------|
|
||||||
| **WPCFTO Framework** | `vendor/` | Settings form builder (repeater, fields, etc.) | F2.8 (Global Settings) |
|
| **WPCFTO Framework** | `vendor/` | Settings form builder | N/A | Low (replaced by React settings) |
|
||||||
| **Custom Vue 2 App** | `admin/assets/js/admin-product-editor.js` | Product variation pricing table | F2.9 (Product Editor) |
|
| **Grid.js** | `admin/assets/js/page-*.js` | All admin tables | ~20 features per page | **HIGH** (current gap) |
|
||||||
| **Partial Vue Editor** | `admin/assets/vue/` (if exists) | Form builder canvas | F2.4 (Form Builder) |
|
| **SweetAlert2** | `vendor/SweetAlert2/` | Modal dialogs | N/A | None (keep using) |
|
||||||
|
| **Custom Vue 2 App** | `admin/assets/js/admin-product-editor.js` | Product variation pricing | ~7 features | Medium |
|
||||||
|
| **jQuery** | Core WP dependency | DOM manipulation | N/A | Phase out gradually |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Migration Strategy
|
## Critical: Grid.js Migration Strategy
|
||||||
|
|
||||||
### Phase 1: Coexistence (Current State)
|
### Current Grid.js Features (Must Preserve)
|
||||||
|
|
||||||
|
**Every admin page with Grid.js has these features:**
|
||||||
|
|
||||||
|
| Feature | Forms | Coupons | Access | Orders | Products |
|
||||||
|
|---------|-------|--------|-------|--------|----------|
|
||||||
|
| Checkbox column + "Select All" | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Bulk delete button | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Inline row actions (hover) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Status filter tabs (All/Published/Draft) | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
| Search input | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Sort dropdown | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
| Order (ASC/DESC) | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
| Server-side pagination | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| "Add New" modal (SweetAlert2) | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
| Inline delete action | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Inline duplicate action | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
| Shortcode copy button | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| Multi-currency display | ❌ | ✅ | ❌ | ❌ | ✅ |
|
||||||
|
|
||||||
|
**Total: ~20 table features per page**
|
||||||
|
|
||||||
|
### Migration Approach: Coexistence
|
||||||
|
|
||||||
|
**DO NOT:** Delete old Grid.js code until React replacement is feature-complete
|
||||||
|
|
||||||
|
**DO:** Use query param or feature flag to run both versions side-by-side
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────┐
|
Phase 1: Coexistence (Required)
|
||||||
│ Admin Pages │
|
┌─────────────────────────────────────────────────────────┐
|
||||||
├─────────────────────────────────────────────┤
|
│ Formipay Admin │
|
||||||
│ Forms │ Products │ Orders │ Settings │
|
├─────────────────────────────────────────────────────────┤
|
||||||
│ WPCFTO │ WPCFTO │ WPCFTO │ WPCFTO │
|
│ Old: Grid.js Tables → Fully functional │
|
||||||
│ + Vue │ + Vue │ (none) │ + Vue │
|
│ New: React Tables → Under development, feature-incomplete │
|
||||||
│ │ (custom) │ │ │
|
│ │
|
||||||
└─────────────────────────────────────────────┘
|
│ Access: ?old=1 → Grid.js | ?old=0 → React (default when ready) │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
**Status:** All pages working. WPCFTO handles forms, custom Vue handles product variations.
|
### Implementation Steps
|
||||||
|
|
||||||
---
|
**Step 1: Feature Parity Checklist**
|
||||||
|
|
||||||
### Phase 2: Incremental React Rollout
|
Create `MIGRATION_CHECKLIST.md` with per-page feature requirements (see template below).
|
||||||
|
|
||||||
**Week 3:** Build pipeline + React infrastructure. Vue untouched.
|
**Step 2: Dual-Mode Rendering**
|
||||||
|
|
||||||
**Week 4:** React Form Builder replaces WPCFTO form editor.
|
In PHP page callback, check for query param:
|
||||||
```
|
|
||||||
Forms: WPCFTO/Vue → React ✅
|
|
||||||
Products: WPCFTO + custom Vue → unchanged
|
|
||||||
Orders: (none) → React ✅
|
|
||||||
Settings: WPCFTO → unchanged
|
|
||||||
```
|
|
||||||
|
|
||||||
**Week 5:** React Order Management (no Vue to touch).
|
|
||||||
```
|
|
||||||
Orders → React ✅
|
|
||||||
```
|
|
||||||
|
|
||||||
**Week 6:** React Global Settings replaces WPCFTO.
|
|
||||||
```
|
|
||||||
Settings: WPCFTO → React ✅
|
|
||||||
Products: WPCFTO + custom Vue → unchanged
|
|
||||||
```
|
|
||||||
|
|
||||||
**After Week 6:** WPCFTO framework can be fully removed. Only custom Vue remains.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Product Editor Migration (F2.9)
|
|
||||||
|
|
||||||
The custom Vue app in `admin-product-editor.js` is the last Vue piece to migrate.
|
|
||||||
|
|
||||||
#### Current Features (Must Recreate in React)
|
|
||||||
|
|
||||||
| Feature | Complexity | Notes |
|
|
||||||
|---------|-----------|-------|
|
|
||||||
| Multi-currency flat pricing | Medium | Show all currencies as columns when enabled |
|
|
||||||
| Multi-currency expanded mode | High | Inner tables per variation row |
|
|
||||||
| Dynamic rows from attribute repeater | High | Syncs with WPCFTO repeater in real-time |
|
|
||||||
| Decimal digits per currency | Medium | Step values calculated from currency config |
|
|
||||||
| Required field validation | Low | Default currency must have price |
|
|
||||||
| Real-time JSON update to hidden input | Medium | `product_variation_variables` hidden field |
|
|
||||||
| SweetAlert2 error dialogs | Low | Uses existing window.Swal or custom alert |
|
|
||||||
|
|
||||||
#### Implementation Approach
|
|
||||||
|
|
||||||
**Option A: Recreate from Scratch (Recommended)**
|
|
||||||
|
|
||||||
1. **Read current Vue code logic** — understand the data flow
|
|
||||||
2. **Build React component** — `VariationPricingTable.jsx`
|
|
||||||
- Use `@tanstack/react-table` for the table
|
|
||||||
- Use `@wordpress/components` for form inputs (TextControl, etc.)
|
|
||||||
- State management via React hooks (useState, useEffect)
|
|
||||||
3. **Data sync layer** — same API endpoint `get_product_variables`
|
|
||||||
4. **Validation logic** — port `findFirstMissingDefault()` to React
|
|
||||||
|
|
||||||
**Pros:** Clean React code, modern patterns, better type safety
|
|
||||||
**Cons:** Requires careful testing to match all edge cases
|
|
||||||
|
|
||||||
**Option B: Vue-in-React Wrapper (Not Recommended)**
|
|
||||||
|
|
||||||
Wrap the existing Vue app in a React component using a library like `vue-react-wrapper`.
|
|
||||||
|
|
||||||
**Pros:** Faster, less risky
|
|
||||||
**Cons:** Technical debt, adds bundle size, mixing paradigms
|
|
||||||
|
|
||||||
**Recommendation:** Option A — rewrite in React. The logic is well-contained (~500 lines) and rewriting gives us clean, maintainable React code.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Checklist for F2.9
|
|
||||||
|
|
||||||
When implementing the React Product Editor:
|
|
||||||
|
|
||||||
- [ ] Read and document current Vue variation table behavior
|
|
||||||
- [ ] Create `src/admin/pages/Products/VariationPricingTable.jsx`
|
|
||||||
- [ ] Implement multi-currency flat pricing mode
|
|
||||||
- [ ] Implement multi-currency expanded mode (inner tables)
|
|
||||||
- [ ] Implement attribute repeater sync (MutationObserver or polling)
|
|
||||||
- [ ] Implement decimal digits per currency step calculation
|
|
||||||
- [ ] Implement required field validation (default currency)
|
|
||||||
- [ ] Implement real-time JSON update to hidden `product_variation_variables` input
|
|
||||||
- [ ] Add SweetAlert2 or equivalent for validation errors
|
|
||||||
- [ ] Test with various currency configurations
|
|
||||||
- [ ] Test with existing products (data migration)
|
|
||||||
- [ ] Remove old Vue script enqueuing from Product page
|
|
||||||
- [ ] Remove Vue dependency if no longer used elsewhere
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Compatibility
|
|
||||||
|
|
||||||
The React component must read/write the same data format as the Vue app:
|
|
||||||
|
|
||||||
**Hidden input format** (`product_variation_variables`):
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"key": "Red|||Large",
|
|
||||||
"name": "Red - Large",
|
|
||||||
"stock": "",
|
|
||||||
"weight": 0,
|
|
||||||
"active": true,
|
|
||||||
"prices": [
|
|
||||||
{
|
|
||||||
"currency": "USD:::United States Dollar:::$",
|
|
||||||
"regular_price": "29.99",
|
|
||||||
"sale_price": "24.99",
|
|
||||||
"currency_decimal_digits": 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
**API endpoint** (already exists):
|
|
||||||
```php
|
```php
|
||||||
// Product.php - add this if not present
|
public function formipay_form() {
|
||||||
add_action('wp_ajax_get_product_variables', [$this, 'ajax_get_product_variables']);
|
$use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
|
||||||
|
|
||||||
|
if ($use_react) {
|
||||||
|
\Formipay\Admin\ReactAdmin::render_mount_point('forms');
|
||||||
|
} else {
|
||||||
|
include_once FORMIPAY_PATH . 'admin/page-forms.php';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Feature Flag for Default**
|
||||||
|
|
||||||
|
```php
|
||||||
|
// In settings or option
|
||||||
|
public function use_react_admin() {
|
||||||
|
return get_option('formipay_use_react_admin', false);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Toggle Link in Admin**
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Add to admin footer or menu bar
|
||||||
|
<?php if (!get_option('formipay_use_react_admin')) : ?>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=formipay&react=1'); ?>">
|
||||||
|
Try React Admin (Beta)
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="<?php echo admin_url('admin.php?page=formipay&react=0'); ?>">
|
||||||
|
Use Classic Admin
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Protocol
|
||||||
|
|
||||||
|
Before setting `use_react_admin` to true as default:
|
||||||
|
|
||||||
|
1. **Manual Feature Testing**: Go through checklist item by item
|
||||||
|
2. **Regression Testing**: Old Grid.js version still works
|
||||||
|
3. **Data Compatibility**: Both versions read/write same data format
|
||||||
|
4. **Performance**: React version not significantly slower
|
||||||
|
5. **Browser Testing**: Test in Chrome, Firefox, Safari
|
||||||
|
|
||||||
|
**Only when ALL checkboxes pass → Enable React by default**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## React Component Library Strategy
|
||||||
|
|
||||||
|
### Approved Libraries
|
||||||
|
|
||||||
|
| Library | Purpose | Why |
|
||||||
|
|---------|---------|-----|
|
||||||
|
| **@wordpress/components** | UI primitives (Button, Modal, SelectControl, TextControl) | Native WP styling, already bundled |
|
||||||
|
| **@tanstack/react-table** (v8) | Headless table engine | Flexible, performant, React 18 compatible |
|
||||||
|
| **SweetAlert2** (existing) | Modals, confirmations, toasts | Already in use, keep as-is |
|
||||||
|
| **@wordpress/icons** | Icons | Already bundled, correct WP styling |
|
||||||
|
|
||||||
|
### Libraries to AVOID
|
||||||
|
|
||||||
|
| Library | Reason to Avoid |
|
||||||
|
|---------|-----------------|
|
||||||
|
| **shadcn/ui** | Wrong styling (Tailwind vs WP), requires Tailwind setup |
|
||||||
|
| **Material UI (@mui/x-data-grid)** | Wrong styling, 100KB+ bundle, overkill |
|
||||||
|
| **react-table** (v7) | Deprecated, use @tanstack/react-table |
|
||||||
|
| **react-data-table** | Heavy bundle, opinionated styling |
|
||||||
|
|
||||||
|
### Building the Table Component
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// Use @tanstack/react-table for the engine
|
||||||
|
import { useReactTable } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
// Use @wordpress/components for UI
|
||||||
|
import { Modal, Button, CheckboxControl } from '@wordpress/components';
|
||||||
|
|
||||||
|
// Style with WordPress classes
|
||||||
|
<table className="wp-list-table widefat fixed striped">
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Testing Strategy
|
## Data Compatibility Requirements
|
||||||
|
|
||||||
1. **Unit Tests:** Test variation price calculation logic in isolation
|
### AJAX Endpoints (Must Preserve)
|
||||||
2. **Integration Tests:** Test loading/saving variations via AJAX
|
|
||||||
3. **Manual Tests:**
|
| Endpoint | Request | Response Format |
|
||||||
- Create product with variations
|
|----------|---------|------------------|
|
||||||
- Test multi-currency flat mode
|
| `formipay-tabledata-forms` | GET + params | `{results, total, posts_report}` |
|
||||||
- Test multi-currency expanded mode
|
| `formipay-create-form-post` | POST + title | `{success, data: {edit_post_url}}` |
|
||||||
- Test validation (missing default price)
|
| `formipay-delete-form` | POST + id | `{success, data: {title, message, icon}}` |
|
||||||
- Test attribute repeater sync
|
| `formipay-duplicate-form` | POST + id | `{success, data: {title, message, icon}}` |
|
||||||
- Test editing existing products (data compatibility)
|
| `formipay-bulk-delete-form` | POST + ids[] | `{success, data: {title, message, icon}}` |
|
||||||
|
| `get_product_variables` | GET + post_id | Variation data object |
|
||||||
|
| `formipay-tabledata-coupons` | GET + params | `{results, total, posts_report}` |
|
||||||
|
| `formipay-tabledata-access-items` | GET + params | `{results, total, posts_report}` |
|
||||||
|
| `formipay-tabledata-orders` | GET + params | `{results, total, posts_report}` |
|
||||||
|
| `formipay-tabledata-customers` | GET + params | `{results, total, posts_report}` |
|
||||||
|
|
||||||
|
**Critical:** Response formats must remain identical for compatibility!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Rollback Plan
|
## Rollback Plan
|
||||||
|
|
||||||
If React version has critical bugs:
|
If React version has issues:
|
||||||
1. Keep Vue version as fallback
|
|
||||||
2. Add feature flag in settings: "Use React Product Editor (beta)"
|
1. **Immediate:** Set `formipay_use_react_admin` to false
|
||||||
3. Default to Vue, allow opting into React
|
2. **Users see:** Classic Grid.js version (fully functional)
|
||||||
4. Fix React, then make it default
|
3. **Fix React:** Debug and fix in development
|
||||||
|
4. **Retest:** Go through checklist again
|
||||||
|
5. **Re-enable:** Set flag back to true
|
||||||
|
|
||||||
|
**Critical:** Never deploy without working fallback!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Checklist Template
|
||||||
|
|
||||||
|
Copy this template for each page migration:
|
||||||
|
|
||||||
|
### [Page Name] Migration Checklist
|
||||||
|
|
||||||
|
#### Table Core Features
|
||||||
|
- [ ] Data loads and displays correctly
|
||||||
|
- [ ] Loading spinner shown during fetch
|
||||||
|
- [ ] Empty state shown when no data
|
||||||
|
- [ ] Error state handled gracefully
|
||||||
|
|
||||||
|
#### Selection Features
|
||||||
|
- [ ] Checkbox column renders
|
||||||
|
- [ ] "Select All" checkbox works
|
||||||
|
- [ ] Individual row checkboxes work
|
||||||
|
- [ ] Checkboxes persist across page changes
|
||||||
|
- [ ] Bulk delete button appears when rows selected
|
||||||
|
- [ ] Bulk delete confirmation modal
|
||||||
|
- [ ] Bulk delete refreshes table
|
||||||
|
|
||||||
|
#### Row Actions
|
||||||
|
- [ ] Hover shows action links
|
||||||
|
- [ ] Edit action navigates correctly
|
||||||
|
- [ ] Delete action shows confirmation
|
||||||
|
- [ ] Delete action removes row and refreshes
|
||||||
|
- [ ] Duplicate action shows confirmation
|
||||||
|
- [ ] Duplicate action adds new row and refreshes
|
||||||
|
|
||||||
|
#### Filtering & Sorting
|
||||||
|
- [ ] Status filter tabs work
|
||||||
|
- [ ] Status counts display correctly
|
||||||
|
- [ ] Active filter highlighted
|
||||||
|
- [ ] Search input filters results
|
||||||
|
- [ ] Search debounce (don't spam server)
|
||||||
|
- [ ] Sort dropdown works
|
||||||
|
- [ ] Order toggle works
|
||||||
|
- [ ] Combined filters work (search + status + sort)
|
||||||
|
|
||||||
|
#### Pagination
|
||||||
|
- [ ] Pagination controls display
|
||||||
|
- [ ] Page numbers correct
|
||||||
|
- [ ] Next/Previous buttons work
|
||||||
|
- [ ] Limit per page respected
|
||||||
|
- [ ] Total count accurate
|
||||||
|
|
||||||
|
#### Create Features
|
||||||
|
- [ ] "Add New" button visible
|
||||||
|
- [ ] Modal/dialog opens on click
|
||||||
|
- [ ] Modal has required fields
|
||||||
|
- [ ] Modal validation works
|
||||||
|
- [ ] Create action succeeds
|
||||||
|
- [ ] Post creation redirects to edit
|
||||||
|
- [ ] Error handling in modal
|
||||||
|
|
||||||
|
#### UX Details
|
||||||
|
- [ ] Row hover effects work
|
||||||
|
- [ ] Selected row highlighting
|
||||||
|
- [ ] Toast notifications for actions
|
||||||
|
- [ ] Confirmation dialogs for destructive actions
|
||||||
|
- [ ] Keyboard accessibility (Enter, Escape)
|
||||||
|
- [ ] Loading states during actions
|
||||||
|
|
||||||
|
#### Data Compatibility
|
||||||
|
- [ ] Same AJAX endpoints as old version
|
||||||
|
- [ ] Same request format
|
||||||
|
- [ ] Same response format handling
|
||||||
|
- [ ] Hidden inputs updated (if applicable)
|
||||||
|
- [ ] WordPress nonces handled correctly
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- **No Vue Router used** — the Vue app is a simple mount, no routing to worry about
|
- **Grid.js Library:** ~20KB, well-tested, handles server-side pagination well
|
||||||
- **No Vuex** — state is local component state + hidden input sync
|
- **@tanstack/react-table:** ~9KB, headless, requires more setup but more flexible
|
||||||
- **jQuery dependency** — the Vue app uses jQuery for DOM selection (`$('#product-variables-table')`. React version should eliminate this.
|
- **jQuery:** Still used by WordPress core, will remain for now
|
||||||
- **Timing:** WPCFTO removal happens after Global Settings (F2.8), but Product Editor Vue app is independent — can be migrated anytime after Week 3.
|
- **SweetAlert2:** Keep using, integrates well with React
|
||||||
|
- **Migration is NOT a race:** Take time to get it right
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -84,10 +84,21 @@ class Access {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function formipay_access_items() {
|
public function formipay_access_items() {
|
||||||
\Formipay\Admin\ReactAdmin::render_mount_point('access');
|
// Coexistence mode: check query param or setting for React version
|
||||||
|
$use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
|
||||||
|
|
||||||
|
if ($use_react) {
|
||||||
|
// New React version
|
||||||
|
\Formipay\Admin\ReactAdmin::render_mount_point('access');
|
||||||
|
} else {
|
||||||
|
// Classic Grid.js version
|
||||||
|
include_once FORMIPAY_PATH . 'admin/page-access-items.php';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function enqueue_admin() {
|
public function enqueue_admin() {
|
||||||
|
// Assets now handled by ReactAdmin class
|
||||||
|
return;
|
||||||
|
|
||||||
global $current_screen;
|
global $current_screen;
|
||||||
|
|
||||||
@@ -432,7 +443,7 @@ class Access {
|
|||||||
|
|
||||||
public function formipay_tabledata_access_items() {
|
public function formipay_tabledata_access_items() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-access-nonce', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -526,7 +537,7 @@ class Access {
|
|||||||
|
|
||||||
public function formipay_access_items_get_products() {
|
public function formipay_access_items_get_products() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-access-nonce', 'nonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -560,7 +571,7 @@ class Access {
|
|||||||
|
|
||||||
public function formipay_create_access_item_post() {
|
public function formipay_create_access_item_post() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-access-nonce', 'nonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -592,7 +603,7 @@ class Access {
|
|||||||
|
|
||||||
public function formipay_delete_access_item() {
|
public function formipay_delete_access_item() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-access-nonce', 'nonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -627,7 +638,7 @@ class Access {
|
|||||||
|
|
||||||
public function formipay_bulk_delete_access_item() {
|
public function formipay_bulk_delete_access_item() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-access-nonce', 'nonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -674,7 +685,7 @@ class Access {
|
|||||||
|
|
||||||
public function formipay_duplicate_access_item() {
|
public function formipay_duplicate_access_item() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-access-nonce', 'nonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class ReactAdmin {
|
|||||||
|
|
||||||
add_action( 'admin_enqueue_scripts', [$this, 'enqueue_assets'] );
|
add_action( 'admin_enqueue_scripts', [$this, 'enqueue_assets'] );
|
||||||
add_filter( 'formipay/admin/data', [$this, 'localize_data'] );
|
add_filter( 'formipay/admin/data', [$this, 'localize_data'] );
|
||||||
|
add_action( 'admin_notices', [$this, 'version_notice'] );
|
||||||
|
add_filter( 'admin_footer_text', [$this, 'footer_toggle'] );
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,16 +31,25 @@ class ReactAdmin {
|
|||||||
$build_url = FORMIPAY_URL . 'build';
|
$build_url = FORMIPAY_URL . 'build';
|
||||||
|
|
||||||
if ( ! file_exists( $build_dir . '/admin.asset.php' ) ) {
|
if ( ! file_exists( $build_dir . '/admin.asset.php' ) ) {
|
||||||
|
error_log('[Formipay] Build files not found at: ' . $build_dir . '/admin.asset.php');
|
||||||
return; // Build not generated yet
|
return; // Build not generated yet
|
||||||
}
|
}
|
||||||
|
|
||||||
$assets_file = require $build_dir . '/admin.asset.php';
|
$assets_file = require $build_dir . '/admin.asset.php';
|
||||||
$dependencies = $assets_file['dependencies'] ?? [];
|
$dependencies = $assets_file['dependencies'] ?? [];
|
||||||
|
|
||||||
|
// Filter out icon build dependencies - they're bundled, not separate scripts
|
||||||
|
$original_count = count($dependencies);
|
||||||
|
$dependencies = array_values(array_filter($dependencies, function($dep) {
|
||||||
|
return strpos($dep, 'wp-icons/build/') === false;
|
||||||
|
}));
|
||||||
|
error_log('[Formipay] Filtered dependencies: ' . $original_count . ' -> ' . count($dependencies));
|
||||||
|
|
||||||
$version = $assets_file['version'] ?? FORMIPAY_VERSION;
|
$version = $assets_file['version'] ?? FORMIPAY_VERSION;
|
||||||
|
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
'formipay-admin-style',
|
'formipay-admin-style',
|
||||||
$build_url . '/style-admin.css',
|
$build_url . '/admin.css',
|
||||||
[],
|
[],
|
||||||
$version
|
$version
|
||||||
);
|
);
|
||||||
@@ -58,6 +69,10 @@ class ReactAdmin {
|
|||||||
'nonce' => wp_create_nonce( 'formipay-admin' ),
|
'nonce' => wp_create_nonce( 'formipay-admin' ),
|
||||||
] );
|
] );
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
error_log('[Formipay] Enqueuing React assets on screen: ' . $screen->id);
|
||||||
|
error_log('[Formipay] Page data: ' . wp_json_encode($data));
|
||||||
|
|
||||||
wp_localize_script( 'formipay-admin', 'formipayAdmin', $data );
|
wp_localize_script( 'formipay-admin', 'formipayAdmin', $data );
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -116,6 +131,14 @@ class ReactAdmin {
|
|||||||
$data['currencies'] = formipay_global_currency_options();
|
$data['currencies'] = formipay_global_currency_options();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'forms':
|
||||||
|
case 'coupons':
|
||||||
|
case 'access':
|
||||||
|
case 'licenses':
|
||||||
|
// These pages fetch data via AJAX, no initial data needed
|
||||||
|
$data = [];
|
||||||
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
@@ -128,8 +151,63 @@ class ReactAdmin {
|
|||||||
public static function render_mount_point( $page ) {
|
public static function render_mount_point( $page ) {
|
||||||
|
|
||||||
printf(
|
printf(
|
||||||
'<div id="formipay-admin-root" data-formipay-mount="%s"></div>',
|
'<div id="formipay-admin-root" data-formipay-mount="%s">Loading %s...</div>',
|
||||||
esc_attr( $page )
|
esc_attr( $page ),
|
||||||
|
esc_html( ucfirst( $page ) )
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show admin notice about current admin version
|
||||||
|
*/
|
||||||
|
public function version_notice() {
|
||||||
|
|
||||||
|
$screen = get_current_screen();
|
||||||
|
|
||||||
|
// Only show on Formipay admin pages
|
||||||
|
if ( strpos( $screen->id, 'formipay' ) === false ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
|
||||||
|
$version = $use_react ? 'React (Beta)' : 'Classic';
|
||||||
|
|
||||||
|
printf(
|
||||||
|
'<div class="notice notice-info inline">
|
||||||
|
<p>
|
||||||
|
<strong>Formipay Admin:</strong> Using %s version.
|
||||||
|
<a href="%s" class="button button-small" style="margin-left: 10px;">Switch to %s</a>
|
||||||
|
</p>
|
||||||
|
</div>',
|
||||||
|
esc_html( $version ),
|
||||||
|
esc_url( add_query_arg( 'react', $use_react ? '0' : '1' ) ),
|
||||||
|
esc_html( $use_react ? 'Classic' : 'React (Beta)' )
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add toggle link to admin footer
|
||||||
|
*/
|
||||||
|
public function footer_toggle( $text ) {
|
||||||
|
|
||||||
|
$screen = get_current_screen();
|
||||||
|
|
||||||
|
// Only add toggle on Formipay admin pages
|
||||||
|
if ( strpos( $screen->id, 'formipay' ) === false ) {
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
$use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
|
||||||
|
$toggle_url = add_query_arg( 'react', $use_react ? '0' : '1' );
|
||||||
|
$toggle_text = $use_react ? 'Switch to Classic' : 'Try React (Beta)';
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'%s | <a href="%s">%s</a>',
|
||||||
|
$text,
|
||||||
|
esc_url( $toggle_url ),
|
||||||
|
esc_html( $toggle_text )
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,10 +94,21 @@ class Coupon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function formipay_coupon() {
|
public function formipay_coupon() {
|
||||||
\Formipay\Admin\ReactAdmin::render_mount_point('coupons');
|
// Coexistence mode: check query param or setting for React version
|
||||||
|
$use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
|
||||||
|
|
||||||
|
if ($use_react) {
|
||||||
|
// New React version
|
||||||
|
\Formipay\Admin\ReactAdmin::render_mount_point('coupons');
|
||||||
|
} else {
|
||||||
|
// Classic Grid.js version
|
||||||
|
include_once FORMIPAY_PATH . 'admin/page-coupons.php';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function enqueue_admin() {
|
public function enqueue_admin() {
|
||||||
|
// Assets now handled by ReactAdmin class
|
||||||
|
return;
|
||||||
|
|
||||||
global $current_screen;
|
global $current_screen;
|
||||||
|
|
||||||
@@ -568,7 +579,7 @@ class Coupon {
|
|||||||
|
|
||||||
public function formipay_tabledata_coupons() {
|
public function formipay_tabledata_coupons() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-coupon-page', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -676,7 +687,7 @@ class Coupon {
|
|||||||
|
|
||||||
public function formipay_coupon_get_products() {
|
public function formipay_coupon_get_products() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-coupon-page', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -710,7 +721,7 @@ class Coupon {
|
|||||||
|
|
||||||
public function formipay_create_coupon_post() {
|
public function formipay_create_coupon_post() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-coupon-page', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -745,7 +756,7 @@ class Coupon {
|
|||||||
|
|
||||||
public function formipay_delete_coupon() {
|
public function formipay_delete_coupon() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-coupon-page', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -773,7 +784,7 @@ class Coupon {
|
|||||||
|
|
||||||
public function formipay_bulk_delete_coupon() {
|
public function formipay_bulk_delete_coupon() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-coupon-page', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -820,7 +831,7 @@ class Coupon {
|
|||||||
|
|
||||||
public function formipay_duplicate_coupon() {
|
public function formipay_duplicate_coupon() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-coupon-page', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
|
|||||||
@@ -219,12 +219,21 @@ class Customer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function customers_page() {
|
public function customers_page() {
|
||||||
\Formipay\Admin\ReactAdmin::render_mount_point('customers');
|
// Coexistence mode: check query param or setting for React version
|
||||||
|
$use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
|
||||||
|
|
||||||
|
if ($use_react) {
|
||||||
|
// New React version
|
||||||
|
\Formipay\Admin\ReactAdmin::render_mount_point('customers');
|
||||||
|
} else {
|
||||||
|
// Classic Grid.js version
|
||||||
|
include_once FORMIPAY_PATH . 'admin/page-customers.php';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function formipay_tabledata_customers() {
|
public function formipay_tabledata_customers() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-access-nonce', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
|
|||||||
@@ -93,7 +93,16 @@ class Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function formipay_form() {
|
public function formipay_form() {
|
||||||
\Formipay\Admin\ReactAdmin::render_mount_point('forms');
|
// Coexistence mode: check query param or setting for React version
|
||||||
|
$use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
|
||||||
|
|
||||||
|
if ($use_react) {
|
||||||
|
// New React version
|
||||||
|
\Formipay\Admin\ReactAdmin::render_mount_point('forms');
|
||||||
|
} else {
|
||||||
|
// Classic Grid.js version
|
||||||
|
include_once FORMIPAY_PATH . 'admin/page-forms.php';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function metaboxes($post) {
|
public function metaboxes($post) {
|
||||||
@@ -1248,6 +1257,8 @@ class Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function enqueue_admin() {
|
public function enqueue_admin() {
|
||||||
|
// Assets now handled by ReactAdmin class
|
||||||
|
return;
|
||||||
global $current_screen, $post;
|
global $current_screen, $post;
|
||||||
|
|
||||||
// Check that we are on the 'Checker' post editor screen
|
// Check that we are on the 'Checker' post editor screen
|
||||||
@@ -1547,7 +1558,14 @@ class Form {
|
|||||||
|
|
||||||
public function formipay_tabledata_forms() {
|
public function formipay_tabledata_forms() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-post', '_wpnonce' );
|
error_log('[Formipay] formipay_tabledata_forms called');
|
||||||
|
|
||||||
|
$nonce_check = check_ajax_referer( 'formipay-admin', '_wpnonce', false );
|
||||||
|
error_log('[Formipay] Nonce check result: ' . ($nonce_check ? 'valid' : 'invalid'));
|
||||||
|
|
||||||
|
if ( ! $nonce_check ) {
|
||||||
|
wp_send_json_error( [ 'message' => 'Invalid nonce' ] );
|
||||||
|
}
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -1629,7 +1647,7 @@ class Form {
|
|||||||
|
|
||||||
public function formipay_create_form_post() {
|
public function formipay_create_form_post() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-post', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -1702,7 +1720,7 @@ class Form {
|
|||||||
|
|
||||||
public function formipay_delete_form() {
|
public function formipay_delete_form() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-post', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -1730,7 +1748,7 @@ class Form {
|
|||||||
|
|
||||||
public function formipay_bulk_delete_form() {
|
public function formipay_bulk_delete_form() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-post', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -1777,7 +1795,7 @@ class Form {
|
|||||||
|
|
||||||
public function formipay_duplicate_form() {
|
public function formipay_duplicate_form() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-admin-post', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
|
|||||||
@@ -70,7 +70,16 @@ class License {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function page_licenses() {
|
public function page_licenses() {
|
||||||
\Formipay\Admin\ReactAdmin::render_mount_point('licenses');
|
// Coexistence mode: check query param or setting for React version
|
||||||
|
$use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
|
||||||
|
|
||||||
|
if ($use_react) {
|
||||||
|
// New React version
|
||||||
|
\Formipay\Admin\ReactAdmin::render_mount_point('licenses');
|
||||||
|
} else {
|
||||||
|
// Classic Grid.js version
|
||||||
|
include_once FORMIPAY_PATH . 'admin/page-licenses.php';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Enqueue admin assets for Licenses page */
|
/** Enqueue admin assets for Licenses page */
|
||||||
@@ -121,7 +130,7 @@ class License {
|
|||||||
|
|
||||||
/** GridJS data source */
|
/** GridJS data source */
|
||||||
public function tabledata() {
|
public function tabledata() {
|
||||||
check_ajax_referer('formipay-admin-licenses', '_wpnonce');
|
check_ajax_referer('formipay-admin', '_wpnonce');
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -202,7 +211,7 @@ class License {
|
|||||||
|
|
||||||
/** Delete single license */
|
/** Delete single license */
|
||||||
public function delete() {
|
public function delete() {
|
||||||
check_ajax_referer('formipay-admin-licenses', '_wpnonce');
|
check_ajax_referer('formipay-admin', '_wpnonce');
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -238,7 +247,7 @@ class License {
|
|||||||
|
|
||||||
/** Bulk delete */
|
/** Bulk delete */
|
||||||
public function bulk_delete() {
|
public function bulk_delete() {
|
||||||
check_ajax_referer('formipay-admin-licenses', '_wpnonce');
|
check_ajax_referer('formipay-admin', '_wpnonce');
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
|
|||||||
@@ -644,91 +644,29 @@ class Order {
|
|||||||
$order_id = isset($_GET['order_id']) ? intval($_GET['order_id']) : 0;
|
$order_id = isset($_GET['order_id']) ? intval($_GET['order_id']) : 0;
|
||||||
$page = $order_id ? 'order-detail' : 'orders';
|
$page = $order_id ? 'order-detail' : 'orders';
|
||||||
|
|
||||||
// Render React mount point
|
// Coexistence mode: check query param or setting for React version
|
||||||
printf(
|
$use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
|
||||||
'<div id="formipay-admin-root" data-formipay-mount="%s"></div>',
|
|
||||||
esc_attr($page)
|
if ($use_react) {
|
||||||
);
|
// New React version
|
||||||
|
printf(
|
||||||
|
'<div id="formipay-admin-root" data-formipay-mount="%s"></div>',
|
||||||
|
esc_attr($page)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Classic Grid.js version
|
||||||
|
if ($order_id) {
|
||||||
|
include_once FORMIPAY_PATH . 'admin/page-order-details.php';
|
||||||
|
} else {
|
||||||
|
include_once FORMIPAY_PATH . 'admin/page-orders.php';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function enqueue() {
|
public function enqueue() {
|
||||||
|
// Assets now handled by ReactAdmin class
|
||||||
global $current_screen;
|
return;
|
||||||
|
|
||||||
if($current_screen->id == 'formipay_page_formipay-orders') {
|
|
||||||
|
|
||||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
||||||
$order_id = isset($_GET['order_id']) ? intval($_GET['order_id']) : 0;
|
|
||||||
if(empty($order_id)){
|
|
||||||
wp_enqueue_style( 'page-orders', FORMIPAY_URL . 'admin/assets/css/admin-orders.css', [], FORMIPAY_VERSION, 'all' );
|
|
||||||
wp_enqueue_script( 'page-orders', FORMIPAY_URL . 'admin/assets/js/admin-orders.js', ['jquery', 'gridjs'], FORMIPAY_VERSION, true );
|
|
||||||
|
|
||||||
wp_localize_script( 'page-orders', 'formipay_orders_page', [
|
|
||||||
'ajax_url' => admin_url('admin-ajax.php'),
|
|
||||||
'site_url' => site_url(),
|
|
||||||
'columns' => [
|
|
||||||
'id' => esc_html__( 'ID', 'formipay' ),
|
|
||||||
'form' => esc_html__( 'Form', 'formipay' ),
|
|
||||||
'total' => esc_html__( 'Total', 'formipay' ),
|
|
||||||
'date' => esc_html__( 'Date', 'formipay' ),
|
|
||||||
'payment_gateway' => esc_html__( 'Payment Gateway', 'formipay' ),
|
|
||||||
'status' => esc_html__( 'Status', 'formipay' ),
|
|
||||||
],
|
|
||||||
'filter_form' => [
|
|
||||||
'products' => [
|
|
||||||
'placeholder' => esc_html__( 'Filter by Product', 'formipay' ),
|
|
||||||
'noresult_text' => esc_html__( 'No results found', 'formipay' )
|
|
||||||
],
|
|
||||||
'currencies' => [
|
|
||||||
'placeholder' => esc_html__( 'Filter by Currency', 'formipay' ),
|
|
||||||
'noresult_text' => esc_html__( 'No results found', 'formipay' )
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'nonce' => wp_create_nonce( 'formipay-order-details' )
|
|
||||||
] );
|
|
||||||
}else{
|
|
||||||
|
|
||||||
wp_enqueue_style( 'bootstrap-icon', FORMIPAY_URL . 'vendor/Bootstrap/bootstrap-icons.css', [], '1.11.1', 'all');
|
|
||||||
wp_enqueue_style( 'bootstrap', FORMIPAY_URL . 'vendor/Bootstrap/bootstrap.min.css', [], '5.3.2' );
|
|
||||||
wp_enqueue_style( 'page-orders', FORMIPAY_URL . 'admin/assets/css/admin-order-details.css', [], FORMIPAY_VERSION, 'all' );
|
|
||||||
wp_enqueue_script( 'handlebars', FORMIPAY_URL . 'vendor/HandleBars/handlebars.min.js', [], '4.7.7', true);
|
|
||||||
wp_enqueue_script( 'bootstrap', FORMIPAY_URL . 'vendor/Bootstrap/bootstrap.bundle.min.js', ['jquery'], '5.3.2', true );
|
|
||||||
wp_enqueue_script( 'page-orders', FORMIPAY_URL . 'admin/assets/js/admin-order-details.js', ['jquery'], FORMIPAY_VERSION, true );
|
|
||||||
|
|
||||||
wp_localize_script( 'page-orders', 'formipay_order_details_page', [
|
|
||||||
'ajax_url' => admin_url('admin-ajax.php'),
|
|
||||||
'site_url' => site_url(),
|
|
||||||
'order_id' => $order_id,
|
|
||||||
'order_detail' => [
|
|
||||||
'change_order_status_confirmation' => esc_html__( 'Are you sure to change status?', 'formipay' ),
|
|
||||||
'change_order_status_button_confirm' => esc_html__( 'Change', 'formipay' ),
|
|
||||||
'change_order_status_button_cancel' => esc_html__( 'Cancel', 'formipay' ),
|
|
||||||
'edit_button_loading_text' => esc_html__( 'Preparing...', 'formipay' ),
|
|
||||||
'update_button_loading_text' => esc_html__( 'Updating...', 'formipay' ),
|
|
||||||
'pass_method' => [
|
|
||||||
'magic_link' => esc_html__( 'Magic Link', 'formipay' ),
|
|
||||||
'static_password' => esc_html__( 'Static Password', 'formipay' )
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'modal' => [
|
|
||||||
'delete' => [
|
|
||||||
'question' => esc_html__( 'Do you want to delete the order?', 'formipay' ),
|
|
||||||
'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
|
|
||||||
'confirmButton' => esc_html__( 'Delete Permanently', 'formipay' )
|
|
||||||
],
|
|
||||||
'bulk_delete' => [
|
|
||||||
'question' => esc_html__( 'Do you want to delete the selected the order(s)?', 'formipay' ),
|
|
||||||
'cancelButton' => esc_html__( 'Cancel', 'formipay' ),
|
|
||||||
'confirmButton' => esc_html__( 'Confirm', 'formipay' )
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'nonce' => wp_create_nonce( 'formipay-order-details' )
|
|
||||||
] );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function formipay_get_all_forms() {
|
public function formipay_get_all_forms() {
|
||||||
@@ -767,7 +705,7 @@ class Order {
|
|||||||
|
|
||||||
public function formipay_orders_get_choices() {
|
public function formipay_orders_get_choices() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-order-details', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -935,7 +873,7 @@ class Order {
|
|||||||
|
|
||||||
public function formipay_tabledata_orders() {
|
public function formipay_tabledata_orders() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-order-details', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -1038,7 +976,7 @@ class Order {
|
|||||||
|
|
||||||
public function formipay_delete_order() {
|
public function formipay_delete_order() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-order-details', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -1066,7 +1004,7 @@ class Order {
|
|||||||
|
|
||||||
public function formipay_bulk_delete_order() {
|
public function formipay_bulk_delete_order() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-order-details', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -1113,7 +1051,7 @@ class Order {
|
|||||||
|
|
||||||
public function formipay_load_order_data() {
|
public function formipay_load_order_data() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-order-details', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -1127,7 +1065,7 @@ class Order {
|
|||||||
|
|
||||||
public function formipay_change_order_status() {
|
public function formipay_change_order_status() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-order-details', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -1165,7 +1103,7 @@ class Order {
|
|||||||
|
|
||||||
public function formipay_check_editable_field() {
|
public function formipay_check_editable_field() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-order-details', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -1213,7 +1151,7 @@ class Order {
|
|||||||
|
|
||||||
public function formipay_update_editable_field_data() {
|
public function formipay_update_editable_field_data() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-order-details', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
@@ -1268,7 +1206,7 @@ class Order {
|
|||||||
|
|
||||||
public function formipay_update_digital_access() {
|
public function formipay_update_digital_access() {
|
||||||
|
|
||||||
check_ajax_referer( 'formipay-order-details', '_wpnonce' );
|
check_ajax_referer( 'formipay-admin', '_wpnonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
wp_send_json_error( [ 'message' => 'Unauthorized' ] );
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ class Product {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function enqueue_admin() {
|
public function enqueue_admin() {
|
||||||
|
// Assets now handled by ReactAdmin class
|
||||||
|
return;
|
||||||
|
|
||||||
global $current_screen;
|
global $current_screen;
|
||||||
|
|
||||||
@@ -228,7 +230,16 @@ class Product {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function formipay_products() {
|
public function formipay_products() {
|
||||||
\Formipay\Admin\ReactAdmin::render_mount_point('products');
|
// Coexistence mode: check query param or setting for React version
|
||||||
|
$use_react = isset($_GET['react']) || get_option('formipay_use_react_admin', false);
|
||||||
|
|
||||||
|
if ($use_react) {
|
||||||
|
// New React version
|
||||||
|
\Formipay\Admin\ReactAdmin::render_mount_point('products');
|
||||||
|
} else {
|
||||||
|
// Classic Grid.js version
|
||||||
|
include_once FORMIPAY_PATH . 'admin/page-products.php';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function cpt_post_fields_box($boxes) {
|
public function cpt_post_fields_box($boxes) {
|
||||||
|
|||||||
26
src/admin/components/shared/DataTable.css
Normal file
26
src/admin/components/shared/DataTable.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
.formipay-data-table-loading,
|
||||||
|
.formipay-data-table-empty {
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-data-table {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-data-table thead th {
|
||||||
|
padding: 12px 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-data-table tbody td {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-data-table tbody tr.is-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-data-table tbody tr.is-clickable:hover {
|
||||||
|
background-color: #f0f0f1;
|
||||||
|
}
|
||||||
57
src/admin/components/shared/DataTable.js
Normal file
57
src/admin/components/shared/DataTable.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Data Table - Simple table component for admin listings
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import './DataTable.css';
|
||||||
|
|
||||||
|
export default function DataTable({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
emptyMessage = __('No items found', 'formipay'),
|
||||||
|
onRowClick
|
||||||
|
}) {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="formipay-data-table-loading">
|
||||||
|
<span className="spinner is-active" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="formipay-data-table-empty">
|
||||||
|
<p>{ emptyMessage }</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="formipay-data-table wp-list-table widefat fixed striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<th key={column.key}>{column.label}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.map((row, rowIndex) => (
|
||||||
|
<tr
|
||||||
|
key={rowIndex}
|
||||||
|
onClick={onRowClick ? () => onRowClick(row) : undefined}
|
||||||
|
className={onRowClick ? 'is-clickable' : ''}
|
||||||
|
>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<td key={column.key}>
|
||||||
|
{column.render ? column.render(row) : row[column.key]}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
src/admin/pages/AdminPages.css
Normal file
45
src/admin/pages/AdminPages.css
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
.formipay-page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-page-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 23px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.status-publish,
|
||||||
|
.status-badge.status-active {
|
||||||
|
background-color: #edfaef;
|
||||||
|
color: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.status-draft,
|
||||||
|
.status-badge.status-inactive {
|
||||||
|
background-color: #f0f0f1;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.status-expired {
|
||||||
|
background-color: #f6f7f7;
|
||||||
|
color: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: #f0f0f1;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user