From 875ab7af342299c4b783b112561c606ad0713cc5 Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Thu, 1 Jan 2026 01:53:22 +0700 Subject: [PATCH] fix: dialog portal scope + UX improvements 1. Dialog Portal: Render inside #woonoow-admin-app container instead of document.body to fix Tailwind CSS scoping in WordPress admin 2. Variables Panel: Redesigned from flat list to collapsible accordion - Collapsed by default (less visual noise) - Categorized: Order (blue), Customer (green), Shipping (orange), Store (purple) - Color-coded pills for quick recognition - Shows count of available variables 3. StarterKit: Disable built-in Link to prevent duplicate extension warning --- .../components/EmailBuilder/EmailBuilder.tsx | 3 + admin-spa/src/components/ui/dialog.tsx | 75 ++++-- .../src/components/ui/rich-text-editor.tsx | 214 +++++++++++------- 3 files changed, 192 insertions(+), 100 deletions(-) diff --git a/admin-spa/src/components/EmailBuilder/EmailBuilder.tsx b/admin-spa/src/components/EmailBuilder/EmailBuilder.tsx index adf41f8..21e682e 100644 --- a/admin-spa/src/components/EmailBuilder/EmailBuilder.tsx +++ b/admin-spa/src/components/EmailBuilder/EmailBuilder.tsx @@ -101,11 +101,13 @@ export function EmailBuilder({ blocks, onChange, variables = [] }: EmailBuilderP }; const openEditDialog = (block: EmailBlock) => { + console.log('[EmailBuilder] openEditDialog called', { blockId: block.id, blockType: block.type }); setEditingBlockId(block.id); if (block.type === 'card') { // Convert markdown to HTML for rich text editor const htmlContent = parseMarkdownBasics(block.content); + console.log('[EmailBuilder] Card content parsed', { original: block.content, html: htmlContent }); setEditingContent(htmlContent); setEditingCardType(block.cardType); } else if (block.type === 'button') { @@ -122,6 +124,7 @@ export function EmailBuilder({ blocks, onChange, variables = [] }: EmailBuilderP setEditingAlign(block.align); } + console.log('[EmailBuilder] Setting editDialogOpen to true'); setEditDialogOpen(true); }; diff --git a/admin-spa/src/components/ui/dialog.tsx b/admin-spa/src/components/ui/dialog.tsx index 3e3dc51..1e67770 100644 --- a/admin-spa/src/components/ui/dialog.tsx +++ b/admin-spa/src/components/ui/dialog.tsx @@ -30,25 +30,43 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - {children} - - - Close - - - -)) +>(({ className, children, ...props }, ref) => { + // Get or create portal container inside the app for proper CSS scoping + const getPortalContainer = () => { + const appContainer = document.getElementById('woonoow-admin-app'); + if (!appContainer) return document.body; + + let portalRoot = document.getElementById('woonoow-dialog-portal'); + if (!portalRoot) { + portalRoot = document.createElement('div'); + portalRoot.id = 'woonoow-dialog-portal'; + appContainer.appendChild(portalRoot); + } + return portalRoot; + }; + + return ( + + + e.preventDefault()} + onInteractOutside={(e) => e.preventDefault()} + className={cn( + "fixed left-[50%] top-[50%] z-[99999] flex flex-col w-full max-w-lg max-h-[90vh] translate-x-[-50%] translate-y-[-50%] border bg-background shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", + className + )} + {...props} + > + {children} + + + Close + + + + ); +}) DialogContent.displayName = DialogPrimitive.Content.displayName const DialogHeader = ({ @@ -57,7 +75,7 @@ const DialogHeader = ({ }: React.HTMLAttributes) => (
) => (
) => ( +
+) +DialogBody.displayName = "DialogBody" + export { Dialog, DialogPortal, @@ -117,4 +149,5 @@ export { DialogFooter, DialogTitle, DialogDescription, + DialogBody, } diff --git a/admin-spa/src/components/ui/rich-text-editor.tsx b/admin-spa/src/components/ui/rich-text-editor.tsx index 4e39421..e3571c8 100644 --- a/admin-spa/src/components/ui/rich-text-editor.tsx +++ b/admin-spa/src/components/ui/rich-text-editor.tsx @@ -25,7 +25,7 @@ import { Button } from './button'; import { Input } from './input'; import { Label } from './label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select'; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './dialog'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogBody } from './dialog'; import { __ } from '@/lib/i18n'; interface RichTextEditorProps { @@ -45,11 +45,8 @@ export function RichTextEditor({ }: RichTextEditorProps) { const editor = useEditor({ extensions: [ - // StarterKit 3.10+ includes Link by default, so disable it here - // since we configure Link separately below with custom options - StarterKit.configure({ - link: false, - }), + // StarterKit 3.10+ includes Link by default, disable since we configure separately + StarterKit.configure({ link: false }), Placeholder.configure({ placeholder, }), @@ -94,12 +91,9 @@ export function RichTextEditor({ useEffect(() => { if (editor && content) { const currentContent = editor.getHTML(); - // Normalize whitespace before comparing to avoid infinite loops - // The editor normalizes HTML, so "\n" becomes "" in output - const normalizedContent = content.replace(/\s+/g, ' ').trim(); - const normalizedCurrent = currentContent.replace(/\s+/g, ' ').trim(); - - if (normalizedContent !== normalizedCurrent) { + // Only update if content is different (avoid infinite loops) + if (content !== currentContent) { + console.log('RichTextEditor: Updating content', { content, currentContent }); editor.commands.setContent(content); } } @@ -299,36 +293,96 @@ export function RichTextEditor({
{/* Editor */} -
- -
+ - {/* Variables Dropdown */} + {/* Variables - Collapsible and Categorized */} {variables.length > 0 && ( -
-
- - +
+ + + {__('Insert Variable')} + ({variables.length}) + +
+ {/* Order Variables */} + {variables.some(v => v.startsWith('order')) && ( +
+
{__('Order')}
+
+ {variables.filter(v => v.startsWith('order')).map((variable) => ( + + ))} +
+
+ )} + {/* Customer Variables */} + {variables.some(v => v.startsWith('customer') || v.includes('_name') && !v.startsWith('order') && !v.startsWith('site')) && ( +
+
{__('Customer')}
+
+ {variables.filter(v => v.startsWith('customer') || (v.includes('address') && !v.startsWith('shipping'))).map((variable) => ( + + ))} +
+
+ )} + {/* Shipping/Payment Variables */} + {variables.some(v => v.startsWith('shipping') || v.startsWith('payment') || v.startsWith('tracking')) && ( +
+
{__('Shipping & Payment')}
+
+ {variables.filter(v => v.startsWith('shipping') || v.startsWith('payment') || v.startsWith('tracking')).map((variable) => ( + + ))} +
+
+ )} + {/* Store/Site Variables */} + {variables.some(v => v.startsWith('site') || v.startsWith('store') || v.startsWith('shop') || v.includes('_url') || v.startsWith('support') || v.startsWith('review')) && ( +
+
{__('Store & Links')}
+
+ {variables.filter(v => v.startsWith('site') || v.startsWith('store') || v.startsWith('shop') || v.startsWith('my_account') || v.startsWith('support') || v.startsWith('review') || (v.includes('_url') && !v.startsWith('order') && !v.startsWith('tracking') && !v.startsWith('payment'))).map((variable) => ( + + ))} +
+
+ )}
-
+ )} {/* Button Dialog */} - + {__('Insert Button')} @@ -336,53 +390,55 @@ export function RichTextEditor({ -
-
- - setButtonText(e.target.value)} - placeholder={__('e.g., View Order')} - /> -
+ +
+
+ + setButtonText(e.target.value)} + placeholder={__('e.g., View Order')} + /> +
-
- - setButtonHref(e.target.value)} - placeholder="{order_url}" - /> - {variables.length > 0 && ( -
- {variables.filter(v => v.includes('_url')).map((variable) => ( - setButtonHref(buttonHref + `{${variable}}`)} - > - {`{${variable}}`} - - ))} -
- )} -
+
+ + setButtonHref(e.target.value)} + placeholder="{order_url}" + /> + {variables.length > 0 && ( +
+ {variables.filter(v => v.includes('_url')).map((variable) => ( + setButtonHref(buttonHref + `{${variable}}`)} + > + {`{${variable}}`} + + ))} +
+ )} +
-
- - +
+ + +
-
+