Files
WooNooW/IMPLEMENTATION_PLAN_META_COMPAT.md
dwindown cb91d0841c plan: Complete implementation plan for Level 1 meta compatibility
**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!
2025-11-20 12:17:35 +07:00

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:

  1. WooNooW Core = Zero addon dependencies
  2. We listen to WP/WooCommerce hooks (NOT WooNooW-specific)
  3. Community does NOTHING extra
  4. We do NOT support specific plugins
  5. We do NOT integrate plugins into core

From ADDON_DEVELOPMENT_GUIDE.md:

  1. Hook system for functional extensions
  2. Zero coupling with core
  3. WordPress-style filters and actions

From ADDON_REACT_INTEGRATION.md:

  1. Expose React runtime on window
  2. Support vanilla JS/jQuery addons
  3. 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.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 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