Skip to content

Commit f5c19f8

Browse files
committed
mj-liquid in visual email editor
1 parent 15661bd commit f5c19f8

22 files changed

Lines changed: 306 additions & 30 deletions

.claude/scripts/gemini.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
set -e
77

8-
MODEL="gemini-3-pro-preview"
8+
MODEL="gemini-3.1-pro-preview"
99
API_URL="https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent"
1010

1111
if [ -z "$GEMINI_API_KEY" ]; then

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
99
- **Demo**: Demo workspace now includes French and Spanish translations for all 4 email templates, showcasing the multi-language feature
1010
- **Templates**: Downloaded template files now use the template's name as filename instead of a generic name (#286)
1111
- **Email Builder**: Added `mj-liquid` block type for embedding raw MJML+Liquid code in the visual editor, enabling dynamic structural content like for-loops generating columns or conditional sections
12+
- **Security**: Upgraded liquidjs to 10.25.0
1213

1314
## [28.0] - 2026-03-05
1415

console/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

console/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"highlight.js": "^11.10.0",
7474
"html2canvas": "^1.4.1",
7575
"is-hotkey": "^0.2.0",
76-
"liquidjs": "^10.24.0",
76+
"liquidjs": "^10.25.0",
7777
"lodash": "^4.17.23",
7878
"lodash.throttle": "^4.1.1",
7979
"lowlight": "^3.3.0",

console/src/__tests__/SignInPage.test.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import { App } from 'antd'
99
vi.mock('../services/api/auth', () => ({
1010
authService: {
1111
signIn: vi.fn(),
12-
verifyCode: vi.fn()
13-
}
12+
verifyCode: vi.fn(),
13+
getCurrentUser: vi.fn().mockRejectedValue(new Error('Not authenticated')),
14+
logout: vi.fn().mockResolvedValue(undefined)
15+
},
16+
isRootUser: vi.fn().mockReturnValue(false)
1417
}))
1518

1619
// Mock the navigate function and useSearch

console/src/__tests__/i18n.test.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,33 @@ const catalanMessages = {
6363
'Welcome {name}': 'Benvingut {name}',
6464
}
6565

66+
const portugueseBRMessages = {
67+
Hello: 'Olá',
68+
Goodbye: 'Tchau',
69+
'Welcome {name}': 'Bem-vindo {name}',
70+
}
71+
72+
const japaneseMessages = {
73+
Hello: 'こんにちは',
74+
Goodbye: 'さようなら',
75+
'Welcome {name}': 'ようこそ {name}',
76+
}
77+
78+
const italianMessages = {
79+
Hello: 'Ciao',
80+
Goodbye: 'Arrivederci',
81+
'Welcome {name}': 'Benvenuto {name}',
82+
}
83+
6684
// Mock the dynamic imports for locale files
6785
vi.mock('../i18n/locales/en.po', () => ({ messages: englishMessages }))
6886
vi.mock('../i18n/locales/fr.po', () => ({ messages: frenchMessages }))
6987
vi.mock('../i18n/locales/es.po', () => ({ messages: spanishMessages }))
7088
vi.mock('../i18n/locales/de.po', () => ({ messages: germanMessages }))
7189
vi.mock('../i18n/locales/ca.po', () => ({ messages: catalanMessages }))
90+
vi.mock('../i18n/locales/pt-BR.po', () => ({ messages: portugueseBRMessages }))
91+
vi.mock('../i18n/locales/ja.po', () => ({ messages: japaneseMessages }))
92+
vi.mock('../i18n/locales/it.po', () => ({ messages: italianMessages }))
7293

