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:
Dwindi Ramadhana
2026-03-04 01:14:56 +07:00
parent 7ff429502d
commit 90169b508d
46 changed files with 2337 additions and 1278 deletions

View File

@@ -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;
}
}