@nyaruka/temba-components 0.167.0 → 0.168.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.
- package/CHANGELOG.md +21 -0
- package/dist/locales/es.js +3 -1
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +3 -1
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/pt.js +3 -1
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +2791 -1668
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/Icons.ts +2 -0
- package/src/display/Button.ts +8 -6
- package/src/display/Chat.ts +541 -191
- package/src/display/Dropdown.ts +40 -17
- package/src/display/Label.ts +25 -4
- package/src/display/TembaDate.ts +11 -2
- package/src/display/Tip.ts +157 -18
- package/src/events/eventRenderers.ts +678 -268
- package/src/events.ts +16 -1
- package/src/flow/NodeEditor.ts +82 -9
- package/src/flow/actions/add_contact_urn.ts +1 -0
- package/src/flow/actions/play_audio.ts +1 -0
- package/src/flow/actions/send_broadcast.ts +17 -1
- package/src/flow/actions/send_msg.ts +12 -1
- package/src/flow/actions/set_run_result.ts +1 -0
- package/src/flow/actions/start_session.ts +10 -1
- package/src/flow/nodes/shared-rules.ts +2 -0
- package/src/flow/nodes/split_by_airtime.ts +6 -0
- package/src/flow/nodes/split_by_expression.ts +1 -0
- package/src/flow/nodes/split_by_llm.ts +2 -0
- package/src/flow/nodes/split_by_llm_categorize.ts +3 -1
- package/src/flow/nodes/split_by_random.ts +2 -1
- package/src/flow/nodes/split_by_resthook.ts +4 -1
- package/src/flow/nodes/split_by_webhook.ts +9 -1
- package/src/flow/types.ts +3 -0
- package/src/flow/utils.ts +49 -0
- package/src/form/DatePicker.ts +2 -1
- package/src/layout/Card.ts +9 -0
- package/src/layout/CardLayout.ts +426 -21
- package/src/layout/CardStack.ts +4 -1
- package/src/list/ContactList.ts +22 -17
- package/src/list/ContentList.ts +632 -29
- package/src/list/MsgList.ts +4 -2
- package/src/list/SortableList.ts +5 -1
- package/src/list/TembaMenu.ts +67 -6
- package/src/live/ContactChat.ts +655 -336
- package/src/live/ContactDetails.ts +961 -88
- package/src/live/ContactFieldEditor.ts +46 -11
- package/src/live/ContactFields.ts +57 -16
- package/src/live/ContactStoreElement.ts +15 -8
- package/src/locales/es.ts +3 -2
- package/src/locales/fr.ts +3 -2
- package/src/locales/pt.ts +3 -2
- package/src/simulator/Simulator.ts +28 -0
- package/src/styles/pillVariants.ts +13 -1
- package/web-test-runner.config.mjs +2 -0
- package/xliff/es.xlf +6 -3
- package/xliff/fr.xlf +6 -3
- package/xliff/pt.xlf +6 -3
|
@@ -1,47 +1,79 @@
|
|
|
1
|
-
import { css, html, TemplateResult } from 'lit';
|
|
2
|
-
import {
|
|
1
|
+
import { css, html, PropertyValues, TemplateResult } from 'lit';
|
|
2
|
+
import { property, state } from 'lit/decorators.js';
|
|
3
3
|
import { Icon } from '../Icons';
|
|
4
|
-
import {
|
|
4
|
+
import { CustomEventType, Group, URN } from '../interfaces';
|
|
5
5
|
import { getLanguageName } from '../languages';
|
|
6
|
+
import { capitalize, WebResponse } from '../utils';
|
|
7
|
+
import { Select } from '../form/select/Select';
|
|
8
|
+
import { TextInput } from '../form/TextInput';
|
|
9
|
+
import { ContactFieldEditor } from './ContactFieldEditor';
|
|
10
|
+
import { ContactStoreElement, getDestinationURN } from './ContactStoreElement';
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
archived: 'Archived'
|
|
12
|
-
};
|
|
12
|
+
interface Option {
|
|
13
|
+
name: string;
|
|
14
|
+
value: string;
|
|
15
|
+
}
|
|
13
16
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
const STATUS_OPTIONS: Option[] = [
|
|
18
|
+
{ value: 'active', name: 'Active' },
|
|
19
|
+
{ value: 'blocked', name: 'Blocked' },
|
|
20
|
+
{ value: 'stopped', name: 'Stopped' },
|
|
21
|
+
{ value: 'archived', name: 'Archived' }
|
|
22
|
+
];
|
|
20
23
|
|
|
21
24
|
export class ContactDetails extends ContactStoreElement {
|
|
25
|
+
@property({ type: Boolean })
|
|
26
|
+
editable = false;
|
|
27
|
+
|
|
28
|
+
@property({ type: Boolean })
|
|
29
|
+
anon = false;
|
|
30
|
+
|
|
31
|
+
@property({ type: Array })
|
|
32
|
+
schemes: Option[] = [];
|
|
33
|
+
|
|
34
|
+
@state()
|
|
35
|
+
private newScheme = '';
|
|
36
|
+
|
|
37
|
+
@state()
|
|
38
|
+
private newUrn = '';
|
|
39
|
+
|
|
40
|
+
@state()
|
|
41
|
+
private urnDialogOpen = false;
|
|
42
|
+
|
|
43
|
+
@state()
|
|
44
|
+
private draftUrns: URN[] = [];
|
|
45
|
+
|
|
46
|
+
@state()
|
|
47
|
+
private saving = new Set<string>();
|
|
48
|
+
|
|
49
|
+
@state()
|
|
50
|
+
private failed = new Set<string>();
|
|
51
|
+
|
|
52
|
+
private saveGeneration = 0;
|
|
53
|
+
|
|
54
|
+
private saveGenerations = new Map<string, number>();
|
|
55
|
+
|
|
56
|
+
private fieldSaveGenerations = new Map<string, number>();
|
|
57
|
+
|
|
58
|
+
private groupsSave: Promise<WebResponse> | null = null;
|
|
59
|
+
|
|
22
60
|
static get styles() {
|
|
23
61
|
return css`
|
|
24
62
|
.wrapper {
|
|
25
|
-
padding-top:
|
|
63
|
+
padding-top: 0;
|
|
26
64
|
}
|
|
27
65
|
|
|
28
|
-
/* Mirrors the disabled <temba-contact-field> row so the Groups
|
|
29
|
-
entry reads as just another field — same label color/size,
|
|
30
|
-
same horizontal inset, same bottom separator. Margin matches
|
|
31
|
-
the combined .wrapper + :host margins of contact-field
|
|
32
|
-
(0.5em + 1em) so spacing between rows stays uniform. */
|
|
33
66
|
.row {
|
|
34
67
|
padding-bottom: 0.6em;
|
|
35
68
|
border-bottom: 1px solid #ececec;
|
|
36
|
-
margin-bottom:
|
|
69
|
+
margin-bottom: 1em;
|
|
37
70
|
}
|
|
38
71
|
|
|
39
72
|
.row .label {
|
|
40
73
|
color: var(--text-2);
|
|
41
74
|
font-size: 11px;
|
|
42
75
|
font-weight: var(--w-medium);
|
|
43
|
-
margin
|
|
44
|
-
margin-left: 0.25em;
|
|
76
|
+
margin: 0 0 4px 2px;
|
|
45
77
|
}
|
|
46
78
|
|
|
47
79
|
.row .value {
|
|
@@ -53,10 +85,227 @@ export class ContactDetails extends ContactStoreElement {
|
|
|
53
85
|
gap: 0.4em;
|
|
54
86
|
}
|
|
55
87
|
|
|
56
|
-
.
|
|
88
|
+
.editable-row {
|
|
89
|
+
position: relative;
|
|
90
|
+
margin-bottom: 1em;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.editable-row > label {
|
|
94
|
+
display: block;
|
|
95
|
+
color: var(--text-2);
|
|
96
|
+
font-size: 12px;
|
|
97
|
+
font-weight: var(--w-medium);
|
|
98
|
+
margin: 0 0 4px 2px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.editable-row temba-select {
|
|
102
|
+
display: block;
|
|
103
|
+
--color-widget-bg: white;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.editable-row.saving {
|
|
107
|
+
pointer-events: none;
|
|
108
|
+
opacity: 0.7;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.editable-row.failed {
|
|
112
|
+
--color-widget-border: rgba(var(--error-rgb), 0.5);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.error {
|
|
116
|
+
position: absolute;
|
|
117
|
+
right: 8px;
|
|
118
|
+
top: 29px;
|
|
119
|
+
z-index: 1;
|
|
120
|
+
--icon-color: rgb(var(--error-rgb));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.dynamic-groups {
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-wrap: wrap;
|
|
126
|
+
gap: 0.4em;
|
|
127
|
+
margin: 0.5em 0 0 0.25em;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.urn-display {
|
|
131
|
+
display: grid;
|
|
132
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
133
|
+
gap: 8px;
|
|
134
|
+
align-items: start;
|
|
135
|
+
margin-bottom: 14px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.urn-display.editable {
|
|
139
|
+
grid-template-columns: minmax(0, 1fr) auto 24px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.urn-display temba-contact-field {
|
|
143
|
+
--contact-field-separator: none;
|
|
144
|
+
--contact-field-padding-bottom: 0;
|
|
145
|
+
--contact-field-wrapper-margin-bottom: 0;
|
|
146
|
+
margin-bottom: 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.urn-display.only-unsendable {
|
|
150
|
+
border-left: 3px solid rgba(var(--error-rgb), 0.65);
|
|
151
|
+
background: rgba(var(--error-rgb), 0.05);
|
|
152
|
+
padding: 6px 8px 6px 10px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.urn-display.only-unsendable temba-contact-field {
|
|
156
|
+
--contact-field-value-icon-color: rgb(var(--error-rgb));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.urn-more-count {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
align-self: center;
|
|
163
|
+
height: 24px;
|
|
164
|
+
margin-top: 22px;
|
|
165
|
+
color: var(--text-2);
|
|
166
|
+
font-size: 12px;
|
|
167
|
+
white-space: nowrap;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.urn-edit-icon {
|
|
171
|
+
display: flex;
|
|
172
|
+
width: 24px;
|
|
173
|
+
height: 24px;
|
|
174
|
+
margin-top: 22px;
|
|
175
|
+
justify-self: center;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
--icon-color: var(--text-2);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.urn-edit-icon:hover,
|
|
181
|
+
.urn-edit-icon:focus-visible {
|
|
182
|
+
--icon-color: var(--text-1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.urn-sendability {
|
|
186
|
+
display: flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
justify-content: center;
|
|
189
|
+
--icon-color: var(--text-3);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
temba-textinput .urn-sendability {
|
|
193
|
+
margin: 0 8px 0 10px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.urn-delete-action {
|
|
197
|
+
display: flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
margin: 0 10px 0 4px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.urn-delete-icon {
|
|
203
|
+
display: flex;
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
--icon-color: var(--text-2);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.urn-delete-icon:hover,
|
|
209
|
+
.urn-delete-icon:focus-visible {
|
|
210
|
+
--icon-color: rgb(var(--error-rgb));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.urn-dialog {
|
|
214
|
+
display: grid;
|
|
215
|
+
gap: 20px;
|
|
216
|
+
padding: 24px;
|
|
217
|
+
width: 100%;
|
|
218
|
+
box-sizing: border-box;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.urn-list {
|
|
222
|
+
display: block;
|
|
223
|
+
width: 100%;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.urn-row {
|
|
227
|
+
display: grid;
|
|
228
|
+
grid-template-columns: 18px minmax(0, 1fr);
|
|
229
|
+
gap: 10px;
|
|
230
|
+
align-items: center;
|
|
231
|
+
width: 100%;
|
|
232
|
+
min-width: 0;
|
|
233
|
+
box-sizing: border-box;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.urn-row temba-textinput {
|
|
237
|
+
display: block;
|
|
238
|
+
width: 100%;
|
|
239
|
+
min-width: 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.drag-slot {
|
|
243
|
+
align-self: end;
|
|
244
|
+
display: flex;
|
|
245
|
+
align-items: center;
|
|
246
|
+
justify-content: center;
|
|
247
|
+
height: calc(var(--input-h, 34px) + 2px);
|
|
248
|
+
box-sizing: border-box;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.drag-handle {
|
|
252
|
+
cursor: grab;
|
|
253
|
+
--icon-color: var(--text-2);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.drag-handle:active {
|
|
257
|
+
cursor: grabbing;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.add-urn-fields {
|
|
261
|
+
display: grid;
|
|
262
|
+
grid-template-columns: minmax(120px, 0.8fr) minmax(160px, 1.2fr) 72px;
|
|
263
|
+
gap: 8px;
|
|
264
|
+
align-items: end;
|
|
265
|
+
margin-left: 28px;
|
|
266
|
+
padding-top: 16px;
|
|
267
|
+
border-top: 1px solid var(--border-light, #ececec);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.add-urn-fields label {
|
|
271
|
+
display: block;
|
|
272
|
+
color: var(--text-2);
|
|
273
|
+
font-size: 12px;
|
|
274
|
+
font-weight: var(--w-medium);
|
|
275
|
+
margin: 0 0 4px 2px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.add-urn-fields temba-select,
|
|
279
|
+
.add-urn-fields temba-textinput {
|
|
280
|
+
display: block;
|
|
281
|
+
width: 100%;
|
|
282
|
+
min-width: 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.add-urn-button {
|
|
286
|
+
min-height: var(--input-h, 34px);
|
|
287
|
+
display: flex;
|
|
288
|
+
align-items: center;
|
|
289
|
+
justify-content: flex-end;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.add-urn-button temba-button {
|
|
293
|
+
--button-height: calc(var(--input-h, 34px) + 2px);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.add-urn-error {
|
|
297
|
+
grid-column: 1 / -1;
|
|
298
|
+
color: rgb(var(--error-rgb));
|
|
299
|
+
font-size: 12px;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@media (max-width: 480px) {
|
|
303
|
+
.add-urn-fields {
|
|
304
|
+
grid-template-columns: 1fr;
|
|
305
|
+
margin-left: 28px;
|
|
306
|
+
}
|
|
57
307
|
}
|
|
58
308
|
|
|
59
|
-
/* the last row needs no separator — the card edge closes it off */
|
|
60
309
|
temba-contact-field.last-field {
|
|
61
310
|
--contact-field-separator: none;
|
|
62
311
|
margin-bottom: 0;
|
|
@@ -64,36 +313,652 @@ export class ContactDetails extends ContactStoreElement {
|
|
|
64
313
|
`;
|
|
65
314
|
}
|
|
66
315
|
|
|
67
|
-
|
|
68
|
-
|
|
316
|
+
private getSchemeName(scheme: string): string {
|
|
317
|
+
return (
|
|
318
|
+
this.schemes.find((option) => option.value === scheme)?.name ||
|
|
319
|
+
capitalize(scheme as any)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
69
322
|
|
|
70
|
-
|
|
71
|
-
|
|
323
|
+
private getManualGroups(): Group[] {
|
|
324
|
+
return this.data.groups.filter((group: Group) => !group.is_dynamic);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private getDynamicGroups(): Group[] {
|
|
328
|
+
return this.data.groups.filter((group: Group) => group.is_dynamic);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private getLanguageOptions(): Option[] {
|
|
332
|
+
const codes = [...(this.store.getWorkspace()?.languages || [])];
|
|
333
|
+
if (this.data.language && !codes.includes(this.data.language)) {
|
|
334
|
+
codes.unshift(this.data.language);
|
|
335
|
+
}
|
|
336
|
+
return [
|
|
337
|
+
{ value: 'none', name: 'No Preference' },
|
|
338
|
+
...codes.map((code) => ({ value: code, name: getLanguageName(code) }))
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private setSaveState(key: string, status: 'saving' | 'failed' | 'ready') {
|
|
343
|
+
const saving = new Set(this.saving);
|
|
344
|
+
const failed = new Set(this.failed);
|
|
345
|
+
saving.delete(key);
|
|
346
|
+
failed.delete(key);
|
|
347
|
+
if (status === 'saving') saving.add(key);
|
|
348
|
+
if (status === 'failed') failed.add(key);
|
|
349
|
+
this.saving = saving;
|
|
350
|
+
this.failed = failed;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private finishSave(
|
|
354
|
+
contactId: string,
|
|
355
|
+
key: string,
|
|
356
|
+
generation: number,
|
|
357
|
+
status: 'failed' | 'ready'
|
|
358
|
+
) {
|
|
359
|
+
if (
|
|
360
|
+
this.contact !== contactId ||
|
|
361
|
+
this.saveGenerations.get(key) !== generation
|
|
362
|
+
) {
|
|
72
363
|
return;
|
|
73
364
|
}
|
|
365
|
+
this.saveGenerations.delete(key);
|
|
366
|
+
this.setSaveState(key, status);
|
|
367
|
+
}
|
|
74
368
|
|
|
75
|
-
|
|
369
|
+
private async save(key: string, payload: any): Promise<WebResponse> {
|
|
370
|
+
const contactId = this.contact;
|
|
371
|
+
if (
|
|
372
|
+
!contactId ||
|
|
373
|
+
!this.data ||
|
|
374
|
+
this.data.uuid !== contactId ||
|
|
375
|
+
this.url !== `${this.endpoint}${contactId}`
|
|
376
|
+
) {
|
|
377
|
+
return { status: 409, json: {}, headers: new Headers() };
|
|
378
|
+
}
|
|
76
379
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
380
|
+
const contactUuid = this.data.uuid;
|
|
381
|
+
const generation = ++this.saveGeneration;
|
|
382
|
+
const fields = Object.keys(payload);
|
|
383
|
+
this.saveGenerations.set(key, generation);
|
|
384
|
+
fields.forEach((field) => this.fieldSaveGenerations.set(field, generation));
|
|
385
|
+
this.setSaveState(key, 'saving');
|
|
386
|
+
try {
|
|
387
|
+
const response = await this.store.postJSON(
|
|
388
|
+
`${this.writeEndpoint}${contactUuid}`,
|
|
389
|
+
payload
|
|
390
|
+
);
|
|
391
|
+
if (response.status !== 200) {
|
|
392
|
+
fields.forEach((field) => {
|
|
393
|
+
if (this.fieldSaveGenerations.get(field) === generation) {
|
|
394
|
+
this.fieldSaveGenerations.delete(field);
|
|
83
395
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
396
|
+
});
|
|
397
|
+
this.finishSave(contactId, key, generation, 'failed');
|
|
398
|
+
return response;
|
|
399
|
+
}
|
|
400
|
+
if (
|
|
401
|
+
this.contact === contactId &&
|
|
402
|
+
this.data?.uuid === contactId &&
|
|
403
|
+
this.url === `${this.endpoint}${contactId}`
|
|
404
|
+
) {
|
|
405
|
+
const updated = { ...this.data };
|
|
406
|
+
let hasUpdates = false;
|
|
407
|
+
fields.forEach((field) => {
|
|
408
|
+
if (this.fieldSaveGenerations.get(field) === generation) {
|
|
409
|
+
if (response.json[field] !== undefined) {
|
|
410
|
+
updated[field] = response.json[field];
|
|
411
|
+
hasUpdates = true;
|
|
412
|
+
}
|
|
413
|
+
this.fieldSaveGenerations.delete(field);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
if (hasUpdates) {
|
|
417
|
+
this.setContact(updated, contactId);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
this.finishSave(contactId, key, generation, 'ready');
|
|
421
|
+
return response;
|
|
422
|
+
} catch (error) {
|
|
423
|
+
fields.forEach((field) => {
|
|
424
|
+
if (this.fieldSaveGenerations.get(field) === generation) {
|
|
425
|
+
this.fieldSaveGenerations.delete(field);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
this.finishSave(contactId, key, generation, 'failed');
|
|
429
|
+
return { status: 500, json: {}, headers: new Headers() };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
public willUpdate(changed: PropertyValues): void {
|
|
434
|
+
if (changed.has('contact') || changed.has('endpoint')) {
|
|
435
|
+
const url = this.contact ? `${this.endpoint}${this.contact}` : null;
|
|
436
|
+
if (url !== this.url) {
|
|
437
|
+
this.data = null;
|
|
438
|
+
}
|
|
439
|
+
this.saveGeneration++;
|
|
440
|
+
this.saveGenerations.clear();
|
|
441
|
+
this.fieldSaveGenerations.clear();
|
|
442
|
+
this.groupsSave = null;
|
|
443
|
+
this.saving.clear();
|
|
444
|
+
this.failed.clear();
|
|
445
|
+
this.urnDialogOpen = false;
|
|
446
|
+
this.draftUrns = [];
|
|
447
|
+
this.newScheme = '';
|
|
448
|
+
this.newUrn = '';
|
|
449
|
+
}
|
|
450
|
+
super.willUpdate(changed);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
public async handleTextChanged(event: Event) {
|
|
454
|
+
const field = event.currentTarget as ContactFieldEditor;
|
|
455
|
+
const value = field.value;
|
|
456
|
+
const response = await this.save(field.key, {
|
|
457
|
+
[field.key]: field.key === 'name' && !value ? null : value
|
|
458
|
+
});
|
|
459
|
+
field.handleResponse(
|
|
460
|
+
response,
|
|
461
|
+
response.status === 200 ? response.json[field.key] || '' : value
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
public handleDraftUrnChanged(event: Event, index: number) {
|
|
466
|
+
this.draftUrns[index].path = (event.currentTarget as TextInput).value;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
public handleNewSchemeChanged(event: Event) {
|
|
470
|
+
const select = event.currentTarget as Select<Option>;
|
|
471
|
+
this.newScheme = select.values[0]?.value || '';
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
public handleNewUrnChanged(event: Event) {
|
|
475
|
+
this.newUrn = (event.currentTarget as TextInput).value;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
public showUrnDialog() {
|
|
479
|
+
this.newScheme = this.newScheme || this.schemes[0]?.value || '';
|
|
480
|
+
this.newUrn = '';
|
|
481
|
+
this.draftUrns = this.data.urns.map((urn) => ({ ...urn }));
|
|
482
|
+
this.setSaveState('urn-dialog', 'ready');
|
|
483
|
+
this.urnDialogOpen = true;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
public hideUrnDialog() {
|
|
487
|
+
this.urnDialogOpen = false;
|
|
488
|
+
this.newUrn = '';
|
|
489
|
+
this.draftUrns = [];
|
|
490
|
+
this.setSaveState('urn-dialog', 'ready');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
public handleAddUrn() {
|
|
494
|
+
const scheme = this.newScheme || this.schemes[0]?.value;
|
|
495
|
+
const path = this.newUrn.trim();
|
|
496
|
+
if (!scheme || !path) return;
|
|
497
|
+
|
|
498
|
+
this.draftUrns = [...this.draftUrns, { scheme, path }];
|
|
499
|
+
this.newUrn = '';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
public async handleUrnDialogButton(event: CustomEvent) {
|
|
503
|
+
event.stopImmediatePropagation();
|
|
504
|
+
if (event.detail?.button?.name === 'Save') {
|
|
505
|
+
const response = await this.save('urn-dialog', {
|
|
506
|
+
urns: this.draftUrns
|
|
507
|
+
.filter((urn) => urn.path.trim())
|
|
508
|
+
.map((urn) => `${urn.scheme}:${urn.path.trim()}`)
|
|
509
|
+
});
|
|
510
|
+
if (response.status === 200) {
|
|
511
|
+
this.hideUrnDialog();
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
this.hideUrnDialog();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
public handleUrnOrderChanged(event: CustomEvent) {
|
|
519
|
+
event.stopPropagation();
|
|
520
|
+
const [fromIndex, toIndex] = event.detail.swap;
|
|
521
|
+
const urns = [...this.draftUrns];
|
|
522
|
+
const moved = urns.splice(fromIndex, 1)[0];
|
|
523
|
+
urns.splice(toIndex, 0, moved);
|
|
524
|
+
this.draftUrns = urns;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
public handleUrnOrderKeyDown(event: KeyboardEvent, index: number) {
|
|
528
|
+
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
|
|
529
|
+
|
|
530
|
+
const toIndex = index + (event.key === 'ArrowUp' ? -1 : 1);
|
|
531
|
+
if (toIndex < 0 || toIndex >= this.draftUrns.length) return;
|
|
532
|
+
|
|
533
|
+
event.preventDefault();
|
|
534
|
+
event.stopPropagation();
|
|
535
|
+
const urns = [...this.draftUrns];
|
|
536
|
+
const moved = urns.splice(index, 1)[0];
|
|
537
|
+
urns.splice(toIndex, 0, moved);
|
|
538
|
+
this.draftUrns = urns;
|
|
539
|
+
void this.updateComplete.then(() => {
|
|
540
|
+
(
|
|
541
|
+
this.shadowRoot.querySelector(
|
|
542
|
+
`#urn-${toIndex} .drag-handle`
|
|
543
|
+
) as HTMLElement
|
|
544
|
+
)?.focus();
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
public handleDeleteUrn(event: Event, index: number) {
|
|
549
|
+
event.preventDefault();
|
|
550
|
+
event.stopPropagation();
|
|
551
|
+
this.draftUrns = this.draftUrns.filter((_, urnIndex) => urnIndex !== index);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
public handleDeleteUrnKeyDown(event: KeyboardEvent, index: number) {
|
|
555
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
556
|
+
this.handleDeleteUrn(event, index);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
public handleUrnEditKeyDown(event: KeyboardEvent) {
|
|
561
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
562
|
+
event.preventDefault();
|
|
563
|
+
this.showUrnDialog();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private prepareUrnGhost = (ghost: HTMLElement) => {
|
|
568
|
+
ghost.style.setProperty('display', 'grid', 'important');
|
|
569
|
+
ghost.style.setProperty(
|
|
570
|
+
'grid-template-columns',
|
|
571
|
+
'18px minmax(0, 1fr)',
|
|
572
|
+
'important'
|
|
573
|
+
);
|
|
574
|
+
ghost.style.setProperty('column-gap', '10px', 'important');
|
|
575
|
+
ghost.style.setProperty('align-items', 'center', 'important');
|
|
576
|
+
|
|
577
|
+
const dragSlot = ghost.querySelector('.drag-slot') as HTMLElement;
|
|
578
|
+
if (dragSlot) {
|
|
579
|
+
dragSlot.style.setProperty('display', 'flex', 'important');
|
|
580
|
+
dragSlot.style.setProperty('align-items', 'center', 'important');
|
|
581
|
+
dragSlot.style.setProperty('justify-content', 'center', 'important');
|
|
582
|
+
dragSlot.style.setProperty('align-self', 'end', 'important');
|
|
583
|
+
dragSlot.style.setProperty(
|
|
584
|
+
'height',
|
|
585
|
+
'calc(var(--input-h, 34px) + 2px)',
|
|
586
|
+
'important'
|
|
587
|
+
);
|
|
588
|
+
dragSlot.style.setProperty('box-sizing', 'border-box', 'important');
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
public handleSearch(event: CustomEvent) {
|
|
593
|
+
event.stopPropagation();
|
|
594
|
+
this.fireCustomEvent(CustomEventType.ButtonClicked, event.detail);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
public async handleGroupsChanged(event: Event) {
|
|
598
|
+
const select = event.currentTarget as Select<any>;
|
|
599
|
+
const groupsSave = this.save('groups', {
|
|
600
|
+
groups: select.values.map((group) => group.uuid)
|
|
601
|
+
});
|
|
602
|
+
this.groupsSave = groupsSave;
|
|
603
|
+
try {
|
|
604
|
+
await groupsSave;
|
|
605
|
+
} finally {
|
|
606
|
+
if (this.groupsSave === groupsSave) {
|
|
607
|
+
this.groupsSave = null;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
public async handleLanguageChanged(event: Event) {
|
|
613
|
+
const select = event.currentTarget as Select<Option>;
|
|
614
|
+
await this.save('language', {
|
|
615
|
+
language:
|
|
616
|
+
!select.values[0]?.value || select.values[0].value === 'none'
|
|
617
|
+
? null
|
|
618
|
+
: select.values[0].value
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
public async handleStatusChanged(event: Event) {
|
|
623
|
+
const select = event.currentTarget as Select<Option>;
|
|
624
|
+
const status = select.values[0]?.value;
|
|
625
|
+
const contactId = this.contact;
|
|
626
|
+
if (this.groupsSave) {
|
|
627
|
+
await this.groupsSave;
|
|
628
|
+
}
|
|
629
|
+
if (this.contact !== contactId || this.data?.uuid !== contactId) return;
|
|
630
|
+
if (!status || status === this.data.status) return;
|
|
631
|
+
|
|
632
|
+
const payload: any = { status };
|
|
633
|
+
if (this.data.status === 'active') {
|
|
634
|
+
// Leaving active normally clears group membership. Include the current
|
|
635
|
+
// manual groups so an inline status edit preserves that membership.
|
|
636
|
+
payload.groups = this.getManualGroups().map((group) => group.uuid);
|
|
637
|
+
}
|
|
638
|
+
await this.save('status', payload);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private renderFailure(key: string): TemplateResult {
|
|
642
|
+
return this.failed.has(key)
|
|
643
|
+
? html`<temba-tip class="error" text="Failed to save changes.">
|
|
644
|
+
<temba-icon name=${Icon.alert_warning}></temba-icon>
|
|
645
|
+
</temba-tip>`
|
|
646
|
+
: null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private renderEditableSelect(
|
|
650
|
+
key: string,
|
|
651
|
+
label: string,
|
|
652
|
+
options: Option[],
|
|
653
|
+
value: string,
|
|
654
|
+
handler: (event: Event) => void
|
|
655
|
+
): TemplateResult {
|
|
656
|
+
return html`<div
|
|
657
|
+
class="editable-row ${this.saving.has(key)
|
|
658
|
+
? 'saving'
|
|
659
|
+
: ''} ${this.failed.has(key) ? 'failed' : ''}"
|
|
660
|
+
>
|
|
661
|
+
<label>${label}</label>
|
|
662
|
+
${this.renderFailure(key)}
|
|
663
|
+
<temba-select
|
|
664
|
+
valueKey="value"
|
|
665
|
+
.values=${options.filter((option) => option.value === (value || ''))}
|
|
666
|
+
@change=${handler}
|
|
667
|
+
>
|
|
668
|
+
${options.map(
|
|
669
|
+
(option) =>
|
|
670
|
+
html`<temba-option
|
|
671
|
+
name=${option.name}
|
|
672
|
+
value=${option.value}
|
|
673
|
+
></temba-option>`
|
|
674
|
+
)}
|
|
675
|
+
</temba-select>
|
|
676
|
+
</div>`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
private renderDynamicGroups(editable: boolean): TemplateResult {
|
|
680
|
+
const groups = this.getDynamicGroups();
|
|
681
|
+
if (!groups.length) return null;
|
|
682
|
+
return html`<div class="smart-groups ${editable ? 'editable-row' : 'row'}">
|
|
683
|
+
${editable
|
|
684
|
+
? html`<label>Smart Groups</label>`
|
|
685
|
+
: html`<div class="label">Smart Groups</div>`}
|
|
686
|
+
<div class=${editable ? 'dynamic-groups' : 'value'}>
|
|
687
|
+
${groups.map(
|
|
688
|
+
(group) =>
|
|
689
|
+
html`<temba-label
|
|
690
|
+
onclick="goto(event)"
|
|
691
|
+
href="/contact/group/${group.uuid}/"
|
|
692
|
+
icon=${Icon.group_smart}
|
|
693
|
+
type="group"
|
|
694
|
+
clickable
|
|
695
|
+
>
|
|
696
|
+
${group.name}
|
|
697
|
+
</temba-label>`
|
|
698
|
+
)}
|
|
699
|
+
</div>
|
|
700
|
+
</div>`;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
private renderDisplayedUrn(
|
|
704
|
+
urn: URN | undefined,
|
|
705
|
+
className = ''
|
|
706
|
+
): TemplateResult {
|
|
707
|
+
const unsendable = urn?.channel === null;
|
|
708
|
+
return html`<temba-contact-field
|
|
709
|
+
class=${className}
|
|
710
|
+
name=${urn ? this.getSchemeName(urn.scheme) : 'URN'}
|
|
711
|
+
value=${urn ? urn.display || urn.path : 'No URNs'}
|
|
712
|
+
valueIcon=${unsendable ? Icon.contact_stopped : ''}
|
|
713
|
+
valueIconLabel=${unsendable ? 'Not sendable: no channel available' : ''}
|
|
714
|
+
disabled
|
|
715
|
+
></temba-contact-field>`;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
private renderUrnSendability(urn: URN): TemplateResult {
|
|
719
|
+
if (urn.channel !== null) return null;
|
|
720
|
+
const label = 'Not sendable: no channel available';
|
|
721
|
+
return html`<temba-tip
|
|
722
|
+
class="urn-sendability unsendable"
|
|
723
|
+
text=${label}
|
|
724
|
+
position="top"
|
|
725
|
+
slot="prefix"
|
|
726
|
+
>
|
|
727
|
+
<temba-icon name=${Icon.contact_stopped} aria-label=${label}></temba-icon>
|
|
728
|
+
</temba-tip>`;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private renderUrnDialog(): TemplateResult {
|
|
732
|
+
const selectedScheme = this.newScheme || this.schemes[0]?.value || '';
|
|
733
|
+
return html`<temba-dialog
|
|
734
|
+
header="Edit URNs"
|
|
735
|
+
size="medium"
|
|
736
|
+
primaryButtonName="Save"
|
|
737
|
+
cancelButtonName="Cancel"
|
|
738
|
+
.open=${this.urnDialogOpen}
|
|
739
|
+
?disabled=${this.saving.has('urn-dialog')}
|
|
740
|
+
?submitting=${this.saving.has('urn-dialog')}
|
|
741
|
+
@temba-button-clicked=${this.handleUrnDialogButton}
|
|
742
|
+
>
|
|
743
|
+
<div class="urn-dialog">
|
|
744
|
+
<temba-sortable-list
|
|
745
|
+
class="urn-list"
|
|
746
|
+
dragHandle="drag-handle"
|
|
747
|
+
gap="12px"
|
|
748
|
+
.prepareGhost=${this.prepareUrnGhost}
|
|
749
|
+
@temba-order-changed=${this.handleUrnOrderChanged}
|
|
750
|
+
>
|
|
751
|
+
${this.draftUrns.map((urn, index) => {
|
|
752
|
+
return html`
|
|
753
|
+
<div
|
|
754
|
+
id="urn-${index}"
|
|
755
|
+
class="urn-row sortable ${this.draftUrns.length > 1
|
|
756
|
+
? ''
|
|
757
|
+
: 'single'}"
|
|
758
|
+
>
|
|
759
|
+
<div class="drag-slot">
|
|
760
|
+
${this.draftUrns.length > 1
|
|
761
|
+
? html`<temba-icon
|
|
762
|
+
class="drag-handle"
|
|
763
|
+
name=${Icon.drag}
|
|
764
|
+
title="Drag to reorder"
|
|
765
|
+
aria-label="Reorder ${this.getSchemeName(
|
|
766
|
+
urn.scheme
|
|
767
|
+
)} URN. Use the up and down arrow keys to move it."
|
|
768
|
+
role="button"
|
|
769
|
+
tabindex="0"
|
|
770
|
+
@keydown=${(event: KeyboardEvent) =>
|
|
771
|
+
this.handleUrnOrderKeyDown(event, index)}
|
|
772
|
+
></temba-icon>`
|
|
773
|
+
: null}
|
|
774
|
+
</div>
|
|
775
|
+
<temba-textinput
|
|
776
|
+
class="urn-input"
|
|
777
|
+
name="urn-${index}"
|
|
778
|
+
label=${this.getSchemeName(urn.scheme)}
|
|
779
|
+
.value=${urn.path}
|
|
780
|
+
@input=${(event: Event) =>
|
|
781
|
+
this.handleDraftUrnChanged(event, index)}
|
|
782
|
+
@change=${(event: Event) =>
|
|
783
|
+
this.handleDraftUrnChanged(event, index)}
|
|
784
|
+
>
|
|
785
|
+
${this.renderUrnSendability(urn)}
|
|
786
|
+
<temba-tip
|
|
787
|
+
class="urn-delete-action"
|
|
788
|
+
text="Delete URN"
|
|
789
|
+
position="top"
|
|
790
|
+
>
|
|
791
|
+
<temba-icon
|
|
792
|
+
class="urn-delete-icon"
|
|
793
|
+
name=${Icon.delete}
|
|
794
|
+
aria-label="Delete URN"
|
|
795
|
+
role="button"
|
|
796
|
+
tabindex="0"
|
|
797
|
+
@click=${(event: Event) =>
|
|
798
|
+
this.handleDeleteUrn(event, index)}
|
|
799
|
+
@keydown=${(event: KeyboardEvent) =>
|
|
800
|
+
this.handleDeleteUrnKeyDown(event, index)}
|
|
801
|
+
></temba-icon>
|
|
802
|
+
</temba-tip>
|
|
803
|
+
</temba-textinput>
|
|
804
|
+
</div>
|
|
805
|
+
`;
|
|
806
|
+
})}
|
|
807
|
+
</temba-sortable-list>
|
|
808
|
+
${this.failed.has('urn-dialog')
|
|
809
|
+
? html`<div class="add-urn-error">
|
|
810
|
+
Failed to save URNs. Check the addresses and try again.
|
|
811
|
+
</div>`
|
|
812
|
+
: null}
|
|
813
|
+
${this.schemes.length
|
|
814
|
+
? html`<div class="add-urn-fields">
|
|
815
|
+
<div>
|
|
816
|
+
<label>URN Type</label>
|
|
817
|
+
<temba-select
|
|
818
|
+
valueKey="value"
|
|
819
|
+
.values=${this.schemes.filter(
|
|
820
|
+
(option) => option.value === selectedScheme
|
|
821
|
+
)}
|
|
822
|
+
@change=${this.handleNewSchemeChanged}
|
|
823
|
+
>
|
|
824
|
+
${this.schemes.map(
|
|
825
|
+
(option) =>
|
|
826
|
+
html`<temba-option
|
|
827
|
+
name=${option.name}
|
|
828
|
+
value=${option.value}
|
|
829
|
+
></temba-option>`
|
|
830
|
+
)}
|
|
831
|
+
</temba-select>
|
|
832
|
+
</div>
|
|
833
|
+
<temba-textinput
|
|
834
|
+
name="urn"
|
|
835
|
+
label="Address"
|
|
836
|
+
.value=${this.newUrn}
|
|
837
|
+
@input=${this.handleNewUrnChanged}
|
|
838
|
+
></temba-textinput>
|
|
839
|
+
<div class="add-urn-button">
|
|
840
|
+
<temba-button
|
|
841
|
+
name="Add"
|
|
842
|
+
icon=${Icon.add}
|
|
843
|
+
light
|
|
844
|
+
?disabled=${!this.newUrn.trim()}
|
|
845
|
+
@click=${this.handleAddUrn}
|
|
846
|
+
></temba-button>
|
|
847
|
+
</div>
|
|
848
|
+
</div>`
|
|
849
|
+
: null}
|
|
850
|
+
</div>
|
|
851
|
+
</temba-dialog>`;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
private renderPrimaryUrn(editable: boolean): TemplateResult {
|
|
855
|
+
if (this.anon || (!editable && this.data.urns.length === 0)) return null;
|
|
856
|
+
const additional = Math.max(0, this.data.urns.length - 1);
|
|
857
|
+
const displayedUrn = getDestinationURN(this.data) || this.data.urns[0];
|
|
858
|
+
const noUrns = this.data.urns.length === 0;
|
|
859
|
+
const onlyUnsendable =
|
|
860
|
+
this.data.urns.length === 1 && this.data.urns[0].channel === null;
|
|
861
|
+
return html`<div
|
|
862
|
+
class="urn-display primary-urn ${editable
|
|
863
|
+
? 'editable'
|
|
864
|
+
: ''} ${onlyUnsendable ? 'only-unsendable' : ''}"
|
|
865
|
+
aria-label=${noUrns
|
|
866
|
+
? 'This contact has no URNs'
|
|
867
|
+
: onlyUnsendable
|
|
868
|
+
? 'This contact has no sendable URNs'
|
|
869
|
+
: 'Contact URN'}
|
|
870
|
+
>
|
|
871
|
+
${this.renderDisplayedUrn(displayedUrn, 'primary-urn')}
|
|
872
|
+
${editable && additional
|
|
873
|
+
? html`<span
|
|
874
|
+
class="urn-more-count"
|
|
875
|
+
aria-label="${additional} additional URNs"
|
|
876
|
+
>+${additional} more</span
|
|
877
|
+
>`
|
|
878
|
+
: null}
|
|
879
|
+
${editable
|
|
880
|
+
? html`<temba-icon
|
|
881
|
+
class="urn-edit-icon"
|
|
882
|
+
name=${Icon.edit}
|
|
883
|
+
title="Edit URNs"
|
|
884
|
+
aria-label="Edit URNs"
|
|
885
|
+
role="button"
|
|
886
|
+
tabindex="0"
|
|
887
|
+
@click=${this.showUrnDialog}
|
|
888
|
+
@keydown=${this.handleUrnEditKeyDown}
|
|
889
|
+
></temba-icon>`
|
|
890
|
+
: null}
|
|
891
|
+
</div>
|
|
892
|
+
${editable ? this.renderUrnDialog() : null}`;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
private renderEditable(): TemplateResult {
|
|
896
|
+
const manualGroups = this.getManualGroups();
|
|
897
|
+
return html`
|
|
898
|
+
${this.renderPrimaryUrn(true)}
|
|
899
|
+
<temba-contact-field
|
|
900
|
+
key="name"
|
|
901
|
+
name="Name"
|
|
902
|
+
value=${this.data.name || ''}
|
|
903
|
+
@change=${this.handleTextChanged}
|
|
904
|
+
@temba-button-clicked=${this.handleSearch}
|
|
905
|
+
></temba-contact-field>
|
|
906
|
+
${this.anon && this.data.ref
|
|
907
|
+
? html`<temba-contact-field
|
|
908
|
+
name="Ref"
|
|
909
|
+
value=${this.data.ref}
|
|
87
910
|
disabled
|
|
88
|
-
></temba-contact-field
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
911
|
+
></temba-contact-field>`
|
|
912
|
+
: null}
|
|
913
|
+
${this.data.status === 'active'
|
|
914
|
+
? html`<div
|
|
915
|
+
class="editable-row ${this.saving.has('groups')
|
|
916
|
+
? 'saving'
|
|
917
|
+
: ''} ${this.failed.has('groups') ? 'failed' : ''}"
|
|
918
|
+
>
|
|
919
|
+
<label>Groups</label>
|
|
920
|
+
${this.renderFailure('groups')}
|
|
921
|
+
<temba-select
|
|
922
|
+
endpoint="/api/v2/groups.json?manual_only=1"
|
|
923
|
+
valueKey="uuid"
|
|
924
|
+
.values=${manualGroups}
|
|
925
|
+
@change=${this.handleGroupsChanged}
|
|
926
|
+
searchable
|
|
927
|
+
clearable
|
|
928
|
+
multi
|
|
929
|
+
></temba-select>
|
|
930
|
+
</div>`
|
|
931
|
+
: null}
|
|
932
|
+
${this.renderDynamicGroups(true)}
|
|
933
|
+
${this.renderEditableSelect(
|
|
934
|
+
'status',
|
|
935
|
+
'Status',
|
|
936
|
+
STATUS_OPTIONS,
|
|
937
|
+
this.data.status,
|
|
938
|
+
this.handleStatusChanged
|
|
939
|
+
)}
|
|
940
|
+
${this.renderEditableSelect(
|
|
941
|
+
'language',
|
|
942
|
+
'Language',
|
|
943
|
+
this.getLanguageOptions(),
|
|
944
|
+
this.data.language || 'none',
|
|
945
|
+
this.handleLanguageChanged
|
|
946
|
+
)}
|
|
947
|
+
`;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
private renderReadonly(): TemplateResult {
|
|
951
|
+
const lang = getLanguageName(this.data.language);
|
|
952
|
+
const manualGroups = this.getManualGroups();
|
|
953
|
+
return html`
|
|
954
|
+
${this.renderPrimaryUrn(false)}
|
|
955
|
+
${manualGroups.length
|
|
956
|
+
? html`<div class="row">
|
|
957
|
+
<div class="label">Groups</div>
|
|
958
|
+
<div class="value">
|
|
959
|
+
${manualGroups.map(
|
|
960
|
+
(group) =>
|
|
961
|
+
html`<temba-label
|
|
97
962
|
onclick="goto(event)"
|
|
98
963
|
href="/contact/group/${group.uuid}/"
|
|
99
964
|
icon=${group.is_dynamic ? Icon.group_smart : Icon.group}
|
|
@@ -101,46 +966,54 @@ export class ContactDetails extends ContactStoreElement {
|
|
|
101
966
|
clickable
|
|
102
967
|
>
|
|
103
968
|
${group.name}
|
|
104
|
-
</temba-label
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
969
|
+
</temba-label>`
|
|
970
|
+
)}
|
|
971
|
+
</div>
|
|
972
|
+
</div>`
|
|
973
|
+
: null}
|
|
974
|
+
${this.renderDynamicGroups(false)}
|
|
975
|
+
${this.data.ref
|
|
976
|
+
? html`<temba-contact-field
|
|
977
|
+
name="Ref"
|
|
978
|
+
value=${this.data.ref}
|
|
979
|
+
disabled
|
|
980
|
+
></temba-contact-field>`
|
|
981
|
+
: null}
|
|
982
|
+
<temba-contact-field
|
|
983
|
+
name="Status"
|
|
984
|
+
value=${STATUS_OPTIONS.find(
|
|
985
|
+
(option) => option.value === this.data.status
|
|
986
|
+
)?.name || capitalize(this.data.status)}
|
|
987
|
+
disabled
|
|
988
|
+
></temba-contact-field>
|
|
989
|
+
${lang
|
|
990
|
+
? html`<temba-contact-field
|
|
991
|
+
name="Language"
|
|
992
|
+
value=${lang}
|
|
993
|
+
disabled
|
|
994
|
+
></temba-contact-field>`
|
|
995
|
+
: null}
|
|
996
|
+
`;
|
|
997
|
+
}
|
|
116
998
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
value=${STATUS[this.data.status]}
|
|
120
|
-
disabled
|
|
121
|
-
></temba-contact-field>
|
|
122
|
-
${lang
|
|
123
|
-
? html`<temba-contact-field
|
|
124
|
-
name="Language"
|
|
125
|
-
value=${lang}
|
|
126
|
-
disabled
|
|
127
|
-
></temba-contact-field>`
|
|
128
|
-
: null}
|
|
999
|
+
public render(): TemplateResult {
|
|
1000
|
+
if (!this.data) return super.render();
|
|
129
1001
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
1002
|
+
return html`<div class="wrapper">
|
|
1003
|
+
${this.editable ? this.renderEditable() : this.renderReadonly()}
|
|
1004
|
+
<temba-contact-field
|
|
1005
|
+
name="Created"
|
|
1006
|
+
value=${this.data.created_on}
|
|
1007
|
+
type="datetime"
|
|
1008
|
+
disabled
|
|
1009
|
+
></temba-contact-field>
|
|
1010
|
+
<temba-contact-field
|
|
1011
|
+
class="last-field"
|
|
1012
|
+
name="Last Seen"
|
|
1013
|
+
value=${this.data.last_seen_on}
|
|
1014
|
+
type="datetime"
|
|
1015
|
+
disabled
|
|
1016
|
+
></temba-contact-field>
|
|
1017
|
+
</div>`;
|
|
145
1018
|
}
|
|
146
1019
|
}
|