@dev.smartpricing/message-composer-layer 1.0.13 → 1.0.15

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