@dev.smartpricing/message-composer-layer 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,277 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  definePageMeta({
3
+ name: 'whatsapp-message-create',
3
4
  middleware: ['channel-guard'],
5
+ channel: 'whatsapp',
4
6
  })
5
7
 
6
- import WhatsappEditorSyncWrapper from '@/components/Whatsapp/Editor/SyncWrapper.vue'
7
- import WhatsappPreview from '@/components/Whatsapp/PreviewPanel.vue'
8
- import TranslationEditor from '@/components/Whatsapp/TranslationEditor.vue'
9
-
10
- import IncompleteTranslationsDialog from '@/components/Common/IncompleteTranslationsDialog.vue'
11
-
12
- import CategoryChanger from '@/components/Whatsapp/CategoryChanger.vue'
13
- import { computed, ref, watch } from 'vue'
14
- import {
15
- type MessageGroup,
16
- type MessageGroupCategory,
17
- type PostEntity,
18
- type WhatsappMessage,
19
- type LanguageFromDb,
20
- } from '@dev.smartpricing/message-composer-utils/types'
21
- import { getWhatsappEmpty } from '@dev.smartpricing/message-composer-utils/utils'
22
- import LanguageList from '@/components/Common/LanguageList.vue'
23
- import { useMetaSubmissionStore } from '@/stores/metaSubmission'
24
- import { useLanguagesQuery } from '~/queries'
25
- import { useAppContextStore } from '@/stores/appContext'
26
- import type { WaHeaderType } from '~/types/tracking'
27
-
28
- const { trackEvent } = useTracking()
29
- const { mainLanguage } = storeToRefs(useAppContextStore())
30
-
31
- const adminStore = useAdminStore()
32
-
33
- const name = ref('')
34
- const category = ref<MessageGroupCategory>('marketing')
35
- const isInternal = ref(false)
36
- const language = ref(mainLanguage.value)
37
- const { goBackOrFallback } = useGoBack()
38
-
39
- const activeTab = ref<'content' | 'translations'>('content')
40
- const baseLanguage = ref(mainLanguage.value)
41
- const targetLanguage = ref(mainLanguage.value)
42
-
43
- // Create temporary draft ID for change tracking
44
- const draftId = ref(`draft-${Date.now()}`)
45
- const changeTracking = useChangeTracking(draftId)
46
-
47
- // Store original messageGroup values (empty for new message group)
48
- const originalMessageGroupName = ref('')
49
- const originalMessageGroupCategory = ref<MessageGroupCategory>('marketing')
50
-
51
- const { saving, createTemplate } = useTemplateOperations({
52
- onSuccess: () => {
53
- created.value = true
54
- changeTracking.clearAllChanges()
55
- goBackOrFallback()
56
- },
57
- })
58
-
59
- const { submitToMeta } = useMetaSubmissionStore()
60
-
61
- const messageGroup = computed(() => {
62
- return {
63
- name: name.value,
64
- category: category.value,
65
- visibility: isInternal.value ? 'internal' : 'public',
66
- } satisfies PostEntity<MessageGroup>
67
- })
68
- const messages = ref<Record<string, PostEntity<WhatsappMessage>>>({
69
- [mainLanguage.value]: getWhatsappEmpty(mainLanguage.value),
70
- })
71
-
72
- const { formState, draftSchema, validationErrors, validateDraft, validateFull, clearErrors } =
73
- useTemplateFormValidation({
74
- type: 'whatsapp',
75
- name,
76
- category,
77
- messages,
78
- })
79
-
80
- // Record original state for initial mainLanguage
81
- if (messages.value[mainLanguage.value]) {
82
- changeTracking.recordOriginalState(
83
- mainLanguage.value,
84
- messages.value[mainLanguage.value] as WhatsappMessage,
85
- )
86
- }
87
-
88
- // Initialize WhatsApp message group behaviors composable
89
- const { ensureUnsubscribeButton } = useWhatsappTemplateBehaviors({
90
- category,
91
- messages: messages,
92
- })
93
- ensureUnsubscribeButton()
94
-
95
- const created = ref(false)
96
-
97
- const hasAnyChanges = computed(() => {
98
- // Check if message group name or category changed
99
- const nameChanged = name.value !== originalMessageGroupName.value
100
- const categoryChanged = category.value !== originalMessageGroupCategory.value
101
- if (nameChanged || categoryChanged) return true
102
-
103
- // Check if any language has changes
104
- for (const languageId in messages.value) {
105
- const message = messages.value[languageId]
106
- if (message && changeTracking.hasChanges(languageId, message as WhatsappMessage)) {
107
- return true
108
- }
109
- }
110
- return false
111
- })
112
-
113
- useExitConfirmation(computed(() => hasAnyChanges.value && !created.value))
114
-
115
- // ── Tracking helpers ──
116
- function getWaBaseProps() {
117
- return { name: name.value, category: category.value }
118
- }
119
-
120
- function getWaDraftProps() {
121
- const msg = messages.value[mainLanguage.value]
122
- return {
123
- ...getWaBaseProps(),
124
- languages: Object.keys(messages.value),
125
- header: (msg?.body?.header?.type ?? 'none') as WaHeaderType,
126
- footer: !!msg?.body?.footer?.enabled,
127
- buttons: msg?.body?.buttons?.buttons?.length ?? 0,
128
- is_internal: isInternal.value,
129
- }
130
- }
131
-
132
- watch(activeTab, (tab) => {
133
- trackEvent('wa_create.change_tab', { ...getWaBaseProps(), tab })
134
- })
135
-
136
- function onAddLanguage(lang: string) {
137
- trackEvent('wa_create.add_language', { ...getWaBaseProps(), language: lang })
138
- }
139
-
140
- function onRemoveLanguage(lang: string) {
141
- trackEvent('wa_create.remove_language', {
142
- ...getWaBaseProps(),
143
- language: lang,
144
- })
145
- }
146
-
147
- function onAutoTranslateTracked(lang: string) {
148
- trackEvent('wa_create.auto_translate', {
149
- ...getWaBaseProps(),
150
- language: lang,
151
- })
152
- }
153
-
154
- function onPreview(lang: string) {
155
- trackEvent('wa_create.preview', { ...getWaBaseProps(), language: lang })
156
- }
157
-
158
- function onFiltersChanged(payload: {
159
- language: string
160
- search_term: string
161
- show_only_missing: boolean
162
- show_only_changes: boolean
163
- }) {
164
- trackEvent('wa_create.translations_filters', { ...getWaBaseProps(), ...payload })
165
- }
166
-
167
- // Get languages for incomplete translations check
168
- const { data: languages } = useLanguagesQuery()
169
-
170
- // Check for incomplete translations
171
- const { incompleteLanguages, hasIncompleteTranslations } = useIncompleteTranslations({
172
- messages,
173
- languages,
174
- })
175
-
176
- // Dialog state and pending action
177
- const showIncompleteDialog = ref(false)
178
- const pendingAction = ref<'save_draft' | 'request_approval' | null>(null)
179
-
180
- // Auto-translate composable
181
- const { translateMessage, isTranslating } = useAutoTranslate()
182
-
183
- async function saveDraft() {
184
- trackEvent('wa_create.save_draft', getWaDraftProps())
185
- const templateData = {
186
- messageGroup: messageGroup.value,
187
- messages: messages.value,
188
- }
189
- const result = await createTemplate(templateData, 'draft')
190
- if (result) {
191
- notifyParent('TEMPLATE_SAVED_DRAFT', { id: result.id, name: result.name })
192
- }
193
- }
194
-
195
- async function requestApproval() {
196
- trackEvent('wa_create.request_approval', getWaDraftProps())
197
- const templateData = {
198
- messageGroup: messageGroup.value,
199
- messages: messages.value,
200
- }
201
- const result = await createTemplate(templateData, 'draft')
202
-
203
- if (result) {
204
- await submitToMeta(result.id.toString())
205
- notifyParent('TEMPLATE_APPROVAL_REQUESTED', { id: result.id, name: result.name })
206
- }
207
- }
208
-
209
- // Wrapper functions to validate then check for incomplete translations
210
- async function handleSaveDraft() {
211
- if (!(await validateDraft())) return
212
- if (hasIncompleteTranslations.value) {
213
- pendingAction.value = 'save_draft'
214
- showIncompleteDialog.value = true
215
- } else {
216
- saveDraft()
217
- }
218
- }
219
-
220
- async function handleRequestApproval() {
221
- if (!(await validateFull())) return
222
- if (hasIncompleteTranslations.value) {
223
- pendingAction.value = 'request_approval'
224
- showIncompleteDialog.value = true
225
- } else {
226
- requestApproval()
227
- }
228
- }
229
-
230
- // Proceed with save without translating
231
- function proceedWithSave() {
232
- if (pendingAction.value === 'save_draft') {
233
- saveDraft()
234
- } else if (pendingAction.value === 'request_approval') {
235
- requestApproval()
236
- }
237
- pendingAction.value = null
238
- }
239
-
240
- // Go to translations tab
241
- function goToTranslations() {
242
- activeTab.value = 'translations'
243
- pendingAction.value = null
244
- }
245
-
246
- // Translate automatically and save
247
- async function translateAndSave() {
248
- if (!incompleteLanguages.value.length) {
249
- proceedWithSave()
250
- return
251
- }
252
-
253
- const baseMessage = messages.value[baseLanguage.value]
254
- if (!baseMessage) {
255
- proceedWithSave()
256
- return
257
- }
258
-
259
- // Translate all incomplete languages
260
- for (const { language: lang } of incompleteLanguages.value) {
261
- const targetMessage = messages.value[lang.id]
262
- if (!targetMessage) continue
263
-
264
- await translateMessage(
265
- baseMessage as WhatsappMessage,
266
- targetMessage as WhatsappMessage,
267
- baseLanguage.value,
268
- lang.id,
269
- )
270
- }
271
-
272
- // After translation, proceed with save
273
- proceedWithSave()
274
- }
8
+ const composerRef = ref<InstanceType<typeof ComposerWhatsappComposer>>()
9
+ const store = useWhatsappComposerStore()
275
10
  </script>
