fix: resolve container width issues, spa redirects, and appearance settings overwrite. feat: enhance order/sub details and newsletter layout
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
10
admin-spa/src/components/LegacyCampaignRedirect.tsx
Normal file
10
admin-spa/src/components/LegacyCampaignRedirect.tsx
Normal 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 />;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user