Files
WooNooW/METABOX_COMPAT.md
dwindown 64e6fa6da0 docs: Align METABOX_COMPAT with 3-level compatibility strategy
**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
2025-11-20 11:37:27 +07:00

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_number meta
  • Frontend can't read/write this data
  • Plugin's data exists in DB but not accessible

Expected WooNooW Behavior (Level 1):

  • API exposes meta object 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.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)