Files
WooNooW/METABOX_COMPAT.md
dwindown f7dca7bc28 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)
2025-11-20 11:11:06 +07:00

15 KiB

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:

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.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

// 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)