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:
Dwindi Ramadhana
2026-05-30 13:02:08 +07:00
parent e70aa1f554
commit 396ca25be4
118 changed files with 10162 additions and 3726 deletions

View File

@@ -527,9 +527,11 @@ class ProductsController
// Virtual and downloadable
if (array_key_exists('virtual', $data)) {
error_log("Setting virtual to: " . ($data['virtual'] ? 'true' : 'false') . " for product ID: " . $product->get_id());
$product->set_virtual((bool) $data['virtual']);
}
if (array_key_exists('downloadable', $data)) {
error_log("Setting downloadable to: " . ($data['downloadable'] ? 'true' : 'false') . " for product ID: " . $product->get_id());
$product->set_downloadable((bool) $data['downloadable']);
}
if (array_key_exists('featured', $data)) {
@@ -887,6 +889,62 @@ class ProductsController
}
$data['gallery'] = $gallery;
// Video / rich media - stored in product meta
$video_url = get_post_meta($product->get_id(), '_woonoow_video_url', true) ?: '';
$data['video_url'] = $video_url;
// Detect embed type (youtube / vimeo / direct mp4)
$data['video_type'] = '';
if ($video_url) {
if (strpos($video_url, 'youtube.com') !== false || strpos($video_url, 'youtu.be') !== false) {
$data['video_type'] = 'youtube';
} elseif (strpos($video_url, 'vimeo.com') !== false) {
$data['video_type'] = 'vimeo';
} else {
$data['video_type'] = 'mp4';
}
}
// Media attachments - allow plugins/themes to inject additional media (e.g. 3D models, extra videos)
$media_attachments = apply_filters('woonoow_product_media_attachments', [], $product->get_id(), $product);
$data['media_attachments'] = is_array($media_attachments) ? $media_attachments : [];
// Cross-sells (shown in mini-cart / cart page)
$cross_sell_ids = $product->get_cross_sell_ids();
$cross_sells = [];
foreach (array_slice($cross_sell_ids, 0, 4) as $cs_id) {
$cs = wc_get_product($cs_id);
if (!$cs || !$cs->is_visible()) continue;
$cs_image_id = $cs->get_image_id();
$cross_sells[] = [
'id' => $cs->get_id(),
'name' => $cs->get_name(),
'slug' => $cs->get_slug(),
'price' => $cs->get_price(),
'regular_price' => $cs->get_regular_price(),
'image' => $cs_image_id ? wp_get_attachment_url($cs_image_id) : '',
];
}
$data['cross_sells'] = $cross_sells;
// Upsells (shown on product page below main content)
$upsell_ids = $product->get_upsell_ids();
$upsells = [];
foreach (array_slice($upsell_ids, 0, 4) as $up_id) {
$up = wc_get_product($up_id);
if (!$up || !$up->is_visible()) continue;
$up_image_id = $up->get_image_id();
$upsells[] = [
'id' => $up->get_id(),
'name' => $up->get_name(),
'slug' => $up->get_slug(),
'price' => $up->get_price(),
'regular_price' => $up->get_regular_price(),
'image' => $up_image_id ? wp_get_attachment_url($up_image_id) : '',
];
}
$data['upsells'] = $upsells;
// Variable product specifics
if ($product->is_type('variable')) {
$data['attributes'] = self::get_product_attributes($product);
@@ -993,6 +1051,8 @@ class ProductsController
'stock_status' => $variation->get_stock_status(),
'stock_quantity' => $variation->get_stock_quantity(),
'manage_stock' => $variation->get_manage_stock(),
'virtual' => $variation->is_virtual(),
'downloadable' => $variation->is_downloadable(),
'attributes' => $formatted_attributes,
'image_id' => $variation->get_image_id(),
'image_url' => $image_url,
@@ -1118,9 +1178,11 @@ class ProductsController
$variation->set_image_id($var_data['image_id']);
}
// Inherit virtual status from parent if parent is virtual
if ($product->is_virtual()) {
$variation->set_virtual(true);
if (array_key_exists('virtual', $var_data)) {
$variation->set_virtual((bool) $var_data['virtual']);
}
if (array_key_exists('downloadable', $var_data)) {
$variation->set_downloadable((bool) $var_data['downloadable']);
}
// Save variation first