501 lines
22 KiB
Vue
501 lines
22 KiB
Vue
<template>
|
|
<q-page>
|
|
<!-- Top bar: template selector + saved chip + quick actions. The Unlayer
|
|
editor below has its own toolbar for blocks/preview/etc. -->
|
|
<div class="row items-center q-px-md q-py-sm bg-white" style="border-bottom:1px solid #e5e7eb">
|
|
<q-btn flat dense round icon="arrow_back" :to="'/campaigns'" class="q-mr-sm" />
|
|
<div class="text-h6 q-mr-md">Éditeur de template</div>
|
|
<q-select v-model="currentName" :options="templateOptions" emit-value map-options dense outlined
|
|
style="min-width:240px" @update:model-value="onSelectTemplate" />
|
|
<q-btn flat dense icon="add" label="Nouveau" color="primary" class="q-ml-sm" @click="newTemplateOpen = true">
|
|
<q-tooltip>Créer un nouveau template (vide ou copié depuis un existant)</q-tooltip>
|
|
</q-btn>
|
|
<q-chip v-if="lastSavedTs" dense size="sm" color="grey-2" text-color="grey-9" class="q-ml-sm" icon="cloud_done">
|
|
Sauvegardé · {{ lastSavedLabel }}
|
|
</q-chip>
|
|
<q-btn flat dense icon="code" class="q-ml-sm" color="grey-7">
|
|
<q-tooltip max-width="320px">
|
|
<strong>9 variables disponibles</strong> (Client / Offre / Système).
|
|
Insertion : clic dans un texte → barre flottante → icône <code>{}</code> Merge Tags.
|
|
Marche aussi dans les champs URL (boutons, images, mailto).
|
|
</q-tooltip>
|
|
</q-btn>
|
|
<q-space />
|
|
<q-btn flat color="primary" icon="visibility" label="Aperçu inbox" class="q-mr-sm" @click="openPreview">
|
|
<q-tooltip>Voir le HTML rendu (substitué) tel que reçu par le destinataire</q-tooltip>
|
|
</q-btn>
|
|
<q-btn flat color="purple-7" icon="translate" label="Traduire (AI)" class="q-mr-sm"
|
|
:disable="!aiTranslateTargetName" @click="aiTranslateOpen = true">
|
|
<q-tooltip>{{ aiTranslateTargetName ? `Traduire vers ${aiTranslateTargetName} via Gemini` : 'Disponible pour les templates avec suffixe -fr ou -en' }}</q-tooltip>
|
|
</q-btn>
|
|
<q-btn flat color="orange-9" icon="send" label="Envoyer un test" class="q-mr-sm" @click="testSendOpen = true">
|
|
<q-tooltip>Envoyer un courriel réel à une adresse de test</q-tooltip>
|
|
</q-btn>
|
|
<q-btn unelevated color="primary" icon="save" label="Enregistrer"
|
|
:loading="saving" @click="saveTemplate" />
|
|
</div>
|
|
|
|
<!-- Unlayer editor — Vue 3 native, no iframe, full features:
|
|
responsive preview, AMP blocks, Unsplash, file manager, dark mode,
|
|
layers/structure panel, design tokens, etc.
|
|
Wrapped in an explicit-sized container so the inner iframe gets
|
|
enough height/width (Quasar's q-page doesn't propagate dimensions
|
|
that easyEditor's nested iframe can pick up automatically). -->
|
|
<div style="height: calc(100vh - 60px); width: 100%; overflow: hidden; position: relative;">
|
|
<EmailEditor
|
|
ref="editor"
|
|
:options="editorOptions"
|
|
:min-height="'100%'"
|
|
style="height: 100%; width: 100%;"
|
|
@load="onEditorLoad"
|
|
@ready="onEditorReady"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Aperçu dialog — renders the latest saved HTML with sample vars -->
|
|
<q-dialog v-model="previewOpen" maximized persistent>
|
|
<q-card class="bg-grey-2">
|
|
<q-toolbar class="bg-white" style="border-bottom:1px solid #e5e7eb">
|
|
<q-icon name="visibility" class="q-mr-sm" />
|
|
<q-toolbar-title>Aperçu inbox · {{ currentName }}</q-toolbar-title>
|
|
<q-btn flat dense round icon="close" @click="previewOpen = false" />
|
|
</q-toolbar>
|
|
<q-banner v-if="previewLoading" class="bg-blue-1 text-blue-9"><q-spinner size="sm" /> Rendu en cours…</q-banner>
|
|
<q-card-section style="height: calc(100vh - 60px); overflow:hidden">
|
|
<iframe :srcdoc="previewHtmlContent" style="width:100%; height:100%; border:1px solid #e5e7eb; background:#fff;" />
|
|
</q-card-section>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<!-- New template dialog — name + starter -->
|
|
<q-dialog v-model="newTemplateOpen" persistent>
|
|
<q-card style="min-width: 500px; max-width: 90vw">
|
|
<q-card-section class="row items-center bg-blue-1 text-blue-9">
|
|
<q-icon name="add" class="q-mr-sm" />
|
|
<div class="text-h6">Nouveau template</div>
|
|
<q-space />
|
|
<q-btn flat dense round icon="close" v-close-popup />
|
|
</q-card-section>
|
|
<q-card-section>
|
|
<q-input v-model="newTemplateForm.suffix" outlined dense autofocus
|
|
label="Nom (suffixe après gift-email-)" :prefix="newTemplateForm.prefix + '-'"
|
|
:rules="[v => /^[a-z0-9-]+$/.test(v) || 'Lettres minuscules, chiffres et tirets seulement']"
|
|
hint="Exemple: summer-2026, automne-promo, anniversaire" class="q-mb-md" />
|
|
<q-select v-model="newTemplateForm.prefix" :options="['gift-email','newsletter','transactional']"
|
|
label="Type" outlined dense class="q-mb-md" />
|
|
<q-select v-model="newTemplateForm.starter" :options="starterOptions" emit-value map-options
|
|
label="Démarrer depuis" outlined dense class="q-mb-md" />
|
|
<q-banner v-if="newTemplateFinal" class="bg-grey-2 text-grey-8 q-mt-sm" rounded dense>
|
|
<q-icon name="info" class="q-mr-xs" />
|
|
Le template sera créé sous le nom <strong>{{ newTemplateFinal }}</strong>
|
|
<span v-if="newTemplateForm.starter !== 'blank'">
|
|
· contenu copié depuis <em>{{ newTemplateForm.starter }}</em>
|
|
</span>
|
|
</q-banner>
|
|
</q-card-section>
|
|
<q-card-actions align="right">
|
|
<q-btn flat label="Annuler" v-close-popup />
|
|
<q-btn unelevated color="primary" icon="add" label="Créer le template"
|
|
:loading="creating" :disable="!newTemplateValid" @click="createTemplate" />
|
|
</q-card-actions>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<!-- AI translate dialog -->
|
|
<q-dialog v-model="aiTranslateOpen" persistent>
|
|
<q-card style="min-width: 500px; max-width: 90vw">
|
|
<q-card-section class="row items-center bg-purple-1 text-purple-9">
|
|
<q-icon name="translate" class="q-mr-sm" />
|
|
<div class="text-h6">Traduire avec Gemini AI</div>
|
|
<q-space />
|
|
<q-btn flat dense round icon="close" v-close-popup />
|
|
</q-card-section>
|
|
<q-card-section>
|
|
<div class="q-mb-md">
|
|
Le contenu de <strong>{{ currentName }}</strong> sera traduit vers
|
|
<strong>{{ aiTranslateTargetName }}</strong> via Gemini Flash.
|
|
</div>
|
|
<q-banner class="bg-grey-2 text-grey-8 q-mb-md" rounded dense>
|
|
<q-icon name="info" class="q-mr-xs" />
|
|
<!-- v-pre on this span so Vue doesn't try to compile the literal
|
|
{{...}} braces inside the explanatory <code> tag. -->
|
|
<span v-pre>
|
|
Le AI préserve la structure HTML, les variables <code>{{...}}</code>,
|
|
les URLs, les noms de marque (TARGO, Giftbit) et les emojis.
|
|
Il traduit seulement le texte visible.
|
|
</span>
|
|
</q-banner>
|
|
<q-banner v-if="targetTemplateExists" class="bg-amber-1 text-amber-9 q-mb-md" rounded dense>
|
|
<q-icon name="warning" class="q-mr-xs" />
|
|
Le template <strong>{{ aiTranslateTargetName }}</strong> existe déjà.
|
|
La traduction va l'écraser (backup automatique avant).
|
|
<q-toggle v-model="aiTranslateOverride" label="Confirmer l'écrasement" class="q-mt-sm" />
|
|
</q-banner>
|
|
</q-card-section>
|
|
<q-card-actions align="right">
|
|
<q-btn flat label="Annuler" v-close-popup />
|
|
<q-btn unelevated color="purple-7" icon="translate" label="Traduire maintenant"
|
|
:loading="aiTranslating"
|
|
:disable="targetTemplateExists && !aiTranslateOverride"
|
|
@click="doAiTranslate" />
|
|
</q-card-actions>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<!-- Test-send dialog -->
|
|
<q-dialog v-model="testSendOpen" persistent>
|
|
<q-card style="min-width: 500px; max-width: 90vw">
|
|
<q-card-section class="row items-center bg-orange-1 text-orange-9">
|
|
<q-icon name="send" class="q-mr-sm" />
|
|
<div class="text-h6">Envoyer un test du courriel</div>
|
|
<q-space />
|
|
<q-btn flat dense round icon="close" v-close-popup />
|
|
</q-card-section>
|
|
<q-card-section>
|
|
<q-input v-model="testSendForm.to" label="Adresse de destination" outlined dense type="email" autofocus class="q-mb-md" />
|
|
<q-input v-model="testSendForm.subject" label="Sujet" outlined dense class="q-mb-md" />
|
|
<div class="text-subtitle2 q-mb-sm">Variables</div>
|
|
<div class="row q-col-gutter-sm">
|
|
<q-input v-model="testSendForm.vars.firstname" label="firstname" outlined dense class="col-6" />
|
|
<q-input v-model="testSendForm.vars.lastname" label="lastname" outlined dense class="col-6" />
|
|
<q-input v-model="testSendForm.vars.amount" label="amount" outlined dense class="col-6" />
|
|
<q-input v-model="testSendForm.vars.commitment_months" label="commitment_months" outlined dense class="col-6" />
|
|
<q-input v-model="testSendForm.vars.gift_url" label="gift_url" outlined dense class="col-12" />
|
|
<q-input v-model="testSendForm.vars.description" label="description" outlined dense class="col-12" />
|
|
<q-input v-model="testSendForm.vars.expiry" label="expiry" outlined dense class="col-12" />
|
|
</div>
|
|
</q-card-section>
|
|
<q-card-section class="bg-grey-2">
|
|
<div class="text-caption text-grey-8">
|
|
Sauvegarde l'éditeur d'abord pour tester la dernière version.
|
|
Envoyé via Mailjet depuis <code>TARGO <support@targointernet.com></code>.
|
|
</div>
|
|
</q-card-section>
|
|
<q-card-actions align="right">
|
|
<q-btn flat label="Annuler" v-close-popup />
|
|
<q-btn unelevated color="primary" icon-right="send" label="Envoyer le test"
|
|
:loading="testSending" :disable="!testSendForm.to" @click="doTestSend" />
|
|
</q-card-actions>
|
|
</q-card>
|
|
</q-dialog>
|
|
</q-page>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useQuasar } from 'quasar'
|
|
import { EmailEditor } from 'vue-email-editor'
|
|
import { listTemplates, getTemplate, saveTemplate as saveTemplateApi,
|
|
previewTemplate, testSendTemplate, translateTemplate } from 'src/api/campaigns'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const $q = useQuasar()
|
|
|
|
// ── Editor ref + Unlayer configuration ──────────────────────────────────────
|
|
const editor = ref(null)
|
|
const editorReady = ref(false)
|
|
const saving = ref(false)
|
|
const currentName = ref(route.params.name || 'gift-email-fr')
|
|
|
|
// Unlayer editor options:
|
|
// - mergeTags: shown in the "Merge tags" panel, drag-droppable into text
|
|
// - features: Unsplash, file manager, etc. are all on by default
|
|
// - tools: which block types to expose
|
|
// - appearance: light theme matching our ops UI
|
|
const editorOptions = {
|
|
appearance: {
|
|
theme: 'modern_light',
|
|
panels: {
|
|
tools: { dock: 'left' },
|
|
},
|
|
},
|
|
// Merge tags organized by category — Unlayer shows these in a dropdown
|
|
// when editing a text block (click into text → toolbar → {} icon) and
|
|
// ALSO in URL fields (Button "Action URL", Image "Source", mailto links).
|
|
// The `sample` field is what Unlayer shows as a preview (so the user sees
|
|
// realistic content while editing); on send, the hub's Mustache renderer
|
|
// substitutes the actual value.
|
|
mergeTags: [
|
|
{
|
|
name: 'Client',
|
|
mergeTags: [
|
|
{ name: 'Prénom', value: '{{firstname}}', sample: 'Louis' },
|
|
{ name: 'Nom de famille', value: '{{lastname}}', sample: 'Tremblay' },
|
|
{ name: 'Courriel', value: '{{email}}', sample: 'louis@targo.ca' },
|
|
{ name: 'Adresse service', value: '{{description}}', sample: '123 Rue de la Rivière, Ste-Clotilde' },
|
|
],
|
|
},
|
|
{
|
|
name: 'Offre',
|
|
mergeTags: [
|
|
{ name: 'Montant', value: '{{amount}}', sample: '60 $' },
|
|
{ name: 'Lien cadeau (URL)', value: '{{gift_url}}', sample: 'https://gft.link/abc' },
|
|
{ name: "Date d'expiration", value: '{{expiry}}', sample: '31 décembre 2026' },
|
|
{ name: 'Engagement (mois)', value: '{{commitment_months}}', sample: '3' },
|
|
],
|
|
},
|
|
{
|
|
name: 'Système',
|
|
mergeTags: [
|
|
{ name: 'Année courante', value: '{{year}}', sample: '2026' },
|
|
],
|
|
},
|
|
],
|
|
// Display mode: 'email' (default, with mobile preview), 'web' for landing pages
|
|
displayMode: 'email',
|
|
// Locale for built-in strings
|
|
locale: 'fr-CA',
|
|
// Enable optional sidebar features (free tier limits some — see Unlayer docs)
|
|
features: {
|
|
// Built-in Unlayer template library — limited selection without projectId
|
|
// but still gives the user some pre-built starts to pick from.
|
|
templates: true,
|
|
// Unsplash image search panel
|
|
stockImages: true,
|
|
// Image upload (uses Unlayer's CDN by default; can be wired to our hub
|
|
// /campaigns/assets/upload endpoint via customJS later if we want to
|
|
// self-host uploads — for now their CDN is fine for ad-hoc images)
|
|
imageEditor: true,
|
|
// Undo/redo + history
|
|
undoRedo: true,
|
|
},
|
|
// Use Unlayer's free CDN. For paid users this would carry a projectId.
|
|
// Without a projectId the "Powered by Unlayer" badge shows in the sidebar.
|
|
}
|
|
|
|
// ── Template list (selector at the top) ─────────────────────────────────────
|
|
const templates = ref([])
|
|
const templateOptions = computed(() => templates.value.map(t => ({
|
|
label: `${t.name} (${Math.round(t.size / 1024)} KB)`,
|
|
value: t.name,
|
|
})))
|
|
|
|
async function loadAvailableTemplates () {
|
|
templates.value = await listTemplates()
|
|
}
|
|
|
|
// ── AI translation (Gemini via hub) ─────────────────────────────────────────
|
|
// Auto-detect source language from the template name suffix (-fr / -en) and
|
|
// compute the target name with the OPPOSITE suffix.
|
|
const aiTranslateOpen = ref(false)
|
|
const aiTranslating = ref(false)
|
|
const aiTranslateOverride = ref(false)
|
|
|
|
const aiTranslateTargetName = computed(() => {
|
|
const m = (currentName.value || '').match(/^(.+)-(fr|en)$/)
|
|
if (!m) return ''
|
|
const opposite = m[2] === 'fr' ? 'en' : 'fr'
|
|
return `${m[1]}-${opposite}`
|
|
})
|
|
|
|
const targetTemplateExists = computed(() =>
|
|
!!aiTranslateTargetName.value && !!templates.value.find(t => t.name === aiTranslateTargetName.value),
|
|
)
|
|
|
|
async function doAiTranslate () {
|
|
if (!aiTranslateTargetName.value) return
|
|
aiTranslating.value = true
|
|
try {
|
|
const r = await translateTemplate(currentName.value, aiTranslateTargetName.value, {
|
|
override: aiTranslateOverride.value,
|
|
})
|
|
aiTranslateOpen.value = false
|
|
aiTranslateOverride.value = false
|
|
await loadAvailableTemplates()
|
|
$q.notify({
|
|
type: 'positive',
|
|
message: `Traduction terminée : ${r.source} → ${r.target}`,
|
|
caption: `${r.src_bytes} → ${r.out_bytes} octets (${r.from_lang} → ${r.to_lang})`,
|
|
timeout: 6000,
|
|
actions: [
|
|
{ label: 'Ouvrir', color: 'white', handler: () => { onSelectTemplate(r.target) } },
|
|
],
|
|
})
|
|
} catch (e) {
|
|
$q.notify({ type: 'negative', message: 'Échec traduction: ' + e.message, timeout: 6000 })
|
|
} finally {
|
|
aiTranslating.value = false
|
|
}
|
|
}
|
|
|
|
// ── New template creation ───────────────────────────────────────────────────
|
|
const newTemplateOpen = ref(false)
|
|
const creating = ref(false)
|
|
const newTemplateForm = ref({
|
|
prefix: 'gift-email',
|
|
suffix: '',
|
|
starter: 'blank', // 'blank' | template name to copy from
|
|
})
|
|
|
|
const newTemplateFinal = computed(() => {
|
|
const { prefix, suffix } = newTemplateForm.value
|
|
if (!suffix || !/^[a-z0-9-]+$/.test(suffix)) return ''
|
|
return `${prefix}-${suffix}`
|
|
})
|
|
|
|
const newTemplateValid = computed(() =>
|
|
!!newTemplateFinal.value &&
|
|
!templates.value.find(t => t.name === newTemplateFinal.value),
|
|
)
|
|
|
|
const starterOptions = computed(() => [
|
|
{ label: 'Vide (canevas blanc)', value: 'blank' },
|
|
...templates.value.map(t => ({ label: `Copier depuis ${t.name}`, value: t.name })),
|
|
])
|
|
|
|
async function createTemplate () {
|
|
if (!newTemplateValid.value) return
|
|
creating.value = true
|
|
try {
|
|
const newName = newTemplateFinal.value
|
|
let html = '<div style="padding:32px;text-align:center;color:#64748B;font-family:sans-serif;">Nouveau template — drag des blocs depuis la sidebar pour commencer.</div>'
|
|
let design = null
|
|
if (newTemplateForm.value.starter !== 'blank') {
|
|
// Copy html + design from the chosen source template
|
|
const src = await getTemplate(newTemplateForm.value.starter)
|
|
html = src.html || html
|
|
design = src.design || null
|
|
}
|
|
await saveTemplateApi(newName, html, { design })
|
|
await loadAvailableTemplates()
|
|
newTemplateOpen.value = false
|
|
// Switch to the new template
|
|
currentName.value = newName
|
|
router.replace({ path: `/campaigns/templates/${newName}` })
|
|
loadTemplateIntoEditor(newName)
|
|
newTemplateForm.value.suffix = '' // reset for next time
|
|
$q.notify({ type: 'positive', message: `Template "${newName}" créé`, timeout: 3000 })
|
|
} catch (e) {
|
|
$q.notify({ type: 'negative', message: 'Erreur création: ' + e.message })
|
|
} finally {
|
|
creating.value = false
|
|
}
|
|
}
|
|
|
|
// ── Load template into the Unlayer canvas on editor ready + on switch ──────
|
|
async function loadTemplateIntoEditor (name) {
|
|
if (!editorReady.value || !editor.value) return
|
|
try {
|
|
const data = await getTemplate(name)
|
|
// Priority: Unlayer design JSON saved by a previous edit > nothing.
|
|
// Legacy MJML/HTML templates aren't auto-importable into Unlayer's
|
|
// component tree — the user reconstructs visually once, and the
|
|
// design saved by the next "Enregistrer" fixes future loads.
|
|
if (data.design) {
|
|
const design = typeof data.design === 'string' ? JSON.parse(data.design) : data.design
|
|
editor.value.loadDesign(design)
|
|
} else {
|
|
// No saved design — vue-email-editor 2.x doesn't have a loadBlank()
|
|
// method on the ref, so we just let the editor show its default
|
|
// empty state ("No content here. Drag content from left.") and
|
|
// notify the user to start building.
|
|
$q.notify({
|
|
type: 'info',
|
|
message: `Pas encore de design pour "${name}" — drag des blocs depuis la sidebar gauche pour construire la template, puis "Enregistrer".`,
|
|
timeout: 8000,
|
|
})
|
|
}
|
|
} catch (e) {
|
|
$q.notify({ type: 'negative', message: 'Erreur de chargement: ' + e.message })
|
|
}
|
|
}
|
|
|
|
function onEditorLoad () {
|
|
// Fired once when the editor IFRAME loads (before the editor inside is ready)
|
|
}
|
|
|
|
async function onEditorReady () {
|
|
// Fired when the editor INSIDE the iframe is ready to accept loadDesign()
|
|
editorReady.value = true
|
|
await loadTemplateIntoEditor(currentName.value)
|
|
}
|
|
|
|
function onSelectTemplate (name) {
|
|
currentName.value = name
|
|
router.replace({ path: `/campaigns/templates/${name}` })
|
|
loadTemplateIntoEditor(name)
|
|
}
|
|
|
|
// ── Save: export HTML + design JSON, POST to hub ────────────────────────────
|
|
const lastSavedTs = ref(null)
|
|
const lastSavedLabel = computed(() => {
|
|
if (!lastSavedTs.value) return ''
|
|
const diff = Math.floor((Date.now() - lastSavedTs.value) / 1000)
|
|
if (diff < 60) return `il y a ${diff}s`
|
|
if (diff < 3600) return `il y a ${Math.floor(diff / 60)}min`
|
|
return new Date(lastSavedTs.value).toLocaleTimeString('fr-CA')
|
|
})
|
|
|
|
function saveTemplate () {
|
|
if (!editor.value) return
|
|
saving.value = true
|
|
// exportHtml uses a callback (legacy Unlayer API) — wrap in a Promise
|
|
editor.value.exportHtml(async (data) => {
|
|
try {
|
|
const { html, design } = data
|
|
await saveTemplateApi(currentName.value, html, { design })
|
|
lastSavedTs.value = Date.now()
|
|
$q.notify({ type: 'positive', message: 'Template enregistré ✓', timeout: 2500 })
|
|
} catch (e) {
|
|
$q.notify({ type: 'negative', message: 'Erreur: ' + e.message })
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
})
|
|
}
|
|
|
|
// ── Preview dialog ──────────────────────────────────────────────────────────
|
|
const previewOpen = ref(false)
|
|
const previewLoading = ref(false)
|
|
const previewHtmlContent = ref('')
|
|
|
|
async function openPreview () {
|
|
previewOpen.value = true
|
|
previewLoading.value = true
|
|
try {
|
|
const r = await previewTemplate(currentName.value)
|
|
previewHtmlContent.value = r.rendered || ''
|
|
} catch (e) {
|
|
$q.notify({ type: 'negative', message: 'Aperçu impossible: ' + e.message })
|
|
} finally {
|
|
previewLoading.value = false
|
|
}
|
|
}
|
|
|
|
// ── Test-send dialog ────────────────────────────────────────────────────────
|
|
const testSendOpen = ref(false)
|
|
const testSending = ref(false)
|
|
const testSendForm = ref({
|
|
to: 'louis@targo.ca',
|
|
subject: '[TEST] Aperçu du courriel TARGO',
|
|
vars: {
|
|
firstname: 'Louis', lastname: 'Test', amount: '60 $',
|
|
commitment_months: '3',
|
|
gift_url: 'https://gft.link/TEST123',
|
|
description: '123 Rue de Test, Ste-Clotilde',
|
|
expiry: '31 décembre 2026',
|
|
},
|
|
})
|
|
|
|
async function doTestSend () {
|
|
testSending.value = true
|
|
try {
|
|
const r = await testSendTemplate(currentName.value, {
|
|
to: testSendForm.value.to.trim(),
|
|
subject: testSendForm.value.subject,
|
|
vars: testSendForm.value.vars,
|
|
})
|
|
$q.notify({ type: 'positive', message: `Test envoyé à ${r.to}`, caption: `${r.bytes} octets`, timeout: 5000 })
|
|
testSendOpen.value = false
|
|
} catch (e) {
|
|
$q.notify({ type: 'negative', message: 'Échec envoi: ' + e.message })
|
|
} finally {
|
|
testSending.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(loadAvailableTemplates)
|
|
</script>
|