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:
269
admin-spa/src/components/layout/AppRoutes.tsx
Normal file
269
admin-spa/src/components/layout/AppRoutes.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import React from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { __ } from '@/lib/i18n';
|
||||
|
||||
// Import all routes
|
||||
import ResetPassword from '@/routes/ResetPassword';
|
||||
import Dashboard from '@/routes/Dashboard';
|
||||
import DashboardRevenue from '@/routes/Dashboard/Revenue';
|
||||
import DashboardOrders from '@/routes/Dashboard/Orders';
|
||||
import DashboardProducts from '@/routes/Dashboard/Products';
|
||||
import DashboardCustomers from '@/routes/Dashboard/Customers';
|
||||
import DashboardCoupons from '@/routes/Dashboard/Coupons';
|
||||
import DashboardTaxes from '@/routes/Dashboard/Taxes';
|
||||
import OrdersIndex from '@/routes/Orders';
|
||||
import OrderNew from '@/routes/Orders/New';
|
||||
import OrderEdit from '@/routes/Orders/Edit';
|
||||
import OrderDetail from '@/routes/Orders/Detail';
|
||||
import OrderInvoice from '@/routes/Orders/Invoice';
|
||||
import OrderLabel from '@/routes/Orders/Label';
|
||||
import ProductsIndex from '@/routes/Products';
|
||||
import ProductNew from '@/routes/Products/New';
|
||||
import ProductEdit from '@/routes/Products/Edit';
|
||||
import ProductCategories from '@/routes/Products/Categories';
|
||||
import ProductTags from '@/routes/Products/Tags';
|
||||
import ProductAttributes from '@/routes/Products/Attributes';
|
||||
import Licenses from '@/routes/Products/Licenses';
|
||||
import LicenseDetail from '@/routes/Products/Licenses/Detail';
|
||||
import SoftwareVersions from '@/routes/Products/SoftwareVersions';
|
||||
import SubscriptionsIndex from '@/routes/Subscriptions';
|
||||
import SubscriptionDetail from '@/routes/Subscriptions/Detail';
|
||||
import CouponsIndex from '@/routes/Marketing/Coupons';
|
||||
import CouponNew from '@/routes/Marketing/Coupons/New';
|
||||
import CouponEdit from '@/routes/Marketing/Coupons/Edit';
|
||||
import CustomersIndex from '@/routes/Customers';
|
||||
import CustomerNew from '@/routes/Customers/New';
|
||||
import CustomerEdit from '@/routes/Customers/Edit';
|
||||
import CustomerDetail from '@/routes/Customers/Detail';
|
||||
import SettingsIndex from '@/routes/Settings';
|
||||
import SettingsStore from '@/routes/Settings/Store';
|
||||
import SettingsPayments from '@/routes/Settings/Payments';
|
||||
import SettingsShipping from '@/routes/Settings/Shipping';
|
||||
import SettingsTax from '@/routes/Settings/Tax';
|
||||
import SettingsCustomers from '@/routes/Settings/Customers';
|
||||
import SettingsSecurity from '@/routes/Settings/Security';
|
||||
import SettingsLocalPickup from '@/routes/Settings/LocalPickup';
|
||||
import SettingsNotifications from '@/routes/Settings/Notifications';
|
||||
import StaffNotifications from '@/routes/Settings/Notifications/Staff';
|
||||
import CustomerNotifications from '@/routes/Settings/Notifications/Customer';
|
||||
import ChannelConfiguration from '@/routes/Settings/Notifications/ChannelConfiguration';
|
||||
import EmailConfiguration from '@/routes/Settings/Notifications/EmailConfiguration';
|
||||
import PushConfiguration from '@/routes/Settings/Notifications/PushConfiguration';
|
||||
import EmailCustomization from '@/routes/Settings/Notifications/EmailCustomization';
|
||||
import EditTemplate from '@/routes/Settings/Notifications/EditTemplate';
|
||||
import ActivityLog from '@/routes/Settings/Notifications/ActivityLog';
|
||||
import SettingsDeveloper from '@/routes/Settings/Developer';
|
||||
import SettingsModules from '@/routes/Settings/Modules';
|
||||
import ModuleSettings from '@/routes/Settings/ModuleSettings';
|
||||
import AppearanceIndex from '@/routes/Appearance';
|
||||
import AppearanceGeneral from '@/routes/Appearance/General';
|
||||
import AppearanceHeader from '@/routes/Appearance/Header';
|
||||
import AppearanceFooter from '@/routes/Appearance/Footer';
|
||||
import AppearanceShop from '@/routes/Appearance/Shop';
|
||||
import AppearanceProduct from '@/routes/Appearance/Product';
|
||||
import AppearanceCart from '@/routes/Appearance/Cart';
|
||||
import AppearanceCheckout from '@/routes/Appearance/Checkout';
|
||||
import AppearanceThankYou from '@/routes/Appearance/ThankYou';
|
||||
import AppearanceAccount from '@/routes/Appearance/Account';
|
||||
import AppearanceMenus from '@/routes/Appearance/Menus/MenuEditor';
|
||||
import AppearancePages from '@/routes/Appearance/Pages';
|
||||
import MarketingIndex from '@/routes/Marketing';
|
||||
import NewsletterLayout from '@/routes/Marketing/Newsletter';
|
||||
import NewsletterSubscribers from '@/routes/Marketing/Newsletter/Subscribers';
|
||||
import NewsletterCampaignsList from '@/routes/Marketing/Campaigns';
|
||||
import CampaignEdit from '@/routes/Marketing/Campaigns/Edit';
|
||||
import MorePage from '@/routes/More';
|
||||
import Help from '@/routes/Help';
|
||||
import Onboarding from '@/routes/Onboarding';
|
||||
import { LegacyCampaignRedirect } from '@/components/LegacyCampaignRedirect';
|
||||
|
||||
// Addon Route Component - Dynamically loads addon components
|
||||
function AddonRoute({ config }: { config: any }) {
|
||||
const [Component, setComponent] = React.useState<any>(null);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!config.component_url) {
|
||||
setError('No component URL provided');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Dynamically import the addon component
|
||||
import(/* @vite-ignore */ config.component_url)
|
||||
.then((mod) => {
|
||||
setComponent(() => mod.default || mod);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[AddonRoute] Failed to load component:', err);
|
||||
setError(err.message || 'Failed to load addon component');
|
||||
setLoading(false);
|
||||
});
|
||||
}, [config.component_url]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm opacity-70">{__('Loading addon...')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-lg border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-950/50 p-4">
|
||||
<h3 className="font-semibold text-red-900 dark:text-red-200 mb-2">{__('Failed to Load Addon')}</h3>
|
||||
<p className="text-sm text-red-700 dark:text-red-300">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!Component) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-lg border border-yellow-200 dark:border-yellow-800 bg-yellow-50 dark:bg-yellow-950/50 p-4">
|
||||
<p className="text-sm text-yellow-700 dark:text-yellow-300">{__('Addon component not found')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render the addon component with props
|
||||
return <Component {...(config.props || {})} />;
|
||||
}
|
||||
|
||||
export function AppRoutes() {
|
||||
const addonRoutes = window.WNW_ADDON_ROUTES || [];
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
{/* Dashboard */}
|
||||
<Route path="/" element={<Navigate to={window.WNW_CONFIG?.onboardingCompleted ? "/dashboard" : "/setup"} replace />} />
|
||||
<Route path="/setup" element={<Onboarding />} />
|
||||
<Route path="/reset-password" element={<ResetPassword />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/dashboard/revenue" element={<DashboardRevenue />} />
|
||||
<Route path="/dashboard/orders" element={<DashboardOrders />} />
|
||||
<Route path="/dashboard/products" element={<DashboardProducts />} />
|
||||
<Route path="/dashboard/customers" element={<DashboardCustomers />} />
|
||||
<Route path="/dashboard/coupons" element={<DashboardCoupons />} />
|
||||
<Route path="/dashboard/taxes" element={<DashboardTaxes />} />
|
||||
|
||||
{/* Products */}
|
||||
<Route path="/products" element={<ProductsIndex />} />
|
||||
<Route path="/products/new" element={<ProductNew />} />
|
||||
<Route path="/products/:id/edit" element={<ProductEdit />} />
|
||||
<Route path="/products/categories" element={<ProductCategories />} />
|
||||
<Route path="/products/tags" element={<ProductTags />} />
|
||||
<Route path="/products/attributes" element={<ProductAttributes />} />
|
||||
<Route path="/products/licenses" element={<Licenses />} />
|
||||
<Route path="/products/licenses/:id" element={<LicenseDetail />} />
|
||||
<Route path="/products/software" element={<SoftwareVersions />} />
|
||||
|
||||
{/* Orders */}
|
||||
<Route path="/orders" element={<OrdersIndex />} />
|
||||
<Route path="/orders/new" element={<OrderNew />} />
|
||||
<Route path="/orders/:id" element={<OrderDetail />} />
|
||||
<Route path="/orders/:id/edit" element={<OrderEdit />} />
|
||||
<Route path="/orders/:id/invoice" element={<OrderInvoice />} />
|
||||
<Route path="/orders/:id/label" element={<OrderLabel />} />
|
||||
|
||||
{/* Subscriptions */}
|
||||
<Route path="/subscriptions" element={<SubscriptionsIndex />} />
|
||||
<Route path="/subscriptions/:id" element={<SubscriptionDetail />} />
|
||||
|
||||
{/* Coupons (under Marketing) */}
|
||||
<Route path="/coupons" element={<Navigate to="/marketing/coupons" replace />} />
|
||||
<Route path="/coupons/new" element={<Navigate to="/marketing/coupons/new" replace />} />
|
||||
<Route path="/coupons/:id/edit" element={<CouponEdit />} />
|
||||
<Route path="/marketing/coupons" element={<CouponsIndex />} />
|
||||
<Route path="/marketing/coupons/new" element={<CouponNew />} />
|
||||
<Route path="/marketing/coupons/:id/edit" element={<CouponEdit />} />
|
||||
|
||||
{/* Customers */}
|
||||
<Route path="/customers" element={<CustomersIndex />} />
|
||||
<Route path="/customers/new" element={<CustomerNew />} />
|
||||
<Route path="/customers/:id/edit" element={<CustomerEdit />} />
|
||||
<Route path="/customers/:id" element={<CustomerDetail />} />
|
||||
|
||||
{/* More */}
|
||||
<Route path="/more" element={<MorePage />} />
|
||||
|
||||
{/* Settings */}
|
||||
<Route path="/settings" element={<SettingsIndex />} />
|
||||
<Route path="/settings/store" element={<SettingsStore />} />
|
||||
<Route path="/settings/payments" element={<SettingsPayments />} />
|
||||
<Route path="/settings/shipping" element={<SettingsShipping />} />
|
||||
<Route path="/settings/tax" element={<SettingsTax />} />
|
||||
<Route path="/settings/customers" element={<SettingsCustomers />} />
|
||||
<Route path="/settings/security" element={<SettingsSecurity />} />
|
||||
<Route path="/settings/taxes" element={<Navigate to="/settings/tax" replace />} />
|
||||
<Route path="/settings/local-pickup" element={<SettingsLocalPickup />} />
|
||||
<Route path="/settings/checkout" element={<SettingsIndex />} />
|
||||
<Route path="/settings/notifications" element={<SettingsNotifications />} />
|
||||
<Route path="/settings/notifications/staff" element={<StaffNotifications />} />
|
||||
<Route path="/settings/notifications/customer" element={<CustomerNotifications />} />
|
||||
<Route path="/settings/notifications/channels" element={<ChannelConfiguration />} />
|
||||
<Route path="/settings/notifications/channels/email" element={<EmailConfiguration />} />
|
||||
<Route path="/settings/notifications/channels/push" element={<PushConfiguration />} />
|
||||
<Route path="/settings/notifications/email-customization" element={<EmailCustomization />} />
|
||||
<Route path="/settings/notifications/edit-template" element={<EditTemplate />} />
|
||||
<Route path="/settings/notifications/activity-log" element={<ActivityLog />} />
|
||||
<Route path="/settings/brand" element={<SettingsIndex />} />
|
||||
<Route path="/settings/developer" element={<SettingsDeveloper />} />
|
||||
<Route path="/settings/modules" element={<SettingsModules />} />
|
||||
<Route path="/settings/modules/:moduleId" element={<ModuleSettings />} />
|
||||
|
||||
{/* Appearance */}
|
||||
<Route path="/appearance" element={<AppearanceIndex />} />
|
||||
<Route path="/appearance/general" element={<AppearanceGeneral />} />
|
||||
<Route path="/appearance/header" element={<AppearanceHeader />} />
|
||||
<Route path="/appearance/footer" element={<AppearanceFooter />} />
|
||||
<Route path="/appearance/shop" element={<AppearanceShop />} />
|
||||
<Route path="/appearance/product" element={<AppearanceProduct />} />
|
||||
<Route path="/appearance/cart" element={<AppearanceCart />} />
|
||||
<Route path="/appearance/checkout" element={<AppearanceCheckout />} />
|
||||
<Route path="/appearance/thankyou" element={<AppearanceThankYou />} />
|
||||
<Route path="/appearance/account" element={<AppearanceAccount />} />
|
||||
<Route path="/appearance/menus" element={<AppearanceMenus />} />
|
||||
<Route path="/appearance/pages" element={<AppearancePages />} />
|
||||
|
||||
{/* Marketing */}
|
||||
<Route path="/marketing" element={<MarketingIndex />} />
|
||||
<Route path="/marketing/newsletter" element={<NewsletterLayout />}>
|
||||
<Route index element={<Navigate to="subscribers" replace />} />
|
||||
<Route path="subscribers" element={<NewsletterSubscribers />} />
|
||||
<Route path="campaigns" element={<NewsletterCampaignsList />} />
|
||||
<Route path="campaigns/:id" element={<CampaignEdit />} />
|
||||
</Route>
|
||||
|
||||
{/* Legacy Redirects for Newsletter (using component to preserve params) */}
|
||||
<Route path="/marketing/campaigns" element={<Navigate to="/marketing/newsletter/campaigns" replace />} />
|
||||
<Route path="/marketing/campaigns/new" element={<Navigate to="/marketing/newsletter/campaigns/new" replace />} />
|
||||
<Route path="/marketing/campaigns/:id" element={<LegacyCampaignRedirect />} />
|
||||
|
||||
{/* Help - Main menu route with no submenu */}
|
||||
<Route path="/help" element={<Help />} />
|
||||
|
||||
{/* Dynamic Addon Routes */}
|
||||
{addonRoutes.map((route: any) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={<AddonRoute config={route} />}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user