**Implementation Plan Created: IMPLEMENTATION_PLAN_META_COMPAT.md** Following all documentation guidelines: - ADDON_BRIDGE_PATTERN.md (3-level strategy) - ADDON_DEVELOPMENT_GUIDE.md (hook system) - ADDON_REACT_INTEGRATION.md (React exposure) - METABOX_COMPAT.md (compatibility requirements) **Key Principles:** 1. ✅ Zero addon dependencies in core 2. ✅ Listen to WP/WooCommerce hooks (NOT WooNooW-specific) 3. ✅ Community does NOTHING extra 4. ❌ Do NOT support specific plugins 5. ❌ Do NOT integrate plugins into core **3 Phases:** Phase 1: Backend API Enhancement (2-3 days) - Add get_order_meta_data() / get_product_meta_data() - Add update_order_meta_data() / update_product_meta_data() - Expose meta in API responses - Add filters: woonoow/order_allowed_private_meta - Add filters: woonoow/order_updatable_meta - Add filters: woonoow/order_api_data - Add actions: woonoow/order_updated Phase 2: Frontend Components (3-4 days) - Create MetaFields.tsx component (generic field renderer) - Create useMetaFields.ts hook (registry access) - Update Orders/Edit.tsx to include meta fields - Update Products/Edit.tsx to include meta fields - Support all field types: text, textarea, number, select, checkbox Phase 3: PHP Registry System (2-3 days) - Create MetaFieldsRegistry.php - Add action: woonoow/register_meta_fields - Auto-register fields to allowed meta lists - Localize to JavaScript (window.WooNooWMetaFields) - Initialize in Plugin.php **Testing Plan:** - WooCommerce Shipment Tracking plugin - Advanced Custom Fields (ACF) - Custom metabox plugins - Meta data save/update - Field registration **Timeline:** 8-12 days (1.5-2 weeks) **Success Criteria:** ✅ Plugins using standard WP/WooCommerce meta work automatically ✅ No special integration needed ✅ Meta fields visible and editable ✅ Zero coupling with specific plugins ✅ Community does NOTHING extra Ready to start implementation!
18 KiB
Implementation Plan: Level 1 Meta Compatibility
Objective
Make WooNooW listen to ALL standard WordPress/WooCommerce hooks for custom meta fields automatically.
Principles (From Documentation Review)
From ADDON_BRIDGE_PATTERN.md:
- ✅ WooNooW Core = Zero addon dependencies
- ✅ We listen to WP/WooCommerce hooks (NOT WooNooW-specific)
- ✅ Community does NOTHING extra
- ❌ We do NOT support specific plugins
- ❌ We do NOT integrate plugins into core
From ADDON_DEVELOPMENT_GUIDE.md:
- ✅ Hook system for functional extensions
- ✅ Zero coupling with core
- ✅ WordPress-style filters and actions
From ADDON_REACT_INTEGRATION.md:
- ✅ Expose React runtime on window
- ✅ Support vanilla JS/jQuery addons
- ✅ No build process required for simple addons
Implementation Strategy
Phase 1: Backend API Enhancement (2-3 days)
1.1 OrdersController - Expose Meta Data
File: includes/Api/OrdersController.php
Changes:
public static function show(WP_REST_Request $req) {
$order = wc_get_order($id);
// ... existing data ...
// Expose meta data (Level 1 compatibility)
$meta_data = self::get_order_meta_data($order);
$data['meta'] = $meta_data;
// Allow plugins to modify response
$data = apply_filters('woonoow/order_api_data', $data, $order, $req);
return new WP_REST_Response($data, 200);
}
/**
* Get order meta data for API exposure
* Filters out internal meta unless explicitly allowed
*/
private static function get_order_meta_data($order) {
$meta_data = [];
foreach ($order->get_meta_data() as $meta) {
$key = $meta->key;
$value = $meta->value;
// Skip internal WooCommerce meta (starts with _wc_)
if (strpos($key, '_wc_') === 0) {
continue;
}
// Public meta (no underscore) - always expose
if (strpos($key, '_') !== 0) {
$meta_data[$key] = $value;
continue;
}
// Private meta (starts with _) - check if allowed
$allowed_private = apply_filters('woonoow/order_allowed_private_meta', [
// Common shipping tracking fields
'_tracking_number',
'_tracking_provider',
'_tracking_url',
'_shipment_tracking_items',
'_wc_shipment_tracking_items',
// Allow plugins to add their meta
], $order);
if (in_array($key, $allowed_private, true)) {
$meta_data[$key] = $value;
}
}
return $meta_data;
}
Update Method:
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 (Level 1 compatibility)
if (isset($data['meta']) && is_array($data['meta'])) {
self::update_order_meta_data($order, $data['meta']);
}
$order->save();
// Allow plugins to perform additional updates
do_action('woonoow/order_updated', $order, $data, $req);
return new WP_REST_Response(['success' => true], 200);
}
/**
* Update order meta data from API
*/
private static function update_order_meta_data($order, $meta_updates) {
// Get allowed updatable meta keys
$allowed = apply_filters('woonoow/order_updatable_meta', [
'_tracking_number',
'_tracking_provider',
'_tracking_url',
// Allow plugins to add their meta
], $order);
foreach ($meta_updates as $key => $value) {
// Public meta (no underscore) - always allow
if (strpos($key, '_') !== 0) {
$order->update_meta_data($key, $value);
continue;
}
// Private meta - check if allowed
if (in_array($key, $allowed, true)) {
$order->update_meta_data($key, $value);
}
}
}
1.2 ProductsController - Expose Meta Data
File: includes/Api/ProductsController.php
Changes: (Same pattern as OrdersController)
public static function get_product(WP_REST_Request $request) {
$product = wc_get_product($id);
// ... existing data ...
// Expose meta data (Level 1 compatibility)
$meta_data = self::get_product_meta_data($product);
$data['meta'] = $meta_data;
// Allow plugins to modify response
$data = apply_filters('woonoow/product_api_data', $data, $product, $request);
return new WP_REST_Response($data, 200);
}
private static function get_product_meta_data($product) {
// Same logic as orders
}
public static function update_product(WP_REST_Request $request) {
// ... existing logic ...
if (isset($data['meta']) && is_array($data['meta'])) {
self::update_product_meta_data($product, $data['meta']);
}
do_action('woonoow/product_updated', $product, $data, $request);
}
Phase 2: Frontend Components (3-4 days)
2.1 MetaFields Component
File: admin-spa/src/components/MetaFields.tsx
Purpose: Generic component to display/edit meta fields
interface MetaField {
key: string;
label: string;
type: 'text' | 'textarea' | 'number' | 'select' | 'date' | 'checkbox';
options?: Array<{value: string; label: string}>;
section?: string;
description?: string;
placeholder?: string;
}
interface MetaFieldsProps {
meta: Record<string, any>;
fields: MetaField[];
onChange: (key: string, value: any) => void;
readOnly?: boolean;
}
export function MetaFields({ meta, fields, onChange, readOnly }: MetaFieldsProps) {
if (fields.length === 0) return null;
// Group fields by section
const sections = fields.reduce((acc, field) => {
const section = field.section || 'Additional Fields';
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 htmlFor={field.key}>
{field.label}
{field.description && (
<span className="text-xs text-muted-foreground ml-2">
{field.description}
</span>
)}
</Label>
{field.type === 'text' && (
<Input
id={field.key}
value={meta[field.key] || ''}
onChange={(e) => onChange(field.key, e.target.value)}
disabled={readOnly}
placeholder={field.placeholder}
/>
)}
{field.type === 'textarea' && (
<Textarea
id={field.key}
value={meta[field.key] || ''}
onChange={(e) => onChange(field.key, e.target.value)}
disabled={readOnly}
placeholder={field.placeholder}
rows={4}
/>
)}
{field.type === 'number' && (
<Input
id={field.key}
type="number"
value={meta[field.key] || ''}
onChange={(e) => onChange(field.key, e.target.value)}
disabled={readOnly}
placeholder={field.placeholder}
/>
)}
{field.type === 'select' && field.options && (
<Select
value={meta[field.key] || ''}
onValueChange={(value) => onChange(field.key, value)}
disabled={readOnly}
>
<SelectTrigger id={field.key}>
<SelectValue placeholder={field.placeholder || 'Select...'} />
</SelectTrigger>
<SelectContent>
{field.options.map(opt => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{field.type === 'checkbox' && (
<div className="flex items-center space-x-2">
<Checkbox
id={field.key}
checked={!!meta[field.key]}
onCheckedChange={(checked) => onChange(field.key, checked)}
disabled={readOnly}
/>
<label htmlFor={field.key} className="text-sm cursor-pointer">
{field.placeholder || 'Enable'}
</label>
</div>
)}
</div>
))}
</CardContent>
</Card>
))}
</div>
);
}
2.2 useMetaFields Hook
File: admin-spa/src/hooks/useMetaFields.ts
Purpose: Hook to get registered meta fields from global registry
interface MetaFieldsRegistry {
orders: MetaField[];
products: MetaField[];
}
// Global registry exposed by PHP
declare global {
interface Window {
WooNooWMetaFields?: MetaFieldsRegistry;
}
}
export function useMetaFields(type: 'orders' | 'products'): MetaField[] {
const [fields, setFields] = useState<MetaField[]>([]);
useEffect(() => {
// Get fields from global registry (set by PHP)
const registry = window.WooNooWMetaFields || { orders: [], products: [] };
setFields(registry[type] || []);
// Listen for dynamic field registration
const handleFieldsUpdated = (e: CustomEvent) => {
if (e.detail.type === type) {
setFields(e.detail.fields);
}
};
window.addEventListener('woonoow:meta_fields_updated', handleFieldsUpdated as EventListener);
return () => {
window.removeEventListener('woonoow:meta_fields_updated', handleFieldsUpdated as EventListener);
};
}, [type]);
return fields;
}
2.3 Integration in Order Edit
File: 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 [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 className="space-y-6">
{/* Existing order form fields */}
<OrderForm data={formData} onChange={setFormData} />
{/* Custom meta fields (Level 1 compatibility) */}
{metaFields.length > 0 && (
<MetaFields
meta={formData.meta}
fields={metaFields}
onChange={handleMetaChange}
/>
)}
</div>
);
}
Phase 3: PHP Registry System (2-3 days)
3.1 MetaFieldsRegistry Class
File: includes/Compat/MetaFieldsRegistry.php
Purpose: Allow plugins to register meta fields for display in SPA
<?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
*
* @param string $key Meta key (e.g., '_tracking_number')
* @param array $args Field configuration
*/
public static function register_order_field($key, $args = []) {
$defaults = [
'key' => $key,
'label' => self::format_label($key),
'type' => 'text',
'section' => 'Additional Fields',
'description' => '',
'placeholder' => '',
];
self::$order_fields[$key] = array_merge($defaults, $args);
// Auto-add to allowed meta lists
add_filter('woonoow/order_allowed_private_meta', function($allowed) use ($key) {
if (!in_array($key, $allowed, true)) {
$allowed[] = $key;
}
return $allowed;
});
add_filter('woonoow/order_updatable_meta', function($allowed) use ($key) {
if (!in_array($key, $allowed, true)) {
$allowed[] = $key;
}
return $allowed;
});
}
/**
* Register product meta field
*/
public static function register_product_field($key, $args = []) {
$defaults = [
'key' => $key,
'label' => self::format_label($key),
'type' => 'text',
'section' => 'Additional Fields',
'description' => '',
'placeholder' => '',
];
self::$product_fields[$key] = array_merge($defaults, $args);
// Auto-add to allowed meta lists
add_filter('woonoow/product_allowed_private_meta', function($allowed) use ($key) {
if (!in_array($key, $allowed, true)) {
$allowed[] = $key;
}
return $allowed;
});
add_filter('woonoow/product_updatable_meta', function($allowed) use ($key) {
if (!in_array($key, $allowed, true)) {
$allowed[] = $key;
}
return $allowed;
});
}
/**
* Format meta key to human-readable label
*/
private static function format_label($key) {
// Remove leading underscore
$label = ltrim($key, '_');
// Replace underscores with spaces
$label = str_replace('_', ' ', $label);
// Capitalize words
$label = ucwords($label);
return $label;
}
/**
* Localize fields to JavaScript
*/
public static function localize_fields() {
if (!is_admin()) return;
// Allow plugins to modify fields before localizing
$order_fields = apply_filters('woonoow/meta_fields_orders', array_values(self::$order_fields));
$product_fields = apply_filters('woonoow/meta_fields_products', array_values(self::$product_fields));
wp_localize_script('woonoow-admin', 'WooNooWMetaFields', [
'orders' => $order_fields,
'products' => $product_fields,
]);
}
}
3.2 Initialize Registry
File: includes/Core/Plugin.php
// Add to init() method
\WooNooW\Compat\MetaFieldsRegistry::init();
Testing Plan
Test Case 1: WooCommerce Shipment Tracking
// Plugin stores tracking number
update_post_meta($order_id, '_tracking_number', '1234567890');
// Expected: Field visible in WooNooW order edit
// Expected: Can edit and save tracking number
Test Case 2: Advanced Custom Fields (ACF)
// ACF stores custom field
update_post_meta($product_id, 'custom_field', 'value');
// Expected: Field visible in WooNooW product edit
// Expected: Can edit and save custom field
Test Case 3: Custom Metabox Plugin
// Plugin registers field
add_action('woonoow/register_meta_fields', function() {
\WooNooW\Compat\MetaFieldsRegistry::register_order_field('_custom_field', [
'label' => 'Custom Field',
'type' => 'text',
'section' => 'My Plugin',
]);
});
// Expected: Field appears in "My Plugin" section
// Expected: Can edit and save
Implementation Checklist
Backend (PHP)
- Add
get_order_meta_data()to OrdersController - Add
update_order_meta_data()to OrdersController - Add
get_product_meta_data()to ProductsController - Add
update_product_meta_data()to ProductsController - Add filters:
woonoow/order_allowed_private_meta - Add filters:
woonoow/order_updatable_meta - Add filters:
woonoow/product_allowed_private_meta - Add filters:
woonoow/product_updatable_meta - Add filters:
woonoow/order_api_data - Add filters:
woonoow/product_api_data - Add actions:
woonoow/order_updated - Add actions:
woonoow/product_updated - Create
MetaFieldsRegistry.php - Add action:
woonoow/register_meta_fields - Initialize registry in Plugin.php
Frontend (React/TypeScript)
- 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 Product detail page
Testing
- Test with WooCommerce Shipment Tracking
- Test with ACF (Advanced Custom Fields)
- Test with custom metabox plugin
- Test meta data save/update
- Test meta data display in detail view
- Test field registration via
woonoow/register_meta_fields
Timeline
- Phase 1 (Backend): 2-3 days
- Phase 2 (Frontend): 3-4 days
- Phase 3 (Registry): 2-3 days
- Testing: 1-2 days
Total: 8-12 days (1.5-2 weeks)
Success Criteria
✅ Plugins using standard WP/WooCommerce meta storage work automatically ✅ No special integration needed from plugin developers ✅ Meta fields visible and editable in WooNooW admin ✅ Data saved correctly to WooCommerce database ✅ Compatible with popular plugins (Shipment Tracking, ACF, etc.) ✅ Follows 3-level compatibility strategy ✅ Zero coupling with specific plugins ✅ Community does NOTHING extra for Level 1 compatibility