276
11
 
277
12
  <template>
@@ -281,184 +16,38 @@ async function translateAndSave() {
281
16
  { label: $t('pages.whatsapp.tabs.content'), value: 'content' },
282
17
  { label: $t('pages.whatsapp.tabs.translations'), value: 'translations' },
283
18
  ]"
284
- :active-tab="activeTab"
19
+ :active-tab="store.activeTab"
285
20
  :test-id="templateWhatsappCreateTestIds.navbar"
286
- @tab-change="activeTab = $event"
21
+ @tab-change="store.activeTab = $event as 'content' | 'translations'"
287
22
  >
288
23
  <template #actions>
289
24
  <UButton
290
- @click="
291
- () => {
292
- notifyParent('TEMPLATE_CANCELLED')
293
- goBackOrFallback()
294
- }
295
- "
25
+ @click="composerRef?.handleCancel()"
296
26
  :label="$t('common.actions.cancel')"
297
27
  color="primary"
298
28
  variant="ghost"
299
- :loading="saving || isTranslating"
29
+ :loading="composerRef?.saving || composerRef?.isTranslating"
300
30
  :data-testid="templateWhatsappCreateTestIds.cancelButton"
301
31
  />
302
32
  <UButton
303
- @click="handleSaveDraft()"
33
+ @click="composerRef?.handleSaveDraft()"
304
34
  :label="$t('pages.whatsapp.create_message_group.save_draft')"
