feat: product page layout toggle (flat/card), fix email shortcode rendering
- Add layout_style setting (flat default) to product appearance
- AppearanceController: sanitize & persist layout_style, add to default settings
- Admin SPA: Layout Style select in Appearance > Product
- Customer SPA: useEffect targets <main> bg-white in flat mode (full-width),
card mode uses per-section white floating cards on gray background
- Accordion sections styled per mode: flat=border-t dividers, card=white cards
- Fix email shortcode gaps (EmailRenderer, EmailManager)
- Add missing variables: return_url, contact_url, account_url (alias),
payment_error_reason, order_items_list (alias for order_items_table)
- Fix customer_note extra_data key mismatch (note → customer_note)
- Pass low_stock_threshold via extra_data in low_stock email send
This commit is contained in:
@@ -122,7 +122,7 @@ class OnboardingController extends WP_REST_Controller
|
||||
}
|
||||
|
||||
// 4. Mark as Complete
|
||||
update_option('woonoow_onboarding_completed', true);
|
||||
update_option('woonoow_onboarding_completed', 'yes');
|
||||
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
|
||||
@@ -58,7 +58,7 @@ class PagesController
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// Get/Save CPT templates
|
||||
// Get/Save/Delete CPT templates
|
||||
register_rest_route($namespace, '/templates/(?P<cpt>[a-zA-Z0-9_-]+)', [
|
||||
[
|
||||
'methods' => 'GET',
|
||||
@@ -70,6 +70,11 @@ class PagesController
|
||||
'callback' => [__CLASS__, 'save_template'],
|
||||
'permission_callback' => [__CLASS__, 'check_admin_permission'],
|
||||
],
|
||||
[
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [__CLASS__, 'delete_template'],
|
||||
'permission_callback' => [__CLASS__, 'check_admin_permission'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Get post with template applied (for SPA rendering)
|
||||
@@ -337,6 +342,34 @@ class PagesController
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete CPT template (abort SPA handling for this post type)
|
||||
*/
|
||||
public static function delete_template(WP_REST_Request $request)
|
||||
{
|
||||
$cpt = $request->get_param('cpt');
|
||||
|
||||
// Validate CPT exists
|
||||
if (!post_type_exists($cpt) && $cpt !== 'post') {
|
||||
return new WP_Error('invalid_cpt', 'Invalid post type', ['status' => 400]);
|
||||
}
|
||||
|
||||
$option_key = "wn_template_{$cpt}";
|
||||
$exists = get_option($option_key, null);
|
||||
|
||||
if ($exists === null) {
|
||||
return new WP_Error('not_found', 'No template found for this post type', ['status' => 404]);
|
||||
}
|
||||
|
||||
delete_option($option_key);
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'cpt' => $cpt,
|
||||
'message' => 'Template deleted. WordPress will now handle this post type natively.',
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content with template applied (for SPA rendering)
|
||||
*/
|
||||
@@ -378,7 +411,37 @@ class PagesController
|
||||
if ($template && !empty($template['sections'])) {
|
||||
foreach ($template['sections'] as $section) {
|
||||
$resolved_section = $section;
|
||||
$resolved_section['props'] = PageSSR::resolve_props($section['props'] ?? [], $post_data);
|
||||
|
||||
// Pre-resolve special dynamic sources that produce arrays before PageSSR::resolve_props
|
||||
$props = $section['props'] ?? [];
|
||||
foreach ($props as $key => $prop) {
|
||||
if (is_array($prop) && ($prop['type'] ?? '') === 'dynamic' && ($prop['source'] ?? '') === 'related_posts') {
|
||||
$props[$key] = [
|
||||
'type' => 'static',
|
||||
'value' => PlaceholderRenderer::get_related_posts($post->ID, 3, $type),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$resolved_section['props'] = PageSSR::resolve_props($props, $post_data);
|
||||
|
||||
// Resolve dynamicBackground in styles
|
||||
// If styles.dynamicBackground === 'post_featured_image', set styles.backgroundImage from post data
|
||||
$styles = $resolved_section['styles'] ?? [];
|
||||
if (!empty($styles['dynamicBackground']) && (empty($styles['backgroundType']) || $styles['backgroundType'] === 'image')) {
|
||||
$dyn_source = $styles['dynamicBackground'];
|
||||
if ($dyn_source === 'post_featured_image' || $dyn_source === 'featured_image') {
|
||||
$featured_url = $post_data['featured_image'] ?? '';
|
||||
if (!empty($featured_url)) {
|
||||
$styles['backgroundImage'] = $featured_url;
|
||||
$styles['backgroundType'] = 'image';
|
||||
}
|
||||
}
|
||||
// Remove the internal marker from the rendered output
|
||||
unset($styles['dynamicBackground']);
|
||||
$resolved_section['styles'] = $styles;
|
||||
}
|
||||
|
||||
$rendered_sections[] = $resolved_section;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,18 @@ class ProductsController
|
||||
return trim($sanitized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize rich text (allows HTML tags)
|
||||
*/
|
||||
private static function sanitize_rich_text($value)
|
||||
{
|
||||
if (!isset($value) || $value === '') {
|
||||
return '';
|
||||
}
|
||||
$sanitized = wp_kses_post($value);
|
||||
return trim($sanitized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize numeric value
|
||||
*/
|
||||
@@ -335,8 +347,12 @@ class ProductsController
|
||||
$product->set_slug(self::sanitize_slug($data['slug']));
|
||||
}
|
||||
$product->set_status(sanitize_key($data['status'] ?? 'publish'));
|
||||
$product->set_description(self::sanitize_textarea($data['description'] ?? ''));
|
||||
$product->set_short_description(self::sanitize_textarea($data['short_description'] ?? ''));
|
||||
if (isset($data['description'])) {
|
||||
$product->set_description(self::sanitize_rich_text($data['description'] ?? ''));
|
||||
}
|
||||
if (isset($data['short_description'])) {
|
||||
$product->set_short_description(self::sanitize_textarea($data['short_description'] ?? ''));
|
||||
}
|
||||
|
||||
if (!empty($data['sku'])) {
|
||||
$product->set_sku(self::sanitize_text($data['sku']));
|
||||
@@ -489,7 +505,7 @@ class ProductsController
|
||||
if (isset($data['name'])) $product->set_name(self::sanitize_text($data['name']));
|
||||
if (isset($data['slug'])) $product->set_slug(self::sanitize_slug($data['slug']));
|
||||
if (isset($data['status'])) $product->set_status(sanitize_key($data['status']));
|
||||
if (isset($data['description'])) $product->set_description(self::sanitize_textarea($data['description']));
|
||||
if (isset($data['description'])) $product->set_description(self::sanitize_rich_text($data['description']));
|
||||
if (isset($data['short_description'])) $product->set_short_description(self::sanitize_textarea($data['short_description']));
|
||||
if (isset($data['sku'])) $product->set_sku(self::sanitize_text($data['sku']));
|
||||
|
||||
@@ -942,10 +958,17 @@ class ProductsController
|
||||
$value = $term ? $term->name : $value;
|
||||
}
|
||||
} else {
|
||||
// Custom attribute - stored as lowercase in meta
|
||||
$meta_key = 'attribute_' . strtolower($attr_name);
|
||||
// Custom attribute - stored as sanitize_title in meta
|
||||
$sanitized_name = sanitize_title($attr_name);
|
||||
$meta_key = 'attribute_' . $sanitized_name;
|
||||
$value = get_post_meta($variation_id, $meta_key, true);
|
||||
|
||||
// Fallback to legacy lowercase if not found
|
||||
if ($value === '') {
|
||||
$meta_key_legacy = 'attribute_' . strtolower($attr_name);
|
||||
$value = get_post_meta($variation_id, $meta_key_legacy, true);
|
||||
}
|
||||
|
||||
// Capitalize the attribute name for display to match admin SPA
|
||||
$clean_name = ucfirst($attr_name);
|
||||
}
|
||||
@@ -1029,8 +1052,27 @@ class ProductsController
|
||||
|
||||
foreach ($parent_attributes as $attr_name => $parent_attr) {
|
||||
if (!$parent_attr->get_variation()) continue;
|
||||
if (strcasecmp($display_name, $attr_name) === 0 || strcasecmp($display_name, ucfirst($attr_name)) === 0) {
|
||||
$wc_attributes[strtolower($attr_name)] = strtolower($value);
|
||||
|
||||
$is_match = false;
|
||||
if (strpos($attr_name, 'pa_') === 0) {
|
||||
$label = wc_attribute_label($attr_name);
|
||||
if (strcasecmp($display_name, $label) === 0 || strcasecmp($display_name, $attr_name) === 0) {
|
||||
$is_match = true;
|
||||
}
|
||||
} else {
|
||||
// Custom attribute: Check exact name, or sanitized version
|
||||
if (
|
||||
strcasecmp($display_name, $attr_name) === 0 ||
|
||||
strcasecmp($display_name, $parent_attr->get_name()) === 0 ||
|
||||
sanitize_title($display_name) === sanitize_title($attr_name)
|
||||
) {
|
||||
$is_match = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($is_match) {
|
||||
// WooCommerce expects the exact attribute slug as the key
|
||||
$wc_attributes[$attr_name] = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1095,7 +1137,7 @@ class ProductsController
|
||||
global $wpdb;
|
||||
|
||||
foreach ($wc_attributes as $attr_name => $attr_value) {
|
||||
$meta_key = 'attribute_' . $attr_name;
|
||||
$meta_key = 'attribute_' . sanitize_title($attr_name);
|
||||
|
||||
$wpdb->delete(
|
||||
$wpdb->postmeta,
|
||||
|
||||
Reference in New Issue
Block a user