feat: Page Editor v1.0 - canonical schema, SSR parity, and migration
Major improvements to WooNooW Page Editor system: Schema & Architecture: - Canonical section schema with unified sectionSchema.ts - Normalized feature-grid to use items (not features) - Standardized default values across all section types - Schema versioning with automatic migration on read Backend (PHP): - Enhanced PlaceholderRenderer with typed output contracts - Added fallback behavior for empty/invalid dynamic sources - Added caching support for post data resolution - New SchemaMigration class for backward compatibility - New Features class for feature flags - Enhanced PageSSR with full style support - Removed controller-level special-casing for related_posts Frontend (Admin SPA): - Updated CanvasRenderer with schema-aware transformation - Enhanced InspectorPanel with canonical schema metadata - Added new section renderers Frontend (Customer SPA): - New section components: BentoCategoryGrid, MarqueeBanner, ProductCarousel, ShoppableImage - Updated FeatureGridSection for items prop contract Testing: - Add PHP tests: SchemaMigrationTest, PlaceholderRendererTest, PageSSRTest - Add TypeScript tests: schema-integration, feature-grid-regression - Add parity tests for React vs SSR content matching - Add CI script: check-schema-drift.mjs - Add VERIFICATION_CHECKLIST.md Documentation: - RELEASE_NOTES-v1.0.md with full release notes - docs/PAGE_EDITOR_SECTION_SCHEMA_V1.md - docs/PAGE_EDITOR_SSR_COVERAGE_AUDIT.md
This commit is contained in:
@@ -13,10 +13,21 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { usePageHeader } from '@/contexts/PageHeaderContext';
|
||||
import { __ } from '@/lib/i18n';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
interface SubscriptionOrder {
|
||||
id: number;
|
||||
@@ -103,29 +114,13 @@ const formatPrice = (amount: string | number) => {
|
||||
};
|
||||
|
||||
async function fetchSubscription(id: string) {
|
||||
const res = await fetch(`${window.WNW_API.root}/subscriptions/${id}`, {
|
||||
headers: { 'X-WP-Nonce': window.WNW_API.nonce },
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed to fetch subscription');
|
||||
return res.json();
|
||||
const res = await api.get(`/subscriptions/${id}`);
|
||||
return res;
|
||||
}
|
||||
|
||||
async function subscriptionAction(id: number, action: string, reason?: string) {
|
||||
const res = await fetch(`${window.WNW_API.root}/subscriptions/${id}/${action}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': window.WNW_API.nonce,
|
||||
},
|
||||
body: JSON.stringify({ reason }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new Error(error.message || `Failed to ${action} subscription`);
|
||||
}
|
||||
return res.json();
|
||||
const res = await api.post(`/subscriptions/${id}/${action}`, { reason });
|
||||
return res;
|
||||
}
|
||||
|
||||
export default function SubscriptionDetail() {
|
||||
@@ -133,6 +128,7 @@ export default function SubscriptionDetail() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { setPageHeader, clearPageHeader } = usePageHeader();
|
||||
const [showCancelDialog, setShowCancelDialog] = React.useState(false);
|
||||
|
||||
const { data: subscription, isLoading, error } = useQuery<Subscription>({
|
||||
queryKey: ['subscription', id],
|
||||
@@ -154,19 +150,26 @@ export default function SubscriptionDetail() {
|
||||
queryClient.invalidateQueries({ queryKey: ['subscription', id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['subscriptions'] });
|
||||
toast.success(__(`Subscription ${action}d successfully`));
|
||||
setShowCancelDialog(false);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message);
|
||||
setShowCancelDialog(false);
|
||||
},
|
||||
});
|
||||
|
||||
const handleAction = (action: string) => {
|
||||
if (action === 'cancel' && !confirm(__('Are you sure you want to cancel this subscription?'))) {
|
||||
if (action === 'cancel') {
|
||||
setShowCancelDialog(true);
|
||||
return;
|
||||
}
|
||||
actionMutation.mutate({ action });
|
||||
};
|
||||
|
||||
const confirmCancel = () => {
|
||||
actionMutation.mutate({ action: 'cancel' });
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -418,6 +421,35 @@ export default function SubscriptionDetail() {
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Cancel Confirmation Dialog (replaces native confirm()) */}
|
||||
<AlertDialog open={showCancelDialog} onOpenChange={setShowCancelDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{__('Cancel Subscription')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{__('Are you sure you want to cancel this subscription?')}
|
||||
<br />
|
||||
<span className="text-red-600 font-medium">{__('This action cannot be undone.')}</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
onClick={() => setShowCancelDialog(false)}
|
||||
disabled={actionMutation.isPending}
|
||||
>
|
||||
{__('Keep Subscription')}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmCancel}
|
||||
disabled={actionMutation.isPending}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{actionMutation.isPending ? __('Cancelling...') : __('Cancel Subscription')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user