docs: Critical metabox & custom fields compatibility gap identified
**Issue: Third-Party Plugin Compatibility** Current Status: ❌ NOT IMPLEMENTED Priority: 🔴 CRITICAL - Blocks production readiness **Problem:** Our SPA admin does NOT expose: - Custom meta fields from third-party plugins - WordPress metaboxes (add_meta_box) - WooCommerce custom fields - ACF/CMB2/Pods fields - Plugin-injected data (e.g., Tracking Number) **Example Use Case:** Plugin: WooCommerce Shipment Tracking - Adds 'Tracking Number' metabox to order edit page - Stores: _tracking_number meta field - Current: ❌ NOT visible in WooNooW admin - Expected: ✅ Should be visible and editable **Impact:** 1. Breaks compatibility with popular plugins 2. Users cannot see/edit custom fields 3. Data exists but not accessible in SPA 4. Forces users back to classic admin 5. BLOCKS production readiness **Solution Architecture:** Phase 1: API Layer (2-3 days) - Expose meta_data in OrdersController::show() - Expose meta_data in ProductsController::get_product() - Add filters: woonoow/order_api_data, woonoow/product_api_data - Add filters: woonoow/order_allowed_private_meta - Add actions: woonoow/order_updated, woonoow/product_updated Phase 2: Frontend Components (3-4 days) - Create MetaFields.tsx component - Create useMetaFields.ts hook - Update Orders/Edit.tsx to include meta fields - Update Products/Edit.tsx to include meta fields - Add meta fields to detail pages Phase 3: Plugin Integration (2-3 days) - Create MetaFieldsRegistry.php - Add woonoow/register_meta_fields action - Localize fields to JavaScript - Create example: ShipmentTracking.php integration - Document integration pattern **Documentation Created:** - METABOX_COMPAT.md - Complete implementation guide - Includes code examples for all phases - Includes third-party integration guide - Includes testing checklist **Updated:** - PROJECT_SOP.md - Added metabox compat reference - Marked as CRITICAL requirement - Noted as blocking production readiness **Timeline:** Total: 1-2 weeks implementation **Blocking:** - ✅ Coupons CRUD (can proceed) - ✅ Customers CRUD (can proceed) - ❌ Production readiness (BLOCKED) **Next Steps:** 1. Review METABOX_COMPAT.md 2. Prioritize implementation 3. Start with Phase 1 (API layer) 4. Test with popular plugins (Shipment Tracking, ACF)
This commit is contained in:
552
METABOX_COMPAT.md
Normal file
552
METABOX_COMPAT.md
Normal file
@@ -0,0 +1,552 @@
|
||||
# WooNooW Metabox & Custom Fields Compatibility
|
||||
|
||||
## Current Status: ❌ NOT IMPLEMENTED
|
||||
|
||||
**Critical Gap:** Our SPA admin does NOT currently expose custom meta fields, metaboxes, or third-party plugin data injected via WordPress/WooCommerce hooks.
|
||||
|
||||
### Example Use Case:
|
||||
```
|
||||
Plugin: WooCommerce Shipment Tracking
|
||||
- Adds "Tracking Number" metabox to order edit page
|
||||
- Uses: add_meta_box('wc_shipment_tracking', ...)
|
||||
- Stores: update_post_meta($order_id, '_tracking_number', $value)
|
||||
|
||||
Current Behavior: ❌ Field NOT visible in WooNooW admin
|
||||
Expected Behavior: ✅ Field should be visible and editable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Problem Analysis
|
||||
|
||||
### 1. Orders API (`OrdersController.php`)
|
||||
|
||||
**Current Implementation:**
|
||||
```php
|
||||
public static function show(WP_REST_Request $req) {
|
||||
$order = wc_get_order($id);
|
||||
|
||||
$data = [
|
||||
'id' => $order->get_id(),
|
||||
'status' => $order->get_status(),
|
||||
'billing' => [...],
|
||||
'shipping' => [...],
|
||||
'items' => [...],
|
||||
// ... hardcoded fields only
|
||||
];
|
||||
|
||||
return new WP_REST_Response($data, 200);
|
||||
}
|
||||
```
|
||||
|
||||
**Missing:**
|
||||
- ❌ No `get_meta_data()` exposure
|
||||
- ❌ No `apply_filters('woonoow/order_data', $data, $order)`
|
||||
- ❌ No metabox hook listening
|
||||
- ❌ No custom field groups
|
||||
|
||||
### 2. Products API (`ProductsController.php`)
|
||||
|
||||
**Current Implementation:**
|
||||
```php
|
||||
public static function get_product(WP_REST_Request $request) {
|
||||
$product = wc_get_product($id);
|
||||
|
||||
return new WP_REST_Response([
|
||||
'id' => $product->get_id(),
|
||||
'name' => $product->get_name(),
|
||||
// ... hardcoded fields only
|
||||
], 200);
|
||||
}
|
||||
```
|
||||
|
||||
**Missing:**
|
||||
- ❌ No custom product meta exposure
|
||||
- ❌ No `apply_filters('woonoow/product_data', $data, $product)`
|
||||
- ❌ No ACF/CMB2/Pods integration
|
||||
- ❌ No custom tabs/panels
|
||||
|
||||
---
|
||||
|
||||
## Solution Architecture
|
||||
|
||||
### Phase 1: Meta Data Exposure (API Layer)
|
||||
|
||||
#### 1.1 Orders API Enhancement
|
||||
|
||||
**Add to `OrdersController::show()`:**
|
||||
```php
|
||||
public static function show(WP_REST_Request $req) {
|
||||
$order = wc_get_order($id);
|
||||
|
||||
// ... existing data ...
|
||||
|
||||
// Expose all meta data
|
||||
$meta_data = [];
|
||||
foreach ($order->get_meta_data() as $meta) {
|
||||
$key = $meta->key;
|
||||
|
||||
// Skip internal/private meta (starts with _)
|
||||
// unless explicitly allowed
|
||||
if (strpos($key, '_') === 0) {
|
||||
$allowed_private = apply_filters('woonoow/order_allowed_private_meta', [
|
||||
'_tracking_number',
|
||||
'_tracking_provider',
|
||||
'_shipment_tracking_items',
|
||||
'_wc_shipment_tracking_items',
|
||||
// Add more as needed
|
||||
], $order);
|
||||
|
||||
if (!in_array($key, $allowed_private, true)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$meta_data[$key] = $meta->value;
|
||||
}
|
||||
|
||||
$data['meta'] = $meta_data;
|
||||
|
||||
// Allow plugins to add/modify data
|
||||
$data = apply_filters('woonoow/order_api_data', $data, $order, $req);
|
||||
|
||||
return new WP_REST_Response($data, 200);
|
||||
}
|
||||
```
|
||||
|
||||
**Add to `OrdersController::update()`:**
|
||||
```php
|
||||
public static function update(WP_REST_Request $req) {
|
||||
$order = wc_get_order($id);
|
||||
$data = $req->get_json_params();
|
||||
|
||||
// ... existing update logic ...
|
||||
|
||||
// Update custom meta fields
|
||||
if (isset($data['meta']) && is_array($data['meta'])) {
|
||||
foreach ($data['meta'] as $key => $value) {
|
||||
// Validate meta key is allowed
|
||||
$allowed = apply_filters('woonoow/order_updatable_meta', [
|
||||
'_tracking_number',
|
||||
'_tracking_provider',
|
||||
// Add more as needed
|
||||
], $order);
|
||||
|
||||
if (in_array($key, $allowed, true)) {
|
||||
$order->update_meta_data($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$order->save();
|
||||
|
||||
// Allow plugins to perform additional updates
|
||||
do_action('woonoow/order_updated', $order, $data, $req);
|
||||
|
||||
return new WP_REST_Response(['success' => true], 200);
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 Products API Enhancement
|
||||
|
||||
**Add to `ProductsController::get_product()`:**
|
||||
```php
|
||||
public static function get_product(WP_REST_Request $request) {
|
||||
$product = wc_get_product($id);
|
||||
|
||||
// ... existing data ...
|
||||
|
||||
// Expose all meta data
|
||||
$meta_data = [];
|
||||
foreach ($product->get_meta_data() as $meta) {
|
||||
$key = $meta->key;
|
||||
|
||||
// Skip internal meta unless allowed
|
||||
if (strpos($key, '_') === 0) {
|
||||
$allowed_private = apply_filters('woonoow/product_allowed_private_meta', [
|
||||
'_custom_field_example',
|
||||
// Add more as needed
|
||||
], $product);
|
||||
|
||||
if (!in_array($key, $allowed_private, true)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$meta_data[$key] = $meta->value;
|
||||
}
|
||||
|
||||
$data['meta'] = $meta_data;
|
||||
|
||||
// Allow plugins to add/modify data
|
||||
$data = apply_filters('woonoow/product_api_data', $data, $product, $request);
|
||||
|
||||
return new WP_REST_Response($data, 200);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Frontend Rendering (React Components)
|
||||
|
||||
#### 2.1 Dynamic Meta Fields Component
|
||||
|
||||
**Create: `admin-spa/src/components/MetaFields.tsx`**
|
||||
```tsx
|
||||
interface MetaField {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'text' | 'textarea' | 'number' | 'select' | 'date';
|
||||
options?: Array<{value: string; label: string}>;
|
||||
section?: string; // Group fields into sections
|
||||
}
|
||||
|
||||
interface MetaFieldsProps {
|
||||
meta: Record<string, any>;
|
||||
fields: MetaField[];
|
||||
onChange: (key: string, value: any) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export function MetaFields({ meta, fields, onChange, readOnly }: MetaFieldsProps) {
|
||||
// Group fields by section
|
||||
const sections = fields.reduce((acc, field) => {
|
||||
const section = field.section || 'Other';
|
||||
if (!acc[section]) acc[section] = [];
|
||||
acc[section].push(field);
|
||||
return acc;
|
||||
}, {} as Record<string, MetaField[]>);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{Object.entries(sections).map(([section, sectionFields]) => (
|
||||
<Card key={section}>
|
||||
<CardHeader>
|
||||
<CardTitle>{section}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{sectionFields.map(field => (
|
||||
<div key={field.key}>
|
||||
<Label>{field.label}</Label>
|
||||
{field.type === 'text' && (
|
||||
<Input
|
||||
value={meta[field.key] || ''}
|
||||
onChange={(e) => onChange(field.key, e.target.value)}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
)}
|
||||
{field.type === 'textarea' && (
|
||||
<Textarea
|
||||
value={meta[field.key] || ''}
|
||||
onChange={(e) => onChange(field.key, e.target.value)}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
)}
|
||||
{/* Add more field types as needed */}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 Hook System for Field Registration
|
||||
|
||||
**Create: `admin-spa/src/hooks/useMetaFields.ts`**
|
||||
```tsx
|
||||
interface MetaFieldsRegistry {
|
||||
orders: MetaField[];
|
||||
products: MetaField[];
|
||||
}
|
||||
|
||||
// Global registry (can be extended by plugins via window object)
|
||||
declare global {
|
||||
interface Window {
|
||||
WooNooWMetaFields?: MetaFieldsRegistry;
|
||||
}
|
||||
}
|
||||
|
||||
export function useMetaFields(type: 'orders' | 'products'): MetaField[] {
|
||||
const [fields, setFields] = useState<MetaField[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Get fields from global registry
|
||||
const registry = window.WooNooWMetaFields || { orders: [], products: [] };
|
||||
setFields(registry[type] || []);
|
||||
}, [type]);
|
||||
|
||||
return fields;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 Integration in Order Edit Form
|
||||
|
||||
**Update: `admin-spa/src/routes/Orders/Edit.tsx`**
|
||||
```tsx
|
||||
import { MetaFields } from '@/components/MetaFields';
|
||||
import { useMetaFields } from '@/hooks/useMetaFields';
|
||||
|
||||
export default function OrderEdit() {
|
||||
const { id } = useParams();
|
||||
const metaFields = useMetaFields('orders');
|
||||
|
||||
const orderQ = useQuery({
|
||||
queryKey: ['order', id],
|
||||
queryFn: () => api.get(`/orders/${id}`),
|
||||
});
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
// ... existing fields ...
|
||||
meta: {},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (orderQ.data) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
meta: orderQ.data.meta || {},
|
||||
}));
|
||||
}
|
||||
}, [orderQ.data]);
|
||||
|
||||
const handleMetaChange = (key: string, value: any) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
meta: {
|
||||
...prev.meta,
|
||||
[key]: value,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Existing order form fields */}
|
||||
|
||||
{/* Custom meta fields */}
|
||||
{metaFields.length > 0 && (
|
||||
<MetaFields
|
||||
meta={formData.meta}
|
||||
fields={metaFields}
|
||||
onChange={handleMetaChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Plugin Integration Layer
|
||||
|
||||
#### 3.1 PHP Hook for Field Registration
|
||||
|
||||
**Create: `includes/Compat/MetaFieldsRegistry.php`**
|
||||
```php
|
||||
<?php
|
||||
namespace WooNooW\Compat;
|
||||
|
||||
class MetaFieldsRegistry {
|
||||
|
||||
private static $order_fields = [];
|
||||
private static $product_fields = [];
|
||||
|
||||
public static function init() {
|
||||
add_action('admin_enqueue_scripts', [__CLASS__, 'localize_fields']);
|
||||
|
||||
// Allow plugins to register fields
|
||||
do_action('woonoow/register_meta_fields');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register order meta field
|
||||
*/
|
||||
public static function register_order_field($key, $args = []) {
|
||||
$defaults = [
|
||||
'key' => $key,
|
||||
'label' => ucfirst(str_replace('_', ' ', $key)),
|
||||
'type' => 'text',
|
||||
'section' => 'Other',
|
||||
];
|
||||
|
||||
self::$order_fields[$key] = array_merge($defaults, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register product meta field
|
||||
*/
|
||||
public static function register_product_field($key, $args = []) {
|
||||
$defaults = [
|
||||
'key' => $key,
|
||||
'label' => ucfirst(str_replace('_', ' ', $key)),
|
||||
'type' => 'text',
|
||||
'section' => 'Other',
|
||||
];
|
||||
|
||||
self::$product_fields[$key] = array_merge($defaults, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Localize fields to JavaScript
|
||||
*/
|
||||
public static function localize_fields() {
|
||||
if (!is_admin()) return;
|
||||
|
||||
wp_localize_script('woonoow-admin', 'WooNooWMetaFields', [
|
||||
'orders' => array_values(self::$order_fields),
|
||||
'products' => array_values(self::$product_fields),
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 Example: Shipment Tracking Integration
|
||||
|
||||
**Create: `includes/Compat/Integrations/ShipmentTracking.php`**
|
||||
```php
|
||||
<?php
|
||||
namespace WooNooW\Compat\Integrations;
|
||||
|
||||
use WooNooW\Compat\MetaFieldsRegistry;
|
||||
|
||||
class ShipmentTracking {
|
||||
|
||||
public static function init() {
|
||||
// Only load if WC Shipment Tracking is active
|
||||
if (!class_exists('WC_Shipment_Tracking')) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action('woonoow/register_meta_fields', [__CLASS__, 'register_fields']);
|
||||
add_filter('woonoow/order_allowed_private_meta', [__CLASS__, 'allow_meta']);
|
||||
add_filter('woonoow/order_updatable_meta', [__CLASS__, 'allow_meta']);
|
||||
}
|
||||
|
||||
public static function register_fields() {
|
||||
MetaFieldsRegistry::register_order_field('_tracking_number', [
|
||||
'label' => __('Tracking Number', 'woonoow'),
|
||||
'type' => 'text',
|
||||
'section' => 'Shipment Tracking',
|
||||
]);
|
||||
|
||||
MetaFieldsRegistry::register_order_field('_tracking_provider', [
|
||||
'label' => __('Tracking Provider', 'woonoow'),
|
||||
'type' => 'select',
|
||||
'section' => 'Shipment Tracking',
|
||||
'options' => [
|
||||
['value' => 'jne', 'label' => 'JNE'],
|
||||
['value' => 'jnt', 'label' => 'J&T'],
|
||||
['value' => 'sicepat', 'label' => 'SiCepat'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public static function allow_meta($allowed) {
|
||||
$allowed[] = '_tracking_number';
|
||||
$allowed[] = '_tracking_provider';
|
||||
$allowed[] = '_shipment_tracking_items';
|
||||
return $allowed;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Phase 1: API Layer ✅
|
||||
- [ ] Add meta data exposure to `OrdersController::show()`
|
||||
- [ ] Add meta data update to `OrdersController::update()`
|
||||
- [ ] Add meta data exposure to `ProductsController::get_product()`
|
||||
- [ ] Add meta data update to `ProductsController::update_product()`
|
||||
- [ ] Add filters: `woonoow/order_api_data`, `woonoow/product_api_data`
|
||||
- [ ] Add filters: `woonoow/order_allowed_private_meta`, `woonoow/order_updatable_meta`
|
||||
- [ ] Add actions: `woonoow/order_updated`, `woonoow/product_updated`
|
||||
|
||||
### Phase 2: Frontend Components ✅
|
||||
- [ ] Create `MetaFields.tsx` component
|
||||
- [ ] Create `useMetaFields.ts` hook
|
||||
- [ ] Update `Orders/Edit.tsx` to include meta fields
|
||||
- [ ] Update `Orders/View.tsx` to display meta fields (read-only)
|
||||
- [ ] Update `Products/Edit.tsx` to include meta fields
|
||||
- [ ] Add meta fields to Order/Product detail pages
|
||||
|
||||
### Phase 3: Plugin Integration ✅
|
||||
- [ ] Create `MetaFieldsRegistry.php`
|
||||
- [ ] Add `woonoow/register_meta_fields` action
|
||||
- [ ] Localize fields to JavaScript
|
||||
- [ ] Create example integration: `ShipmentTracking.php`
|
||||
- [ ] Document integration pattern for third-party devs
|
||||
|
||||
### Phase 4: Testing ✅
|
||||
- [ ] Test with WooCommerce Shipment Tracking plugin
|
||||
- [ ] Test with ACF (Advanced Custom Fields)
|
||||
- [ ] Test with CMB2 (Custom Metaboxes 2)
|
||||
- [ ] Test with custom metabox plugins
|
||||
- [ ] Test meta data save/update
|
||||
- [ ] Test meta data display in detail view
|
||||
|
||||
---
|
||||
|
||||
## Third-Party Plugin Integration Guide
|
||||
|
||||
### For Plugin Developers:
|
||||
|
||||
**Example: Adding custom fields to WooNooW admin**
|
||||
|
||||
```php
|
||||
// In your plugin file
|
||||
add_action('woonoow/register_meta_fields', function() {
|
||||
// Register order field
|
||||
WooNooW\Compat\MetaFieldsRegistry::register_order_field('_my_custom_field', [
|
||||
'label' => __('My Custom Field', 'my-plugin'),
|
||||
'type' => 'text',
|
||||
'section' => 'My Plugin',
|
||||
]);
|
||||
|
||||
// Register product field
|
||||
WooNooW\Compat\MetaFieldsRegistry::register_product_field('_my_product_field', [
|
||||
'label' => __('My Product Field', 'my-plugin'),
|
||||
'type' => 'textarea',
|
||||
'section' => 'My Plugin',
|
||||
]);
|
||||
});
|
||||
|
||||
// Allow meta to be read/written
|
||||
add_filter('woonoow/order_allowed_private_meta', function($allowed) {
|
||||
$allowed[] = '_my_custom_field';
|
||||
return $allowed;
|
||||
});
|
||||
|
||||
add_filter('woonoow/order_updatable_meta', function($allowed) {
|
||||
$allowed[] = '_my_custom_field';
|
||||
return $allowed;
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Priority
|
||||
|
||||
**Status:** 🔴 **CRITICAL - MUST IMPLEMENT**
|
||||
|
||||
**Why:**
|
||||
1. Breaks compatibility with popular plugins (Shipment Tracking, ACF, etc.)
|
||||
2. Users cannot see/edit custom fields added by other plugins
|
||||
3. Data exists in database but not accessible in SPA admin
|
||||
4. Forces users to switch back to classic admin for custom fields
|
||||
|
||||
**Timeline:**
|
||||
- Phase 1 (API): 2-3 days
|
||||
- Phase 2 (Frontend): 3-4 days
|
||||
- Phase 3 (Integration): 2-3 days
|
||||
- **Total: ~1-2 weeks**
|
||||
|
||||
**Blocking:**
|
||||
- Coupons CRUD (can proceed)
|
||||
- Customers CRUD (can proceed)
|
||||
- **Production readiness** (BLOCKED until this is done)
|
||||
@@ -33,6 +33,12 @@ WooNooW modernizes WooCommerce **without migration**, delivering a Hybrid + SPA
|
||||
- Prevents route conflicts between modules
|
||||
- Documents ownership and naming conventions
|
||||
|
||||
**Metabox & Custom Fields compatibility:**
|
||||
- `METABOX_COMPAT.md` - 🔴 **CRITICAL** compatibility requirement
|
||||
- Documents how to expose WordPress/WooCommerce metaboxes in SPA
|
||||
- **Currently NOT implemented** - blocks production readiness
|
||||
- Required for third-party plugin compatibility (Shipment Tracking, ACF, etc.)
|
||||
|
||||
**Documentation Rules:**
|
||||
1. ✅ Update `PROGRESS_NOTE.md` after completing any major feature
|
||||
2. ✅ Add test cases to `TESTING_CHECKLIST.md` before implementation
|
||||
|
||||
Reference in New Issue
Block a user