@nyaruka/temba-components 0.27.1 → 0.27.2

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.
@@ -1,11 +1,11 @@
1
1
  import { TemplateResult, html, css } from 'lit';
2
2
  import { property } from 'lit/decorators';
3
- import { getUrl, WebResponse } from '../utils';
3
+ import { getClasses, postJSON, WebResponse } from '../utils';
4
4
  import { TextInput } from '../textinput/TextInput';
5
5
  import '../alert/Alert';
6
6
  import { Contact, CustomEventType } from '../interfaces';
7
- import { styleMap } from 'lit-html/directives/style-map';
8
7
  import { FormElement } from '../FormElement';
8
+ import { Checkbox } from '../checkbox/Checkbox';
9
9
 
10
10
  const QUEIT_MILLIS = 1000;
11
11
 
@@ -79,9 +79,17 @@ export class ContactSearch extends FormElement {
79
79
  }
80
80
 
81
81
  temba-loading {
82
- margin-top: 10px;
83
- margin-right: 10px;
82
+ transform: scale(0);
83
+ max-width: 0;
84
84
  opacity: 0;
85
+ transition: transform 200ms ease-in-out;
86
+ }
87
+
88
+ .fetching temba-loading {
89
+ transform: scale(1);
90
+ max-width: 500px;
91
+ opacity: 1;
92
+ display: block;
85
93
  }
86
94
 
