@dev.smartpricing/message-composer-layer 4.5.1-next.3 → 4.5.1-next.4
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.
- package/README.md +133 -4
- package/app/api/automations.ts +74 -0
- package/app/api/index.ts +1 -0
- package/app/api/messageGroups.ts +5 -1
- package/app/app.config.ts +10 -0
- package/app/components/Automations/ActionCard.vue +174 -0
- package/app/components/Automations/ActionEditor.vue +398 -0
- package/app/components/Automations/Actions.vue +148 -0
- package/app/components/Automations/CloneModal.vue +78 -0
- package/app/components/Automations/Conditions.vue +155 -0
- package/app/components/Automations/Create.vue +114 -0
- package/app/components/Automations/Data.vue +46 -0
- package/app/components/Automations/DeleteModal.vue +80 -0
- package/app/components/Automations/Edit.vue +145 -0
- package/app/components/Automations/Form.vue +282 -0
- package/app/components/Automations/List.vue +370 -0
- package/app/components/Automations/PageGate.vue +50 -0
- package/app/components/Automations/SaveProblems.vue +54 -0
- package/app/components/Automations/Unavailable.vue +71 -0
- package/app/components/Automations/UnavailableBadge.vue +35 -0
- package/app/components/Common/MessageGroupSelect.vue +30 -1
- package/app/components/Composer/EmailComposer.vue +5 -1
- package/app/components/Composer/WabaSelectModal.vue +3 -1
- package/app/components/Composer/WhatsappComposer.vue +5 -1
- package/app/components/Filters/DateInput.vue +57 -0
- package/app/components/Filters/Editor.vue +163 -0
- package/app/components/Filters/Row.vue +119 -0
- package/app/components/Filters/Summary.vue +215 -0
- package/app/components/Filters/UnavailableRow.vue +74 -0
- package/app/components/Filters/ValueInput.vue +215 -0
- package/app/components/Layout/BasePage.vue +2 -0
- package/app/components/TemplateList/BaseTable.vue +1 -22
- package/app/composables/apiErrorMessages.ts +1 -0
- package/app/composables/automationsContext.ts +42 -0
- package/app/composables/composerContext.ts +3 -3
- package/app/composables/useAutomationClone.ts +33 -0
- package/app/composables/useAutomationEditorTrip.ts +77 -0
- package/app/composables/useAutomationSaveProblems.ts +82 -0
- package/app/composables/useAutomationUnavailable.ts +98 -0
- package/app/composables/useAutomationsGate.ts +68 -0
- package/app/composables/useBuiltInOptionLists.ts +44 -0
- package/app/composables/useEditorTripBack.ts +15 -0
- package/app/composables/useMessageGroupWaba.ts +7 -10
- package/app/composables/useTemplateOperations.ts +8 -3
- package/app/middleware/automations-enabled.ts +12 -0
- package/app/pages/automations/[id].vue +87 -0
- package/app/pages/automations/index.vue +28 -0
- package/app/pages/automations/new.vue +53 -0
- package/app/pages/template/email/[id].vue +5 -0
- package/app/pages/template/email/create.vue +5 -0
- package/app/pages/template/whatsapp/[id].vue +5 -0
- package/app/pages/template/whatsapp/create.vue +5 -0
- package/app/queries/automations.ts +206 -0
- package/app/queries/index.ts +1 -0
- package/app/queries/messageGroups.ts +1 -3
- package/app/stores/appContext.ts +1 -1
- package/app/stores/editorTrip.ts +67 -0
- package/app/types/app-config.d.ts +3 -0
- package/app/types/tracking.ts +33 -0
- package/app/utils/automations/form.ts +389 -0
- package/app/utils/automations/metadata.ts +71 -0
- package/app/utils/automations/unavailable.ts +90 -0
- package/app/utils/filters/data/countries.json +998 -0
- package/app/utils/filters/data/languages.json +754 -0
- package/app/utils/filters/rows.ts +367 -0
- package/app/utils/testIds.const.ts +107 -1
- package/i18n/locales/de.ts +269 -1
- package/i18n/locales/en.ts +255 -1
- package/i18n/locales/es.ts +258 -1
- package/i18n/locales/fr.ts +264 -1
- package/i18n/locales/it.ts +260 -1
- package/package.json +4 -4
- package/app.config.ts +0 -5
|
@@ -8,6 +8,9 @@ definePageMeta({
|
|
|
8
8
|
const route = useRoute()
|
|
9
9
|
const composerRef = useTemplateRef('composerRef')
|
|
10
10
|
const store = useWhatsappComposerStore()
|
|
11
|
+
const tripBack = useEditorTripBack(
|
|
12
|
+
() => composerRef.value?.saving || composerRef.value?.isTranslating,
|
|
13
|
+
)
|
|
11
14
|
</script>
|
|
12
15
|
|
|
13
16
|
<template>
|
|
@@ -19,6 +22,8 @@ const store = useWhatsappComposerStore()
|
|
|
19
22
|
]"
|
|
20
23
|
:active-tab="store.activeTab"
|
|
21
24
|
:test-id="templateWhatsappEditTestIds.navbar"
|
|
25
|
+
:back="tripBack"
|
|
26
|
+
@back="composerRef?.handleCancel()"
|
|
22
27
|
@tab-change="store.activeTab = $event as 'content' | 'translations'"
|
|
23
28
|
>
|
|
24
29
|
<template #actions>
|
|
@@ -7,6 +7,9 @@ definePageMeta({
|
|
|
7
7
|
|
|
8
8
|
const composerRef = useTemplateRef('composerRef')
|
|
9
9
|
const store = useWhatsappComposerStore()
|
|
10
|
+
const tripBack = useEditorTripBack(
|
|
11
|
+
() => composerRef.value?.saving || composerRef.value?.isTranslating,
|
|
12
|
+
)
|
|
10
13
|
</script>
|
|
11
14
|
|
|
12
15
|
<template>
|
|
@@ -18,6 +21,8 @@ const store = useWhatsappComposerStore()
|
|
|
18
21
|
]"
|
|
19
22
|
:active-tab="store.activeTab"
|
|
20
23
|
:test-id="templateWhatsappCreateTestIds.navbar"
|
|
24
|
+
:back="tripBack"
|
|
25
|
+
@back="composerRef?.handleCancel()"
|
|
21
26
|
@tab-change="store.activeTab = $event as 'content' | 'translations'"
|
|
22
27
|
>
|
|
23
28
|
<template #actions>
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryCache } from '@pinia/colada'
|
|
2
|
+
import {
|
|
3
|
+
AUTOMATION_CATALOG_LANGUAGES,
|
|
4
|
+
type Automation,
|
|
5
|
+
type AutomationCatalogLanguage,
|
|
6
|
+
type CreateAutomationBody,
|
|
7
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
8
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
9
|
+
import { toValue } from 'vue'
|
|
10
|
+
import { automationsApi, messageGroupsApi } from '~/api'
|
|
11
|
+
import type { AutomationChanges } from '~/utils/automations/form'
|
|
12
|
+
|
|
13
|
+
function isCatalogLanguage(language: string): language is AutomationCatalogLanguage {
|
|
14
|
+
return (AUTOMATION_CATALOG_LANGUAGES as readonly string[]).includes(language)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The Automations of the Effective Organization for the Host's Product. */
|
|
18
|
+
export function useAutomationsQuery() {
|
|
19
|
+
const { product } = storeToRefs(useAppContextStore())
|
|
20
|
+
const organization = useEffectiveOrganizationStore()
|
|
21
|
+
|
|
22
|
+
return useQuery({
|
|
23
|
+
key: () => ['automations', product.value ?? '', organization.organizationId],
|
|
24
|
+
query: () =>
|
|
25
|
+
automationsApi.getAll({
|
|
26
|
+
organization_id: organization.requireOrganizationId(),
|
|
27
|
+
product: product.value!,
|
|
28
|
+
}),
|
|
29
|
+
enabled: () => Boolean(product.value && organization.organizationId),
|
|
30
|
+
staleTime: 2 * 60 * 1000,
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One Automation of the Host's Product, read once for the page that edits it:
|
|
36
|
+
* nothing is kept for the next visit, and no refetch replaces what the user
|
|
37
|
+
* is editing. Its key is not under the list's, so a save does not refetch it.
|
|
38
|
+
*/
|
|
39
|
+
export function useAutomationQuery(id: MaybeRefOrGetter<string>) {
|
|
40
|
+
const { product } = storeToRefs(useAppContextStore())
|
|
41
|
+
const organization = useEffectiveOrganizationStore()
|
|
42
|
+
|
|
43
|
+
return useQuery({
|
|
44
|
+
key: () => ['automation', product.value ?? '', organization.organizationId, toValue(id)],
|
|
45
|
+
query: () => automationsApi.getById(toValue(id), { product: product.value! }),
|
|
46
|
+
enabled: () => Boolean(product.value && organization.organizationId),
|
|
47
|
+
staleTime: 0,
|
|
48
|
+
gcTime: 0,
|
|
49
|
+
refetchOnWindowFocus: false,
|
|
50
|
+
refetchOnReconnect: false,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The Automation Catalog of the Host's Product, with its labels in the
|
|
56
|
+
* language of the interface (ADR 0017).
|
|
57
|
+
*/
|
|
58
|
+
export function useAutomationCatalogQuery() {
|
|
59
|
+
const { product } = storeToRefs(useAppContextStore())
|
|
60
|
+
const organization = useEffectiveOrganizationStore()
|
|
61
|
+
const { locale } = useI18n()
|
|
62
|
+
const language = computed(() => (isCatalogLanguage(locale.value) ? locale.value : 'en'))
|
|
63
|
+
|
|
64
|
+
return useQuery({
|
|
65
|
+
key: () => [
|
|
66
|
+
'automation-catalog',
|
|
67
|
+
product.value ?? '',
|
|
68
|
+
language.value,
|
|
69
|
+
organization.organizationId,
|
|
70
|
+
],
|
|
71
|
+
query: () =>
|
|
72
|
+
automationsApi.getCatalog({
|
|
73
|
+
organization_id: organization.requireOrganizationId(),
|
|
74
|
+
product: product.value!,
|
|
75
|
+
language: language.value,
|
|
76
|
+
}),
|
|
77
|
+
enabled: () => Boolean(product.value && organization.organizationId),
|
|
78
|
+
staleTime: 30 * 60 * 1000,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A new Automation names its Organization and its Product (ADR 0014, ADR 0019). */
|
|
83
|
+
export function useCreateAutomationMutation() {
|
|
84
|
+
const queryCache = useQueryCache()
|
|
85
|
+
const { product } = storeToRefs(useAppContextStore())
|
|
86
|
+
const organization = useEffectiveOrganizationStore()
|
|
87
|
+
|
|
88
|
+
return useMutation({
|
|
89
|
+
mutation: (automation: Omit<CreateAutomationBody, 'organization_id' | 'product'>) =>
|
|
90
|
+
automationsApi.create({
|
|
91
|
+
...automation,
|
|
92
|
+
organization_id: organization.requireOrganizationId(),
|
|
93
|
+
product: product.value!,
|
|
94
|
+
}),
|
|
95
|
+
onSuccess: () => {
|
|
96
|
+
queryCache.invalidateQueries({ key: ['automations'] })
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** An update names its Product; the Organization is the stored one (ADR 0016). */
|
|
102
|
+
export function useUpdateAutomationMutation() {
|
|
103
|
+
const queryCache = useQueryCache()
|
|
104
|
+
const { product } = storeToRefs(useAppContextStore())
|
|
105
|
+
|
|
106
|
+
return useMutation({
|
|
107
|
+
mutation: ({ id, ...changes }: AutomationChanges & { id: string }) =>
|
|
108
|
+
automationsApi.update(id, { ...changes, product: product.value! }),
|
|
109
|
+
// After a failure too: an update can be saved in part.
|
|
110
|
+
onSettled: () => {
|
|
111
|
+
queryCache.invalidateQueries({ key: ['automations'] })
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A clone names its Product; the copy stays in the source's Organization (ADR 0016). */
|
|
117
|
+
export function useCloneAutomationMutation() {
|
|
118
|
+
const queryCache = useQueryCache()
|
|
119
|
+
const { product } = storeToRefs(useAppContextStore())
|
|
120
|
+
|
|
121
|
+
return useMutation({
|
|
122
|
+
mutation: ({ id, name }: { id: string; name: string }) =>
|
|
123
|
+
automationsApi.clone(id, { name, product: product.value! }),
|
|
124
|
+
// The copy comes with Message Groups of its own. After a failure too: it
|
|
125
|
+
// can be made when the answer is lost.
|
|
126
|
+
onSettled: () => {
|
|
127
|
+
queryCache.invalidateQueries({ key: ['automations'] })
|
|
128
|
+
queryCache.invalidateQueries({ key: ['message-groups'] })
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The Message Groups the Actions of an Automation use, each once, by their
|
|
135
|
+
* current name. One that cannot be read (deleted, or of an Organization the
|
|
136
|
+
* user cannot reach) is left out: it could not be deleted either.
|
|
137
|
+
*/
|
|
138
|
+
export function useAutomationMessageGroupsQuery(automation: MaybeRefOrGetter<Automation>) {
|
|
139
|
+
const ids = computed(() => [
|
|
140
|
+
...new Set(toValue(automation).actions.map((action) => action.config.template.toLowerCase())),
|
|
141
|
+
])
|
|
142
|
+
|
|
143
|
+
return useQuery({
|
|
144
|
+
key: () => ['automation-message-groups', toValue(automation).id, ...ids.value],
|
|
145
|
+
query: async () => {
|
|
146
|
+
const results = await Promise.allSettled(
|
|
147
|
+
ids.value.map((id) => messageGroupsApi.getById(id, { skipErrorToast: true })),
|
|
148
|
+
)
|
|
149
|
+
return results.flatMap((result) =>
|
|
150
|
+
result.status === 'fulfilled' ? [{ id: result.value.id, name: result.value.name }] : [],
|
|
151
|
+
)
|
|
152
|
+
},
|
|
153
|
+
staleTime: 0,
|
|
154
|
+
gcTime: 0,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Whether each of these Message Groups is approved in every language, by its
|
|
160
|
+
* id in lower case. One that cannot be read is left out: it is not known to
|
|
161
|
+
* be unapproved. Under `message-groups`, so a change to any of them asks again.
|
|
162
|
+
*/
|
|
163
|
+
export function useMessageGroupApprovalsQuery(messageGroupIds: MaybeRefOrGetter<string[]>) {
|
|
164
|
+
const ids = computed(() =>
|
|
165
|
+
[...new Set(toValue(messageGroupIds).map((id) => id.toLowerCase()))].filter(Boolean).sort(),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return useQuery({
|
|
169
|
+
key: () => ['message-groups', 'approvals', ...ids.value],
|
|
170
|
+
query: async () => {
|
|
171
|
+
const requested = ids.value
|
|
172
|
+
const results = await Promise.allSettled(
|
|
173
|
+
requested.map((id) => messageGroupsApi.getById(id, { skipErrorToast: true })),
|
|
174
|
+
)
|
|
175
|
+
return new Map(
|
|
176
|
+
results.flatMap((result, index) => {
|
|
177
|
+
if (result.status !== 'fulfilled') return []
|
|
178
|
+
const { messages } = result.value
|
|
179
|
+
const approved =
|
|
180
|
+
messages.length > 0 && messages.every((message) => message.status === 'approved')
|
|
181
|
+
return [[requested[index]!, approved] as const]
|
|
182
|
+
}),
|
|
183
|
+
)
|
|
184
|
+
},
|
|
185
|
+
enabled: () => ids.value.length > 0,
|
|
186
|
+
// The set changes with every Action added or re-pointed: what was known stays while it loads.
|
|
187
|
+
placeholderData: (previous) => previous,
|
|
188
|
+
staleTime: 2 * 60 * 1000,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** A delete names its Product, and the Message Groups that go with the Automation (ADR 0016). */
|
|
193
|
+
export function useDeleteAutomationMutation() {
|
|
194
|
+
const queryCache = useQueryCache()
|
|
195
|
+
const { product } = storeToRefs(useAppContextStore())
|
|
196
|
+
|
|
197
|
+
return useMutation({
|
|
198
|
+
mutation: ({ id, messageGroupIds }: { id: string; messageGroupIds: string[] }) =>
|
|
199
|
+
automationsApi.delete(id, { product: product.value!, message_group_ids: messageGroupIds }),
|
|
200
|
+
// After a failure too: the Automation can be gone when the answer is lost.
|
|
201
|
+
onSettled: (_result, _error, { messageGroupIds }) => {
|
|
202
|
+
queryCache.invalidateQueries({ key: ['automations'] })
|
|
203
|
+
if (messageGroupIds.length > 0) queryCache.invalidateQueries({ key: ['message-groups'] })
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
}
|
package/app/queries/index.ts
CHANGED
|
@@ -56,9 +56,7 @@ export function useMessageGroupListQuery(
|
|
|
56
56
|
// Never across Organizations: a switch must not show the previous one's rows.
|
|
57
57
|
placeholderData: (previousData, previousEntry) =>
|
|
58
58
|
previousEntry?.key.at(-1) === listOrganizationId() ? previousData : undefined,
|
|
59
|
-
|
|
60
|
-
// show it as loading) instead of failing into an empty table.
|
|
61
|
-
enabled: () => toValue(queryOptions.enabled ?? true) && !!listOrganizationId(),
|
|
59
|
+
...enabledOption(queryOptions.enabled),
|
|
62
60
|
})
|
|
63
61
|
}
|
|
64
62
|
|
package/app/stores/appContext.ts
CHANGED
|
@@ -23,7 +23,7 @@ export const useAppContextStore = defineStore('appContext', () => {
|
|
|
23
23
|
const mainLanguage = computed(() => ctx.mainLanguage?.value ?? 'en')
|
|
24
24
|
const default_tags = computed<string[]>(() => ctx.default_tags?.value ?? [])
|
|
25
25
|
const isAdmin = computed(() => ctx.isAdmin?.value ?? false)
|
|
26
|
-
// The
|
|
26
|
+
// The Host's preselection among the Organization's WABAs (ADR 0015).
|
|
27
27
|
const metaWabaId = computed(() => ctx.metaWabaId?.value || undefined)
|
|
28
28
|
|
|
29
29
|
// Sync i18n locale when mainLanguage changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** The Message Group an editor saved. */
|
|
2
|
+
export interface EditorTripResult {
|
|
3
|
+
id: string
|
|
4
|
+
name: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface EditorTrip<Kept = unknown> {
|
|
8
|
+
/** Where the trip started: the route it returns to, and the back button's label, already translated. */
|
|
9
|
+
origin: { routeName: string; backLabel: string }
|
|
10
|
+
/** What the opener wants back on its return. The editor never reads it. */
|
|
11
|
+
kept: Kept
|
|
12
|
+
/** The last Message Group saved during the trip; none until a save. */
|
|
13
|
+
result?: EditorTripResult
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The pages a trip goes through, by Channel. */
|
|
17
|
+
export const EDITOR_ROUTES = {
|
|
18
|
+
email: { create: 'email-message-create', edit: 'email-message-edit' },
|
|
19
|
+
whatsapp: { create: 'whatsapp-message-create', edit: 'whatsapp-message-edit' },
|
|
20
|
+
} as const
|
|
21
|
+
|
|
22
|
+
const EDITOR_ROUTE_NAMES: ReadonlySet<string> = new Set(
|
|
23
|
+
Object.values(EDITOR_ROUTES).flatMap((routes) => Object.values(routes)),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A round trip from a page to the Message editor and back. Layer-internal and
|
|
28
|
+
* in memory only: not Host contract, and a reload loses it.
|
|
29
|
+
*
|
|
30
|
+
* The opener starts it and takes it back on its return. The editor knows a
|
|
31
|
+
* trip, not who opened it: it shows the trip's back button and records what
|
|
32
|
+
* it saves.
|
|
33
|
+
*/
|
|
34
|
+
export const useEditorTripStore = defineStore('editorTrip', () => {
|
|
35
|
+
const trip = shallowRef<EditorTrip>()
|
|
36
|
+
|
|
37
|
+
// A trip ends where the user leaves its path: an editor opened later, from
|
|
38
|
+
// the library, is not part of it.
|
|
39
|
+
useRouter().afterEach((to, _from, failure) => {
|
|
40
|
+
if (failure || !trip.value) return
|
|
41
|
+
const name = String(to.name)
|
|
42
|
+
if (name === trip.value.origin.routeName || EDITOR_ROUTE_NAMES.has(name)) return
|
|
43
|
+
trip.value = undefined
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function start<Kept>(origin: EditorTrip['origin'], kept: Kept) {
|
|
47
|
+
trip.value = { origin, kept }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The last Message Group saved wins, also if the user then cancels. */
|
|
51
|
+
function recordSaved(saved: EditorTripResult) {
|
|
52
|
+
if (trip.value) trip.value = { ...trip.value, result: saved }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The trip that started on this route, handed over once. */
|
|
56
|
+
function consume<Kept>(routeName: string): EditorTrip<Kept> | undefined {
|
|
57
|
+
const current = trip.value
|
|
58
|
+
if (current?.origin.routeName !== routeName) return undefined
|
|
59
|
+
trip.value = undefined
|
|
60
|
+
return current as EditorTrip<Kept>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The label of the editor's back button; none without a trip. */
|
|
64
|
+
const backLabel = computed(() => trip.value?.origin.backLabel)
|
|
65
|
+
|
|
66
|
+
return { start, recordSaved, consume, backLabel }
|
|
67
|
+
})
|
|
@@ -2,11 +2,14 @@ declare module 'nuxt/schema' {
|
|
|
2
2
|
interface AppConfigInput {
|
|
3
3
|
composer?: {
|
|
4
4
|
tracking?: boolean
|
|
5
|
+
/** Turns the Automation pages on (ADR 0019). Off, their routes answer 404. */
|
|
6
|
+
automations?: boolean
|
|
5
7
|
}
|
|
6
8
|
}
|
|
7
9
|
interface AppConfig {
|
|
8
10
|
composer: {
|
|
9
11
|
tracking: boolean
|
|
12
|
+
automations: boolean
|
|
10
13
|
}
|
|
11
14
|
}
|
|
12
15
|
}
|
package/app/types/tracking.ts
CHANGED
|
@@ -76,6 +76,39 @@ export interface TrackingEventMap {
|
|
|
76
76
|
// === Global ===
|
|
77
77
|
'Page View': { page_name: string }
|
|
78
78
|
|
|
79
|
+
// === Automation_list ===
|
|
80
|
+
'automation_list.delete': { automation_name: string; automation_id: string }
|
|
81
|
+
'automation_list.duplicate': { automation_name: string; automation_id: string }
|
|
82
|
+
'automation_list.edit': { automation_name: string; automation_id: string }
|
|
83
|
+
'automation_list.filter_changed': {
|
|
84
|
+
search_term: string
|
|
85
|
+
/** A Trigger type, a status, a Channel; `all` when not narrowed. */
|
|
86
|
+
trigger_type: string
|
|
87
|
+
status: string
|
|
88
|
+
channel: string
|
|
89
|
+
}
|
|
90
|
+
'automation_list.sort_changed': { column: string; direction: 'asc' | 'desc' }
|
|
91
|
+
|
|
92
|
+
// === Automation create / edit ===
|
|
93
|
+
'Automation setup': {
|
|
94
|
+
automation_sender: string
|
|
95
|
+
automation_name: string
|
|
96
|
+
automation_trigger: string
|
|
97
|
+
automation_type: string
|
|
98
|
+
automation_rules_audience: 'all' | 'segmented'
|
|
99
|
+
automation_subject: string
|
|
100
|
+
automation_moment: string
|
|
101
|
+
automation_channel: string
|
|
102
|
+
automation_status: string
|
|
103
|
+
automation_days: number
|
|
104
|
+
created_by: 'user'
|
|
105
|
+
}
|
|
106
|
+
'automation.clone': { automation_name: string; source_id: string }
|
|
107
|
+
'automation.trigger_changed': { from: string; to: string }
|
|
108
|
+
'automation.action_added': { channel: string }
|
|
109
|
+
'automation.action_deleted': { channel: string }
|
|
110
|
+
'automation.action_toggled': { channel: string; new_status: string }
|
|
111
|
+
|
|
79
112
|
// === Brand ===
|
|
80
113
|
brand_create: BrandBaseProperties & { has_logo: boolean }
|
|
81
114
|
brand_update: BrandBaseProperties & { changed_fields: string[] }
|