@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
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { CONDITION_OPERATORS_BY_FIELD_TYPE } from '@dev.smartpricing/message-composer-utils/types'
|
|
2
|
+
import type {
|
|
3
|
+
Condition,
|
|
4
|
+
ConditionFieldType,
|
|
5
|
+
ConditionOperator,
|
|
6
|
+
FilterField,
|
|
7
|
+
FilterOptionValue,
|
|
8
|
+
FilterUnavailableReason,
|
|
9
|
+
FiltersReport,
|
|
10
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The filter components' own model (ADR 0018): a list of Conditions seen as
|
|
14
|
+
* rows, one per field. Internal: "between" as one row, prices in units and
|
|
15
|
+
* the rows themselves never leave the components.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The operators of a row: the engine's, plus "between", a pair of Conditions.
|
|
20
|
+
* There is no "not between": the engine holds all the Conditions of a list
|
|
21
|
+
* together, and no pair of them says "outside this range".
|
|
22
|
+
*/
|
|
23
|
+
export type FilterRowOperator = ConditionOperator | 'between'
|
|
24
|
+
|
|
25
|
+
/** Select: one option. Multiselect: a list. Number, price: a number. Date: `YYYY-MM-DD`. Text: a string. */
|
|
26
|
+
export type FilterRowValue = FilterOptionValue | FilterOptionValue[] | undefined
|
|
27
|
+
|
|
28
|
+
/** A row with no field picked yet. Never emitted, and not half-filled either. */
|
|
29
|
+
export interface FilterBlankRow {
|
|
30
|
+
kind: 'blank'
|
|
31
|
+
id: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FilterEditableRow {
|
|
35
|
+
kind: 'editable'
|
|
36
|
+
id: number
|
|
37
|
+
field: string
|
|
38
|
+
type: ConditionFieldType
|
|
39
|
+
operator: FilterRowOperator
|
|
40
|
+
/** The value, or the lower bound of "between". A price is in units, not cents. */
|
|
41
|
+
value: FilterRowValue
|
|
42
|
+
/** The upper bound of "between". */
|
|
43
|
+
to: FilterRowValue
|
|
44
|
+
/**
|
|
45
|
+
* The Conditions the row was read from, until the user edits it: an
|
|
46
|
+
* untouched row is emitted exactly as it came in.
|
|
47
|
+
*/
|
|
48
|
+
original?: Condition[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A Condition the editor cannot edit: shown locked, emitted as it is. */
|
|
52
|
+
export interface FilterUnavailableRow {
|
|
53
|
+
kind: 'unavailable'
|
|
54
|
+
id: number
|
|
55
|
+
reason: FilterUnavailableReason
|
|
56
|
+
condition: Condition
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type FilterRow = FilterBlankRow | FilterEditableRow | FilterUnavailableRow
|
|
60
|
+
|
|
61
|
+
const ORDERED_TYPES: readonly ConditionFieldType[] = ['number', 'price', 'date']
|
|
62
|
+
|
|
63
|
+
function isOrdered(type: ConditionFieldType): boolean {
|
|
64
|
+
return ORDERED_TYPES.includes(type)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The operators a row on a field of this type offers, in display order. */
|
|
68
|
+
export function filterRowOperators(type: ConditionFieldType): FilterRowOperator[] {
|
|
69
|
+
const operators: FilterRowOperator[] = [...CONDITION_OPERATORS_BY_FIELD_TYPE[type]]
|
|
70
|
+
if (!isOrdered(type)) return operators
|
|
71
|
+
// "Is set" stays last.
|
|
72
|
+
operators.splice(operators.indexOf('exists'), 0, 'between')
|
|
73
|
+
return operators
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isFilterRange(operator: FilterRowOperator): boolean {
|
|
77
|
+
return operator === 'between'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isOptionValue(value: unknown): value is FilterOptionValue {
|
|
81
|
+
return typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isIsoDate(value: unknown): value is string {
|
|
85
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false
|
|
86
|
+
const date = new Date(`${value}T00:00:00Z`)
|
|
87
|
+
// "2026-02-31" parses, as the 3rd of March.
|
|
88
|
+
return !Number.isNaN(date.getTime()) && date.toISOString().startsWith(value)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The row value of a stored one, or `undefined` when it does not fit the type. */
|
|
92
|
+
function readValue(type: ConditionFieldType, value: unknown): FilterRowValue {
|
|
93
|
+
switch (type) {
|
|
94
|
+
case 'select':
|
|
95
|
+
return isOptionValue(value) ? value : undefined
|
|
96
|
+
case 'multiselect':
|
|
97
|
+
return Array.isArray(value) && value.every(isOptionValue) ? [...value] : undefined
|
|
98
|
+
case 'number':
|
|
99
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
100
|
+
case 'price':
|
|
101
|
+
return typeof value === 'number' && Number.isFinite(value) ? value / 100 : undefined
|
|
102
|
+
case 'date':
|
|
103
|
+
return isIsoDate(value) ? value : undefined
|
|
104
|
+
case 'text':
|
|
105
|
+
return typeof value === 'string' ? value : undefined
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeValue(type: ConditionFieldType, value: FilterRowValue): unknown {
|
|
110
|
+
return type === 'price' ? Math.round((value as number) * 100) : value
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isComplete(type: ConditionFieldType, value: FilterRowValue): boolean {
|
|
114
|
+
switch (type) {
|
|
115
|
+
case 'select':
|
|
116
|
+
return isOptionValue(value) && value !== ''
|
|
117
|
+
case 'multiselect':
|
|
118
|
+
return Array.isArray(value) && value.length > 0
|
|
119
|
+
case 'number':
|
|
120
|
+
case 'price':
|
|
121
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
122
|
+
case 'date':
|
|
123
|
+
return isIsoDate(value)
|
|
124
|
+
case 'text':
|
|
125
|
+
return typeof value === 'string' && value !== ''
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Numbers compare as numbers, ISO dates as strings. */
|
|
130
|
+
function isAtMost(a: FilterRowValue, b: FilterRowValue): boolean {
|
|
131
|
+
return (a as number) <= (b as number)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type RowContent = Pick<FilterEditableRow, 'operator' | 'value' | 'to'>
|
|
135
|
+
|
|
136
|
+
function readSingle(field: FilterField, condition: Condition): RowContent | undefined {
|
|
137
|
+
const operators: readonly ConditionOperator[] = CONDITION_OPERATORS_BY_FIELD_TYPE[field.type]
|
|
138
|
+
if (!operators.includes(condition.operator)) return undefined
|
|
139
|
+
if (condition.operator === 'exists') {
|
|
140
|
+
return { operator: 'exists', value: undefined, to: undefined }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const value = readValue(field.type, condition.value)
|
|
144
|
+
if (value === undefined) return undefined
|
|
145
|
+
return { operator: condition.operator, value, to: undefined }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* "Between" is `gte` the lower bound and `lte` the upper one. A `gte` above
|
|
150
|
+
* its `lte` is no range: the two are read one by one.
|
|
151
|
+
*/
|
|
152
|
+
function readPair(
|
|
153
|
+
field: FilterField,
|
|
154
|
+
first: Condition | undefined,
|
|
155
|
+
second: Condition | undefined,
|
|
156
|
+
): RowContent | undefined {
|
|
157
|
+
if (!first || !second || !isOrdered(field.type)) return undefined
|
|
158
|
+
|
|
159
|
+
const gte = [first, second].find((condition) => condition.operator === 'gte')
|
|
160
|
+
const lte = [first, second].find((condition) => condition.operator === 'lte')
|
|
161
|
+
if (!gte || !lte) return undefined
|
|
162
|
+
|
|
163
|
+
const from = readValue(field.type, gte.value)
|
|
164
|
+
const until = readValue(field.type, lte.value)
|
|
165
|
+
if (from === undefined || until === undefined) return undefined
|
|
166
|
+
|
|
167
|
+
return isAtMost(from, until) ? { operator: 'between', value: from, to: until } : undefined
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The rows of a list of Conditions, in the order the Conditions are in. No
|
|
172
|
+
* Condition is left out: one the editor cannot edit is an unavailable row.
|
|
173
|
+
* A field gets one editable row, from its first two Conditions when they are
|
|
174
|
+
* a "between", else from the first one that can be edited.
|
|
175
|
+
*/
|
|
176
|
+
export function conditionsToFilterRows(
|
|
177
|
+
conditions: readonly Condition[],
|
|
178
|
+
fields: readonly FilterField[],
|
|
179
|
+
nextId: () => number,
|
|
180
|
+
): FilterRow[] {
|
|
181
|
+
const fieldsByKey = new Map(fields.map((field) => [field.key, field]))
|
|
182
|
+
|
|
183
|
+
const indexesByField = new Map<string, number[]>()
|
|
184
|
+
conditions.forEach((condition, index) => {
|
|
185
|
+
const indexes = indexesByField.get(condition.field) ?? []
|
|
186
|
+
indexes.push(index)
|
|
187
|
+
indexesByField.set(condition.field, indexes)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// By the index of the Condition a row starts at; `null` for the second of a pair.
|
|
191
|
+
const editable = new Map<number, (RowContent & { original: Condition[] }) | null>()
|
|
192
|
+
for (const [key, indexes] of indexesByField) {
|
|
193
|
+
const field = fieldsByKey.get(key)
|
|
194
|
+
if (!field) continue
|
|
195
|
+
|
|
196
|
+
const [first, second] = indexes as [number, number | undefined]
|
|
197
|
+
const pair = readPair(
|
|
198
|
+
field,
|
|
199
|
+
conditions[first],
|
|
200
|
+
second === undefined ? undefined : conditions[second],
|
|
201
|
+
)
|
|
202
|
+
if (pair && second !== undefined) {
|
|
203
|
+
editable.set(first, { ...pair, original: [conditions[first]!, conditions[second]!] })
|
|
204
|
+
editable.set(second, null)
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const index of indexes) {
|
|
209
|
+
const single = readSingle(field, conditions[index]!)
|
|
210
|
+
if (!single) continue
|
|
211
|
+
editable.set(index, { ...single, original: [conditions[index]!] })
|
|
212
|
+
break
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const rows: FilterRow[] = []
|
|
217
|
+
conditions.forEach((condition, index) => {
|
|
218
|
+
const content = editable.get(index)
|
|
219
|
+
if (content === null) return
|
|
220
|
+
|
|
221
|
+
const field = fieldsByKey.get(condition.field)
|
|
222
|
+
if (content && field) {
|
|
223
|
+
rows.push({ kind: 'editable', id: nextId(), field: field.key, type: field.type, ...content })
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
rows.push({
|
|
227
|
+
kind: 'unavailable',
|
|
228
|
+
id: nextId(),
|
|
229
|
+
reason: field ? 'not_editable' : 'unknown_field',
|
|
230
|
+
condition,
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
return rows
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The Conditions of an editable row; `null` while it is half-filled. */
|
|
237
|
+
export function filterRowToConditions(row: FilterEditableRow): Condition[] | null {
|
|
238
|
+
if (row.original) return row.original
|
|
239
|
+
|
|
240
|
+
const { field, type, operator } = row
|
|
241
|
+
if (operator === 'exists') return [{ field, operator }]
|
|
242
|
+
|
|
243
|
+
if (!isFilterRange(operator)) {
|
|
244
|
+
if (!isComplete(type, row.value)) return null
|
|
245
|
+
return [{ field, operator: operator as ConditionOperator, value: writeValue(type, row.value) }]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!isComplete(type, row.value) || !isComplete(type, row.to)) return null
|
|
249
|
+
// Whichever way round the user typed them.
|
|
250
|
+
const [lower, upper] = isAtMost(row.value, row.to) ? [row.value, row.to] : [row.to, row.value]
|
|
251
|
+
return [
|
|
252
|
+
{ field, operator: 'gte', value: writeValue(type, lower) },
|
|
253
|
+
{ field, operator: 'lte', value: writeValue(type, upper) },
|
|
254
|
+
]
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** What the rows are worth as Conditions. Blank and half-filled rows are left out. */
|
|
258
|
+
export function filterRowsToConditions(rows: readonly FilterRow[]): Condition[] {
|
|
259
|
+
return rows.flatMap((row) => {
|
|
260
|
+
if (row.kind === 'unavailable') return [row.condition]
|
|
261
|
+
if (row.kind === 'editable') return filterRowToConditions(row) ?? []
|
|
262
|
+
return []
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The option values a row holds that its field's `options` do not list. */
|
|
267
|
+
export function missingFilterOptions(
|
|
268
|
+
row: FilterEditableRow,
|
|
269
|
+
field: FilterField | undefined,
|
|
270
|
+
): FilterOptionValue[] {
|
|
271
|
+
if (row.operator === 'exists') return []
|
|
272
|
+
if (row.type !== 'select' && row.type !== 'multiselect') return []
|
|
273
|
+
|
|
274
|
+
const known = new Set((field?.options ?? []).map((option) => option.value))
|
|
275
|
+
const selected = Array.isArray(row.value) ? row.value : [row.value]
|
|
276
|
+
return selected.filter(
|
|
277
|
+
(value): value is FilterOptionValue => value !== undefined && value !== '' && !known.has(value),
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function reportFilterRows(
|
|
282
|
+
rows: readonly FilterRow[],
|
|
283
|
+
fields: readonly FilterField[],
|
|
284
|
+
): FiltersReport {
|
|
285
|
+
const fieldsByKey = new Map(fields.map((field) => [field.key, field]))
|
|
286
|
+
const report: FiltersReport = {
|
|
287
|
+
unknownFields: [],
|
|
288
|
+
unavailable: [],
|
|
289
|
+
unavailableOptions: [],
|
|
290
|
+
halfFilled: 0,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const row of rows) {
|
|
294
|
+
if (row.kind === 'unavailable') {
|
|
295
|
+
report.unavailable.push({ condition: row.condition, reason: row.reason })
|
|
296
|
+
const { field } = row.condition
|
|
297
|
+
if (row.reason === 'unknown_field' && !report.unknownFields.includes(field)) {
|
|
298
|
+
report.unknownFields.push(field)
|
|
299
|
+
}
|
|
300
|
+
} else if (row.kind === 'editable') {
|
|
301
|
+
if (filterRowToConditions(row) === null) report.halfFilled += 1
|
|
302
|
+
for (const value of missingFilterOptions(row, fieldsByKey.get(row.field))) {
|
|
303
|
+
report.unavailableOptions.push({ field: row.field, value })
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return report
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** A row on a field the user just picked: its first operator, no value. */
|
|
311
|
+
export function newFilterRow(id: number, field: FilterField): FilterEditableRow {
|
|
312
|
+
return {
|
|
313
|
+
kind: 'editable',
|
|
314
|
+
id,
|
|
315
|
+
field: field.key,
|
|
316
|
+
type: field.type,
|
|
317
|
+
operator: filterRowOperators(field.type)[0]!,
|
|
318
|
+
value: field.type === 'multiselect' ? [] : undefined,
|
|
319
|
+
to: undefined,
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The row under another operator: a value is kept wherever it still means something. */
|
|
324
|
+
export function withFilterOperator(
|
|
325
|
+
row: FilterEditableRow,
|
|
326
|
+
operator: FilterRowOperator,
|
|
327
|
+
): FilterEditableRow {
|
|
328
|
+
return {
|
|
329
|
+
...row,
|
|
330
|
+
operator,
|
|
331
|
+
to: isFilterRange(operator) ? row.to : undefined,
|
|
332
|
+
original: undefined,
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** The locale key of an operator's label, under `filters.operators`. */
|
|
337
|
+
export const FILTER_OPERATOR_LABEL_KEYS: Record<FilterRowOperator, string> = {
|
|
338
|
+
eq: 'is',
|
|
339
|
+
neq: 'is_not',
|
|
340
|
+
gt: 'is_more',
|
|
341
|
+
gte: 'is_at_least',
|
|
342
|
+
lt: 'is_less',
|
|
343
|
+
lte: 'is_at_most',
|
|
344
|
+
between: 'is_between',
|
|
345
|
+
in: 'in',
|
|
346
|
+
not_in: 'not_in',
|
|
347
|
+
contains: 'contains',
|
|
348
|
+
exclude: 'does_not_contain',
|
|
349
|
+
exists: 'is_set',
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** A stored value the components cannot label, as it is. */
|
|
353
|
+
export function rawConditionValue(value: unknown): string {
|
|
354
|
+
if (value === undefined) return ''
|
|
355
|
+
return typeof value === 'string' ? value : JSON.stringify(value)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** The currency to show prices in: the caller's, or EUR when `Intl` does not take it. */
|
|
359
|
+
export function filterCurrency(currency: string | undefined): string {
|
|
360
|
+
if (!currency) return 'EUR'
|
|
361
|
+
try {
|
|
362
|
+
new Intl.NumberFormat('en', { style: 'currency', currency })
|
|
363
|
+
return currency
|
|
364
|
+
} catch {
|
|
365
|
+
return 'EUR'
|
|
366
|
+
}
|
|
367
|
+
}
|
|
@@ -519,7 +519,6 @@ export const templateListTableTestIds = {
|
|
|
519
519
|
paginationSummary: 'template-list-table-pagination-summary',
|
|
520
520
|
card: 'template-list-table-card',
|
|
521
521
|
table: 'template-list-table-table',
|
|
522
|
-
loading: 'template-list-table-loading',
|
|
523
522
|
rowName: 'template-list-table-row-name',
|
|
524
523
|
rowInternalBadge: 'template-list-table-row-internal-badge',
|
|
525
524
|
rowCategoryBadge: 'template-list-table-row-category-badge',
|
|
@@ -785,3 +784,110 @@ export const messageSendFlowTestIds = {
|
|
|
785
784
|
cancel: 'message-send-flow-cancel',
|
|
786
785
|
confirm: 'message-send-flow-confirm',
|
|
787
786
|
} as const
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* The Automation pages. The values are the ones Smartconnect's own pages used
|
|
790
|
+
* (`automationsTestIds` in its `testIds.const.ts`): host e2e suites target them.
|
|
791
|
+
*/
|
|
792
|
+
export const automationsTestIds = {
|
|
793
|
+
// List page
|
|
794
|
+
page: 'automations-page',
|
|
795
|
+
newButton: 'automations-new-button',
|
|
796
|
+
|
|
797
|
+
// List
|
|
798
|
+
searchInput: 'automations-search-input',
|
|
799
|
+
filterTrigger: 'automations-filter-trigger',
|
|
800
|
+
filterStatus: 'automations-filter-status',
|
|
801
|
+
filterChannel: 'automations-filter-channel',
|
|
802
|
+
table: 'automations-table',
|
|
803
|
+
rowActionsButton: 'automations-row-actions-button',
|
|
804
|
+
editAction: 'automations-edit-action',
|
|
805
|
+
duplicateAction: 'automations-duplicate-action',
|
|
806
|
+
deleteAction: 'automations-delete-action',
|
|
807
|
+
channelsCell: 'automations-channels-cell',
|
|
808
|
+
statusCell: 'automations-status-cell',
|
|
809
|
+
|
|
810
|
+
// Delete modal
|
|
811
|
+
deleteModal: 'automations-delete-modal',
|
|
812
|
+
deleteModalCloseButton: 'automations-delete-modal-close-button',
|
|
813
|
+
deleteModalCancel: 'automations-delete-modal-cancel',
|
|
814
|
+
deleteModalConfirm: 'automations-delete-modal-confirm',
|
|
815
|
+
deleteModalMessages: 'automations-delete-modal-messages',
|
|
816
|
+
|
|
817
|
+
// New page
|
|
818
|
+
newPage: 'automations-new-page',
|
|
819
|
+
newPageCancelButton: 'automations-new-page-cancel-button',
|
|
820
|
+
newPageCreateButton: 'automations-new-page-create-button',
|
|
821
|
+
|
|
822
|
+
// Edit page
|
|
823
|
+
editPage: 'automations-edit-page',
|
|
824
|
+
editPageCancelButton: 'automations-edit-page-cancel-button',
|
|
825
|
+
editPageCreateButton: 'automations-edit-page-create-button',
|
|
826
|
+
|
|
827
|
+
// Form
|
|
828
|
+
form: 'automations-form',
|
|
829
|
+
errorAlert: 'automations-error-alert',
|
|
830
|
+
nameInput: 'automations-name-input',
|
|
831
|
+
triggerSelect: 'automations-trigger-select',
|
|
832
|
+
|
|
833
|
+
// Send to (audience). The rows of the slideover are `audience-filter-*`.
|
|
834
|
+
sendToSummary: 'automations-send-to-summary',
|
|
835
|
+
addFiltersButton: 'automations-add-filters-button',
|
|
836
|
+
editFiltersButton: 'automations-edit-filters-button',
|
|
837
|
+
filtersSlideover: 'automations-filters-slideover',
|
|
838
|
+
filtersSlideoverCloseButton: 'automations-filters-slideover-close-button',
|
|
839
|
+
filtersSlideoverCancel: 'automations-filters-slideover-cancel',
|
|
840
|
+
filtersSlideoverSave: 'automations-filters-slideover-save',
|
|
841
|
+
|
|
842
|
+
// Actions list
|
|
843
|
+
actionsEmpty: 'automations-actions-empty',
|
|
844
|
+
addChannelButton: 'automations-add-channel-button',
|
|
845
|
+
|
|
846
|
+
// Action card (suffixed with `-${action.key}`, which is also on `data-testvalue`)
|
|
847
|
+
actionCard: 'automations-action-card',
|
|
848
|
+
actionEnabledSwitch: 'automations-action-enabled-switch',
|
|
849
|
+
actionDeleteButton: 'automations-action-delete-button',
|
|
850
|
+
actionPreviewButton: 'automations-action-preview-button',
|
|
851
|
+
actionEditButton: 'automations-action-edit-button',
|
|
852
|
+
|
|
853
|
+
// Message slideover. The message picker is `messageGroupSelectTestIds.root`.
|
|
854
|
+
channelSlideover: 'automations-channel-slideover',
|
|
855
|
+
channelSlideoverCloseButton: 'automations-channel-slideover-close-button',
|
|
856
|
+
channelTypeSelect: 'automations-channel-type-select',
|
|
857
|
+
channelDaysInput: 'automations-channel-days-input',
|
|
858
|
+
channelDateRefSelect: 'automations-channel-date-ref-select',
|
|
859
|
+
channelSendingTimeSelect: 'automations-channel-sending-time-select',
|
|
860
|
+
channelCreateNewButton: 'automations-channel-create-new-button',
|
|
861
|
+
channelEditTemplateButton: 'automations-channel-edit-template-button',
|
|
862
|
+
channelPreview: 'automations-channel-preview',
|
|
863
|
+
channelAdvancedCollapse: 'automations-channel-advanced-collapse',
|
|
864
|
+
channelAdvancedCollapseButton: 'automations-channel-advanced-collapse-button',
|
|
865
|
+
channelRecurrenceSelect: 'automations-channel-recurrence-select',
|
|
866
|
+
channelStrictDelaySwitch: 'automations-channel-strict-delay-switch',
|
|
867
|
+
channelPreventDuplicatesSwitch: 'automations-channel-prevent-duplicates-switch',
|
|
868
|
+
channelSlideoverCancel: 'automations-channel-slideover-cancel',
|
|
869
|
+
channelSlideoverSave: 'automations-channel-slideover-save',
|
|
870
|
+
|
|
871
|
+
// Preview slideover (action card)
|
|
872
|
+
previewSlideover: 'automations-preview-slideover',
|
|
873
|
+
previewSlideoverCloseButton: 'automations-preview-slideover-close-button',
|
|
874
|
+
|
|
875
|
+
// System Automation
|
|
876
|
+
lockedBadge: 'automations-locked-badge',
|
|
877
|
+
lockedAlert: 'automations-locked-alert',
|
|
878
|
+
lockedDuplicateButton: 'automations-locked-duplicate-button',
|
|
879
|
+
|
|
880
|
+
// Clone modal
|
|
881
|
+
cloneModal: 'automations-clone-modal',
|
|
882
|
+
cloneModalNameInput: 'automations-clone-modal-name-input',
|
|
883
|
+
cloneModalConfirm: 'automations-clone-modal-confirm',
|
|
884
|
+
cloneModalCancel: 'automations-clone-modal-cancel',
|
|
885
|
+
} as const
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* The `content` of a UModal or a USlideover that puts a test id on the dialog:
|
|
889
|
+
* the two render no attribute of their own, and `content` types no `data-*`.
|
|
890
|
+
*/
|
|
891
|
+
export function dialogTestId(testId: string): object {
|
|
892
|
+
return { 'data-testid': testId }
|
|
893
|
+
}
|