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:
@@ -2,7 +2,7 @@ export const api = {
|
||||
root: () => (window.WNW_API?.root?.replace(/\/$/, '') || ''),
|
||||
nonce: () => (window.WNW_API?.nonce || ''),
|
||||
|
||||
async wpFetch(path: string, options: RequestInit = {}) {
|
||||
async wpFetch<T = any>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const url = /^https?:\/\//.test(path) ? path : api.root() + path;
|
||||
const headers = new Headers(options.headers || {});
|
||||
if (!headers.has('X-WP-Nonce') && api.nonce()) headers.set('X-WP-Nonce', api.nonce());
|
||||
@@ -33,13 +33,13 @@ export const api = {
|
||||
}
|
||||
|
||||
try {
|
||||
return await res.json();
|
||||
return await res.json() as T;
|
||||
} catch {
|
||||
return await res.text();
|
||||
return await res.text() as unknown as T;
|
||||
}
|
||||
},
|
||||
|
||||
async get(path: string, params?: Record<string, any>) {
|
||||
async get<T = any>(path: string, params?: Record<string, any>): Promise<T> {
|
||||
const usp = new URLSearchParams();
|
||||
if (params) {
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
@@ -48,71 +48,38 @@ export const api = {
|
||||
}
|
||||
}
|
||||
const qs = usp.toString();
|
||||
return api.wpFetch(path + (qs ? `?${qs}` : ''));
|
||||
return api.wpFetch<T>(path + (qs ? `?${qs}` : ''));
|
||||
},
|
||||
|
||||
async post(path: string, body?: any) {
|
||||
return api.wpFetch(path, {
|
||||
async post<T = any>(path: string, body?: any): Promise<T> {
|
||||
return api.wpFetch<T>(path, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: body != null ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
},
|
||||
|
||||
async put(path: string, body?: any) {
|
||||
return api.wpFetch(path, {
|
||||
async put<T = any>(path: string, body?: any): Promise<T> {
|
||||
return api.wpFetch<T>(path, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: body != null ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
},
|
||||
|
||||
async del(path: string) {
|
||||
return api.wpFetch(path, { method: 'DELETE' });
|
||||
async del<T = any>(path: string): Promise<T> {
|
||||
return api.wpFetch<T>(path, { method: 'DELETE' });
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export type CreateOrderPayload = {
|
||||
items: { product_id: number; qty: number }[];
|
||||
billing?: Record<string, any>;
|
||||
shipping?: Record<string, any>;
|
||||
status?: string;
|
||||
payment_method?: string;
|
||||
};
|
||||
|
||||
export const OrdersApi = {
|
||||
list: (params?: Record<string, any>) => api.get('/orders', params),
|
||||
get: (id: number) => api.get(`/orders/${id}`),
|
||||
create: (payload: CreateOrderPayload) => api.post('/orders', payload),
|
||||
update: (id: number, payload: any) => api.wpFetch(`/orders/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
payments: async () => api.get('/payments'),
|
||||
shippings: async () => api.get('/shippings'),
|
||||
countries: () => api.get('/countries'),
|
||||
};
|
||||
|
||||
export const ProductsApi = {
|
||||
search: (search: string, limit = 10) => api.get('/products/search', { search, limit }),
|
||||
list: (params?: { page?: number; per_page?: number }) => api.get('/products', { params }),
|
||||
categories: () => api.get('/products/categories'),
|
||||
};
|
||||
|
||||
export const CustomersApi = {
|
||||
search: (search: string) => api.get('/customers/search', { search }),
|
||||
searchByEmail: (email: string) => api.get('/customers/search', { email }),
|
||||
};
|
||||
|
||||
export async function getMenus() {
|
||||
// Prefer REST; fall back to localized snapshot
|
||||
try {
|
||||
const res = await fetch(`${(window as any).WNW_API}/menus`, { credentials: 'include' });
|
||||
const res = await fetch(`${window.WNW_API}/menus`, { credentials: 'include' });
|
||||
if (!res.ok) throw new Error('menus fetch failed');
|
||||
return (await res.json()).items || [];
|
||||
} catch {
|
||||
return ((window as any).WNW_WC_MENUS?.items) || [];
|
||||
return (window.WNW_WC_MENUS?.items) || [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user