@nyaruka/temba-components 0.23.0 → 0.25.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/demo/index.html +136 -97
  3. package/dist/a29f77aa.js +356 -0
  4. package/dist/index.js +2 -2
  5. package/dist/sw.js +1 -1
  6. package/dist/sw.js.map +1 -1
  7. package/dist/templates/components-body.html +1 -1
  8. package/dist/templates/components-head.html +1 -1
  9. package/out-tsc/src/checkbox/Checkbox.js +29 -14
  10. package/out-tsc/src/checkbox/Checkbox.js.map +1 -1
  11. package/out-tsc/src/contactsearch/ContactSearch.js +146 -72
  12. package/out-tsc/src/contactsearch/ContactSearch.js.map +1 -1
  13. package/out-tsc/src/dialog/Dialog.js +12 -3
  14. package/out-tsc/src/dialog/Dialog.js.map +1 -1
  15. package/out-tsc/src/dialog/Modax.js +12 -2
  16. package/out-tsc/src/dialog/Modax.js.map +1 -1
  17. package/out-tsc/src/interfaces.js +1 -0
  18. package/out-tsc/src/interfaces.js.map +1 -1
  19. package/out-tsc/src/omnibox/Omnibox.js +7 -1
  20. package/out-tsc/src/omnibox/Omnibox.js.map +1 -1
  21. package/out-tsc/src/options/Options.js +36 -6
  22. package/out-tsc/src/options/Options.js.map +1 -1
  23. package/out-tsc/src/select/Select.js +86 -35
  24. package/out-tsc/src/select/Select.js.map +1 -1
  25. package/out-tsc/src/textinput/TextInput.js +42 -1
  26. package/out-tsc/src/textinput/TextInput.js.map +1 -1
  27. package/out-tsc/test/temba-select.test.js +2 -1
  28. package/out-tsc/test/temba-select.test.js.map +1 -1
  29. package/package.json +1 -1
  30. package/screenshots/truth/checkbox/checked.png +0 -0
  31. package/screenshots/truth/checkbox/default.png +0 -0
  32. package/screenshots/truth/select/disabled-multi-selection.png +0 -0
  33. package/screenshots/truth/select/disabled-selection.png +0 -0
  34. package/screenshots/truth/select/disabled.png +0 -0
  35. package/screenshots/truth/select/embedded.png +0 -0
  36. package/screenshots/truth/select/expression-selected.png +0 -0
  37. package/screenshots/truth/select/expressions.png +0 -0
  38. package/screenshots/truth/select/functions.png +0 -0
  39. package/screenshots/truth/select/local-options.png +0 -0
  40. package/screenshots/truth/select/remote-options.png +0 -0
  41. package/screenshots/truth/select/search-enabled.png +0 -0
  42. package/screenshots/truth/select/search-multi-no-matches.png +0 -0
  43. package/screenshots/truth/select/search-selected-focus.png +0 -0
  44. package/screenshots/truth/select/search-selected.png +0 -0
  45. package/screenshots/truth/select/search-with-selected.png +0 -0
  46. package/screenshots/truth/select/searching.png +0 -0
  47. package/screenshots/truth/select/selected-multi.png +0 -0
  48. package/screenshots/truth/select/selected-single.png +0 -0
  49. package/screenshots/truth/select/selection-clearable.png +0 -0
  50. package/screenshots/truth/select/with-placeholder.png +0 -0
  51. package/screenshots/truth/select/without-placeholder.png +0 -0
  52. package/src/checkbox/Checkbox.ts +31 -16
  53. package/src/contactsearch/ContactSearch.ts +157 -80
  54. package/src/dialog/Dialog.ts +13 -3
  55. package/src/dialog/Modax.ts +9 -2
  56. package/src/interfaces.ts +1 -0
  57. package/src/omnibox/Omnibox.ts +9 -1
  58. package/src/options/Options.ts +41 -10
  59. package/src/select/Select.ts +91 -37
  60. package/src/textinput/TextInput.ts +47 -1
  61. package/static/css/temba-components.css +1 -2
  62. package/test/temba-select.test.ts +3 -1
  63. package/test-assets/style.css +1 -1
  64. package/dist/28f45617.js +0 -356
