Files
WooNooW/admin-spa/src/routes/Orders/Edit.tsx
dwindown 0c5efa3efc 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
2025-11-20 12:32:06 +07:00

139 lines
4.7 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import OrderForm from '@/routes/Orders/partials/OrderForm';
import { OrdersApi } from '@/lib/api';
import { showErrorToast, showSuccessToast, getPageLoadErrorMessage } from '@/lib/errorHandling';
import { ErrorCard } from '@/components/ErrorCard';
import { LoadingState } from '@/components/LoadingState';
import { __, sprintf } from '@/lib/i18n';
import { usePageHeader } from '@/contexts/PageHeaderContext';
import { Button } from '@/components/ui/button';
import { useFABConfig } from '@/hooks/useFABConfig';
import { MetaFields } from '@/components/MetaFields';
import { useMetaFields } from '@/hooks/useMetaFields';
export default function OrdersEdit() {
const { id } = useParams();
const orderId = Number(id);
const nav = useNavigate();
const qc = useQueryClient();
const { setPageHeader, clearPageHeader } = usePageHeader();
const formRef = useRef<HTMLFormElement>(null);
// Level 1 compatibility: Meta fields from plugins
const metaFields = useMetaFields('orders');
const [metaData, setMetaData] = useState<Record<string, any>>({});
// Hide FAB on edit page
useFABConfig('none');
const countriesQ = useQuery({ queryKey: ['countries'], queryFn: OrdersApi.countries });
const paymentsQ = useQuery({ queryKey: ['payments'], queryFn: OrdersApi.payments });
const shippingsQ = useQuery({ queryKey: ['shippings'], queryFn: OrdersApi.shippings });
const orderQ = useQuery({ queryKey: ['order', orderId], enabled: Number.isFinite(orderId), queryFn: () => OrdersApi.get(orderId) });
const upd = useMutation({
mutationFn: (payload: any) => OrdersApi.update(orderId, payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['orders'] });
qc.invalidateQueries({ queryKey: ['order', orderId] });
showSuccessToast(__('Order updated successfully'));
nav(`/orders/${orderId}`);
},
onError: (error: any) => {
showErrorToast(error);
}
});
const countriesData = React.useMemo(() => {
const list = countriesQ.data?.countries ?? [];
return list.map((c: any) => ({ code: String(c.code), name: String(c.name) }));
}, [countriesQ.data]);
const order = orderQ.data || {};
// Sync meta data from order
useEffect(() => {
if (order.meta) {
setMetaData(order.meta);
}
}, [order.meta]);
// Set page header with back button and save button
useEffect(() => {
const actions = (
<div className="flex gap-2">
<Button size="sm" variant="ghost" onClick={() => nav(`/orders/${orderId}`)}>
{__('Back')}
</Button>
<Button
size="sm"
onClick={() => formRef.current?.requestSubmit()}
disabled={upd.isPending}
>
{upd.isPending ? __('Saving...') : __('Save')}
</Button>
</div>
);
const title = order.number
? sprintf(__('Edit Order #%s'), order.number)
: __('Edit Order');
setPageHeader(title, actions);
return () => clearPageHeader();
}, [order.number, orderId, upd.isPending, setPageHeader, clearPageHeader, nav]);
if (!Number.isFinite(orderId)) {
return <div className="p-4 text-sm text-red-600">{__('Invalid order id.')}</div>;
}
if (orderQ.isLoading || countriesQ.isLoading) {
return <LoadingState message={sprintf(__('Loading order #%s...'), orderId)} />;
}
if (orderQ.isError) {
return <ErrorCard
title={__('Failed to load order')}
message={getPageLoadErrorMessage(orderQ.error)}
onRetry={() => orderQ.refetch()}
/>;
}
return (
<div className="space-y-4">
<OrderForm
mode="edit"
initial={order}
currency={order.currency}
currencySymbol={order.currency_symbol}
countries={countriesData}
states={countriesQ.data?.states || {}}
defaultCountry={countriesQ.data?.default_country}
payments={(paymentsQ.data || [])}
shippings={(shippingsQ.data || [])}
itemsEditable={['pending', 'on-hold', 'failed', 'draft'].includes(order.status)}
showCoupons
formRef={formRef}
hideSubmitButton={true}
onSubmit={(form) => {
const payload = { ...form, meta: metaData } as any;
upd.mutate(payload);
}}
/>
{/* Level 1 compatibility: Custom meta fields from plugins */}
{metaFields.length > 0 && (
<MetaFields
meta={metaData}
fields={metaFields}
onChange={(key, value) => {
setMetaData(prev => ({ ...prev, [key]: value }));
}}
/>
)}
</div>
);
}