@nyaruka/temba-components 0.157.1 → 0.158.1
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 +16 -0
- package/dist/temba-components.js +1305 -530
- package/dist/temba-components.js.map +1 -1
- package/orca/setup.sh +81 -0
- package/orca.yaml +3 -0
- package/package.json +1 -1
- package/src/display/Dropdown.ts +17 -5
- package/src/events/eventRenderers.ts +4 -9
- package/src/flow/CanvasNode.ts +14 -6
- package/src/flow/DragManager.ts +4 -2
- package/src/flow/utils.ts +1 -0
- package/src/form/DatePicker.ts +2 -1
- package/src/interfaces.ts +4 -1
- package/src/layout/Tab.ts +0 -15
- package/src/layout/TabPane.ts +76 -164
- package/src/list/ContactList.ts +225 -0
- package/src/list/ContentList.ts +1298 -0
- package/src/list/FlowList.ts +251 -0
- package/src/list/MsgList.ts +144 -0
- package/src/live/ContactChat.ts +6 -2
- package/src/live/ContactDetails.ts +40 -35
- package/src/live/ContactFieldEditor.ts +35 -55
- package/src/live/ContactFields.ts +1 -2
- package/src/live/ContactNotepad.ts +9 -1
- package/src/live/ContactPending.ts +1 -0
- package/src/styles/designTokens.ts +2 -0
- package/static/api/flow-labels.json +31 -0
- package/static/css/temba-components.css +2 -0
- package/temba-modules.ts +8 -0
- package/web-dev-server.config.mjs +156 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { css, html, TemplateResult } from 'lit';
|
|
2
|
+
import { ContentList, ContentListColumn } from './ContentList';
|
|
3
|
+
import { Icon } from '../Icons';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Flow CRUDL list — drop-in replacement for the rapidpro
|
|
7
|
+
* `flows/flow_list.html` table. Each row's leading icon reflects
|
|
8
|
+
* the flow type (messaging / voice / background / surveyor).
|
|
9
|
+
* Columns: name, runs, ongoing, completion bar, status, modified.
|
|
10
|
+
*/
|
|
11
|
+
export class FlowList extends ContentList<any> {
|
|
12
|
+
static get styles() {
|
|
13
|
+
return css`
|
|
14
|
+
${ContentList.styles}
|
|
15
|
+
.flow-name {
|
|
16
|
+
color: inherit;
|
|
17
|
+
font-weight: var(--w-medium);
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
text-overflow: ellipsis;
|
|
20
|
+
white-space: nowrap;
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: 6px;
|
|
24
|
+
}
|
|
25
|
+
.flow-labels {
|
|
26
|
+
display: inline-flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
gap: 4px;
|
|
29
|
+
margin-left: 4px;
|
|
30
|
+
}
|
|
31
|
+
.issue-icon {
|
|
32
|
+
--icon-color: var(--warning);
|
|
33
|
+
}
|
|
34
|
+
.num {
|
|
35
|
+
font-variant-numeric: tabular-nums;
|
|
36
|
+
color: inherit;
|
|
37
|
+
}
|
|
38
|
+
.completion-bar {
|
|
39
|
+
display: inline-flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: 8px;
|
|
42
|
+
justify-content: flex-end;
|
|
43
|
+
}
|
|
44
|
+
.completion-bar .bar {
|
|
45
|
+
width: 50px;
|
|
46
|
+
height: 6px;
|
|
47
|
+
border-radius: 999px;
|
|
48
|
+
background: var(--sunken);
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
}
|
|
51
|
+
.completion-bar .fill {
|
|
52
|
+
height: 100%;
|
|
53
|
+
background: var(--success);
|
|
54
|
+
border-radius: 999px;
|
|
55
|
+
}
|
|
56
|
+
.completion-bar .pct {
|
|
57
|
+
font-variant-numeric: tabular-nums;
|
|
58
|
+
color: var(--text-2);
|
|
59
|
+
min-width: 36px;
|
|
60
|
+
text-align: right;
|
|
61
|
+
}
|
|
62
|
+
.sparkline {
|
|
63
|
+
display: inline-block;
|
|
64
|
+
vertical-align: middle;
|
|
65
|
+
}
|
|
66
|
+
.sparkline .line {
|
|
67
|
+
fill: none;
|
|
68
|
+
stroke: var(--accent-600);
|
|
69
|
+
stroke-width: 1.25;
|
|
70
|
+
stroke-linejoin: round;
|
|
71
|
+
stroke-linecap: round;
|
|
72
|
+
}
|
|
73
|
+
.sparkline .area {
|
|
74
|
+
fill: var(--accent-100);
|
|
75
|
+
opacity: 0.7;
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
constructor() {
|
|
81
|
+
super();
|
|
82
|
+
this.valueKey = 'uuid';
|
|
83
|
+
this.emptyMessage = 'No flows';
|
|
84
|
+
this.searchPlaceholder = 'Search flows';
|
|
85
|
+
this.columns = [
|
|
86
|
+
{ key: 'name', label: 'Name', sortable: true, grow: 3 },
|
|
87
|
+
{
|
|
88
|
+
key: 'status',
|
|
89
|
+
label: 'Status',
|
|
90
|
+
width: '110px',
|
|
91
|
+
grow: 0
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
key: 'runs',
|
|
95
|
+
label: 'Runs',
|
|
96
|
+
sortable: true,
|
|
97
|
+
width: '80px',
|
|
98
|
+
grow: 0,
|
|
99
|
+
align: 'right'
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
key: 'ongoing',
|
|
103
|
+
label: 'Ongoing',
|
|
104
|
+
sortable: true,
|
|
105
|
+
width: '80px',
|
|
106
|
+
grow: 0,
|
|
107
|
+
align: 'right'
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
key: 'completion',
|
|
111
|
+
label: 'Completion',
|
|
112
|
+
width: '120px',
|
|
113
|
+
grow: 0,
|
|
114
|
+
align: 'right'
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
key: 'activity',
|
|
118
|
+
label: 'Activity',
|
|
119
|
+
width: '120px',
|
|
120
|
+
grow: 0,
|
|
121
|
+
align: 'right'
|
|
122
|
+
}
|
|
123
|
+
];
|
|
124
|
+
this.bulkActions = [
|
|
125
|
+
{
|
|
126
|
+
key: 'label',
|
|
127
|
+
label: 'Label',
|
|
128
|
+
icon: Icon.label,
|
|
129
|
+
labelsEndpoint: '/api/v2/flow-labels.json'
|
|
130
|
+
},
|
|
131
|
+
{ key: 'export', label: 'Export results', icon: Icon.export },
|
|
132
|
+
{ key: 'archive', label: 'Archive', icon: Icon.archive }
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
protected getRowIcon(item: any): string | null {
|
|
137
|
+
switch (item?.type) {
|
|
138
|
+
case 'voice':
|
|
139
|
+
case 'ivr':
|
|
140
|
+
return Icon.flow_ivr;
|
|
141
|
+
case 'background':
|
|
142
|
+
return Icon.flow_background;
|
|
143
|
+
case 'survey':
|
|
144
|
+
return Icon.flow_surveyor;
|
|
145
|
+
default:
|
|
146
|
+
return Icon.flow_message;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
protected getRowHref(item: any): string | null {
|
|
151
|
+
return item?.uuid ? `/flow/editor/${item.uuid}/` : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
protected renderCell(
|
|
155
|
+
item: any,
|
|
156
|
+
column: ContentListColumn
|
|
157
|
+
): TemplateResult | string {
|
|
158
|
+
switch (column.key) {
|
|
159
|
+
case 'name': {
|
|
160
|
+
const labels = item.labels || [];
|
|
161
|
+
return html`<span class="flow-name"
|
|
162
|
+
>${item.name || ''}
|
|
163
|
+
${item.has_issues
|
|
164
|
+
? html`<temba-icon
|
|
165
|
+
class="issue-icon"
|
|
166
|
+
name=${Icon.issue}
|
|
167
|
+
size="0.9"
|
|
168
|
+
></temba-icon>`
|
|
169
|
+
: null}
|
|
170
|
+
${labels.length > 0
|
|
171
|
+
? html`<span class="flow-labels">
|
|
172
|
+
${labels.map(
|
|
173
|
+
(l: any) =>
|
|
174
|
+
html`<temba-label type="label" icon=${Icon.label}
|
|
175
|
+
>${l.name}</temba-label
|
|
176
|
+
>`
|
|
177
|
+
)}
|
|
178
|
+
</span>`
|
|
179
|
+
: null}
|
|
180
|
+
</span>`;
|
|
181
|
+
}
|
|
182
|
+
case 'runs':
|
|
183
|
+
return html`<span class="num"
|
|
184
|
+
>${(item.runs ?? 0).toLocaleString()}</span
|
|
185
|
+
>`;
|
|
186
|
+
case 'ongoing':
|
|
187
|
+
return html`<span class="num">${item.ongoing ?? 0}</span>`;
|
|
188
|
+
case 'completion':
|
|
189
|
+
return this.renderCompletion(item);
|
|
190
|
+
case 'activity':
|
|
191
|
+
return this.renderSparkline(item.activity || []);
|
|
192
|
+
case 'status':
|
|
193
|
+
return this.renderFlowStatus(item);
|
|
194
|
+
default:
|
|
195
|
+
return super.renderCell(item, column);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private renderCompletion(item: any): TemplateResult {
|
|
200
|
+
const value = typeof item.completion === 'number' ? item.completion : 0;
|
|
201
|
+
const pct = Math.round(value * 100);
|
|
202
|
+
return html`
|
|
203
|
+
<div class="completion-bar">
|
|
204
|
+
<div class="bar">
|
|
205
|
+
<div class="fill" style="width: ${pct}%"></div>
|
|
206
|
+
</div>
|
|
207
|
+
<span class="pct">${pct}%</span>
|
|
208
|
+
</div>
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Render a tiny line+area sparkline from a numeric array.
|
|
213
|
+
* Values are normalized to the column's pixel viewBox; absolute
|
|
214
|
+
* magnitude isn't preserved — the goal is shape, not scale. */
|
|
215
|
+
private renderSparkline(values: number[]): TemplateResult {
|
|
216
|
+
if (!values || values.length < 2) return html``;
|
|
217
|
+
const w = 90;
|
|
218
|
+
const h = 22;
|
|
219
|
+
const max = Math.max(...values, 1);
|
|
220
|
+
const step = w / (values.length - 1);
|
|
221
|
+
const pts = values.map((v, i) => {
|
|
222
|
+
const x = i * step;
|
|
223
|
+
const y = h - (v / max) * (h - 2) - 1;
|
|
224
|
+
return [x.toFixed(1), y.toFixed(1)] as const;
|
|
225
|
+
});
|
|
226
|
+
const line = pts.map(([x, y]) => `${x},${y}`).join(' ');
|
|
227
|
+
const areaCmds = pts
|
|
228
|
+
.map(([x, y], i) => `${i === 0 ? 'M' : 'L'}${x},${y}`)
|
|
229
|
+
.join(' ');
|
|
230
|
+
const area = `${areaCmds} L${w},${h} L0,${h} Z`;
|
|
231
|
+
return html`
|
|
232
|
+
<svg
|
|
233
|
+
class="sparkline"
|
|
234
|
+
width=${w}
|
|
235
|
+
height=${h}
|
|
236
|
+
viewBox="0 0 ${w} ${h}"
|
|
237
|
+
preserveAspectRatio="none"
|
|
238
|
+
>
|
|
239
|
+
<path class="area" d=${area}></path>
|
|
240
|
+
<polyline class="line" points=${line}></polyline>
|
|
241
|
+
</svg>
|
|
242
|
+
`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private renderFlowStatus(item: any): TemplateResult {
|
|
246
|
+
const status = (item.status || 'active').toString().toLowerCase();
|
|
247
|
+
const kind = status === 'archived' ? 'archived' : 'active';
|
|
248
|
+
const label = status === 'archived' ? 'Archived' : 'Active';
|
|
249
|
+
return this.renderStatusPill(kind, label);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { css, html, TemplateResult } from 'lit';
|
|
2
|
+
import { ContentList, ContentListColumn } from './ContentList';
|
|
3
|
+
import { Icon } from '../Icons';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Message CRUDL list — drop-in replacement for the rapidpro
|
|
7
|
+
* `msgs/msg_list.html` table. Reverse-chronological; rows carry
|
|
8
|
+
* contact name, message text + attachments + labels + active-flow
|
|
9
|
+
* pill, and a duration timestamp.
|
|
10
|
+
*/
|
|
11
|
+
export class MsgList extends ContentList<any> {
|
|
12
|
+
static get styles() {
|
|
13
|
+
return css`
|
|
14
|
+
${ContentList.styles}
|
|
15
|
+
.msg-meta {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: 6px;
|
|
19
|
+
flex-wrap: wrap;
|
|
20
|
+
font-size: 12px;
|
|
21
|
+
color: var(--text-3);
|
|
22
|
+
}
|
|
23
|
+
.msg-text {
|
|
24
|
+
display: block;
|
|
25
|
+
color: inherit;
|
|
26
|
+
font-weight: var(--w-regular);
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
text-overflow: ellipsis;
|
|
29
|
+
white-space: nowrap;
|
|
30
|
+
}
|
|
31
|
+
.msg-row {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: 2px;
|
|
35
|
+
}
|
|
36
|
+
.attachment-pill {
|
|
37
|
+
display: inline-flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: 3px;
|
|
40
|
+
padding: 0 6px 0 4px;
|
|
41
|
+
border-radius: 999px;
|
|
42
|
+
background: var(--sunken);
|
|
43
|
+
color: var(--text-3);
|
|
44
|
+
--icon-color: var(--text-3);
|
|
45
|
+
font-size: 11px;
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
super();
|
|
52
|
+
this.valueKey = 'id';
|
|
53
|
+
this.emptyMessage = 'No messages';
|
|
54
|
+
this.searchPlaceholder = 'Search messages';
|
|
55
|
+
this.columns = [
|
|
56
|
+
{ key: 'contact', label: 'Contact', width: '180px', grow: 0 },
|
|
57
|
+
{ key: 'text', label: 'Message', grow: 2 },
|
|
58
|
+
{
|
|
59
|
+
key: 'created_on',
|
|
60
|
+
label: 'Sent',
|
|
61
|
+
width: '110px',
|
|
62
|
+
grow: 0,
|
|
63
|
+
align: 'right'
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
this.bulkActions = [
|
|
67
|
+
{
|
|
68
|
+
key: 'label',
|
|
69
|
+
label: 'Label',
|
|
70
|
+
icon: Icon.label,
|
|
71
|
+
labelsEndpoint: '/api/v2/labels.json'
|
|
72
|
+
},
|
|
73
|
+
{ key: 'archive', label: 'Archive', icon: Icon.archive },
|
|
74
|
+
{ key: 'delete', label: 'Delete', icon: Icon.delete, destructive: true }
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected renderCell(
|
|
79
|
+
item: any,
|
|
80
|
+
column: ContentListColumn
|
|
81
|
+
): TemplateResult | string {
|
|
82
|
+
switch (column.key) {
|
|
83
|
+
case 'contact': {
|
|
84
|
+
const contact = item.contact || {};
|
|
85
|
+
return html`<span class="msg-text"
|
|
86
|
+
>${contact.name || contact.urn || ''}</span
|
|
87
|
+
>`;
|
|
88
|
+
}
|
|
89
|
+
case 'text':
|
|
90
|
+
return this.renderMessageBody(item);
|
|
91
|
+
case 'created_on':
|
|
92
|
+
return html`<temba-date
|
|
93
|
+
value=${item.created_on}
|
|
94
|
+
display="duration"
|
|
95
|
+
></temba-date>`;
|
|
96
|
+
default:
|
|
97
|
+
return super.renderCell(item, column);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private renderMessageBody(item: any): TemplateResult {
|
|
102
|
+
const labels = item.labels || [];
|
|
103
|
+
const attachments = item.attachments || [];
|
|
104
|
+
const isOptin = item.msg_type === 'optin' || item.type === 'optin';
|
|
105
|
+
|
|
106
|
+
return html`
|
|
107
|
+
<div class="msg-row">
|
|
108
|
+
<span class="msg-text">
|
|
109
|
+
${item.text || (isOptin ? 'Opt-in request' : '')}
|
|
110
|
+
</span>
|
|
111
|
+
${labels.length || attachments.length || item.flow || isOptin
|
|
112
|
+
? html`
|
|
113
|
+
<div class="msg-meta">
|
|
114
|
+
${isOptin ? this.renderStatusPill('pending', 'opt-in') : null}
|
|
115
|
+
${attachments.map(
|
|
116
|
+
() => html`
|
|
117
|
+
<span class="attachment-pill">
|
|
118
|
+
<temba-icon
|
|
119
|
+
name=${Icon.attachment}
|
|
120
|
+
size="0.8"
|
|
121
|
+
></temba-icon>
|
|
122
|
+
attachment
|
|
123
|
+
</span>
|
|
124
|
+
`
|
|
125
|
+
)}
|
|
126
|
+
${item.flow
|
|
127
|
+
? html`<temba-label type="flow" icon=${Icon.flow}
|
|
128
|
+
>${item.flow.name}</temba-label
|
|
129
|
+
>`
|
|
130
|
+
: null}
|
|
131
|
+
${labels.map(
|
|
132
|
+
(l: any) => html`
|
|
133
|
+
<temba-label type="label" icon=${Icon.label}
|
|
134
|
+
>${l.name}</temba-label
|
|
135
|
+
>
|
|
136
|
+
`
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
`
|
|
140
|
+
: null}
|
|
141
|
+
</div>
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
}
|
package/src/live/ContactChat.ts
CHANGED
|
@@ -64,11 +64,11 @@ export class ContactChat extends ContactStoreElement {
|
|
|
64
64
|
display: flex;
|
|
65
65
|
flex-direction: row;
|
|
66
66
|
min-height: 0;
|
|
67
|
+
margin-top: var(--gap);
|
|
67
68
|
--compose-shadow: none;
|
|
68
69
|
--compose-border: none;
|
|
69
70
|
--compose-padding: 3px;
|
|
70
71
|
--compose-curvature: none;
|
|
71
|
-
border-top: 1px inset rgba(0, 0, 0, 0.05);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
.chat-wrapper {
|
|
@@ -76,7 +76,11 @@ export class ContactChat extends ContactStoreElement {
|
|
|
76
76
|
flex-grow: 1;
|
|
77
77
|
flex-direction: column;
|
|
78
78
|
min-height: 0;
|
|
79
|
-
background:
|
|
79
|
+
background: var(--surface);
|
|
80
|
+
border: 1px solid var(--border-strong);
|
|
81
|
+
border-radius: var(--r-sm);
|
|
82
|
+
box-shadow: var(--shadow-2);
|
|
83
|
+
overflow: hidden;
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
temba-contact-history {
|
|
@@ -21,36 +21,39 @@ const SCHEMES = {
|
|
|
21
21
|
export class ContactDetails extends ContactStoreElement {
|
|
22
22
|
static get styles() {
|
|
23
23
|
return css`
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
padding: 0.4em 1em 0.8em 1em;
|
|
27
|
-
border-bottom: 1px solid #e6e6e6;
|
|
28
|
-
margin-bottom: 0.5em;
|
|
24
|
+
.wrapper {
|
|
25
|
+
padding-top: 0em;
|
|
29
26
|
}
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
.row {
|
|
34
|
+
padding-bottom: 0.6em;
|
|
35
|
+
border-bottom: 1px solid #ececec;
|
|
36
|
+
margin-bottom: 1.5em;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
.
|
|
36
|
-
|
|
39
|
+
.row .label {
|
|
40
|
+
color: var(--text-2);
|
|
41
|
+
font-size: 12px;
|
|
42
|
+
font-weight: var(--w-medium);
|
|
43
|
+
margin-top: 0.25em;
|
|
44
|
+
margin-left: 0.25em;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
margin-bottom: 0.7em;
|
|
47
|
+
.row .value {
|
|
48
|
+
margin-left: 0.25em;
|
|
49
|
+
margin-top: 0.1em;
|
|
50
|
+
min-height: 1.75em;
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-wrap: wrap;
|
|
53
|
+
gap: 0.4em;
|
|
47
54
|
}
|
|
48
55
|
|
|
49
|
-
.
|
|
50
|
-
font-size: 0.8em;
|
|
51
|
-
color: rgb(136, 136, 136);
|
|
52
|
-
margin-left: 0.5em;
|
|
53
|
-
margin-bottom: 0.4em;
|
|
56
|
+
.group {
|
|
54
57
|
}
|
|
55
58
|
`;
|
|
56
59
|
}
|
|
@@ -68,20 +71,22 @@ export class ContactDetails extends ContactStoreElement {
|
|
|
68
71
|
return html`
|
|
69
72
|
<div class="wrapper">
|
|
70
73
|
${this.data.groups.length > 0
|
|
71
|
-
? html` <div class="
|
|
74
|
+
? html` <div class="row">
|
|
72
75
|
<div class="label">Groups</div>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
<div class="value">
|
|
77
|
+
${this.data.groups.map((group) => {
|
|
78
|
+
return html`<temba-label
|
|
79
|
+
class="group"
|
|
80
|
+
onclick="goto(event)"
|
|
81
|
+
href="/contact/group/${group.uuid}/"
|
|
82
|
+
icon=${group.is_dynamic ? Icon.group_smart : Icon.group}
|
|
83
|
+
type="group"
|
|
84
|
+
clickable
|
|
85
|
+
>
|
|
86
|
+
${group.name}
|
|
87
|
+
</temba-label>`;
|
|
88
|
+
})}
|
|
89
|
+
</div>
|
|
85
90
|
</div>`
|
|
86
91
|
: null}
|
|
87
92
|
${this.data.urns.map((urn) => {
|
|
@@ -93,51 +93,34 @@ export class ContactFieldEditor extends RapidElement {
|
|
|
93
93
|
--color-widget-border: rgb(235, 235, 235);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
.prefix {
|
|
97
|
-
border-top-left-radius: var(--curvature-widget);
|
|
98
|
-
border-bottom-left-radius: var(--curvature-widget);
|
|
99
|
-
cursor: pointer !important;
|
|
100
|
-
white-space: nowrap;
|
|
101
|
-
overflow: hidden;
|
|
102
|
-
text-overflow: ellipsis;
|
|
103
|
-
display: flex;
|
|
104
|
-
/* Pin to the top-left of the host (temba-select :host is
|
|
105
|
-
position: relative). Using top rather than margin-top keeps
|
|
106
|
-
the absolute element out of the flex flow of .left-side so
|
|
107
|
-
it doesn't push the selected value down. */
|
|
108
|
-
position: absolute;
|
|
109
|
-
top: -0.6em;
|
|
110
|
-
left: 0.5em;
|
|
111
|
-
pointer-events: none;
|
|
112
|
-
background: #fff;
|
|
113
|
-
border-radius: var(--curvature);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
temba-select .prefix {
|
|
117
|
-
top: -0.7em;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
96
|
.wrapper {
|
|
121
97
|
margin-bottom: 0.5em;
|
|
122
98
|
}
|
|
123
99
|
|
|
124
|
-
.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
100
|
+
.field-label {
|
|
101
|
+
display: block;
|
|
102
|
+
font-size: 12px;
|
|
103
|
+
font-weight: var(--w-medium);
|
|
104
|
+
color: var(--text-2);
|
|
105
|
+
margin: 0 0 4px 2px;
|
|
128
106
|
white-space: nowrap;
|
|
129
107
|
overflow: hidden;
|
|
130
108
|
text-overflow: ellipsis;
|
|
131
|
-
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.label .name {
|
|
112
|
+
color: var(--text-2);
|
|
113
|
+
font-size: 12px;
|
|
114
|
+
font-weight: var(--w-medium);
|
|
132
115
|
}
|
|
133
116
|
|
|
134
117
|
.disabled .name {
|
|
135
|
-
margin-top:
|
|
136
|
-
margin-left: 0.
|
|
118
|
+
margin-top: 0.25em;
|
|
119
|
+
margin-left: 0.25em;
|
|
137
120
|
}
|
|
138
121
|
|
|
139
122
|
.disabled .value {
|
|
140
|
-
margin-left: 0.
|
|
123
|
+
margin-left: 0.25em;
|
|
141
124
|
margin-top: 0.1em;
|
|
142
125
|
min-height: 1.75em;
|
|
143
126
|
}
|
|
@@ -426,27 +409,30 @@ export class ContactFieldEditor extends RapidElement {
|
|
|
426
409
|
}
|
|
427
410
|
|
|
428
411
|
private renderDateField(state: TemplateResult) {
|
|
429
|
-
return html`
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
412
|
+
return html`
|
|
413
|
+
<label id="field-label" class="field-label">${this.name}</label>
|
|
414
|
+
<temba-datepicker
|
|
415
|
+
aria-labelledby="field-label"
|
|
416
|
+
timezone=${this.timezone}
|
|
417
|
+
value="${this.value ? this.value : ''}"
|
|
418
|
+
@change=${this.handleDateChange}
|
|
419
|
+
?disabled=${this.disabled}
|
|
420
|
+
time
|
|
421
|
+
>
|
|
422
|
+
<div class="postfix" slot="postfix">
|
|
423
|
+
<div class="popper ${this.status} ${this.dirty ? 'dirty' : ''}">
|
|
424
|
+
${state}
|
|
425
|
+
</div>
|
|
442
426
|
</div>
|
|
443
|
-
</
|
|
444
|
-
|
|
427
|
+
</temba-datepicker>
|
|
428
|
+
`;
|
|
445
429
|
}
|
|
446
430
|
|
|
447
431
|
private renderTextField(state: TemplateResult) {
|
|
448
432
|
return html`
|
|
433
|
+
<label id="field-label" class="field-label">${this.name}</label>
|
|
449
434
|
<temba-textinput
|
|
435
|
+
aria-labelledby="field-label"
|
|
450
436
|
class="${this.status} ${this.dirty ? 'dirty' : ''}"
|
|
451
437
|
value="${this.value ? this.value : ''}"
|
|
452
438
|
@keyup=${this.handleInput}
|
|
@@ -454,10 +440,6 @@ export class ContactFieldEditor extends RapidElement {
|
|
|
454
440
|
type=${this.getInputType(this.type)}
|
|
455
441
|
?disabled=${this.disabled}
|
|
456
442
|
>
|
|
457
|
-
<div class="prefix" slot="prefix">
|
|
458
|
-
<div class="name">${this.name}</div>
|
|
459
|
-
</div>
|
|
460
|
-
|
|
461
443
|
<div class="postfix">
|
|
462
444
|
<div
|
|
463
445
|
class="popper ${this.iconClass} ${this.status} ${this.dirty
|
|
@@ -506,7 +488,9 @@ export class ContactFieldEditor extends RapidElement {
|
|
|
506
488
|
|
|
507
489
|
public renderLocationField(level: string = 'state') {
|
|
508
490
|
return html`
|
|
491
|
+
<label id="field-label" class="field-label">${this.name}</label>
|
|
509
492
|
<temba-select
|
|
493
|
+
aria-labelledby="field-label"
|
|
510
494
|
endpoint="/api/internal/locations.json?level=${level}"
|
|
511
495
|
nameKey="path"
|
|
512
496
|
valueKey="path"
|
|
@@ -516,14 +500,10 @@ export class ContactFieldEditor extends RapidElement {
|
|
|
516
500
|
queryParam="query"
|
|
517
501
|
searchable
|
|
518
502
|
clearable
|
|
519
|
-
inpsutStyle=${JSON.stringify({ 'margin-top': '1.1em !important;' })}
|
|
520
503
|
values=${this.value
|
|
521
504
|
? JSON.stringify([{ path: this.value, osm_id: this.value }])
|
|
522
505
|
: '[]'}
|
|
523
506
|
>
|
|
524
|
-
<div class="prefix" slot="prefix">
|
|
525
|
-
<div class="name">${this.name}</div>
|
|
526
|
-
</div>
|
|
527
507
|
</temba-select>
|
|
528
508
|
`;
|
|
529
509
|
}
|