@nyaruka/temba-components 0.163.0 → 0.165.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 +27 -0
- package/DEV_DATA.md +11 -11
- package/README.md +4 -4
- package/TEST_OPTIMIZATION.md +8 -11
- package/dist/locales/es.js +4 -0
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +4 -0
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/pt.js +4 -0
- package/dist/locales/pt.js.map +1 -1
- package/dist/static/svg/index.svg +1 -1
- package/dist/temba-components.js +5266 -3031
- package/dist/temba-components.js.map +1 -1
- package/orca/setup.sh +2 -2
- package/package.json +13 -18
- package/scripts/dev-data-sync.mjs +4 -4
- package/src/Icons.ts +3 -2
- package/src/display/Chat.ts +3 -2
- package/src/display/Lightbox.ts +57 -109
- package/src/display/ProgressBar.ts +15 -2
- package/src/display/Thumbnail.ts +34 -7
- package/src/form/Compose.ts +31 -1
- package/src/form/ContactSearch.ts +229 -126
- package/src/interfaces.ts +131 -2
- package/src/layout/Card.ts +336 -0
- package/src/layout/CardLayout.ts +425 -0
- package/src/layout/CardStack.ts +131 -0
- package/src/layout/Dialog.ts +33 -24
- package/src/layout/HeaderBar.ts +41 -0
- package/src/layout/PageHeader.ts +5 -6
- package/src/layout/Resizer.ts +20 -0
- package/src/list/BroadcastList.ts +912 -0
- package/src/list/CampaignList.ts +144 -0
- package/src/list/ContactList.ts +3 -1
- package/src/list/ContentList.ts +46 -10
- package/src/list/FieldList.ts +1057 -0
- package/src/list/FlowList.ts +31 -7
- package/src/list/MsgList.ts +18 -12
- package/src/list/SortableList.ts +52 -8
- package/src/list/TembaList.ts +8 -0
- package/src/list/TembaMenu.ts +5 -38
- package/src/list/TriggerList.ts +538 -0
- package/src/live/CampaignEvents.ts +1108 -0
- package/src/live/ContactChat.ts +7 -1
- package/src/live/ContactDetails.ts +20 -14
- package/src/live/ContactFieldEditor.ts +7 -3
- package/src/live/ContactFields.ts +1 -1
- package/src/live/ContactNameFetch.ts +5 -2
- package/src/live/ContactNotepad.ts +88 -2
- package/src/live/ContactStoreElement.ts +15 -3
- package/src/live/ContactTimeline.ts +28 -7
- package/src/locales/es.ts +4 -0
- package/src/locales/fr.ts +4 -0
- package/src/locales/pt.ts +4 -0
- package/src/utils.ts +19 -0
- package/static/svg/index.svg +1 -1
- package/static/svg/packs/2-temba/star-filled.svg +3 -0
- package/static/svg/work/traced/star-filled.svg +1 -0
- package/static/svg/work/used/star-filled.svg +3 -0
- package/stress-test.js +3 -3
- package/temba-modules.ts +18 -0
- package/web-dev-server.config.mjs +54 -0
- package/web-test-runner.config.mjs +1 -1
- package/xliff/es.xlf +12 -0
- package/xliff/fr.xlf +12 -0
- package/xliff/pt.xlf +12 -0
package/src/live/ContactChat.ts
CHANGED
|
@@ -605,7 +605,13 @@ export class ContactChat extends ContactStoreElement {
|
|
|
605
605
|
const channels = new Set<string>();
|
|
606
606
|
if (this.isConnected && this.currentContact) {
|
|
607
607
|
channels.add(`history:${this.currentContact.uuid}`);
|
|
608
|
-
|
|
608
|
+
|
|
609
|
+
// on a contact switch the new ticket is set before the new contact has
|
|
610
|
+
// finished fetching, so hold off on the ticket channel until they match
|
|
611
|
+
// - a ticket paired with another contact's channel is denied server-side
|
|
612
|
+
const contactPending =
|
|
613
|
+
this.contact && this.contact !== this.currentContact.uuid;
|
|
614
|
+
if (this.currentTicket && !contactPending) {
|
|
609
615
|
channels.add(
|
|
610
616
|
`history:${this.currentContact.uuid}:${this.currentTicket.uuid}`
|
|
611
617
|
);
|
|
@@ -38,7 +38,7 @@ export class ContactDetails extends ContactStoreElement {
|
|
|
38
38
|
|
|
39
39
|
.row .label {
|
|
40
40
|
color: var(--text-2);
|
|
41
|
-
font-size:
|
|
41
|
+
font-size: 11px;
|
|
42
42
|
font-weight: var(--w-medium);
|
|
43
43
|
margin-top: 0.25em;
|
|
44
44
|
margin-left: 0.25em;
|
|
@@ -46,7 +46,7 @@ export class ContactDetails extends ContactStoreElement {
|
|
|
46
46
|
|
|
47
47
|
.row .value {
|
|
48
48
|
margin-left: 0.25em;
|
|
49
|
-
margin-top: 0.
|
|
49
|
+
margin-top: 0.35em;
|
|
50
50
|
min-height: 1.75em;
|
|
51
51
|
display: flex;
|
|
52
52
|
flex-wrap: wrap;
|
|
@@ -55,6 +55,12 @@ export class ContactDetails extends ContactStoreElement {
|
|
|
55
55
|
|
|
56
56
|
.group {
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
/* the last row needs no separator — the card edge closes it off */
|
|
60
|
+
temba-contact-field.last-field {
|
|
61
|
+
--contact-field-separator: none;
|
|
62
|
+
margin-bottom: 0;
|
|
63
|
+
}
|
|
58
64
|
`;
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -70,6 +76,17 @@ export class ContactDetails extends ContactStoreElement {
|
|
|
70
76
|
|
|
71
77
|
return html`
|
|
72
78
|
<div class="wrapper">
|
|
79
|
+
${this.data.urns.map((urn) => {
|
|
80
|
+
let scheme = SCHEMES[urn.scheme];
|
|
81
|
+
if (!scheme) {
|
|
82
|
+
scheme = capitalize(urn.scheme as any);
|
|
83
|
+
}
|
|
84
|
+
return html`<temba-contact-field
|
|
85
|
+
name=${scheme}
|
|
86
|
+
value=${urn.display || urn.path}
|
|
87
|
+
disabled
|
|
88
|
+
></temba-contact-field>`;
|
|
89
|
+
})}
|
|
73
90
|
${this.data.groups.length > 0
|
|
74
91
|
? html` <div class="row">
|
|
75
92
|
<div class="label">Groups</div>
|
|
@@ -89,18 +106,6 @@ export class ContactDetails extends ContactStoreElement {
|
|
|
89
106
|
</div>
|
|
90
107
|
</div>`
|
|
91
108
|
: null}
|
|
92
|
-
${this.data.urns.map((urn) => {
|
|
93
|
-
const parts = urn.split(':');
|
|
94
|
-
let scheme = SCHEMES[parts[0]];
|
|
95
|
-
if (!scheme) {
|
|
96
|
-
scheme = capitalize(parts[0] as any);
|
|
97
|
-
}
|
|
98
|
-
return html`<temba-contact-field
|
|
99
|
-
name=${scheme}
|
|
100
|
-
value=${parts[1]}
|
|
101
|
-
disabled
|
|
102
|
-
></temba-contact-field>`;
|
|
103
|
-
})}
|
|
104
109
|
${this.data.ref
|
|
105
110
|
? html`<temba-contact-field
|
|
106
111
|
name="Ref"
|
|
@@ -129,6 +134,7 @@ export class ContactDetails extends ContactStoreElement {
|
|
|
129
134
|
disabled
|
|
130
135
|
></temba-contact-field>
|
|
131
136
|
<temba-contact-field
|
|
137
|
+
class="last-field"
|
|
132
138
|
name="Last Seen"
|
|
133
139
|
value=${this.data.last_seen_on}
|
|
134
140
|
type="datetime"
|
|
@@ -61,7 +61,7 @@ export class ContactFieldEditor extends RapidElement {
|
|
|
61
61
|
--color-widget-bg-focused: #fff;
|
|
62
62
|
--widget-box-shadow: none;
|
|
63
63
|
padding-bottom: 0.6em;
|
|
64
|
-
border-bottom: 1px solid #ececec;
|
|
64
|
+
border-bottom: var(--contact-field-separator, 1px solid #ececec);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
.wrapper.disabled {
|
|
@@ -108,9 +108,12 @@ export class ContactFieldEditor extends RapidElement {
|
|
|
108
108
|
text-overflow: ellipsis;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/* read-only rows set the label clearly apart from the value — the
|
|
112
|
+
same quiet label treatment as .field-label on the editable rows,
|
|
113
|
+
a touch smaller, over a full-color value */
|
|
111
114
|
.label .name {
|
|
112
115
|
color: var(--text-2);
|
|
113
|
-
font-size:
|
|
116
|
+
font-size: 11px;
|
|
114
117
|
font-weight: var(--w-medium);
|
|
115
118
|
}
|
|
116
119
|
|
|
@@ -120,8 +123,9 @@ export class ContactFieldEditor extends RapidElement {
|
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
.disabled .value {
|
|
126
|
+
color: var(--text-1);
|
|
123
127
|
margin-left: 0.25em;
|
|
124
|
-
margin-top: 0.
|
|
128
|
+
margin-top: 0.35em;
|
|
125
129
|
min-height: 1.75em;
|
|
126
130
|
}
|
|
127
131
|
|
|
@@ -151,7 +151,7 @@ export class ContactFields extends ContactStoreElement {
|
|
|
151
151
|
const value = field.value;
|
|
152
152
|
|
|
153
153
|
// TODO: Use contact.postChanges instead of postJSON
|
|
154
|
-
postJSON(
|
|
154
|
+
postJSON(this.endpoint + this.data.uuid, {
|
|
155
155
|
fields: { [field.key]: value }
|
|
156
156
|
})
|
|
157
157
|
.then((response: any) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { css, html, TemplateResult } from 'lit';
|
|
2
2
|
import { property } from 'lit/decorators.js';
|
|
3
|
-
import { ContactStoreElement } from './ContactStoreElement';
|
|
3
|
+
import { ContactStoreElement, getDestinationURN } from './ContactStoreElement';
|
|
4
4
|
|
|
5
5
|
export class ContactNameFetch extends ContactStoreElement {
|
|
6
6
|
@property({ type: Number, attribute: 'icon-size' })
|
|
@@ -21,9 +21,12 @@ export class ContactNameFetch extends ContactStoreElement {
|
|
|
21
21
|
|
|
22
22
|
public render(): TemplateResult {
|
|
23
23
|
if (this.data) {
|
|
24
|
+
// show the URN that will be used when messaging the contact, falling
|
|
25
|
+
// back to their highest priority URN if none are sendable
|
|
26
|
+
const urn = getDestinationURN(this.data) || this.data.urns[0] || null;
|
|
24
27
|
return html` <temba-contact-name
|
|
25
28
|
name=${this.data.name || this.data.ref}
|
|
26
|
-
urn=${
|
|
29
|
+
urn=${urn ? `${urn.scheme}:${urn.display || urn.path}` : null}
|
|
27
30
|
></temba-contact-name>
|
|
28
31
|
<slot></slot>`;
|
|
29
32
|
}
|
|
@@ -13,6 +13,11 @@ export class ContactNotepad extends ContactStoreElement {
|
|
|
13
13
|
dirtyMessage =
|
|
14
14
|
'You have unsaved changes to the contact notepad. Are you sure you want to contiunue?';
|
|
15
15
|
|
|
16
|
+
// grow with the note instead of filling the parent and scrolling —
|
|
17
|
+
// used when embedded in a card that must never scroll internally
|
|
18
|
+
@property({ type: Boolean, reflect: true })
|
|
19
|
+
autogrow = false;
|
|
20
|
+
|
|
16
21
|
static get styles() {
|
|
17
22
|
return css`
|
|
18
23
|
${designTokens}
|
|
@@ -52,6 +57,41 @@ export class ContactNotepad extends ContactStoreElement {
|
|
|
52
57
|
outline: none;
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
/* flex-grow only matters in a height-bounded flex parent (the tab
|
|
61
|
+
pane): the textarea stretches to fill the note surface, pinning
|
|
62
|
+
the attribution toolbar to the bottom. In the card column the
|
|
63
|
+
parent is a plain block, so the notepad still hugs its text. */
|
|
64
|
+
:host([autogrow]) {
|
|
65
|
+
height: auto;
|
|
66
|
+
margin-top: 0;
|
|
67
|
+
flex-grow: 1;
|
|
68
|
+
/* in a height-bounded flex parent (tab pane) the autosize can
|
|
69
|
+
measure while flex-stretched and overshoot — allow flex to
|
|
70
|
+
shrink us back so the toolbar never overflows the pane */
|
|
71
|
+
min-height: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* hug the text — the JS autosize sets the exact height, so the only
|
|
75
|
+
floor is a single line for an empty note. border-box so setting
|
|
76
|
+
height = scrollHeight doesn't re-add the padding. Snug under the
|
|
77
|
+
card title, aligned to the 12px inset, with breathing room under
|
|
78
|
+
the last line of text. */
|
|
79
|
+
:host([autogrow]) .notepad {
|
|
80
|
+
box-sizing: border-box;
|
|
81
|
+
overflow-y: hidden;
|
|
82
|
+
min-height: 2em;
|
|
83
|
+
height: 2em;
|
|
84
|
+
padding: 0.25em 10px 1em 10px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* embedded in a card that supplies the chrome — drop our own so the
|
|
88
|
+
note surface bleeds to the card's edges */
|
|
89
|
+
:host([autogrow]) .wrapper {
|
|
90
|
+
border: none;
|
|
91
|
+
border-radius: 0;
|
|
92
|
+
box-shadow: none;
|
|
93
|
+
}
|
|
94
|
+
|
|
55
95
|
.toolbar {
|
|
56
96
|
background: rgba(0, 0, 0, 0.03);
|
|
57
97
|
padding: 0.25em 0.5em;
|
|
@@ -73,8 +113,49 @@ export class ContactNotepad extends ContactStoreElement {
|
|
|
73
113
|
`;
|
|
74
114
|
}
|
|
75
115
|
|
|
116
|
+
private resizer: ResizeObserver;
|
|
117
|
+
private lastWidth = 0;
|
|
118
|
+
|
|
119
|
+
public connectedCallback(): void {
|
|
120
|
+
super.connectedCallback();
|
|
121
|
+
// text rewraps when our width changes (browser resize, layout mode
|
|
122
|
+
// switches), changing the height the note needs — only react to width
|
|
123
|
+
// so our own height writes don't loop the observer
|
|
124
|
+
this.resizer = new ResizeObserver(() => {
|
|
125
|
+
requestAnimationFrame(() => {
|
|
126
|
+
const width = this.offsetWidth;
|
|
127
|
+
if (width > 0 && width !== this.lastWidth) {
|
|
128
|
+
this.lastWidth = width;
|
|
129
|
+
this.autosize();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
this.resizer.observe(this);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public disconnectedCallback(): void {
|
|
137
|
+
this.resizer?.disconnect();
|
|
138
|
+
super.disconnectedCallback();
|
|
139
|
+
}
|
|
140
|
+
|
|
76
141
|
private handleChange() {
|
|
77
142
|
this.markDirty();
|
|
143
|
+
// surface the unsaved state so a wrapping card can show its dirty mark
|
|
144
|
+
this.fireCustomEvent(CustomEventType.DetailsChanged, { dirty: true });
|
|
145
|
+
this.autosize();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private autosize() {
|
|
149
|
+
if (!this.autogrow) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const notepad = this.shadowRoot.querySelector(
|
|
153
|
+
'.notepad'
|
|
154
|
+
) as HTMLTextAreaElement;
|
|
155
|
+
if (notepad) {
|
|
156
|
+
notepad.style.height = 'auto';
|
|
157
|
+
notepad.style.height = notepad.scrollHeight + 'px';
|
|
158
|
+
}
|
|
78
159
|
}
|
|
79
160
|
|
|
80
161
|
private submitChanges() {
|
|
@@ -94,14 +175,19 @@ export class ContactNotepad extends ContactStoreElement {
|
|
|
94
175
|
|
|
95
176
|
if (changes.has('data')) {
|
|
96
177
|
this.note =
|
|
97
|
-
this.data?.notes
|
|
178
|
+
this.data?.notes?.length > 0
|
|
98
179
|
? { ...this.data.notes[this.data.notes.length - 1] }
|
|
99
180
|
: null;
|
|
100
181
|
this.fireCustomEvent(CustomEventType.DetailsChanged, {
|
|
101
|
-
count: this.note && this.note.text.length > 0 ? 1 : 0
|
|
182
|
+
count: this.note && this.note.text.length > 0 ? 1 : 0,
|
|
183
|
+
dirty: false
|
|
102
184
|
});
|
|
103
185
|
this.markClean();
|
|
104
186
|
}
|
|
187
|
+
|
|
188
|
+
if (changes.has('note') || changes.has('data')) {
|
|
189
|
+
this.autosize();
|
|
190
|
+
}
|
|
105
191
|
}
|
|
106
192
|
|
|
107
193
|
public render(): TemplateResult {
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { PropertyValues } from 'lit';
|
|
2
2
|
import { property } from 'lit/decorators.js';
|
|
3
|
-
import { Contact, Group } from '../interfaces';
|
|
3
|
+
import { Contact, Group, URN } from '../interfaces';
|
|
4
4
|
import { EndpointMonitorElement } from '../store/EndpointMonitorElement';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Returns the URN that will be used to message the given contact — URNs are
|
|
8
|
+
* ordered by priority and only ones with a channel are sendable.
|
|
9
|
+
*/
|
|
10
|
+
export const getDestinationURN = (contact: Contact | null): URN | null => {
|
|
11
|
+
return (contact?.urns || []).find((urn) => !!urn.channel) || null;
|
|
12
|
+
};
|
|
13
|
+
|
|
6
14
|
export class ContactStoreElement extends EndpointMonitorElement {
|
|
7
15
|
@property({ type: String })
|
|
8
16
|
contact: string;
|
|
@@ -10,8 +18,10 @@ export class ContactStoreElement extends EndpointMonitorElement {
|
|
|
10
18
|
@property({ type: Object, attribute: false })
|
|
11
19
|
data: Contact;
|
|
12
20
|
|
|
21
|
+
// expand_urns resolves each URN against a channel so we know which one
|
|
22
|
+
// will be used when messaging the contact
|
|
13
23
|
@property({ type: String })
|
|
14
|
-
endpoint = '/api/v2/contacts.json?uuid=';
|
|
24
|
+
endpoint = '/api/v2/contacts.json?expand_urns=true&uuid=';
|
|
15
25
|
|
|
16
26
|
prepareData(data: any) {
|
|
17
27
|
if (data && data.length > 0) {
|
|
@@ -56,7 +66,8 @@ export class ContactStoreElement extends EndpointMonitorElement {
|
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
public willUpdate(changed: PropertyValues): void {
|
|
59
|
-
|
|
69
|
+
// derive our url before the base class runs so it sees the url change
|
|
70
|
+
// in this same pass (clearing stale data when the contact is unset)
|
|
60
71
|
if (changed.has('contact') || changed.has('endpoint')) {
|
|
61
72
|
if (this.contact) {
|
|
62
73
|
this.url = `${this.endpoint}${this.contact}`;
|
|
@@ -64,5 +75,6 @@ export class ContactStoreElement extends EndpointMonitorElement {
|
|
|
64
75
|
this.url = null;
|
|
65
76
|
}
|
|
66
77
|
}
|
|
78
|
+
super.willUpdate(changed);
|
|
67
79
|
}
|
|
68
80
|
}
|
|
@@ -115,7 +115,9 @@ export class ContactTimeline extends EndpointMonitorElement {
|
|
|
115
115
|
flex-direction: column;
|
|
116
116
|
align-items: center;
|
|
117
117
|
text-align: center;
|
|
118
|
-
padding
|
|
118
|
+
/* a host card sets --empty-padding to compact this treatment —
|
|
119
|
+
the default suits a full-height tab pane */
|
|
120
|
+
padding: var(--empty-padding, 7em 1em 4em);
|
|
119
121
|
color: var(--text-color);
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -124,9 +126,17 @@ export class ContactTimeline extends EndpointMonitorElement {
|
|
|
124
126
|
--icon-color: var(--text-3, #7b8593);
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
/* the icon / blurb / link around the title — a host card hides
|
|
130
|
+
these so the empty card is just the stylized message */
|
|
131
|
+
.empty-extras {
|
|
132
|
+
display: var(--empty-extras-display, contents);
|
|
133
|
+
}
|
|
134
|
+
|
|
127
135
|
.empty-title {
|
|
128
136
|
font-weight: 600;
|
|
129
137
|
margin-bottom: 0.4em;
|
|
138
|
+
font-size: var(--empty-title-size, inherit);
|
|
139
|
+
color: var(--empty-title-color, inherit);
|
|
130
140
|
}
|
|
131
141
|
|
|
132
142
|
.empty-help {
|
|
@@ -547,7 +557,14 @@ export class ContactTimeline extends EndpointMonitorElement {
|
|
|
547
557
|
: Array.isArray(this.data.future)
|
|
548
558
|
? this.data.future.length
|
|
549
559
|
: 0;
|
|
550
|
-
|
|
560
|
+
// empty mirrors the render()'s empty-state condition so a host
|
|
561
|
+
// card can drop its chrome entirely when there's nothing to show
|
|
562
|
+
const empty =
|
|
563
|
+
count === 0 &&
|
|
564
|
+
!(Array.isArray(this.data.campaigns) && this.data.campaigns.length) &&
|
|
565
|
+
!(Array.isArray(this.data.future) && this.data.future.length) &&
|
|
566
|
+
!(Array.isArray(this.data.past) && this.data.past.length);
|
|
567
|
+
this.fireCustomEvent(CustomEventType.DetailsChanged, { count, empty });
|
|
551
568
|
}
|
|
552
569
|
}
|
|
553
570
|
}
|
|
@@ -810,12 +827,16 @@ export class ContactTimeline extends EndpointMonitorElement {
|
|
|
810
827
|
) {
|
|
811
828
|
return html`<div class="empty">
|
|
812
829
|
<slot name="empty">
|
|
813
|
-
<
|
|
830
|
+
<div class="empty-extras">
|
|
831
|
+
<temba-icon name=${Icon.schedule} size="2"></temba-icon>
|
|
832
|
+
</div>
|
|
814
833
|
<div class="empty-title">${this.lang_empty}</div>
|
|
815
|
-
<div class="empty-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
834
|
+
<div class="empty-extras">
|
|
835
|
+
<div class="empty-help">${this.lang_empty_help}</div>
|
|
836
|
+
<a class="empty-link" href="/campaign/" onclick="goto(event, this)"
|
|
837
|
+
>${this.lang_campaigns_link}</a
|
|
838
|
+
>
|
|
839
|
+
</div>
|
|
819
840
|
</slot>
|
|
820
841
|
</div>`;
|
|
821
842
|
}
|
package/src/locales/es.ts
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
's73b4d70c02f4b4e0': `No options`,
|
|
14
14
|
'sbc913d7dc0f33877': `to add`,
|
|
15
15
|
's81f17cfc89a04338': `Interrupt flow`,
|
|
16
|
+
's122d4de68bcfcdf4': `It's okay to restart`,
|
|
17
|
+
's3eb2567092b4d7c1': `from the beginning`,
|
|
18
|
+
's28f37776b3901438': `It's okay to interrupt`,
|
|
19
|
+
's6838d20b10f7512a': `and start this one`,
|
|
16
20
|
's8f02e3a18ffc083a': `Are not currently in a flow`,
|
|
17
21
|
's638236250662c6b3': `Have sent a message in the last`,
|
|
18
22
|
's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
|
package/src/locales/fr.ts
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
'scf1453991c986b25': `Tab to complete, enter to select`,
|
|
14
14
|
'sbc913d7dc0f33877': `to add`,
|
|
15
15
|
's81f17cfc89a04338': `Interrupt flow`,
|
|
16
|
+
's122d4de68bcfcdf4': `It's okay to restart`,
|
|
17
|
+
's3eb2567092b4d7c1': `from the beginning`,
|
|
18
|
+
's28f37776b3901438': `It's okay to interrupt`,
|
|
19
|
+
's6838d20b10f7512a': `and start this one`,
|
|
16
20
|
's8f02e3a18ffc083a': `Are not currently in a flow`,
|
|
17
21
|
's638236250662c6b3': `Have sent a message in the last`,
|
|
18
22
|
's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
|
package/src/locales/pt.ts
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
'scf1453991c986b25': `Tab to complete, enter to select`,
|
|
14
14
|
'sbc913d7dc0f33877': `to add`,
|
|
15
15
|
's81f17cfc89a04338': `Interrupt flow`,
|
|
16
|
+
's122d4de68bcfcdf4': `It's okay to restart`,
|
|
17
|
+
's3eb2567092b4d7c1': `from the beginning`,
|
|
18
|
+
's28f37776b3901438': `It's okay to interrupt`,
|
|
19
|
+
's6838d20b10f7512a': `and start this one`,
|
|
16
20
|
's8f02e3a18ffc083a': `Are not currently in a flow`,
|
|
17
21
|
's638236250662c6b3': `Have sent a message in the last`,
|
|
18
22
|
's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
|
package/src/utils.ts
CHANGED
|
@@ -17,6 +17,25 @@ export { isSystemCategory } from './flow/categoryUtils';
|
|
|
17
17
|
|
|
18
18
|
export const DEFAULT_MEDIA_ENDPOINT = '/api/v2/media.json';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Normalize a message attachment into the `contentType:url` string that
|
|
22
|
+
* `temba-thumbnail` expects. The messages CRUDL endpoint serializes
|
|
23
|
+
* attachments as `{content_type, url}` objects, while other message
|
|
24
|
+
* surfaces (e.g. realtime contact events) carry them already as strings —
|
|
25
|
+
* accept either shape.
|
|
26
|
+
*/
|
|
27
|
+
export const attachmentAsString = (attachment: string | Attachment): string => {
|
|
28
|
+
if (typeof attachment === 'string') {
|
|
29
|
+
return attachment;
|
|
30
|
+
}
|
|
31
|
+
// guard against malformed objects — an empty result is a no-op for
|
|
32
|
+
// temba-thumbnail, which beats rendering "undefined:…"
|
|
33
|
+
if (!attachment?.content_type || !attachment?.url) {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
return `${attachment.content_type}:${attachment.url}`;
|
|
37
|
+
};
|
|
38
|
+
|
|
20
39
|
export const colorHash = new ColorHash();
|
|
21
40
|
|
|
22
41
|
export type Asset = KeyedAsset & Ticket & ContactField & Shortcut;
|