@nyaruka/temba-components 0.52.2 → 0.53.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.
@@ -1,11 +1,24 @@
1
1
  import { property } from 'lit/decorators.js';
2
- import { TemplateResult, html, css } from 'lit';
2
+ import { TemplateResult, html, css, PropertyValueMap } from 'lit';
3
3
  import { Button } from '../button/Button';
4
4
  import { RapidElement } from '../RapidElement';
5
5
  import { CustomEventType } from '../interfaces';
6
6
  import { styleMap } from 'lit-html/directives/style-map.js';
7
7
  import { getClasses } from '../utils';
8
8
 
9
+ export enum ButtonType {
10
+ PRIMARY = 'primary',
11
+ SECONDARY = 'secondary',
12
+ DESTRUCTIVE = 'destructive',
13
+ }
14
+ export class DialogButton {
15
+ name?: string;
16
+ id?: string;
17
+ details?: any;
18
+ type?: string;
19
+ closes?: boolean;
20
+ }
21
+
9
22
  export class Dialog extends RapidElement {
10
23
  static get widths(): { [size: string]: string } {
11
24
  return {
@@ -24,6 +37,10 @@ export class Dialog extends RapidElement {
24
37
  background: white;
25
38
  }
26
39
 
40
+ .flex-grow {
41
+ flex-grow: 1;
42
+ }
43
+
27
44
  .flex {
28
45
  display: flex;
29
46
  flex-direction: column;
@@ -99,6 +116,9 @@ export class Dialog extends RapidElement {
99
116
  }
100
117
 
101
118
  .header-text {
119
+ display: flex;
120
+ flex-direction: row;
121
+ align-items: center;
102
122
  font-size: 20px;
103
123
  padding: 12px 20px;
104
124
  font-weight: 300;
@@ -106,11 +126,21 @@ export class Dialog extends RapidElement {
106
126
  background: var(--header-bg);
107
127
  }
108
128
 
129
+ .header-text .title {
130
+ flex-grow: 1;
131
+ }
132
+
133
+ .header-text .status {
134
+ font-size: 0.6em;
135
+ font-weight: bold;
136
+ }
137
+
109
138
  .dialog-footer {
110
139
  background: var(--color-primary-light);
111
140
  padding: 10px;
112
141
  display: flex;
113
- flex-flow: row-reverse;
142
+ flex-flow: row;
143
+ align-items: center;
114
144
  }
115
145
 
116
146
  temba-button {
@@ -198,6 +228,9 @@ export class Dialog extends RapidElement {
198
228
  @property()
199
229
  ready: boolean;
200
230
 
231
+ @property({ type: Array })
232
+ buttons: DialogButton[] = [];
233
+
201
234
  @property({ attribute: false })
202
235
  onButtonClicked: (button: Button) => void;
203
236
 
@@ -207,6 +240,25 @@ export class Dialog extends RapidElement {
207
240
  super();
208
241
  }
209
242
 
243
+ protected firstUpdated(
244
+ changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
245
+ ): void {
246
+ if (changes.has('cancelButtonName') && this.cancelButtonName) {
247
+ this.buttons.push({
248
+ name: this.cancelButtonName,
249
+ type: ButtonType.SECONDARY,
250
+ closes: true,
251
+ });
252
+ }
253
+
254
+ if (changes.has('primaryButtonName') && this.primaryButtonName) {
255
+ this.buttons.push({
256
+ name: this.primaryButtonName,
257
+ type: ButtonType.PRIMARY,
258
+ });
259
+ }
260
+ }
261
+
210
262
  public updated(changedProperties: Map<string, any>) {
211
263
  if (changedProperties.has('open')) {
212
264
  const body = document.querySelector('body');
@@ -270,8 +322,13 @@ export class Dialog extends RapidElement {
270
322
  public handleClick(evt: MouseEvent) {
271
323
  const button = evt.currentTarget as Button;
272
324
  if (!button.disabled) {
273
- this.fireCustomEvent(CustomEventType.ButtonClicked, { button });
274
- if (button.name === this.cancelButtonName) {
325
+ let detail: DialogButton = {};
326
+ if (button.index >= 0 && button.index < this.buttons.length) {
327
+ detail = this.buttons[button.index];
328
+ }
329
+
330
+ this.fireCustomEvent(CustomEventType.ButtonClicked, { button, detail });
331
+ if (button.name === this.cancelButtonName || (detail && detail.closes)) {
275
332
  this.open = false;
276
333
  }
277
334
  }
@@ -343,7 +400,9 @@ export class Dialog extends RapidElement {
343
400
  const header = this.header
344
401
  ? html`
345
402
  <div class="dialog-header">
346
- <div class="header-text">${this.header}</div>
403
+ <div class="header-text">
404
+ <div class="title">${this.header}</div>
405
+ </div>
347
406
  </div>
348
407
  `
349
408
  : null;
@@ -382,26 +441,23 @@ export class Dialog extends RapidElement {
382
441
  </div>
383
442
 
384
443
  <div class="dialog-footer">
385
- ${
386
- this.primaryButtonName
387
- ? html`
388
- <temba-button
389
- @click=${this.handleClick}
390
- .name=${this.primaryButtonName}
391
- ?destructive=${this.destructive}
392
- ?primary=${!this.destructive}
393
- ?submitting=${this.submitting}
394
- ?disabled=${this.disabled}
395
- >}</temba-button
396
- >
397
- `
398
- : null
399
- }
400
- <temba-button
401
- @click=${this.handleClick}
402
- name=${this.cancelButtonName}
403
- secondary
404
- ></temba-button>
444
+ <div class="flex-grow">
445
+ <slot name="gutter"></slot>
446
+ </div>
447
+ ${this.buttons.map(
448
+ (button: DialogButton, index) => html`
449
+ <temba-button
450
+ name=${button.name}
451
+ ?destructive=${button.type == 'primary' && this.destructive}
452
+ ?primary=${button.type == 'primary' && !this.destructive}
453
+ ?secondary=${button.type == 'secondary'}
454
+ ?submitting=${this.submitting}
455
+ ?disabled=${this.disabled}
456
+ index=${index}
457
+ @click=${this.handleClick}
458
+ ></temba-button>
459
+ `
460
+ )}
405
461
  </div>
406
462
  </div>
407
463
  <div class="grow-bottom"></div>
@@ -3,9 +3,9 @@ import { property } from 'lit/decorators.js';
3
3
  import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
4
4
 
5
5
  import { RapidElement } from '../RapidElement';
6
- import { getUrl, serialize, postUrl, WebResponse } from '../utils';
6
+ import { getUrl, serialize, postUrl, WebResponse, getClasses } from '../utils';
7
7
  import { CustomEventType } from '../interfaces';
8
- import { Dialog } from './Dialog';
8
+ import { ButtonType, Dialog, DialogButton } from './Dialog';
9
9
 
10
10
  export class Modax extends RapidElement {
11
11
  static get styles() {
@@ -24,6 +24,11 @@ export class Modax extends RapidElement {
24
24
  display: none;
25
25
  }
26
26
 
27
+ button[type='submit'],
28
+ input[type='submit'] {
29
+ display: none;
30
+ }
31
+
27
32
  .modax-body {
28
33
  padding: 20px;
29
34
  display: block;
@@ -67,6 +72,33 @@ export class Modax extends RapidElement {
67
72
  border-radius: 6px;
68
73
  font-weight: 300;
69
74
  }
75
+
76
+ .step-ball {
77
+ background: rgba(var(--primary-rgb), 0.2);
78
+ width: 1.2em;
79
+ height: 1.2em;
80
+ border-radius: 100%;
81
+ margin-right: 0.5em;
82
+ border: 0.15em solid transparent;
83
+ }
84
+
85
+ .step-ball.complete {
86
+ background: rgba(var(--primary-rgb), 0.7);
87
+ cursor: pointer;
88
+ }
89
+ .step-ball.complete:hover {
90
+ background: rgba(var(--primary-rgb), 0.8);
91
+ }
92
+
93
+ .step-ball.active {
94
+ border: 0.15em solid var(--color-primary-dark);
95
+ }
96
+
97
+ .wizard-steps {
98
+ display: flex;
99
+ flex-direction: row;
100
+ margin-left: 0.6em;
101
+ }
70
102
  `;
71
103
  }
72
104
 
@@ -106,6 +138,15 @@ export class Modax extends RapidElement {
106
138
  @property({ type: Boolean })
107
139
  disabled = false;
108
140
 
141
+ @property({ type: Array })
142
+ buttons: DialogButton[] = [];
143
+
144
+ @property({ type: Number })
145
+ wizardStep = 0;
146
+
147
+ @property({ type: Number })
148
+ wizardStepCount = 0;
149
+
109
150
  @property({ type: Boolean })
110
151
  suspendSubmit = false;
111
152
  // private cancelToken: CancelTokenSource;
@@ -149,18 +190,29 @@ export class Modax extends RapidElement {
149
190
  }
150
191
 
151
192
  public updatePrimaryButton(): void {
193
+ const wizard = this.shadowRoot.querySelector(
194
+ '#wizard-form'
195
+ ) as HTMLDivElement;
196
+ if (wizard) {
197
+ this.wizardStep = parseInt(wizard.dataset.step);
198
+ this.wizardStepCount = parseInt(wizard.dataset.steps);
199
+ }
200
+
152
201
  if (!this.noSubmit) {
153
202
  this.updateComplete.then(() => {
154
203
  const submitButton = this.shadowRoot.querySelector(
155
- "input[type='submit']"
204
+ "input[type='submit'],button[type='submit']"
156
205
  ) as any;
157
206
 
158
207
  if (submitButton) {
159
- this.primaryName = submitButton.value;
160
- this.cancelName = 'Cancel';
208
+ this.buttons = [
209
+ { type: ButtonType.SECONDARY, name: 'Cancel', closes: true },
210
+ { type: ButtonType.PRIMARY, name: submitButton.value },
211
+ ];
161
212
  } else {
162
- this.primaryName = null;
163
- this.cancelName = 'Ok';
213
+ this.buttons = [
214
+ { type: ButtonType.SECONDARY, name: 'Ok', closes: true },
215
+ ];
164
216
  }
165
217
  this.submitting = false;
166
218
  });
@@ -229,22 +281,37 @@ export class Modax extends RapidElement {
229
281
  this.body = this.getLoading();
230
282
  getUrl(this.endpoint, null, this.getHeaders()).then(
231
283
  (response: WebResponse) => {
232
- this.setBody(response.body);
233
- this.fetching = false;
234
- this.updateComplete.then(() => {
235
- this.updatePrimaryButton();
236
- this.fireCustomEvent(CustomEventType.Loaded, {
237
- body: this.getBody(),
284
+ // if it's a full page, breakout of the modal
285
+ if (response.body.indexOf('<!DOCTYPE HTML>') == 0) {
286
+ document.location = response.url;
287
+ } else {
288
+ this.setBody(response.body);
289
+ this.fetching = false;
290
+ this.updateComplete.then(() => {
291
+ this.updatePrimaryButton();
292
+ this.fireCustomEvent(CustomEventType.Loaded, {
293
+ body: this.getBody(),
294
+ });
238
295
  });
239
- });
296
+ }
240
297
  }
241
298
  );
242
299
  }
243
300
 
244
- public submit(): void {
301
+ public submit(extra = {}): void {
245
302
  this.submitting = true;
246
303
  const form = this.shadowRoot.querySelector('form');
247
- const postData = form ? serialize(form) : {};
304
+
305
+ let postData = form ? serialize(form) : '';
306
+ if (extra) {
307
+ Object.keys(extra).forEach(key => {
308
+ postData +=
309
+ (postData.length > 1 ? '&' : '') +
310
+ encodeURIComponent(key) +
311
+ '=' +
312
+ encodeURIComponent(extra[key]);
313
+ });
314
+ }
248
315
 
249
316
  postUrl(
250
317
  this.endpoint,
@@ -278,7 +345,9 @@ export class Modax extends RapidElement {
278
345
  } else {
279
346
  // if we set the body, update our submit button
280
347
  if (this.setBody(response.body)) {
281
- this.updatePrimaryButton();
348
+ this.updateComplete.then(() => {
349
+ this.updatePrimaryButton();
350
+ });
282
351
  }
283
352
  }
284
353
  }, 1000);
@@ -290,15 +359,16 @@ export class Modax extends RapidElement {
290
359
 
291
360
  private handleDialogClick(evt: CustomEvent) {
292
361
  const button = evt.detail.button;
362
+ const detail = evt.detail.detail;
293
363
  if (!button.disabled && !button.submitting) {
294
- if (button.name === this.primaryName) {
364
+ if (button.primary || button.destructive) {
295
365
  if (!this.suspendSubmit) {
296
366
  this.submit();
297
367
  }
298
368
  }
299
369
  }
300
370
 
301
- if (button.name === (this.cancelName || 'Cancel')) {
371
+ if (detail.closes) {
302
372
  this.open = false;
303
373
  this.fetching = false;
304
374
  this.cancelName = undefined;
@@ -315,16 +385,47 @@ export class Modax extends RapidElement {
315
385
  return this.endpoint && this.endpoint.indexOf('delete') > -1;
316
386
  }
317
387
 
388
+ private handleGotoStep(evt: MouseEvent) {
389
+ const step = (evt.target as HTMLDivElement).dataset.gotoStep;
390
+ if (step) {
391
+ this.submit({ wizard_goto_step: step });
392
+ }
393
+ }
394
+
318
395
  public getBody() {
319
396
  return this.shadowRoot.querySelector('.modax-body');
320
397
  }
321
398
 
322
399
  public render(): TemplateResult {
400
+ const wizardStepBalls = [];
401
+
402
+ const wizard = this.shadowRoot.querySelector(
403
+ '#wizard-form'
404
+ ) as HTMLDivElement;
405
+ if (wizard) {
406
+ const completed = (wizard.getAttribute('data-completed') || '')
407
+ .split(',')
408
+ .filter(step => step.length > 0);
409
+
410
+ for (let i = 0; i < this.wizardStepCount; i++) {
411
+ wizardStepBalls.push(
412
+ html`<div
413
+ data-goto-step=${completed[i]}
414
+ @click=${this.handleGotoStep.bind(this)}
415
+ class="${getClasses({
416
+ 'step-ball': true,
417
+ active: this.wizardStep - 1 === i,
418
+ complete: i < completed.length,
419
+ })}"
420
+ ></div>`
421
+ );
422
+ }
423
+ }
424
+
323
425
  return html`
324
426
  <temba-dialog
325
- header=${this.header}
326
- .primaryButtonName=${this.noSubmit ? null : this.primaryName}
327
- .cancelButtonName=${this.cancelName || 'Cancel'}
427
+ .header=${this.header}
428
+ .buttons=${this.buttons}
328
429
  ?open=${this.open}
329
430
  ?loading=${this.fetching}
330
431
  ?submitting=${this.submitting}
@@ -338,6 +439,9 @@ export class Modax extends RapidElement {
338
439
  ${this.body}
339
440
  </div>
340
441
  <div class="scripts"></div>
442
+ <div slot="gutter">
443
+ <div class="wizard-steps">${wizardStepBalls}</div>
444
+ </div>
341
445
  </temba-dialog>
342
446
  <div class="slot-wrapper" @click=${this.handleSlotClicked}>
343
447
  <slot></slot>
@@ -85,6 +85,7 @@ export const getUrl = (
85
85
  controller,
86
86
  body,
87
87
  json,
88
+ url: response.url,
88
89
  headers: response.headers,
89
90
  status: response.status,
90
91
  });
@@ -1,5 +1,5 @@
1
1
  // for cache busting we dynamically generate a fingerprint, use yarn svg to update
2
- export const SVG_FINGERPRINT = '54daaf6fa5347c8e5a331b59d415d2bd';
2
+ export const SVG_FINGERPRINT = '5d22cc3a5fb06da9ea0a632539b39983';
3
3
 
4
4
  // only icons below are included in the sprite sheet
5
5
  export enum Icon {
@@ -116,6 +116,7 @@ export enum Icon {
116
116
  missing = 'maximize-02',
117
117
  missed_call = 'phone-x',
118
118
  new = 'plus',
119
+ next_schedule = 'alarm-clock',
119
120
  notification = 'bell-01',
120
121
  org_active = 'credit-card-02',
121
122
  org_anonymous = 'glasses-01',