305
35
  color="primary"
306
36
  variant="outline"
307
- :loading="saving || isTranslating"
37
+ :loading="composerRef?.saving || composerRef?.isTranslating"
308
38
  :data-testid="templateWhatsappCreateTestIds.saveDraftButton"
309
39
  />
310
40
  <UButton
311
- @click="handleRequestApproval()"
41
+ @click="composerRef?.handleRequestApproval()"
312
42
  :label="$t('pages.whatsapp.create_message_group.request_approval')"
313
43
  color="primary"
314
- :loading="saving || isTranslating"
44
+ :loading="composerRef?.saving || composerRef?.isTranslating"
315
45
  :data-testid="templateWhatsappCreateTestIds.requestApprovalButton"
316
46
  />
317
47
  </template>
318
48
 
319
- <div v-if="validationErrors.length">
320
- <UAlert
321
- color="error"
322
- variant="subtle"
323
- icon="i-lucide-alert-triangle"
324
- :title="$t('editor.validation.error.title')"
325
- :close="true"
326
- @update:open="clearErrors()"
327
- :data-testid="templateWhatsappCreateTestIds.validationAlert"
328
- >
329
- <template #description>
330
- <ul class="list-disc pl-4 space-y-1">
331
- <li v-for="err in validationErrors" :key="err">
332
- {{ err }}
333
- </li>
334
- </ul>
335
- </template>
336
- </UAlert>
337
- </div>
338
-
339
- <UForm
340
- ref="templateForm"
341
- :schema="draftSchema"
342
- :state="formState"
343
- :validate-on="[]"
344
- class="flex-1 relative min-h-0 flex flex-col"
345
- >
346
- <UCard
347
- v-if="activeTab === 'content'"
348
- :ui="{
349
- root: 'flex flex-col',
350
- body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0',
351
- header: 'sm:px-4 px-4 py-4 sm:py-4',
352
- }"
353
- :data-testid="templateWhatsappCreateTestIds.contentCard"
354
- >
355
- <template #header>
356
- <div class="grid grid-cols-3 gap-2">
357
- <UFieldGroup>
358
- <UBadge variant="outline" color="neutral" size="lg">{{
359
- $t('pages.whatsapp.create_message_group.name')
360
- }}</UBadge>
361
- <UInput
362
- v-model="name"
363
- :placeholder="$t('pages.whatsapp.create_message_group.name_placeholder')"
364
- :data-testid="templateWhatsappCreateTestIds.nameInput"
365
- />
366
- </UFieldGroup>
367
-
368
- <UFieldGroup>
369
- <UBadge variant="outline" color="neutral" size="lg">{{
370
- $t('pages.whatsapp.create_message_group.category')
371
- }}</UBadge>
372
- <CategoryChanger v-model:category="category" @changed="ensureUnsubscribeButton" />
373
- </UFieldGroup>
374
-
375
- <USwitch
376
- v-model="isInternal"
377
- :label="$t('common.internal')"
378
- v-if="adminStore.isAdminModeActive"
379
- color="info"
380
- :data-testid="templateWhatsappCreateTestIds.internalSwitch"
381
- />
382
- <!-- <DocumentationPanel
383
- :category="$t('documentation.whatsapp_category.category')"
384
- :title="$t('documentation.whatsapp_category.title')"
385
- :description="$t('documentation.whatsapp_category.description')"
386
- :info="{
387
- title: '',
388
- text: $t('documentation.whatsapp_category.info'),
389
- }"
390
- :content="[
391
- $t('documentation.whatsapp_category.content.marketing'),
392
- $t('documentation.whatsapp_category.content.utility'),
393
- ]"
394
- /> -->
395
- </div>
396
- </template>
397
-
398
- <div class="grid grid-cols-3 divide-x divide-muted overflow-auto max-h-full h-full">
399
- <div class="col-span-2 overflow-y-auto pt-4 pb-4" v-if="messages[language]">
400
- <h2 class="px-4 pb-4 label-lg">
401
- {{ $t('pages.whatsapp.create_message_group.message_content') }}
402
- </h2>
403
- <WhatsappEditorSyncWrapper
404
- v-model="messages[language]"
405
- :messages="messages"
406
- :main-language-id="language"
407
- :key="language"
408
- />
409
- </div>
410
- <div class="col-span-1 overflow-y-auto" v-if="messages[language]">
411
- <WhatsappPreview :message="messages[language]" />
412
- </div>
413
- </div>
414
- </UCard>
415
-
416
- <UCard
417
- v-if="activeTab === 'translations'"
418
- :ui="{
419
- root: 'flex flex-col h-full',
420
- body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0',
421
- header: 'sm:px-4 px-4 py-4 sm:py-4',
422
- }"
423
- :data-testid="templateWhatsappCreateTestIds.translationsCard"
424
- >
425
- <div
426
- class="grid grid-cols-[auto_1fr] divide-x divide-muted overflow-auto max-h-full h-full"
427
- >
428
- <div class="overflow-y-auto">
429
- <LanguageList
430
- v-model:messages="messages"
431
- v-model:language="targetLanguage"
432
- render-type="whatsapp_v1"
433
- :message-group-id="draftId"
434
- @add-language="onAddLanguage"
435
- @remove-language="onRemoveLanguage"
436
- :main-language-id="baseLanguage"
437
- />
438
- </div>
439
- <div class="overflow-y-auto" v-if="messages[baseLanguage] && messages[targetLanguage]">
440
- <TranslationEditor
441
- :base-message="messages[baseLanguage]"
442
- v-model:target-message="messages[targetLanguage]"
443
- :messages="messages"
444
- :current-language="targetLanguage"
445
- :base-language="baseLanguage"
446
- :message-group-id="draftId"
447
- @auto-translate-tracked="onAutoTranslateTracked"
448
- @preview="onPreview"
449
- @filters-changed="onFiltersChanged"
450
- />
451
- </div>
452
- </div>
453
- </UCard>
454
- </UForm>
455
-
456
- <IncompleteTranslationsDialog
457
- v-model="showIncompleteDialog"
458
- :incomplete-languages="incompleteLanguages"
459
- @save-without-translating="proceedWithSave"
460
- @translate-and-save="translateAndSave"
461
- @go-to-translations="goToTranslations"
462
- />
49
+ <ComposerWhatsappComposer ref="composerRef" mode="create">
50
+ <template #actions></template>
51
+ </ComposerWhatsappComposer>
463
52
  </LayoutBasePage>