87
95
  .error {
@@ -127,6 +135,21 @@ export class ContactSearch extends FormElement {
127
135
  display: var(--contact-search-query-display);
128
136
  margin-bottom: 10px;
129
137
  }
138
+
139
+ .results {
140
+ display: none;
141
+ }
142
+
143
+ .summary {
144
+ min-height: 2.2em;
145
+ }
146
+
147
+ .results.initialized {
148
+ display: flex;
149
+ align-items: center;
150
+ margin-top: 0.5em;
151
+ margin-left: 0.6em;
152
+ }
130
153
  `;
131
154
  }
132
155
 
@@ -156,57 +179,67 @@ export class ContactSearch extends FormElement {
156
179
  @property({ type: Number })
157
180
  inactiveDays = 90;
158
181
 
159
- @property({ attribute: false })
182
+ @property({ type: Object, attribute: false })
160
183
  summary: SummaryResponse;
161
184
 
185
+ @property({ type: Object, attribute: false })
186
+ flow: any;
187
+
162
188
  private lastQuery: number;
189
+ private initialized = false;
190
+
191
+ private exclusions = {};
163
192
 
164
193
  public updated(changedProperties: Map<string, any>) {
165
194
  super.updated(changedProperties);
166
195
 
167
- if (changedProperties.has('query')) {
168
- this.fetching = !!this.query;
196
+ if (changedProperties.has('query') || changedProperties.has('endpoint')) {
197
+ this.fetching = !!this.query && !!this.endpoint;
169
198
 
170
- // clear our summary on any change
171
- this.summary = null;
172
- if (this.lastQuery) {
173
- window.clearTimeout(this.lastQuery);
174
- }
199
+ if (this.fetching) {
200
+ this.initialized = true;
201
+ // clear our summary on any change
202
+ this.summary = null;
203
+ if (this.lastQuery) {
204
+ window.clearTimeout(this.lastQuery);
205
+ }
175
206
 
176
- if (this.query.trim().length > 0) {
177
- this.lastQuery = window.setTimeout(() => {
178
- this.fetchSummary(this.query);
179
- }, QUEIT_MILLIS);
207
+ if (this.query.trim().length > 0) {
208
+ this.lastQuery = window.setTimeout(() => {
209
+ this.fetchSummary(this.query);
210
+ }, QUEIT_MILLIS);
211
+ }
180
212
  }
181
213
  }
182
214
  }
183
215
 
184
- public executeQuery(query: string): any {
185
- const url = this.endpoint + query.replace('\n', ' ');
186
- getUrl(url).then((response: WebResponse) => {
187
- if (response.status === 200) {
188
- const summary = response.json as SummaryResponse;
189
- this.fireCustomEvent(CustomEventType.FetchComplete, summary);
190
- }
191
- });
192
- }
193
-
194
216
  public fetchSummary(query: string): any {
195
- const url = this.endpoint + encodeURIComponent(query.replace('\n', ' '));
196
- getUrl(url).then((response: WebResponse) => {
197
- this.fetching = false;
198
- if (response.status === 200) {
199
- this.summary = response.json as SummaryResponse;
200
-
201
- if (this.summary.error) {
202
- this.errors = [this.summary.error];
217
+ if (this.endpoint) {
218
+ postJSON(this.endpoint, {
219
+ include: { query },
220
+ exclude: this.exclusions,
221
+ }).then((response: WebResponse) => {
222
+ this.fetching = false;
223
+ if (response.status === 200) {
224
+ this.summary = response.json as SummaryResponse;
225
+
226
+ if (this.summary.error) {
227
+ this.errors = [this.summary.error];
228
+ } else {
229
+ this.errors = [];
230
+ }
231
+ this.requestUpdate('errors');
232
+ this.fireCustomEvent(CustomEventType.ContentChanged, this.summary);
203
233
  } else {
204
- this.errors = [];
234
+ this.summary = response.json as SummaryResponse;
235
+ if (this.summary.error) {
236
+ this.errors = [this.summary.error];
237
+ }
238
+ this.requestUpdate('errors');
239
+ this.fireCustomEvent(CustomEventType.ContentChanged, this.summary);
205
240
  }
206
- this.requestUpdate('errors');
207
- this.fireCustomEvent(CustomEventType.ContentChanged, this.summary);
208
- }
209
- });
241
+ });
242
+ }
210
243
  }
211
244
 
212
245
  private handleQueryChange(evt: KeyboardEvent) {
@@ -214,6 +247,25 @@ export class ContactSearch extends FormElement {
214
247
  this.query = input.inputElement.value;
215
248
  }
216
249
 
250
+ private handleSlotChanged(evt: any) {
251
+ if (evt.target.tagName === 'TEMBA-CHECKBOX') {
252
+ const checkbox = evt.target as Checkbox;
253
+ let value = checkbox.checked as any;
254
+
255
+ if (!value) {
256
+ delete this.exclusions[checkbox.name];
257
+ } else {
258
+ if (checkbox.name === 'not_seen_since_days') {
259
+ value = 90;
260
+ }
261
+
262
+ this.exclusions[checkbox.name] = value;
263
+ }
264
+ }
265
+
266
+ this.requestUpdate('query');
267
+ }
268
+
217
269
  public render(): TemplateResult {
218
270
  let summary: TemplateResult;
219
271
  if (this.summary) {
@@ -228,79 +280,72 @@ export class ContactSearch extends FormElement {
228
280
  const lastSeenOn = this.summary.query.indexOf('last_seen_on') > -1;
229
281
 
230
282
  summary = html`
231
- <div class="summary ${this.expanded ? 'expanded' : ''}">
232
- <table cellspacing="0" cellpadding="0">
233
- <tr class="header">
234
- <td colspan="2">
235
- Found
236
- <a
237
- class="linked"
238
- target="_"
239
- href="/contact/?search=${encodeURIComponent(
240
- this.summary.query
241
- )}"
242
- >
243
- ${count.toLocaleString()}
244
- </a>
245
- contact${count !== 1 ? 's' : ''}
246
- </td>
247
- ${fields.map(
248
- field => html` <td class="field-header">${field.label}</td> `
249
- )}
250
- <td></td>
251
- <td class="field-header date">
252
- ${lastSeenOn ? 'Last Seen' : 'Created'}
253
- </td>
254
- </tr>
255
-
256
- ${this.summary.sample.map(
257
- (contact: Contact) => html`
258
- <tr class="contact">
259
- <td class="urn">
260
- ${(contact as any).primary_urn_formatted}
261
- </td>
262
- <td class="name">${contact.name}</td>
263
- ${fields.map(
264
- field => html`
265
- <td class="field">
266
- ${(
267
- (contact as any).fields[field.uuid] || { text: '' }
268
- ).text}
269
- </td>
270
- `
271
- )}
272
- <td></td>
273
- <td class="date">
274
- ${lastSeenOn
275
- ? contact.last_seen_on || '--'
276
- : contact.created_on}
277
- </td>
278
- </tr>
279
- `
283
+ <table cellspacing="0" cellpadding="0">
284
+ <tr class="header">
285
+ <td colspan="2">
286
+ Found
287
+ <a
288
+ class="linked"
289
+ target="_"
290
+ href="/contact/?search=${encodeURIComponent(
291
+ this.summary.query
292
+ )}"
293
+ >
294
+ ${count.toLocaleString()}
295
+ </a>
296
+ contact${count !== 1 ? 's' : ''}
297
+ </td>
298
+ ${fields.map(
299
+ field => html` <td class="field-header">${field.label}</td> `
280
300
  )}
