@nyaruka/temba-components 0.76.0 → 0.78.0

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.
@@ -3,12 +3,9 @@ import { FormElement } from '../FormElement';
3
3
  import { TemplateResult, html, css, PropertyValueMap } from 'lit';
4
4
  import { CustomEventType } from '../interfaces';
5
5
 
6
- const KEY_HEADER = 'header';
7
- const KEY_BODY = 'body';
8
- const KEY_FOOTER = 'footer';
9
- const KEY_BUTTONS = 'button';
10
-
11
6
  interface Component {
7
+ name: string;
8
+ type: string;
12
9
  content: string;
13
10
  params: { type: string }[];
14
11
  }
@@ -17,7 +14,7 @@ interface Translation {
17
14
  locale: string;
18
15
  status: string;
19
16
  channel: { uuid: string; name: string };
20
- components: { [key: string]: Component };
17
+ components: Component[];
21
18
  }
22
19
 
23
20
  interface Template {
@@ -95,9 +92,18 @@ export class TemplateEditor extends FormElement {
95
92
  border-radius: var(--curvature);
96
93
  min-height: 23px;
97
94
  display: flex;
95
+ flex-direction: row;
98
96
  align-items: center;
99
97
  margin-right: 0.5em;
100
98
  margin-top: 0.5em;
99
+ align-items: center;
100
+ }
101
+
102
+ .button .display {
103
+ margin-right: 0.5em;
104
+ background: #f9f9f9;
105
+ padding: 0.25em 1em;
106
+ border-radius: var(--curvature);
101
107
  }
102
108
 
103
109
  temba-textinput,
@@ -140,10 +146,6 @@ export class TemplateEditor extends FormElement {
140
146
  @property({ type: Boolean })
141
147
  translating: boolean;
142
148
 
143
- buttonKeys = [];
144
- contentKeys = [];
145
- otherKeys = [];
146
-
147
149
  public firstUpdated(
148
150
  changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
149
151
  ): void {
@@ -166,30 +168,13 @@ export class TemplateEditor extends FormElement {
166
168
  (!loc && translation.locale.split('-')[0] === lang)
167
169
  ) {
168
170
  this.translation = translation;
169
- this.buttonKeys = [];
170
- this.contentKeys = [];
171
- this.otherKeys = [];
172
- const keys = Object.keys(translation.components);
173
- for (const key of keys) {
174
- if (key.startsWith(KEY_BUTTONS)) {
175
- this.buttonKeys.push(key);
176
- } else if (
177
- key === KEY_HEADER ||
178
- key === KEY_BODY ||
179
- key === KEY_FOOTER
180
- ) {
181
- this.contentKeys.push(key);
182
- } else {
183
- this.otherKeys.push(key);
184
- }
185
-
186
- const compParams = translation.components[key].params || [];
171
+ for (const comp of translation.components) {
172
+ const compParams = comp.params || [];
187
173
  if (compParams.length > 0) {
188
174
  // create an array for the length of params
189
- newParams[key] = new Array(compParams.length).fill('');
175
+ newParams[comp.name] = new Array(compParams.length).fill('');
190
176
  }
191
177
  }
192
- this.buttonKeys.sort();
193
178
 
194
179
  // if we are looking at the same template copy our params on top
195
180
  if (this.template === this.selectedTemplate.uuid) {
@@ -217,9 +202,8 @@ export class TemplateEditor extends FormElement {
217
202
 
218
203
  private handleVariableChanged(event: CustomEvent) {
219
204
  const target = event.target as HTMLInputElement;
220
- const key = target.getAttribute('key');
221
205
  const index = parseInt(target.getAttribute('index'));
222
- this.params[key][index - 1] = target.value;
206
+ this.params[target.name][index - 1] = target.value;
223
207
  this.fireCustomEvent(CustomEventType.ContentChanged, {
224
208
  template: this.selectedTemplate,
225
209
  translation: this.translation,
@@ -227,21 +211,21 @@ export class TemplateEditor extends FormElement {
227
211
  });
228
212
  }
229
213
 
230
- private renderVariables(key: string, component: Component) {
214
+ private renderVariables(component: Component) {
231
215
  const parts = component.content.split(/{{(\d+)}}/g);
232
216
  if (parts.length > 0) {
233
217
  const variables = parts.map((part, index) => {
234
- const keyIndex = Math.round(index / 2);
218
+ const paramIndex = Math.round(index / 2);
235
219
  if (index % 2 === 0) {
236
220
  return html`<span class="text">${part}</span>`;
237
221
  }
238
222
  return html`<temba-completion
239
223
  class="variable"
240
224
  type="text"
241
- value=${this.params[key][keyIndex - 1]}
225
+ value=${this.params[component.name][paramIndex - 1]}
242
226
  @change=${this.handleVariableChanged}
243
- key="${key}"
244
- index="${keyIndex}}"
227
+ name="${component.name}"
228
+ index="${paramIndex}"
245
229
  placeholder="variable.."
246
230
  ></temba-completion>`;
247
231
  });
@@ -249,72 +233,54 @@ export class TemplateEditor extends FormElement {
249
233
  }
250
234
  }
251
235
 
252
- private renderComponent(key: string, component: Component) {
253
- return html` <div class="component">
254
- <div>${key}</div>
255
- ${this.renderVariables(key, component)}
256
- </div>`;
257
- }
258
-
259
- public renderContent(components: {
260
- [key: string]: Component;
261
- }): TemplateResult {
262
- let header = null;
263
- let body = null;
264
- let footer = null;
265
-
266
- if (components[KEY_HEADER]) {
267
- header = html`<div class="header">
268
- ${this.renderVariables(KEY_HEADER, components[KEY_HEADER])}
269
- </div>`;
270
- }
271
-
272
- if (components[KEY_BODY]) {
273
- body = html`<div class="body">
274
- ${this.renderVariables(KEY_BODY, components[KEY_BODY])}
275
- </div>`;
276
- }
277
-
278
- if (components[KEY_FOOTER]) {
279
- footer = html`<div class="footer">
280
- ${this.renderVariables(KEY_FOOTER, components[KEY_FOOTER])}
236
+ public renderComponents(components: Component[]): TemplateResult {
237
+ const nonButtons = components
238
+ .filter(comp => !comp.type.startsWith('button/'))
239
+ .map(
240
+ component =>
241
+ html`<div class="${component['name']}">
242
+ ${this.renderVariables(component)}
243
+ </div>`
244
+ );
245
+ const buttonComponents = components.filter(comp =>
246
+ comp.type.startsWith('button/')
247
+ );
248
+ const buttons =
249
+ buttonComponents.length > 0 ? this.renderButtons(buttonComponents) : null;
250
+ return html`<div class="main">${nonButtons}</div>
251
+ <div class="buttons">
252
+ ${buttons}
253
+ <div></div>
281
254
  </div>`;
282
- }
283
-
284
- if (header || body || footer) {
285
- return html`<div class="content">${header}${body}${footer}</div>`;
286
- }
287
- return null;
288
255
  }
289
256
 
290
257
  public renderButtons(components): TemplateResult {
291
- if (this.buttonKeys.length > 0) {
292
- const buttons = this.buttonKeys.map(key => {
293
- const component = components[key];
294
- return html`<div class="button">
295
- ${this.renderVariables(key, component)}
296
- </div>`;
297
- });
298
- return html`<div class="button-wrapper">
299
- <div class="button-header">Template Buttons</div>
300
- <div class="buttons">${buttons}</div>
301
- </div>`;
302
- }
303
- return null;
258
+ const buttons = components.map(component => {
259
+ if (component.display) {
260
+ return html`
261
+ <div class="button">
262
+ <div class="display">${component.display}</div>
263
+ ${this.renderVariables(component)}
264
+ </div>
265
+ `;
266
+ } else {
267
+ return html`
268
+ <div class="button">${this.renderVariables(component)}</div>
269
+ `;
270
+ }
271
+ });
272
+ return html`<div class="button-wrapper">
273
+ <div class="button-header">Template Buttons</div>
274
+ <div class="buttons">${buttons}</div>
275
+ </div>`;
304
276
  }
277
+
305
278
  public render(): TemplateResult {
306
279
  let content = null;
307
- let buttons = null;
308
- let otherComponents = null;
309
280
  if (this.translation) {
310
- content = this.renderContent(this.translation.components);
311
- buttons = this.renderButtons(this.translation.components);
312
- otherComponents = this.otherKeys.map(key => {
313
- const component = this.translation.components[key];
314
- return this.renderComponent(key, component);
315
- });
281
+ content = this.renderComponents(this.translation.components);
316
282
  } else {
317
- otherComponents = html`<div class="error-message">
283
+ content = html`<div class="error-message">
318
284
  No approved translation was found for current language.
319
285
  </div>`;
320
286
  }
@@ -328,7 +294,7 @@ export class TemplateEditor extends FormElement {
328
294
  valuekey="uuid"
329
295
  class="picker"
330
296
  value="${this.template}"
331
- endpoint=${this.url}
297
+ endpoint="${this.url}?comps_as_list=true"
332
298
  shouldExclude=${template => template.status !== 'approved'}
333
299
  placeholder="Select a template"
334
300
  @temba-content-changed=${this.swallowEvent}
@@ -336,11 +302,7 @@ export class TemplateEditor extends FormElement {
336
302
  >
337
303
  </temba-select>
338
304
 
339
- ${this.template
340
- ? html` <div class="template">
341
- ${content} ${buttons} ${otherComponents}
342
- </div>`
343
- : null}
305
+ ${this.template ? html` <div class="template">${content}</div>` : null}
344
306
  </div>
345
307
  `;
346
308
  }
@@ -14,8 +14,10 @@
14
14
  "namespace": "",
15
15
  "locale": "eng-US",
16
16
  "status": "approved",
17
- "components": {
18
- "body": {
17
+ "components": [
18
+ {
19
+ "name": "body",
20
+ "type": "body",
19
21
  "content": "Hi bob {{1}} from {{2}}",
20
22
  "params": [
21
23
  {
@@ -26,11 +28,15 @@
26
28
  }
27
29
  ]
28
30
  },
29
- "buttons.0": {
31
+ {
32
+ "name": "buttons.0",
33
+ "type": "button/quick_reply",
30
34
  "content": "Yes",
31
35
  "params": []
32
36
  },
33
- "buttons.1": {
37
+ {
38
+ "name": "buttons.1",
39
+ "type": "button/quick_reply",
34
40
  "content": "No {{1}}",
35
41
  "params": [
36
42
  {
@@ -38,7 +44,7 @@
38
44
  }
39
45
  ]
40
46
  }
41
- }
47
+ ]
42
48
  },
43
49
  {
44
50
  "channel": {
@@ -50,29 +56,33 @@
50
56
  "status": "approved",
51
57
  "components": [
52
58
  {
53
- "body": {
54
- "content": "bon jour bob {{1}} from {{2}}",
55
- "params": [
56
- {
57
- "type": "text"
58
- },
59
- {
60
- "type": "text"
61
- }
62
- ]
63
- },
64
- "buttons.0": {
65
- "content": "Yes",
66
- "params": []
67
- },
68
- "buttons.1": {
69
- "content": "No {{1}}",
70
- "params": [
71
- {
72
- "type": "text"
73
- }
74
- ]
75
- }
59
+ "name": "body",
60
+ "type": "body",
61
+ "content": "bon jour bob {{1}} from {{2}}",
62
+ "params": [
63
+ {
64
+ "type": "text"
65
+ },
66
+ {
67
+ "type": "text"
68
+ }
69
+ ]
70
+ },
71
+ {
72
+ "name": "buttons.0",
73
+ "type": "button/quick_reply",
74
+ "content": "Yes",
75
+ "params": []
76
+ },
77
+ {
78
+ "name": "buttons.1",
79
+ "type": "button/quick_reply",
80
+ "content": "No {{1}}",
81
+ "params": [
82
+ {
83
+ "type": "text"
84
+ }
85
+ ]
76
86
  }
77
87
  ]
78
88
  }
@@ -92,8 +102,10 @@
92
102
  "namespace": "",
93
103
  "locale": "eng-US",
94
104
  "status": "approved",
95
- "components": {
96
- "body": {
105
+ "components": [
106
+ {
107
+ "name": "body",
108
+ "type": "body",
97
109
  "content": "Hi bob {{1}} from {{2}}",
98
110
  "params": [
99
111
  {
@@ -104,11 +116,15 @@
104
116
  }
105
117
  ]
106
118
  },
107
- "buttons.0": {
119
+ {
120
+ "name": "buttons.0",
121
+ "type": "button/quick_reply",
108
122
  "content": "Yes",
109
123
  "params": []
110
124
  },
111
- "buttons.1": {
125
+ {
126
+ "name": "buttons.1",
127
+ "type": "button/quick_reply",
112
128
  "content": "No {{1}}",
113
129
  "params": [
114
130
  {
@@ -116,7 +132,7 @@
116
132
  }
117
133
  ]
118
134
  }
119
- }
135
+ ]
120
136
  },
121
137
  {
122
138
  "channel": {
@@ -128,29 +144,33 @@
128
144
  "status": "pending",
129
145
  "components": [
130
146
  {
131
- "body": {
132
- "content": "bon jour bob {{1}} from {{2}}",
133
- "params": [
134
- {
135
- "type": "text"
136
- },
137
- {
138
- "type": "text"
139
- }
140
- ]
141
- },
142
- "buttons.0": {
143
- "content": "Yes",
144
- "params": []
145
- },
146
- "buttons.1": {
147
- "content": "No {{1}}",
148
- "params": [
149
- {
150
- "type": "text"
151
- }
152
- ]
153
- }
147
+ "name": "body",
148
+ "type": "body",
149
+ "content": "bon jour bob {{1}} from {{2}}",
150
+ "params": [
151
+ {
152
+ "type": "text"
153
+ },
154
+ {
155
+ "type": "text"
156
+ }
157
+ ]
158
+ },
159
+ {
160
+ "name": "buttons.0",
161
+ "type": "button/quick_reply",
162
+ "content": "Yes",
163
+ "params": []
164
+ },
165
+ {
166
+ "name": "buttons.1",
167
+ "type": "button/quick_reply",
168
+ "content": "No {{1}}",
169
+ "params": [
170
+ {
171
+ "type": "text"
172
+ }
173
+ ]
154
174
  }
155
175
  ]
156
176
  }
@@ -170,8 +190,10 @@
170
190
  "namespace": "",
171
191
  "locale": "eng-US",
172
192
  "status": "approved",
173
- "components": {
174
- "body": {
193
+ "components": [
194
+ {
195
+ "name": "body",
196
+ "type": "body",
175
197
  "content": "Hi there, we are trying to reach {{1}} about their car warranty. It turns out you only have until {{2}} to decide whether to extend it further. Please contact us at {{3}} at your earliest convenience to discuss your options. Are you still interested?",
176
198
  "params": [
177
199
  {
@@ -185,14 +207,20 @@
185
207
  }
186
208
  ]
187
209
  },
188
- "buttons.0": {
210
+ {
211
+ "name": "buttons.0",
212
+ "type": "button/quick_reply",
189
213
  "content": "Yes",
190
214
  "params": []
191
215
  },
192
- "buttons.1": {
216
+ {
217
+ "name": "buttons.1",
218
+ "type": "button/quick_reply",
193
219
  "content": "No"
194
220
  },
195
- "buttons.2": {
221
+ {
222
+ "name": "buttons.2",
223
+ "type": "button/quick_reply",
196
224
  "content": "Wait until {{1}}",
197
225
  "params": [
198
226
  {
@@ -200,7 +228,7 @@
200
228
  }
201
229
  ]
202
230
  }
203
- }
231
+ ]
204
232
  },
205
233
  {
206
234
  "channel": {
@@ -210,8 +238,10 @@
210
238
  "namespace": "",
211
239
  "locale": "fra-FR",
212
240
  "status": "pending",
213
- "components": {
214
- "body": {
241
+ "components": [
242
+ {
243
+ "name": "body",
244
+ "type": "body",
215
245
  "content": "Bonjour, nous essayons d'atteindre {{1}} à propos de leur garantie automobile. Il s'avère que tu n'as que jusqu'à {{2}} pour décider s'il convient de le prolonger davantage. Veuillez nous contacter au {{3}} dès que possible pour discuter de vos options. Êtes vous toujours intéressé?",
216
246
  "params": [
217
247
  {
@@ -225,14 +255,20 @@
225
255
  }
226
256
  ]
227
257
  },
228
- "buttons.0": {
258
+ {
259
+ "name": "buttons.0",
260
+ "type": "button/quick_reply",
229
261
  "content": "Oui",
230
262
  "params": []
231
263
  },
232
- "buttons.1": {
264
+ {
265
+ "name": "buttons.1",
266
+ "type": "button/quick_reply",
233
267
  "content": "No"
234
268
  },
235
- "buttons.2": {
269
+ {
270
+ "name": "buttons.2",
271
+ "type": "button/quick_reply",
236
272
  "content": "Attendre jusqu'à {{1}}",
237
273
  "params": [
238
274
  {
@@ -240,7 +276,7 @@
240
276
  }
241
277
  ]
242
278
  }
243
- }
279
+ ]
244
280
  }
245
281
  ],
246
282
  "created_on": "2023-09-08T22:38:22.710140Z",
@@ -1,3 +1,4 @@
1
+ import { html, fixture, expect } from '@open-wc/testing';
1
2
  import { TembaDate } from '../src/date/TembaDate';
2
3
  import {
3
4
  assertScreenshot,
@@ -6,7 +7,6 @@ import {
6
7
  loadStore,
7
8
  mockNow,
8
9
  } from './utils.test';
9
- import { expect } from '@open-wc/testing';
10
10
 
11
11
  const TAG = 'temba-date';
12
12
 
@@ -25,7 +25,7 @@ describe('temba-date', () => {
25
25
  it('renders default', async () => {
26
26
  const date = await getDate({ value: '1978-11-18T02:22:00.000000-07:00' });
27
27
  const dateString = (
28
- date.shadowRoot.querySelector('.date') as HTMLDivElement
28
+ date.shadowRoot.querySelector('.date') as HTMLSpanElement
29
29
  ).innerText;
30
30
 
31
31
  await assertScreenshot('date/date', getClip(date));
@@ -38,7 +38,7 @@ describe('temba-date', () => {
38
38
  display: 'duration',
39
39
  });
40
40
  const dateString = (
41
- date.shadowRoot.querySelector('.date') as HTMLDivElement
41
+ date.shadowRoot.querySelector('.date') as HTMLSpanElement
42
42
  ).innerText;
43
43
 
44
44
  await assertScreenshot('date/duration', getClip(date));
@@ -51,10 +51,22 @@ describe('temba-date', () => {
51
51
  display: 'datetime',
52
52
  });
53
53
  const dateString = (
54
- date.shadowRoot.querySelector('.date') as HTMLDivElement
54
+ date.shadowRoot.querySelector('.date') as HTMLSpanElement
55
55
  ).innerText;
56
56
 
57
57
  await assertScreenshot('date/datetime', getClip(date));
58
58
  expect(dateString).to.equal('11/18/1978, 9:22 AM');
59
59
  });
60
+
61
+ it('renders inline', async () => {
62
+ const el: HTMLElement = await fixture(html`
63
+ <span
64
+ >Your birthday is
65
+ <temba-date value="1978-11-18T02:22:00.000000-07:00"></temba-date
66
+ >!</span
67
+ >
68
+ `);
69
+
70
+ await assertScreenshot('date/date-inline', getClip(el));
71
+ });
60
72
  });