**Clarification: Level 1 Compatibility** Following ADDON_BRIDGE_PATTERN.md philosophy: **3-Level Compatibility Strategy:** Level 1: Native WP/WooCommerce Hooks 🟢 (THIS IMPLEMENTATION) - Community does NOTHING extra - Plugins use standard add_meta_box(), update_post_meta() - WooNooW listens and exposes data automatically - Status: ❌ NOT IMPLEMENTED - MUST DO NOW Level 2: Bridge Snippets 🟡 (Already documented) - For non-standard behavior (e.g., Rajaongkir custom UI) - Community creates simple bridge - WooNooW provides hook system + docs - Status: ✅ Hook system exists Level 3: Native WooNooW Addons 🔵 (Already documented) - Best experience, native integration - Community builds proper addons - Status: ✅ Addon system exists **Key Principle:** We are NOT asking community to create WooNooW-specific addons. We are asking them to use standard WooCommerce hooks. We LISTEN and INTEGRATE automatically. **Example (Level 1):** Plugin stores: update_post_meta($order_id, '_tracking_number', $value) WooNooW: Exposes via API automatically Result: Plugin works WITHOUT any extra effort **Updated METABOX_COMPAT.md:** - Added 3-level strategy overview - Clarified Level 1 is about listening to standard hooks - Emphasized community does NOTHING extra - Aligned with ADDON_BRIDGE_PATTERN.md philosophy **Confirmation:** ✅ Yes, we MUST implement Level 1 now ✅ This is about listening to WooCommerce bone ✅ Not about special integration ✅ Community uses standard hooks, we listen
16 KiB
WooNooW Metabox & Custom Fields Compatibility
Philosophy: 3-Level Compatibility Strategy
Following ADDON_BRIDGE_PATTERN.md, we support plugins at 3 levels:
Level 1: Native WP/WooCommerce Hooks 🟢 (THIS DOCUMENT)
Community does NOTHING extra - We listen automatically
- Plugins use standard
add_meta_box(),update_post_meta() - Store data in WooCommerce order/product meta
- WooNooW exposes this data via API automatically
- Status: ❌ NOT IMPLEMENTED - MUST DO NOW
Level 2: Bridge Snippets 🟡 (See ADDON_BRIDGE_PATTERN.md)
Community creates simple bridge - For non-standard behavior
- Plugins that bypass standard hooks (e.g., Rajaongkir custom UI)
- WooNooW provides hook system + documentation
- Community creates bridge snippets
- Status: ✅ Hook system exists, documentation provided
Level 3: Native WooNooW Addons 🔵 (See ADDON_BRIDGE_PATTERN.md)
Community builds proper addons - Best experience
- Native WooNooW integration
- Uses WooNooW addon system
- Independent plugins
- Status: ✅ Addon system exists, developer docs provided
Current Status: ❌ LEVEL 1 NOT IMPLEMENTED
Critical Gap: Our SPA admin does NOT currently expose custom meta fields from plugins that use standard WordPress/WooCommerce hooks.
Example Use Case (Level 1):
// Plugin: WooCommerce Shipment Tracking
// Uses STANDARD WooCommerce meta storage
// Plugin stores data (standard WooCommerce way)
update_post_meta($order_id, '_tracking_number', '1234567890');
update_post_meta($order_id, '_tracking_provider', 'JNE');
// Plugin displays in classic admin (standard metabox)
add_meta_box('wc_shipment_tracking', 'Tracking Info', function($post) {
$tracking = get_post_meta($post->ID, '_tracking_number', true);
echo '<input name="_tracking_number" value="' . esc_attr($tracking) . '">';
}, 'shop_order');
Current WooNooW Behavior:
- ❌ API doesn't expose
_tracking_numbermeta - ❌ Frontend can't read/write this data
- ❌ Plugin's data exists in DB but not accessible
Expected WooNooW Behavior (Level 1):
- ✅ API exposes
metaobject with all fields - ✅ Frontend can read/write meta data
- ✅ Plugin works WITHOUT any bridge/addon
- ✅ Community does NOTHING extra
Problem Analysis
1. Orders API (OrdersController.php)
Current Implementation:
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:
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():
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():
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():
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
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
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
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
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
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.tsxcomponent - Create
useMetaFields.tshook - Update
Orders/Edit.tsxto include meta fields - Update
Orders/View.tsxto display meta fields (read-only) - Update
Products/Edit.tsxto include meta fields - Add meta fields to Order/Product detail pages
Phase 3: Plugin Integration ✅
- Create
MetaFieldsRegistry.php - Add
woonoow/register_meta_fieldsaction - 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
// 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:
- Breaks compatibility with popular plugins (Shipment Tracking, ACF, etc.)
- Users cannot see/edit custom fields added by other plugins
- Data exists in database but not accessible in SPA admin
- 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)