464
53
  </template>
@@ -0,0 +1,188 @@
1
+ import { ref, computed } from 'vue'
2
+ import type {
3
+ MessageGroupCategory,
4
+ EmailMessage,
5
+ PostEntity,
6
+ MessageGroupWithMessages,
7
+ } from '@dev.smartpricing/message-composer-utils/types'
8
+ import {
9
+ getEmailEmpty,
10
+ duplicateBlocksForNewLanguage,
11
+ syncLayoutFromBase,
12
+ } from '@dev.smartpricing/message-composer-utils/utils'
13
+
14
+ type ComposerMode = 'create' | 'edit'
15
+
16
+ export const useEmailComposerStore = defineStore('emailComposer', () => {
17
+ // ── Core state ────────────────────────────────────────────────────
18
+ const mode = ref<ComposerMode>('create')
19
+ const messageGroupId = ref<string | null>(null)
20
+ const name = ref('')
21
+ const category = ref<MessageGroupCategory>('marketing')
22
+ const isInternal = ref(false)
23
+ const language = ref('en') // base language
24
+ const targetLanguage = ref('en')
25
+ const activeTab = ref<'content' | 'translations'>('content')
26
+ const messages = ref<Record<string, PostEntity<EmailMessage> | EmailMessage>>({})
27
+
28
+ // Edit-mode original values for change detection
29
+ const originalName = ref('')
30
+ const originalCategory = ref<MessageGroupCategory>('marketing')
31
+
32
+ // Loaded messageGroup data (edit mode)
33
+ const messageGroup = ref<MessageGroupWithMessages | null>(null)
34
+
35
+ // ── Computed ──────────────────────────────────────────────────────
36
+ const messageGroupData = computed(() => ({
37
+ user_id: '',
38
+ name: name.value,
39
+ category: category.value,
40
+ visibility: isInternal.value ? 'internal' : 'public',
41
+ brand_id: useEmailEditorStore().brandId,
42
+ }))
43
+
44
+ const messageGroupUpdateData = computed(() => ({
45
+ name: name.value,
46
+ category: category.value,
47
+ brand_id: useEmailEditorStore().brandId,
48
+ }))
49
+
50
+ const currentMessage = computed(
51
+ () => messages.value[language.value] as PostEntity<EmailMessage> | undefined,
52
+ )
53
+
54
+ const targetMessage = computed(
55
+ () => messages.value[targetLanguage.value] as PostEntity<EmailMessage> | undefined,
56
+ )
57
+
58
+ const languageIds = computed(() => Object.keys(messages.value))
59
+
60
+ // ── Init ──────────────────────────────────────────────────────────
61
+ function initCreate(mainLanguage: string) {
62
+ mode.value = 'create'
63
+ messageGroupId.value = null
64
+ name.value = ''
65
+ category.value = 'marketing'
66
+ isInternal.value = false
67
+ language.value = mainLanguage
68
+ targetLanguage.value = mainLanguage
69
+ activeTab.value = 'content'
70
+ messages.value = { [mainLanguage]: getEmailEmpty(mainLanguage) }
71
+ messageGroup.value = null
72
+ originalName.value = ''
73
+ originalCategory.value = 'marketing'
74
+ }
75
+
76
+ function initEdit(data: MessageGroupWithMessages, mainLanguage: string) {
77
+ mode.value = 'edit'
78
+ messageGroupId.value = data.id?.toString() ?? null
79
+ name.value = data.name
80
+ category.value = data.category
81
+ isInternal.value = data.visibility === 'internal'
82
+ activeTab.value = 'content'
83
+ messageGroup.value = data
84
+
85
+ // Store originals for change detection
86
+ originalName.value = data.name
87
+ originalCategory.value = data.category
88
+
89
+ // Populate messages
90
+ const msgs: Record<string, EmailMessage> = {}
91
+ for (const message of data.messages) {
92
+ if (message.render_type === 'email_v1') {
93
+ const { created_at, updated_at, ...emailMessage } = message
94
+ msgs[message.language_id] = emailMessage as EmailMessage
95
+ }
96
+ }
97
+ messages.value = msgs
98
+
99
+ // Resolve language
100
+ const resolved = resolveDefaultLanguage(msgs, mainLanguage)
101
+ language.value = resolved ?? mainLanguage
102
+ targetLanguage.value = language.value
103
+
104
+ // Set initial target language for translations tab
105
+ const langs = Object.keys(msgs)
106
+ if (langs.length > 1) {
107
+ targetLanguage.value = langs.find((l) => l !== language.value) ?? language.value
108
+ }
109
+ }
110
+
111
+ // ── Language management ───────────────────────────────────────────
112
+ function ensureLanguageMessage(langId: string) {
113
+ if (messages.value[langId]) return
114
+
115
+ const baseMessage = messages.value[language.value]
116
+ if (
117
+ baseMessage &&
118
+ baseMessage.render_type === 'email_v1' &&
119
+ baseMessage.body.blocks.length > 0
120
+ ) {
121
+ const newMessage = getEmailEmpty(langId)
122
+ newMessage.body.blocks = duplicateBlocksForNewLanguage(baseMessage.body.blocks)
123
+ newMessage.body.settings = { ...baseMessage.body.settings }
124
+ messages.value[langId] = newMessage
125
+ } else {
126
+ messages.value[langId] = getEmailEmpty(langId)
127
+ }
128
+ }
129
+
130
+ // ── Sync helpers ──────────────────────────────────────────────────
131
+ function syncLayoutToAllLanguages() {
132
+ const baseMessage = messages.value[language.value]
133
+ if (!baseMessage || baseMessage.render_type !== 'email_v1') return
134
+
135
+ for (const [langId, message] of Object.entries(messages.value)) {
136
+ if (langId !== language.value && message.render_type === 'email_v1') {
137
+ message.body.blocks = syncLayoutFromBase(baseMessage.body.blocks, message.body.blocks)
138
+ message.body.settings = { ...baseMessage.body.settings }
139
+ }
140
+ }
141
+ }
142
+
143
+ // ── Reset ─────────────────────────────────────────────────────────
144
+ function $reset() {
145
+ mode.value = 'create'
146
+ messageGroupId.value = null
147
+ name.value = ''
148
+ category.value = 'marketing'
149
+ isInternal.value = false
150
+ language.value = 'en'
151
+ targetLanguage.value = 'en'
152
+ activeTab.value = 'content'
153
+ messages.value = {}
154
+ messageGroup.value = null
155
+ originalName.value = ''
156
+ originalCategory.value = 'marketing'
157
+ }
158
+
159
+ return {
160
+ // State
161
+ mode,
162
+ messageGroupId,
163
+ name,
164
+ category,
165
+ isInternal,
166
+ language,
167
+ targetLanguage,
168
+ activeTab,
169
+ messages,
170
+ messageGroup,
171
+ originalName,
172
+ originalCategory,
173
+
174
+ // Computed
175
+ messageGroupData,
176
+ messageGroupUpdateData,
177
+ currentMessage,
178
+ targetMessage,
179
+ languageIds,
180
+
181
+ // Actions
182
+ initCreate,
183
+ initEdit,
184
+ ensureLanguageMessage,
185
+ syncLayoutToAllLanguages,
186
+ $reset,
187
+ }
188
+ })