281
- ${this.summary.total > this.summary.sample.length
282
- ? html`<tr class="table-footer">
283
- <td class="query-details" colspan=${fields.length + 3}></td>
284
- <td class="more">
285
- <a
286
- class="linked"
287
- target="_"
288
- href="/contact/?search=${encodeURIComponent(
289
- this.summary.query
290
- )}"
291
- >more</a
292
- >
293
- </td>
294
- </tr>`
295
- : null}
296
- </table>
297
- </div>
301
+ <td></td>
302
+ <td class="field-header date">
303
+ ${lastSeenOn ? 'Last Seen' : 'Created'}
304
+ </td>
305
+ </tr>
306
+
307
+ ${this.summary.sample.map(
308
+ (contact: Contact) => html`
309
+ <tr class="contact">
310
+ <td class="urn">${(contact as any).primary_urn_formatted}</td>
311
+ <td class="name">${contact.name}</td>
312
+ ${fields.map(
313
+ field => html`
314
+ <td class="field">
315
+ ${((contact as any).fields[field.uuid] || { text: '' })
316
+ .text}
317
+ </td>
318
+ `
319
+ )}
320
+ <td></td>
321
+ <td class="date">
322
+ ${lastSeenOn
323
+ ? contact.last_seen_on || '--'
324
+ : contact.created_on}
325
+ </td>
326
+ </tr>
327
+ `
328
+ )}
329
+ ${this.summary.total > this.summary.sample.length
330
+ ? html`<tr class="table-footer">
331
+ <td class="query-details" colspan=${fields.length + 3}></td>
332
+ <td class="more">
333
+ <a
334
+ class="linked"
335
+ target="_"
336
+ href="/contact/?search=${encodeURIComponent(
337
+ this.summary.query
338
+ )}"
339
+ >more</a
340
+ >
341
+ </td>
342
+ </tr>`
343
+ : null}
344
+ </table>
298
345
  `;
299
346
  }
300
347
  }
301
348
 
302
- const loadingStyle = this.fetching ? { opacity: '1' } : {};
303
-
304
349
  return html`
305
350
  <div class="query">
306
351
  <temba-textinput