@@ -1,8 +1,8 @@
1
1
  import { TemplateResult, html, property, css } from 'lit-element';
2
- import { getUrl, fillTemplate, WebResponse } from '../utils';
2
+ import { getUrl, WebResponse } from '../utils';
3
3
  import { TextInput } from '../textinput/TextInput';
4
4
  import '../alert/Alert';
5
- import { Contact } from '../interfaces';
5
+ import { Contact, CustomEventType } from '../interfaces';
6
6
  import { styleMap } from 'lit-html/directives/style-map';
7
7
  import { FormElement } from '../FormElement';
8
8
 
@@ -31,7 +31,7 @@ export class ContactSearch extends FormElement {
31
31
  width: 160px;
32
32
  }
33
33
 
34
- .created-on {
34
+ .date {
35
35
  text-align: right;
36
36
  }
37
37
 
@@ -40,7 +40,7 @@ export class ContactSearch extends FormElement {
40
40
  color: var(--color-text-dark);
41
41
  }
42
42
 
43
- .field-header.created-on {
43
+ .field-header.date {
44
44
  text-align: right;
45
45
  }
46
46
 
@@ -55,12 +55,6 @@ export class ContactSearch extends FormElement {
55
55
 
56
56
  table {
57
57
  width: 100%;
58
- padding-top: 10px;
59
- }
60
-
61
- .header td {
62
- border-bottom: 2px solid var(--color-borders);
63
- padding: 5px 3px;
64
58
  }
65
59
 
66
60
  .contact td {
@@ -92,6 +86,46 @@ export class ContactSearch extends FormElement {
92
86
  .error {
93
87
  margin-top: 10px;
94
88
  }
89
+
90
+ .match-count {
91
+ padding: 4px;
92
+ margin-top: 6px;
93
+ }
94
+
95
+ .linked {
96
+ color: var(--color-link-primary);
97
+ text-decoration: none;
98
+ cursor: pointer;
99
+ }
100
+
101
+ .header td {
102
+ border-bottom: 0px solid var(--color-borders);
103
+ padding: 5px 3px;
104
+ }
105
+
106
+ .expanded .header td {
107
+ border-bottom: 2px solid var(--color-borders);
108
+ }
109
+
110
+ td.field-header,
111
+ tr.table-footer,
112
+ tr.contact {
113
+ display: none;
114
+ }
115
+
116
+ .expanded td.field-header {
117
+ display: table-cell;
118
+ }
119
+
120
+ .expanded tr.contact,
121
+ .expanded tr.table-footer {
122
+ display: table-row;
123
+ }
124
+
125
+ .query {
126
+ display: var(--contact-search-query-display);
127
+ margin-bottom: 10px;
128
+ }
95
129
  `;
96
130
  }
97
131
 
@@ -100,6 +134,9 @@ export class ContactSearch extends FormElement {
100
134
  @property({ type: Boolean })
101
135
  fetching: boolean;
102
136
 
137
+ @property({ type: Boolean })
138
+ expanded: boolean;
139
+
103
140
  @property({ type: String })
104
141
  endpoint: string;
105
142
 
@@ -112,8 +149,11 @@ export class ContactSearch extends FormElement {
112
149
  @property({ type: String })
113
150
  query = '';
114
151
 
115
- @property({ type: String, attribute: 'matches-text' })
116
- matchesText = '';
152
+ @property({ type: Number })
153
+ inactiveThreshold = 1000;
154
+
155
+ @property({ type: Number })
156
+ inactiveDays = 90;
117
157
 
118
158
  @property({ attribute: false })
119
159
  summary: SummaryResponse;
@@ -140,16 +180,22 @@ export class ContactSearch extends FormElement {
140
180
  }
141
181
  }
142
182
 
143
- public fetchSummary(query: string): any {
144
- // const CancelToken = axios.CancelToken;
145
- // this.cancelToken = CancelToken.source();
146
-
147
- const url = this.endpoint + query;
183
+ public executeQuery(query: string): any {
184
+ const url = this.endpoint + query.replace('\n', ' ');
185
+ getUrl(url).then((response: WebResponse) => {
186
+ if (response.status === 200) {
187
+ const summary = response.json as SummaryResponse;
188
+ this.fireCustomEvent(CustomEventType.FetchComplete, summary);
189
+ }
190
+ });
191
+ }
148
192
 
193
+ public fetchSummary(query: string): any {
194
+ const url = this.endpoint + query.replace('\n', ' ');
149
195
  getUrl(url).then((response: WebResponse) => {
196
+ this.fetching = false;
150
197
  if (response.status === 200) {
151
198
  this.summary = response.json as SummaryResponse;
152
- this.fetching = false;
153
199
 
154
200
  if (this.summary.error) {
155
201
  this.errors = [this.summary.error];
@@ -157,6 +203,7 @@ export class ContactSearch extends FormElement {
157
203
  this.errors = [];
158
204
  }
159
205
  this.requestUpdate('errors');
206
+ this.fireCustomEvent(CustomEventType.ContentChanged, this.summary);
160
207
  }
161
208
  });
162
209
  }
@@ -177,54 +224,76 @@ export class ContactSearch extends FormElement {
177
224
 
178
225
  if (!this.summary.error) {
179
226
  const count = this.summary.total;
180
- const message = fillTemplate(this.matchesText, {
181
- query: this.summary.query,
182
- count,
183
- });
227
+ const lastSeenOn = this.summary.query.indexOf('last_seen_on') > -1;
184
228
 
185
229
  summary = html`
186
- <table cellspacing="0" cellpadding="0">
187
- <tr class="header">
188
- <td colspan="2"></td>
189
- ${fields.map(
190
- field => html` <td class="field-header">${field.label}</td> `
230
+ <div class="summary ${this.expanded ? 'expanded' : ''}">
231
+ <table cellspacing="0" cellpadding="0">
232
+ <tr class="header">
233
+ <td colspan="2">
234
+ Found
235
+ <a
236
+ class="linked"
237
+ target="_"
238
+ href="/contact/?search=${encodeURIComponent(
239
+ this.summary.query
240
+ )}"
241
+ >
242
+ ${count.toLocaleString()}
243
+ </a>
244
+ contact${count !== 1 ? 's' : ''}
245
+ </td>
246
+ ${fields.map(
247
+ field => html` <td class="field-header">${field.label}</td> `
248
+ )}
249
+ <td></td>
250
+ <td class="field-header date">
251
+ ${lastSeenOn ? 'Last Seen' : 'Created'}
252
+ </td>
253
+ </tr>
254
+
255
+ ${this.summary.sample.map(
256
+ (contact: Contact) => html`
257
+ <tr class="contact">
258
+ <td class="urn">
259
+ ${(contact as any).primary_urn_formatted}
260
+ </td>
261
+ <td class="name">${contact.name}</td>
262
+ ${fields.map(
263
+ field => html`
264
+ <td class="field">
265
+ ${(
266
+ (contact as any).fields[field.uuid] || { text: '' }
267
+ ).text}
268
+ </td>
269
+ `
270
+ )}
271
+ <td></td>
272
+ <td class="date">
273
+ ${lastSeenOn
274
+ ? contact.last_seen_on || '--'
275
+ : contact.created_on}
276
+ </td>
277
+ </tr>
278
+ `
191
279
  )}
192
- <td></td>
193
- <td class="field-header created-on">Created On</td>
194
- </tr>
195
-
196
- ${this.summary.sample.map(
197
- (contact: Contact) => html`
198
- <tr class="contact">
199
- <td class="urn">${(contact as any).primary_urn_formatted}</td>
200
- <td class="name">${contact.name}</td>
201
- ${fields.map(
202
- field => html`
203
- <td class="field">
204
- ${((contact as any).fields[field.uuid] || { text: '' })
205
- .text}
206
- </td>
207
- `
208
- )}
209
- <td></td>
210
- <td class="created-on">${contact.created_on}</td>
211
- </tr>
212
- `
213
- )}
214
-
215
- <tr class="table-footer">
216
- <td class="query-details" colspan=${fields.length + 3}>
217
- ${message}
218
- </td>
219
- <td class="more">
220
- ${this.summary.total > this.summary.sample.length
221
- ? html`
222
- ${this.summary.total - this.summary.sample.length} more
223
- `
224
- : null}
225
- </td>
226
- </tr>
227
- </table>
280
+ ${this.summary.total > this.summary.sample.length
281
+ ? html`<tr class="table-footer">
282
+ <td class="query-details" colspan=${fields.length + 3}></td>
283
+ <td class="more">
284
+ <a
285
+ class="linked"
286
+ target="_"
287
+ href="/contact/?search=${encodeURIComponent(
288
+ this.summary.query
289
+ )}"
290
+ >more</a
291
+ >
292
+ </td>
293
+ </tr>`
294
+ : null}
295
+ </table>
296
+ </div>
228
297
  `;
229
298
  }
230
299
  }
@@ -232,23 +301,31 @@ export class ContactSearch extends FormElement {
232
301
  const loadingStyle = this.fetching ? { opacity: '1' } : {};
233
302
 
234
303
  return html`
235
- <temba-textinput
236
- .label=${this.label}
237
- .helpText=${this.helpText}
238
- .widgetOnly=${this.widgetOnly}
239
- .errors=${this.errors}
240
- name=${this.name}
241
- .inputRoot=${this}
242
- @input=${this.handleQueryChange}
243
- placeholder=${this.placeholder}
244
- value=${this.query}
245
- >
246
- <temba-loading
247
- units="4"
248
- style=${styleMap(loadingStyle)}
249
- ></temba-loading>
250
- </temba-textinput>
251
- ${this.summary ? html` <div class="summary">${summary}</div> ` : null}
304
+ <div class="query">
305
+ <temba-textinput
306
+ .label=${this.label}
307
+ .helpText=${this.helpText}
308
+ .widgetOnly=${this.widgetOnly}
309
+ .errors=${this.errors}
310
+ name=${this.name}
311
+ .inputRoot=${this}
312
+ @input=${this.handleQueryChange}
313
+ placeholder=${this.placeholder}
314
+ .value=${this.query}
315
+ textarea
316
+ autogrow
317
+ >
318
+ </temba-textinput>
319
+ </div>
320
+
321
+ ${this.fetching
322
+ ? html`<temba-loading
323
+ units="4"
324
+ style=${styleMap(loadingStyle)}
325
+ ></temba-loading>`
326
+ : this.summary
327
+ ? html` <div class="summary">${summary}</div> `
328
+ : null}
252
329
  `;
253
330
  }
254
331
  }
@@ -49,7 +49,7 @@ export class Dialog extends RapidElement {
49
49
  position: fixed;
50
50
  top: 0px;
51
51
  left: 0px;
52
- transition: opacity linear calc(var(--transitions-speed) / 2ms);
52
+ transition: opacity linear calc(var(--transition-speed) / 2ms);
53
53
  pointer-events: none;
54
54
  }
55
55
 
@@ -68,7 +68,7 @@ export class Dialog extends RapidElement {
68
68
 
69
69
  .dialog-body {
70
70
  background: #fff;
71
- max-height: 55vh;
71
+ max-height: 75vh;
72
72
  overflow-y: auto;
73
73
  }
74
74
 
@@ -162,6 +162,9 @@ export class Dialog extends RapidElement {
162
162
  @property({ type: Boolean })
163
163
  destructive: boolean;
164
164
 
165
+ @property({ type: Boolean })
166
+ disabled: boolean;
167
+
165
168
  @property({ type: Boolean })
166
169
  loading: boolean;
167
170
 
@@ -293,6 +296,10 @@ export class Dialog extends RapidElement {
293
296
  );
294
297
  }
295
298
 
299
+ public getPrimaryButton(): Button {
300
+ return this.shadowRoot.querySelector(`temba-button[primary]`);
301
+ }
302
+
296
303
  private handleKeyUp(event: KeyboardEvent) {
297
304
  if (event.key === 'Escape') {
298
305
  this.clickCancel();
@@ -320,7 +327,9 @@ export class Dialog extends RapidElement {
320
327
  public render(): TemplateResult {
321
328
  const height = this.getDocumentHeight();
322
329
 
323
- const maskStyle = { height: `${height + 100}px` };
330
+ const maskStyle = {
331
+ height: `${height + 100}px`,
332
+ };
324
333
  const dialogStyle = { width: Dialog.widths[this.size] };
325
334
 
326
335
  const header = this.header
@@ -375,6 +384,7 @@ export class Dialog extends RapidElement {
375
384
  ?destructive=${this.destructive}
376
385
  ?primary=${!this.destructive}
377
386
  ?submitting=${this.submitting}
387
+ ?disabled=${this.disabled}
378
388
  >}</temba-button
379
389
  >
380
390
  `
@@ -112,6 +112,11 @@ export class Modax extends RapidElement {
112
112
  @property({ type: String })
113
113
  body: any = this.getLoading();
114
114
 
115
+ @property({ type: Boolean })
116
+ disabled = false;
117
+
118
+ @property({ type: Boolean })
119
+ suspendSubmit = false;
115
120
  // private cancelToken: CancelTokenSource;
116
121
 
117
122
  // http promise to monitor for completeness
@@ -301,7 +306,9 @@ export class Modax extends RapidElement {
301
306
  const button = evt.detail.button;
302
307
  if (!button.disabled && !button.submitting) {
303
308
  if (button.name === this.primaryName) {
304
- this.submit();
309
+ if (!this.suspendSubmit) {
310
+ this.submit();
311
+ }
305
312
  }
306
313
  }
307
314
 
@@ -309,7 +316,6 @@ export class Modax extends RapidElement {
309
316
  this.open = false;
310
317
  this.fetching = false;
311
318
  this.cancelName = undefined;
312
- // this.cancelToken.cancel();
313
319
  }
314
320
  }
315
321
 
@@ -338,6 +344,7 @@ export class Modax extends RapidElement {
338
344
  ?submitting=${this.submitting}
339
345
  ?destructive=${this.isDestructive()}
340
346
  ?noFocus=${true}
347
+ ?disabled=${this.disabled}
341
348
  @temba-button-clicked=${this.handleDialogClick.bind(this)}
342
349
  @temba-dialog-hidden=${this.handleDialogHidden.bind(this)}
343
350
  >
package/src/interfaces.ts CHANGED
@@ -174,6 +174,7 @@ export enum CustomEventType {
174
174
  ScrollThreshold = 'temba-scroll-threshold',
175
175
  ContentChanged = 'temba-content-changed',
176
176
  ContextChanged = 'temba-context-changed',
177
+ FetchComplete = 'temba-fetch-complete',
177
178
  Submitted = 'temba-submitted',
178
179
  Redirected = 'temba-redirected',
179
180
  NoPath = 'temba-no-path',
@@ -1,6 +1,7 @@
1
1
  import { TemplateResult, html, css, property } from 'lit-element';
2
2
  import { styleMap } from 'lit-html/directives/style-map';
3
3
  import { RapidElement } from '../RapidElement';
4
+ import { Select } from '../select/Select';
4
5
 
5
6
  enum OmniType {
6
7
  Group = 'group',
@@ -104,7 +105,9 @@ export class Omnibox extends RapidElement {
104
105
  }
105
106
 
106
107
  if (option.type === OmniType.Group) {
107
- return html` <div style=${styleMap(style)}>${option.count}</div> `;
108
+ return html`
109
+ <div style=${styleMap(style)}>${option.count.toLocaleString()}</div>
110
+ `;
108
111
  }
109
112
 
110
113
  return null;
@@ -172,6 +175,11 @@ export class Omnibox extends RapidElement {
172
175
  }
173
176
  }
174
177
 
178
+ public getValues(): any[] {
179
+ const select = this.shadowRoot.querySelector('temba-select') as Select;
180
+ return select.values;
181
+ }
182
+
175
183
  public render(): TemplateResult {
176
184
  return html`
177
185
  <temba-select
@@ -9,25 +9,26 @@ import {
9
9
  throttle,
10
10
  } from '../utils';
11
11
 
12
- interface NameFunction {
13
- (option: any): string;
14
- }
15
-
16
12
  export class Options extends RapidElement {
17
13
  static get styles() {
18
14
  return css`
19
15
  .options-container {
20
- visibility: hidden;
21
- border-radius: var(--curvature-widget);
22
16
  background: var(--color-widget-bg-focused);
23
17
  user-select: none;
24
18
  box-shadow: var(--options-shadow);
25
- border: 1px solid var(--color-widget-border);
26
19
  border-radius: var(--curvature-widget);
27
20
  overflow: hidden;
28
21
  margin-top: var(--options-margin-top);
29
22
  display: flex;
30
23
  flex-direction: column;
24
+ transform: scaleY(0.5) translateY(-5em);
25
+ transition: transform var(--transition-speed)
26
+ cubic-bezier(0.71, 0.18, 0.61, 1.33),
27
+ opacity var(--transition-speed) cubic-bezier(0.71, 0.18, 0.61, 1.33);
28
+ z-index: 10000;
29
+ pointer-events: none;
30
+ opacity: 0;
31
+ border: 1px transparent;
31
32
  }
32
33
 
33
34
  .anchored {
@@ -88,12 +89,15 @@ export class Options extends RapidElement {
88
89
  }
89
90
 
90
91
  .show {
91
- visibility: visible;
92
92
  z-index: 10000;
93
+ transform: scaleY(1) translateY(0);
94
+ border: 1px solid var(--color-widget-border);
95
+ pointer-events: auto;
96
+ opacity: 1;
93
97
  }
94
98
 
95
99
  .option {
96
- font-size: 14px;
100
+ font-size: var(--temba-options-font-size);
97
101
  padding: 5px 10px;
98
102
  border-radius: 4px;
99
103
  margin: 0.3em;
@@ -195,6 +199,9 @@ export class Options extends RapidElement {
195
199
  @property({ type: Array })
196
200
  options: any[];
197
201
 
202
+ @property({ type: Array })
203
+ tempOptions: any[];
204
+
198
205
  @property({ type: Boolean })
199
206
  poppedTop: boolean;
200
207
 
@@ -303,6 +310,15 @@ export class Options extends RapidElement {
303
310
  });
304
311
  }
305
312
 
313
+ if (changedProperties.has('visible') && changedProperties.has('options')) {
314
+ if (!this.visible && this.options.length == 0) {
315
+ this.tempOptions = changedProperties.get('options');
316
+ window.setTimeout(() => {
317
+ this.tempOptions = [];
318
+ }, 300);
319
+ }
320
+ }
321
+
306
322
  if (changedProperties.has('options')) {
307
323
  this.calculatePosition();
308
324
 
@@ -527,6 +543,12 @@ export class Options extends RapidElement {
527
543
  }
528
544
  }
529
545
 
546
+ // we need to swallow mouse down so we don't grab focus
547
+ private handleMouseDown(evt: MouseEvent) {
548
+ evt.preventDefault();
549
+ evt.stopPropagation();
550
+ }
551
+
530
552
  private handleOptionClick(evt: MouseEvent) {
531
553
  evt.preventDefault();
532
554
  evt.stopPropagation();
@@ -572,7 +594,15 @@ export class Options extends RapidElement {
572
594
  options: true,
573
595
  });
574
596
 
575
- const options = this.options || [];
597
+ let options = this.options || [];
598
+ if (
599
+ options.length == 0 &&
600
+ this.tempOptions &&
601
+ this.tempOptions.length > 0
602
+ ) {
603
+ options = this.tempOptions;
604
+ }
605
+
576
606
  return html`
577
607
  <div class=${classes} style=${styleMap(containerStyle)}>
578
608
  <div class="options-scroll" @scroll=${this.handleInnerScroll}>
@@ -582,6 +612,7 @@ export class Options extends RapidElement {
582
612
  data-option-index="${index}"
583
613
  @mousemove=${this.handleMouseMove}
584
614
  @click=${this.handleOptionClick}
615
+ @mousedown=${this.handleMouseDown}
585
616
  class="option ${index === this.cursorIndex ? 'focused' : ''}"
586
617
  >
587
618
  ${this.resolvedRenderOption(option, index === this.cursorIndex)}