fix: resolve container width issues, spa redirects, and appearance settings overwrite. feat: enhance order/sub details and newsletter layout

This commit is contained in:
Dwindi Ramadhana
2026-02-05 00:09:40 +07:00
parent a0b5f8496d
commit 5f08c18ec7
77 changed files with 7027 additions and 4546 deletions

View File

@@ -89,7 +89,7 @@ export function BlockRenderer({
return (
<div style={cardStyles[block.cardType]}>
<div
className="prose prose-sm max-w-none [&_h1]:text-3xl [&_h1]:font-bold [&_h1]:mt-0 [&_h1]:mb-4 [&_h2]:text-2xl [&_h2]:font-bold [&_h2]:mt-0 [&_h2]:mb-3 [&_h3]:text-xl [&_h3]:font-bold [&_h3]:mt-0 [&_h3]:mb-2 [&_h4]:text-lg [&_h4]:font-bold [&_h4]:mt-0 [&_h4]:mb-2 [&_.button]:inline-block [&_.button]:bg-purple-600 [&_.button]:text-white [&_.button]:px-7 [&_.button]:py-3.5 [&_.button]:rounded-md [&_.button]:no-underline [&_.button]:font-semibold [&_.button-outline]:inline-block [&_.button-outline]:bg-transparent [&_.button-outline]:text-purple-600 [&_.button-outline]:px-6 [&_.button-outline]:py-3 [&_.button-outline]:rounded-md [&_.button-outline]:no-underline [&_.button-outline]:font-semibold [&_.button-outline]:border-2 [&_.button-outline]:border-purple-600"
className="prose prose-sm max-w-none [&_h1]:text-3xl [&_h1]:font-bold [&_h1]:mt-0 [&_h1]:mb-4 [&_h2]:text-2xl [&_h2]:font-bold [&_h2]:mt-0 [&_h2]:mb-3 [&_h3]:text-xl [&_h3]:font-bold [&_h3]:mt-0 [&_h3]:mb-2 [&_h4]:text-lg [&_h4]:font-bold [&_h4]:mt-0 [&_h4]:mb-2 [&_.button]:inline-block [&_.button]:bg-purple-600 [&_.button]:text-white [&_.button]:px-7 [&_.button]:py-3.5 [&_.button]:rounded-md [&_.button]:no-underline [&_.button]:font-semibold [&_.button-outline]:inline-block [&_.button-outline]:bg-transparent [&_.button-outline]:text-purple-600 [&_.button-outline]:px-6 [&_.button-outline]:py-3 [&_.button-outline]:rounded-md [&_.button-outline]:no-underline [&_.button-outline]:font-semibold [&_.button-outline]:border-2 [&_.button-outline]:border-purple-600 [&_.text-link]:text-purple-600 [&_.text-link]:underline"
style={block.cardType === 'hero' ? { color: '#fff' } : {}}
dangerouslySetInnerHTML={{ __html: htmlContent }}
/>
@@ -97,17 +97,17 @@ export function BlockRenderer({
);
case 'button': {
const buttonStyle: React.CSSProperties = block.style === 'solid'
? {
display: 'inline-block',
background: 'var(--wn-primary, #7f54b3)',
color: '#fff',
padding: '14px 28px',
borderRadius: '6px',
textDecoration: 'none',
fontWeight: 600,
}
: {
// Different styles based on button type
let buttonStyle: React.CSSProperties;
if (block.style === 'link') {
// Plain link style - just underlined text
buttonStyle = {
color: 'var(--wn-primary, #7f54b3)',
textDecoration: 'underline',
};
} else if (block.style === 'outline') {
buttonStyle = {
display: 'inline-block',
background: 'transparent',
color: 'var(--wn-secondary, #7f54b3)',
@@ -117,18 +117,33 @@ export function BlockRenderer({
textDecoration: 'none',
fontWeight: 600,
};
} else {
// Solid style (default)
buttonStyle = {
display: 'inline-block',
background: 'var(--wn-primary, #7f54b3)',
color: '#fff',
padding: '14px 28px',
borderRadius: '6px',
textDecoration: 'none',
fontWeight: 600,
};
}
const containerStyle: React.CSSProperties = {
textAlign: block.align || 'center',
};
if (block.widthMode === 'full') {
buttonStyle.display = 'block';
buttonStyle.width = '100%';
buttonStyle.textAlign = 'center';
} else if (block.widthMode === 'custom' && block.customMaxWidth) {
buttonStyle.maxWidth = `${block.customMaxWidth}px`;
buttonStyle.width = '100%';
// Width modes don't apply to plain links
if (block.style !== 'link') {
if (block.widthMode === 'full') {
buttonStyle.display = 'block';
buttonStyle.width = '100%';
buttonStyle.textAlign = 'center';
} else if (block.widthMode === 'custom' && block.customMaxWidth) {
buttonStyle.maxWidth = `${block.customMaxWidth}px`;
buttonStyle.width = '100%';
}
}
return (

View File

@@ -101,7 +101,6 @@ 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') {
@@ -123,7 +122,6 @@ export function EmailBuilder({ blocks, onChange, variables = [] }: EmailBuilderP
setEditingAlign(block.align);
}
console.log('[EmailBuilder] Setting editDialogOpen to true');
setEditDialogOpen(true);
};

View File

@@ -2,7 +2,7 @@ export type BlockType = 'card' | 'button' | 'divider' | 'spacer' | 'image';
export type CardType = 'default' | 'success' | 'info' | 'warning' | 'hero' | 'basic';
export type ButtonStyle = 'solid' | 'outline';
export type ButtonStyle = 'solid' | 'outline' | 'link';
export type ContentWidth = 'fit' | 'full' | 'custom';

View File

@@ -0,0 +1,10 @@
import { Navigate, useParams } from 'react-router-dom';
/**
* Legacy redirect for campaign details
* Redirects /marketing/campaigns/:id -> /marketing/newsletter/campaigns/:id
*/
export function LegacyCampaignRedirect() {
const { id } = useParams();
return <Navigate to={`/marketing/newsletter/campaigns/${id}`} replace />;
}

View File

@@ -20,7 +20,7 @@ function fmt(d: Date): string {
}
export default function DateRange({ value, onChange }: Props) {
const [preset, setPreset] = useState<string>(() => "last7");
const [preset, setPreset] = useState<string>(() => "last30");
const [start, setStart] = useState<string | undefined>(value?.date_start);
const [end, setEnd] = useState<string | undefined>(value?.date_end);
@@ -32,8 +32,8 @@ export default function DateRange({ value, onChange }: Props) {
return {
today: { date_start: todayStr, date_end: todayStr },
last7: { date_start: fmt(last7), date_end: todayStr },
last30:{ date_start: fmt(last30), date_end: todayStr },
custom:{ date_start: start, date_end: end },
last30: { date_start: fmt(last30), date_end: todayStr },
custom: { date_start: start, date_end: end },
};
}, [start, end]);
@@ -41,7 +41,7 @@ export default function DateRange({ value, onChange }: Props) {
if (preset === "custom") {
onChange?.({ date_start: start, date_end: end, preset });
} else {
const pr = (presets as any)[preset] || presets.last7;
const pr = (presets as any)[preset] || presets.last30;
onChange?.({ ...pr, preset });
setStart(pr.date_start);
setEnd(pr.date_end);
@@ -53,7 +53,7 @@ export default function DateRange({ value, onChange }: Props) {
<div className="flex flex-col lg:flex-row gap-2 w-full">
<Select value={preset} onValueChange={(v) => setPreset(v)}>
<SelectTrigger className="w-full">
<SelectValue placeholder={__("Last 7 days")} />
<SelectValue placeholder={__("Last 30 days")} />
</SelectTrigger>
<SelectContent position="popper" className="z-[1000]">
<SelectGroup>

View File

@@ -86,7 +86,6 @@ export function RichTextEditor({
const currentContent = editor.getHTML();
// Only update if content is different (avoid infinite loops)
if (content !== currentContent) {
console.log('RichTextEditor: Updating content', { content, currentContent });
editor.commands.setContent(content);
}
}
@@ -113,7 +112,7 @@ export function RichTextEditor({
const [buttonDialogOpen, setButtonDialogOpen] = useState(false);
const [buttonText, setButtonText] = useState('Click Here');
const [buttonHref, setButtonHref] = useState('{order_url}');
const [buttonStyle, setButtonStyle] = useState<'solid' | 'outline'>('solid');
const [buttonStyle, setButtonStyle] = useState<'solid' | 'outline' | 'link'>('solid');
const [isEditingButton, setIsEditingButton] = useState(false);
const [editingButtonPos, setEditingButtonPos] = useState<number | null>(null);
@@ -388,12 +387,12 @@ export function RichTextEditor({
</div>
</div>
)}
{/* Customer Variables */}
{variables.some(v => v.startsWith('customer') || v.includes('_name') && !v.startsWith('order') && !v.startsWith('site')) && (
{/* Subscriber/Customer Variables */}
{variables.some(v => v.startsWith('customer') || v.startsWith('subscriber') || (v.includes('_name') && !v.startsWith('order') && !v.startsWith('site') && !v.startsWith('store'))) && (
<div>
<div className="text-[10px] uppercase text-muted-foreground mb-1.5 font-medium">{__('Customer')}</div>
<div className="text-[10px] uppercase text-muted-foreground mb-1.5 font-medium">{__('Subscriber')}</div>
<div className="flex flex-wrap gap-1">
{variables.filter(v => v.startsWith('customer') || (v.includes('address') && !v.startsWith('shipping'))).map((variable) => (
{variables.filter(v => v.startsWith('customer') || v.startsWith('subscriber') || (v.includes('address') && !v.startsWith('shipping'))).map((variable) => (
<button
key={variable}
type="button"
@@ -425,11 +424,11 @@ export function RichTextEditor({
</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')) && (
{variables.some(v => v.startsWith('site') || v.startsWith('store') || v.startsWith('shop') || v.startsWith('current') || 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) => (
{variables.filter(v => v.startsWith('site') || v.startsWith('store') || v.startsWith('shop') || v.startsWith('current') || 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"
@@ -501,13 +500,14 @@ export function RichTextEditor({
<div className="space-y-2">
<Label htmlFor="btn-style">{__('Button Style')}</Label>
<Select value={buttonStyle} onValueChange={(value: 'solid' | 'outline') => setButtonStyle(value)}>
<Select value={buttonStyle} onValueChange={(value: 'solid' | 'outline' | 'link') => setButtonStyle(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="solid">{__('Solid (Primary)')}</SelectItem>
<SelectItem value="outline">{__('Outline (Secondary)')}</SelectItem>
<SelectItem value="link">{__('Plain Link')}</SelectItem>
</SelectContent>
</Select>
</div>

View File

@@ -7,7 +7,7 @@ export interface ButtonOptions {
declare module '@tiptap/core' {
interface Commands<ReturnType> {
button: {
setButton: (options: { text: string; href: string; style?: 'solid' | 'outline' }) => ReturnType;
setButton: (options: { text: string; href: string; style?: 'solid' | 'outline' | 'link' }) => ReturnType;
};
}
}
@@ -70,20 +70,27 @@ export const ButtonExtension = Node.create<ButtonOptions>({
renderHTML({ HTMLAttributes }) {
const { text, href, style } = HTMLAttributes;
// Simple link styling - no fancy button appearance in editor
// The actual button styling happens in email rendering (EmailRenderer.php)
// In editor, just show as a styled link (differentiable from regular links)
// Different styling based on button style
let inlineStyle: string;
if (style === 'link') {
// Plain link - just underlined text, no button-like appearance
inlineStyle = 'color: #7f54b3; text-decoration: underline; cursor: pointer;';
} else {
// Solid/Outline buttons - show as styled link with background hint
inlineStyle = 'color: #7f54b3; text-decoration: underline; cursor: pointer; font-weight: 600; background: rgba(127,84,179,0.1); padding: 2px 6px; border-radius: 3px;';
}
return [
'a',
mergeAttributes(this.options.HTMLAttributes, {
href,
class: 'button-node',
style: 'color: #7f54b3; text-decoration: underline; cursor: pointer; font-weight: 600; background: rgba(127,84,179,0.1); padding: 2px 6px; border-radius: 3px;',
class: style === 'link' ? 'link-node' : 'button-node',
style: inlineStyle,
'data-button': '',
'data-text': text,
'data-href': href,
'data-style': style,
title: `Button: ${text}${href}`,
title: style === 'link' ? `Link: ${text}` : `Button: ${text}${href}`,
}),
text,
];