@@ -319,14 +364,17 @@ export class ContactSearch extends FormElement {
319
364
  </temba-textinput>
320
365
  </div>
321
366
 
322
- ${this.fetching
323
- ? html`<temba-loading
324
- units="4"
325
- style=${styleMap(loadingStyle)}
326
- ></temba-loading>`
327
- : this.summary
328
- ? html` <div class="summary">${summary}</div> `
329
- : null}
367
+ <slot @change=${this.handleSlotChanged}></slot>
368
+
369
+ <div
370
+ class="results ${getClasses({
371
+ fetching: this.fetching,
372
+ initialized: this.initialized || this.fetching,
373
+ })}"
374
+ >
375
+ <temba-loading units="6" size="8"></temba-loading>
376
+ <div class="summary ${this.expanded ? 'expanded' : ''}">${summary}</div>
377
+ </div>
330
378
  `;
331
379
  }
332
380
  }
@@ -233,7 +233,9 @@ export class Dialog extends RapidElement {
233
233
  if (this.open && !changedProperties.get('open')) {
234
234
  this.shadowRoot
235
235
  .querySelectorAll('temba-button')
236
- .forEach((button: Button) => (button.disabled = false));
236
+ .forEach((button: Button) => {
237
+ if (button) button.submitting = false;
238
+ });
237
239
 
238
240
  if (!this.noFocus) {
239
241
  this.focusFirstInput();
@@ -812,8 +812,8 @@ export class Select extends FormElement {
812
812
  url = this.next;
813
813
  }
814
814
 
815
- if (this.cache && !this.tags && this.lruCache.has(url)) {
816
- const cache = this.lruCache.get(url);
815
+ const cache = this.lruCache.get(url);
816
+ if (this.cache && !this.tags && cache) {
817
817
  if (page === 0 && !this.next) {
818
818
  this.cursorIndex = 0;
819
819
  this.setVisibleOptions([...options, ...cache.options]);
@@ -1071,6 +1071,7 @@ export class Select extends FormElement {
1071
1071
  this.addValue(option);
1072
1072
  } else {
1073
1073
  this.setValue(option);
1074
+ this.fireEvent('change');
1074
1075
  }
1075
1076
  }
1076
1077
  }
@@ -0,0 +1,141 @@
1
+ import { css, html, TemplateResult } from 'lit';
2
+ import { styleMap } from 'lit-html/directives/style-map';
3
+ import { property } from 'lit/decorators';
4
+ import { FormElement } from '../FormElement';
5
+ import { getClasses } from '../utils';
6
+
7
+ export class TembaSlider extends FormElement {
8
+ static get styles() {
9
+ return css`
10
+ :host {
11
+ display: block;
12
+ }
13
+
14
+ .track {
15
+ height: 0.1em;
16
+ border: 12px solid #fff;
17
+ background: #ddd;
18
+ }
19
+
20
+ .circle {
21
+ margin-top: 0.4em;
22
+ margin-left: 1em;
23
+ width: 0.75em;
24
+ height: 0.75em;
25
+ border: 2px solid #999;
26
+ border-radius: 999px;
27
+ position: absolute;
28
+ background: #fff;
29
+ box-shadow: 0 0 0 4px rgb(255, 255, 255);
30
+ transition: transform 200ms ease-in-out;
31
+ }
32
+
33
+ .grabbed .track {
34
+ cursor: pointer;
35
+ }
36
+
37
+ :hover .circle {
38
+ border-color: #777;
39
+ cursor: pointer;
40
+ }
41
+
42
+ .grabbed .circle {
43
+ // border-color: #777;
44
+ }
45
+
46
+ .grabbed .circle {
47
+ border-color: var(--color-primary-dark);
48
+ background: #fff;
49
+ }
50
+
51
+ .grabbed .circle {
52
+ transform: scale(1.2);
53
+ }
54
+ `;
55
+ }
56
+
57
+ @property({ type: Number })
58
+ min = 0;
59
+
60
+ @property({ type: Number })
61
+ max = 100;
62
+
63
+ circleX = 0;
64
+ grabbed = false;
65
+ left = 0;
66
+ gap = 0;
67
+
68
+ public firstUpdated(changes: Map<string, any>) {
69
+ super.firstUpdated(changes);
70
+ this.handleMouseMove = this.handleMouseMove.bind(this);
71
+ this.handleMouseUp = this.handleMouseUp.bind(this);
72
+ this.left = Math.round(this.getBoundingClientRect().left);
73
+ const circle = this.shadowRoot.querySelector('.circle').clientWidth - 4;
74
+ this.left = Math.round(this.getBoundingClientRect().left + circle);
75
+ this.gap = this.offsetWidth * 0.035;
76
+ }
77
+
78
+ public updated(changedProperties: Map<string, any>): void {
79
+ if (changedProperties.has('value')) {
80
+ const pct = parseInt(this.value) / this.max;
81
+ const total = this.offsetWidth - this.gap;
82
+ this.updateCircle(total * pct);
83
+ }
84
+ }
85
+
86
+ public updateValue(evt: MouseEvent) {
87
+ const left = evt.pageX - this.left;
88
+ const pct = left / (this.offsetWidth - this.gap);
89
+ this.value =
90
+ '' + Math.max(this.min, Math.min(Math.round(this.max * pct), this.max));
91
+ }
92
+
93
+ public handleMouseMove(evt: MouseEvent) {
94
+ if (this.grabbed) {
95
+ this.updateValue(evt);
96
+ }
97
+ }
98
+
99
+ public handleTrackDown(evt: MouseEvent) {
100
+ this.grabbed = true;
101
+ document.addEventListener('mousemove', this.handleMouseMove);
102
+ document.addEventListener('mouseup', this.handleMouseUp);
103
+ document.querySelector('html').classList.add('dragging');
104
+ this.updateValue(evt);
105
+
106
+ this.requestUpdate();
107
+ }
108
+
109
+ public handleMouseUp(evt: MouseEvent) {
110
+ this.grabbed = false;
111
+ this.updateValue(evt);
112
+
113
+ this.requestUpdate();
114
+
115
+ document.removeEventListener('mousemove', this.handleMouseMove);
116
+ document.removeEventListener('mouseup', this.handleMouseUp);
117
+ document.querySelector('html').classList.remove('dragging');
118
+ }
119
+
120
+ public updateCircle(x: number) {
121
+ const circle = this.shadowRoot.querySelector('.circle') as HTMLDivElement;
122
+ this.circleX = Math.round(
123
+ Math.min(
124
+ this.offsetWidth - this.gap,
125
+ Math.max(x + circle.offsetWidth / 2, this.gap)
126
+ )
127
+ );
128
+ this.requestUpdate();
129
+ }
130
+
131
+ public render(): TemplateResult {
132
+ return html` <div class="${getClasses({ grabbed: this.grabbed })}">
133
+ <div
134
+ style=${styleMap({ left: this.circleX + 'px' })}
135
+ class="circle"
136
+ @mousedown=${this.handleTrackDown}
137
+ ></div>
138
+ <div class="track" @mousedown=${this.handleTrackDown}></div>
139
+ </div>`;
140
+ }
141
+ }
@@ -1,6 +1,12 @@
1
- html input {
1
+ html input {
2
2
  font-weight: 300;
3
3
  }
4
+
5
+ html.dragging * {
6
+ user-select: none;
7
+ pointer-events: none;
8
+
9
+ }
4
10
 
5
11
  html {
6
12
 
package/temba-modules.ts CHANGED
@@ -34,6 +34,7 @@ import { ContactFieldEditor } from './src/contacts/ContactFieldEditor';
34
34
  import { ContactBadges } from './src/contacts/ContactBadges';
35
35
  import { ContactPending } from './src/contacts/ContactPending';
36
36
  import { ContactTickets } from './src/contacts/ContactTickets';
37
+ import { TembaSlider } from './src/slider/TembaSlider';
37
38
 
38
39
  export function addCustomElement(name: string, comp: any) {
39
40
  if (!window.customElements.get(name)) {
@@ -77,3 +78,4 @@ addCustomElement('temba-tab', Tab);
77
78
  addCustomElement('temba-contact-groups', ContactBadges);
78
79
  addCustomElement('temba-contact-pending', ContactPending);
79
80
  addCustomElement('temba-contact-tickets', ContactTickets);
81
+ addCustomElement('temba-slider', TembaSlider);