7394
describe('i18n utility functions', () => {
7495
beforeEach(() => {
@@ -141,7 +162,7 @@ describe('i18n utility functions', () => {
141162

142163
describe('locales and localeNames', () => {
143164
it('exports all supported locales', () => {
144-
expect(locales).toEqual(['en', 'fr', 'es', 'de', 'ca'])
165+
expect(locales).toEqual(['en', 'fr', 'es', 'de', 'ca', 'pt-BR', 'ja', 'it'])
145166
})
146167

147168
it('exports locale names for all locales', () => {
@@ -151,6 +172,9 @@ describe('i18n utility functions', () => {
151172
es: 'Español',
152173
de: 'Deutsch',
153174
ca: 'Català',
175+
'pt-BR': 'Português (Brasil)',
176+
ja: '日本語',
177+
it: 'Italiano',
154178
})
155179
})
156180

@@ -226,7 +250,7 @@ describe('LocaleContext', () => {
226250
expect(screen.getByTestId('is-loading')).toHaveTextContent('ready')
227251
})
228252

229-
expect(screen.getByTestId('locale-count')).toHaveTextContent('5')
253+
expect(screen.getByTestId('locale-count')).toHaveTextContent('8')
230254
})
231255

232256
it('provides locale name for current locale', async () => {

console/src/__tests__/setup.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,25 @@ export function TestI18nWrapper({ children }: { children: ReactNode }) {
3737
return <I18nProvider i18n={i18n}>{children}</I18nProvider>
3838
}
3939

40+
// Mock localStorage (jsdom doesn't provide full implementation)
41+
const localStorageMock = (() => {
42+
let store: Record<string, string> = {}
43+
return {
44+
getItem: vi.fn((key: string) => store[key] || null),
45+
setItem: vi.fn((key: string, value: string) => {
46+
store[key] = value
47+
}),
48+
removeItem: vi.fn((key: string) => {
49+
delete store[key]
50+
}),
51+
clear: vi.fn(() => {
52+
store = {}
53+
})
54+
}
55+
})()
56+
57+
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
58+
4059
// Mock window.matchMedia for Ant Design
4160
Object.defineProperty(window, 'matchMedia', {
4261
writable: true,

console/src/components/email_builder/EmailBlockClass.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,8 @@ export class EmailBlockClass {
463463
type !== 'mj-preview' &&
464464
type !== 'mj-title' &&
465465
type !== 'mj-style' &&
466-
type !== 'mj-raw'
466+
type !== 'mj-raw' &&
467+
type !== 'mj-liquid'
467468
) {
468469
block.children = []
469470
}
@@ -475,6 +476,7 @@ export class EmailBlockClass {
475476
type === 'mj-title' ||
476477
type === 'mj-preview' ||
477478
type === 'mj-raw' ||
479+
type === 'mj-liquid' ||
478480
type === 'mj-style'
479481
) {
480482
// For mj-text blocks, ensure content is wrapped in <p> tags (Tiptap always wraps in <p>)

console/src/components/email_builder/MjmlCodeEditor.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ const MJML_TAGS = [
156156
insertText: '<mj-raw>\n\t$0\n</mj-raw>',
157157
detail: 'Raw HTML'
158158
},
159+
{
160+
label: 'mj-liquid',
161+
insertText: '{% for item in $1 %}\n\t$0\n{% endfor %}',
162+
detail: 'Liquid template block'
163+
},
159164
{
160165
label: 'mj-head',
161166
insertText: '<mj-head>\n\t$0\n</mj-head>',

console/src/components/email_builder/blocks/EmailBlockFactory.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { MjTitleBlock } from './MjTitleBlock'
2020
import { MjHeadBlock } from './MjHeadBlock'
2121
import { MjAttributesBlock } from './MjAttributesBlock'
2222
import { MjRawBlock } from './MjRawBlock'
23+
import { MjLiquidBlock } from './MjLiquidBlock'
2324
import { MjSocialElementBlock } from './MjSocialElementBlock'
2425
// Import other block types as they're created
2526
// import { MjImageBlock } from './MjImageBlock'
@@ -52,6 +53,7 @@ export class EmailBlockFactory {
5253
['mj-preview', MjPreviewBlock],
5354
['mj-title', MjTitleBlock],
5455
['mj-raw', MjRawBlock],
56+
['mj-liquid', MjLiquidBlock],
5557
['mj-social-element', MjSocialElementBlock]
5658
])
5759

0 commit comments

Comments
 (0)