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
This commit is contained in:
@@ -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({
|
||||
</div>
|
||||
|
||||
{/* Editor */}
|
||||
<div className="overflow-y-auto max-h-[400px] min-h-[200px]">
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
<EditorContent editor={editor} />
|
||||
|
||||
{/* Variables Dropdown */}
|
||||
{/* Variables - Collapsible and Categorized */}
|
||||
{variables.length > 0 && (
|
||||
<div className="border-t bg-muted/30 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="variable-select" className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{__('Insert Variable:')}
|
||||
</Label>
|
||||
<Select onValueChange={(value) => insertVariable(value)}>
|
||||
<SelectTrigger id="variable-select" className="h-8 text-xs">
|
||||
<SelectValue placeholder={__('Choose a variable...')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{variables.map((variable) => (
|
||||
<SelectItem key={variable} value={variable} className="text-xs">
|
||||
{`{${variable}}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<details className="border-t bg-muted/30">
|
||||
<summary className="p-3 text-xs text-muted-foreground cursor-pointer hover:bg-muted/50 flex items-center gap-2 select-none">
|
||||
<span className="text-[10px]">▶</span>
|
||||
{__('Insert Variable')}
|
||||
<span className="text-[10px] opacity-60">({variables.length})</span>
|
||||
</summary>
|
||||
<div className="p-3 pt-0 space-y-3">
|
||||
{/* Order Variables */}
|
||||
{variables.some(v => v.startsWith('order')) && (
|
||||
<div>
|
||||
<div className="text-[10px] uppercase text-muted-foreground mb-1.5 font-medium">{__('Order')}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{variables.filter(v => v.startsWith('order')).map((variable) => (
|
||||
<button
|
||||
key={variable}
|
||||
type="button"
|
||||
onClick={() => insertVariable(variable)}
|
||||
className="text-[11px] px-1.5 py-0.5 bg-blue-50 text-blue-700 rounded hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
{`{${variable}}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Customer Variables */}
|
||||
{variables.some(v => v.startsWith('customer') || v.includes('_name') && !v.startsWith('order') && !v.startsWith('site')) && (
|
||||
<div>
|
||||
<div className="text-[10px] uppercase text-muted-foreground mb-1.5 font-medium">{__('Customer')}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{variables.filter(v => v.startsWith('customer') || (v.includes('address') && !v.startsWith('shipping'))).map((variable) => (
|
||||
<button
|
||||
key={variable}
|
||||
type="button"
|
||||
onClick={() => insertVariable(variable)}
|
||||
className="text-[11px] px-1.5 py-0.5 bg-green-50 text-green-700 rounded hover:bg-green-100 transition-colors"
|
||||
>
|
||||
{`{${variable}}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Shipping/Payment Variables */}
|
||||
{variables.some(v => v.startsWith('shipping') || v.startsWith('payment') || v.startsWith('tracking')) && (
|
||||
<div>
|
||||
<div className="text-[10px] uppercase text-muted-foreground mb-1.5 font-medium">{__('Shipping & Payment')}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{variables.filter(v => v.startsWith('shipping') || v.startsWith('payment') || v.startsWith('tracking')).map((variable) => (
|
||||
<button
|
||||
key={variable}
|
||||
type="button"
|
||||
onClick={() => insertVariable(variable)}
|
||||
className="text-[11px] px-1.5 py-0.5 bg-orange-50 text-orange-700 rounded hover:bg-orange-100 transition-colors"
|
||||
>
|
||||
{`{${variable}}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Store/Site Variables */}
|
||||
{variables.some(v => v.startsWith('site') || v.startsWith('store') || v.startsWith('shop') || v.includes('_url') || v.startsWith('support') || v.startsWith('review')) && (
|
||||
<div>
|
||||
<div className="text-[10px] uppercase text-muted-foreground mb-1.5 font-medium">{__('Store & Links')}</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{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
|
||||
key={variable}
|
||||
type="button"
|
||||
onClick={() => insertVariable(variable)}
|
||||
className="text-[11px] px-1.5 py-0.5 bg-purple-50 text-purple-700 rounded hover:bg-purple-100 transition-colors"
|
||||
>
|
||||
{`{${variable}}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{/* Button Dialog */}
|
||||
<Dialog open={buttonDialogOpen} onOpenChange={setButtonDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{__('Insert Button')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -336,53 +390,55 @@ export function RichTextEditor({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="btn-text">{__('Button Text')}</Label>
|
||||
<Input
|
||||
id="btn-text"
|
||||
value={buttonText}
|
||||
onChange={(e) => setButtonText(e.target.value)}
|
||||
placeholder={__('e.g., View Order')}
|
||||
/>
|
||||
</div>
|
||||
<DialogBody>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="btn-text">{__('Button Text')}</Label>
|
||||
<Input
|
||||
id="btn-text"
|
||||
value={buttonText}
|
||||
onChange={(e) => setButtonText(e.target.value)}
|
||||
placeholder={__('e.g., View Order')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="btn-href">{__('Button Link')}</Label>
|
||||
<Input
|
||||
id="btn-href"
|
||||
value={buttonHref}
|
||||
onChange={(e) => setButtonHref(e.target.value)}
|
||||
placeholder="{order_url}"
|
||||
/>
|
||||
{variables.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{variables.filter(v => v.includes('_url')).map((variable) => (
|
||||
<code
|
||||
key={variable}
|
||||
className="text-xs bg-muted px-2 py-1 rounded cursor-pointer hover:bg-muted/80"
|
||||
onClick={() => setButtonHref(buttonHref + `{${variable}}`)}
|
||||
>
|
||||
{`{${variable}}`}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="btn-href">{__('Button Link')}</Label>
|
||||
<Input
|
||||
id="btn-href"
|
||||
value={buttonHref}
|
||||
onChange={(e) => setButtonHref(e.target.value)}
|
||||
placeholder="{order_url}"
|
||||
/>
|
||||
{variables.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{variables.filter(v => v.includes('_url')).map((variable) => (
|
||||
<code
|
||||
key={variable}
|
||||
className="text-xs bg-muted px-2 py-1 rounded cursor-pointer hover:bg-muted/80"
|
||||
onClick={() => setButtonHref(buttonHref + `{${variable}}`)}
|
||||
>
|
||||
{`{${variable}}`}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="btn-style">{__('Button Style')}</Label>
|
||||
<Select value={buttonStyle} onValueChange={(value: 'solid' | 'outline') => setButtonStyle(value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="solid">{__('Solid (Primary)')}</SelectItem>
|
||||
<SelectItem value="outline">{__('Outline (Secondary)')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="btn-style">{__('Button Style')}</Label>
|
||||
<Select value={buttonStyle} onValueChange={(value: 'solid' | 'outline') => setButtonStyle(value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="solid">{__('Solid (Primary)')}</SelectItem>
|
||||
<SelectItem value="outline">{__('Outline (Secondary)')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogBody>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setButtonDialogOpen(false)}>
|
||||
|
||||
Reference in New Issue
Block a user