feat: Phase 2 - Frontend meta fields components (Level 1)
Implemented: Frontend Components for Level 1 Compatibility Created Components: - MetaFields.tsx - Generic meta field renderer - useMetaFields.ts - Hook for field registry Integrated Into: - Orders/Edit.tsx - Meta fields after OrderForm - Products/Edit.tsx - Meta fields after ProductForm Features: - Supports: text, textarea, number, date, select, checkbox - Groups fields by section - Zero coupling with specific plugins - Renders any registered fields dynamically - Read-only mode support How It Works: 1. Backend exposes meta via API (Phase 1) 2. PHP registers fields via MetaFieldsRegistry (Phase 3 - next) 3. Fields localized to window.WooNooWMetaFields 4. useMetaFields hook reads registry 5. MetaFields component renders fields 6. User edits fields 7. Form submission includes meta 8. Backend saves via update_order_meta_data() Result: - Generic, reusable components - Zero plugin-specific code - Works with any registered fields - Clean separation of concerns Next: Phase 3 - PHP MetaFieldsRegistry system
This commit is contained in:
157
admin-spa/src/components/MetaFields.tsx
Normal file
157
admin-spa/src/components/MetaFields.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* MetaFields Component
|
||||
*
|
||||
* Generic component to display/edit custom meta fields from plugins.
|
||||
* Part of Level 1 compatibility - allows plugins using standard WP/WooCommerce
|
||||
* meta storage to have their fields displayed automatically.
|
||||
*
|
||||
* Zero coupling with specific plugins - renders any registered fields.
|
||||
*/
|
||||
export function MetaFields({ meta, fields, onChange, readOnly = false }: MetaFieldsProps) {
|
||||
// Don't render if no fields registered
|
||||
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} className="space-y-2">
|
||||
<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 === 'date' && (
|
||||
<Input
|
||||
id={field.key}
|
||||
type="date"
|
||||
value={meta[field.key] || ''}
|
||||
onChange={(e) => onChange(field.key, e.target.value)}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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 leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{field.placeholder || 'Enable'}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user