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:
253
includes/Frontend/SchemaMigration.php
Normal file
253
includes/Frontend/SchemaMigration.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
namespace WooNooW\Frontend;
|
||||
|
||||
/**
|
||||
* Schema Migration Handler
|
||||
* Handles backward compatibility for legacy saved structures
|
||||
*/
|
||||
class SchemaMigration
|
||||
{
|
||||
/**
|
||||
* Current schema version
|
||||
*/
|
||||
const CURRENT_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Migration functions per version
|
||||
*/
|
||||
private static $migrations = [
|
||||
1 => 'migrate_to_v1',
|
||||
];
|
||||
|
||||
/**
|
||||
* Migrate a page/template structure to current schema
|
||||
*
|
||||
* @param array $structure Page or template structure
|
||||
* @return array Migrated structure
|
||||
*/
|
||||
public static function migrate($structure)
|
||||
{
|
||||
$version = $structure['schemaVersion'] ?? 0;
|
||||
|
||||
// Already at current version
|
||||
if ($version >= self::CURRENT_VERSION) {
|
||||
return $structure;
|
||||
}
|
||||
|
||||
// Apply migrations in order
|
||||
for ($v = $version + 1; $v <= self::CURRENT_VERSION; $v++) {
|
||||
if (isset(self::$migrations[$v])) {
|
||||
$method = self::$migrations[$v];
|
||||
$structure = self::$method($structure);
|
||||
}
|
||||
}
|
||||
|
||||
// Set current version
|
||||
$structure['schemaVersion'] = self::CURRENT_VERSION;
|
||||
|
||||
return $structure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate to version 1
|
||||
* - Normalize feature-grid items/features
|
||||
* - Normalize style keys
|
||||
* - Add missing defaults
|
||||
*
|
||||
* @param array $structure
|
||||
* @return array
|
||||
*/
|
||||
private static function migrate_to_v1($structure)
|
||||
{
|
||||
if (!isset($structure['sections']) || !is_array($structure['sections'])) {
|
||||
return $structure;
|
||||
}
|
||||
|
||||
foreach ($structure['sections'] as &$section) {
|
||||
// Migrate section type
|
||||
if (!isset($section['type'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Feature grid: normalize items/features
|
||||
if ($section['type'] === 'feature-grid') {
|
||||
$section = self::migrate_feature_grid($section);
|
||||
}
|
||||
|
||||
// Normalize section styles
|
||||
$section = self::migrate_section_styles($section);
|
||||
|
||||
// Normalize element styles
|
||||
$section = self::migrate_element_styles($section);
|
||||
}
|
||||
|
||||
return $structure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate feature-grid section
|
||||
* - Ensure items array exists
|
||||
* - Copy features to items if needed
|
||||
*
|
||||
* @param array $section
|
||||
* @return array
|
||||
*/
|
||||
private static function migrate_feature_grid($section)
|
||||
{
|
||||
$props = $section['props'] ?? [];
|
||||
|
||||
// Handle items/features normalization
|
||||
if (!isset($props['items']) && isset($props['features'])) {
|
||||
// features exists but items doesn't - copy features to items
|
||||
$props['items'] = $props['features'];
|
||||
|
||||
// If features was an empty string, convert to empty array
|
||||
if ($props['items'] === '') {
|
||||
$props['items'] = [];
|
||||
}
|
||||
|
||||
// Keep features for backward compat but prefer items
|
||||
// Don't remove it yet - some old code may still reference it
|
||||
}
|
||||
|
||||
// Ensure items is always an array
|
||||
if (!isset($props['items'])) {
|
||||
$props['items'] = [];
|
||||
} elseif (!is_array($props['items'])) {
|
||||
$props['items'] = [];
|
||||
}
|
||||
|
||||
$section['props'] = $props;
|
||||
|
||||
return $section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate section styles
|
||||
* - Normalize key names
|
||||
* - Add missing defaults
|
||||
*
|
||||
* @param array $section
|
||||
* @return array
|
||||
*/
|
||||
private static function migrate_section_styles($section)
|
||||
{
|
||||
$styles = $section['styles'] ?? [];
|
||||
|
||||
// Normalize contentWidth if missing
|
||||
if (!isset($styles['contentWidth']) && isset($styles['container_width'])) {
|
||||
// Old key name
|
||||
$styles['contentWidth'] = $styles['container_width'];
|
||||
unset($styles['container_width']);
|
||||
}
|
||||
|
||||
// Normalize background type
|
||||
if (isset($styles['backgroundImage']) && !isset($styles['backgroundType'])) {
|
||||
// If there's a background image but no type, assume image type
|
||||
$styles['backgroundType'] = 'image';
|
||||
}
|
||||
|
||||
// Normalize height preset
|
||||
if (isset($styles['height']) && !isset($styles['heightPreset'])) {
|
||||
$height = $styles['height'];
|
||||
$map = [
|
||||
'small' => 'small',
|
||||
'medium' => 'medium',
|
||||
'large' => 'large',
|
||||
'fullscreen' => 'fullscreen',
|
||||
'screen' => 'fullscreen', // Old naming
|
||||
'default' => 'default',
|
||||
];
|
||||
$styles['heightPreset'] = $map[$height] ?? 'default';
|
||||
unset($styles['height']);
|
||||
}
|
||||
|
||||
// Add default background settings if missing
|
||||
if (!isset($styles['backgroundType'])) {
|
||||
$styles['backgroundType'] = 'solid';
|
||||
}
|
||||
|
||||
$section['styles'] = $styles;
|
||||
|
||||
return $section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate element styles
|
||||
* - Normalize key names
|
||||
* - Add missing defaults
|
||||
*
|
||||
* @param array $section
|
||||
* @return array
|
||||
*/
|
||||
private static function migrate_element_styles($section)
|
||||
{
|
||||
$elementStyles = $section['elementStyles'] ?? [];
|
||||
|
||||
// Normalize common element style keys
|
||||
$normalizations = [
|
||||
'cta' => 'cta_text', // Old key
|
||||
'button' => 'cta_text', // Alias for button
|
||||
'heading' => 'heading', // Standardize
|
||||
'text' => 'text', // Standardize
|
||||
'subtitle' => 'subtitle', // Standardize
|
||||
];
|
||||
|
||||
$normalized = [];
|
||||
foreach ($elementStyles as $key => $style) {
|
||||
$normalized_key = $normalizations[$key] ?? $key;
|
||||
$normalized[$normalized_key] = $style;
|
||||
}
|
||||
|
||||
// Add default styling keys if missing
|
||||
$defaults = [
|
||||
'title' => [],
|
||||
'subtitle' => [],
|
||||
'text' => [],
|
||||
'cta_text' => [],
|
||||
'feature_item' => [],
|
||||
];
|
||||
|
||||
foreach ($defaults as $key => $default) {
|
||||
if (!isset($normalized[$key])) {
|
||||
$normalized[$key] = $default;
|
||||
}
|
||||
}
|
||||
|
||||
$section['elementStyles'] = $normalized;
|
||||
|
||||
return $section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a structure needs migration
|
||||
*
|
||||
* @param array $structure
|
||||
* @return bool
|
||||
*/
|
||||
public static function needs_migration($structure)
|
||||
{
|
||||
return ($structure['schemaVersion'] ?? 0) < self::CURRENT_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch migrate multiple structures
|
||||
*
|
||||
* @param array $structures Array of structures
|
||||
* @return array Migrated structures
|
||||
*/
|
||||
public static function migrate_all($structures)
|
||||
{
|
||||
return array_map([self::class, 'migrate'], $structures);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current schema version
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function get_current_version()
|
||||
{
|
||||
return self::CURRENT_VERSION;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user