@nyaruka/temba-components 0.102.2 → 0.104.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.
Files changed (109) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/temba-components.js +172 -70
  3. package/dist/temba-components.js.map +1 -1
  4. package/out-tsc/src/contacts/ContactChat.js +22 -8
  5. package/out-tsc/src/contacts/ContactChat.js.map +1 -1
  6. package/out-tsc/src/contacts/ContactFields.js +5 -0
  7. package/out-tsc/src/contacts/ContactFields.js.map +1 -1
  8. package/out-tsc/src/contacts/ContactNotepad.js +129 -0
  9. package/out-tsc/src/contacts/ContactNotepad.js.map +1 -0
  10. package/out-tsc/src/contacts/ContactPending.js +7 -2
  11. package/out-tsc/src/contacts/ContactPending.js.map +1 -1
  12. package/out-tsc/src/contacts/ContactStoreElement.js +11 -2
  13. package/out-tsc/src/contacts/ContactStoreElement.js.map +1 -1
  14. package/out-tsc/src/contacts/ContactTickets.js +7 -2
  15. package/out-tsc/src/contacts/ContactTickets.js.map +1 -1
  16. package/out-tsc/src/contacts/events.js.map +1 -1
  17. package/out-tsc/src/contacts/helpers.js +3 -0
  18. package/out-tsc/src/contacts/helpers.js.map +1 -1
  19. package/out-tsc/src/fields/FieldManager.js +2 -2
  20. package/out-tsc/src/fields/FieldManager.js.map +1 -1
  21. package/out-tsc/src/flow/FlowStoreElement.js +2 -2
  22. package/out-tsc/src/flow/FlowStoreElement.js.map +1 -1
  23. package/out-tsc/src/interfaces.js +1 -0
  24. package/out-tsc/src/interfaces.js.map +1 -1
  25. package/out-tsc/src/list/TembaMenu.js +10 -0
  26. package/out-tsc/src/list/TembaMenu.js.map +1 -1
  27. package/out-tsc/src/store/{StoreElement.js → EndpointMonitorElement.js} +5 -5
  28. package/out-tsc/src/store/EndpointMonitorElement.js.map +1 -0
  29. package/out-tsc/src/store/Store.js +22 -0
  30. package/out-tsc/src/store/Store.js.map +1 -1
  31. package/out-tsc/src/store/StoreMonitorElement.js +22 -0
  32. package/out-tsc/src/store/StoreMonitorElement.js.map +1 -1
  33. package/out-tsc/src/tabpane/Tab.js +33 -0
  34. package/out-tsc/src/tabpane/Tab.js.map +1 -1
  35. package/out-tsc/src/tabpane/TabPane.js +53 -15
  36. package/out-tsc/src/tabpane/TabPane.js.map +1 -1
  37. package/out-tsc/src/vectoricon/index.js +1 -0
  38. package/out-tsc/src/vectoricon/index.js.map +1 -1
  39. package/out-tsc/temba-modules.js +2 -0
  40. package/out-tsc/temba-modules.js.map +1 -1
  41. package/package.json +1 -1
  42. package/screenshots/truth/compose/attachments-and-send-button.png +0 -0
  43. package/screenshots/truth/compose/attachments-no-send-button.png +0 -0
  44. package/screenshots/truth/compose/attachments-with-all-files-and-click-send.png +0 -0
  45. package/screenshots/truth/compose/attachments-with-all-files.png +0 -0
  46. package/screenshots/truth/compose/attachments-with-failure-files.png +0 -0
  47. package/screenshots/truth/compose/attachments-with-success-files-and-click-send.png +0 -0
  48. package/screenshots/truth/compose/attachments-with-success-files.png +0 -0
  49. package/screenshots/truth/compose/chatbox-attachments-counter-and-send-button.png +0 -0
  50. package/screenshots/truth/compose/chatbox-attachments-counter-no-send-button.png +0 -0
  51. package/screenshots/truth/compose/chatbox-attachments-no-counter-and-send-button.png +0 -0
  52. package/screenshots/truth/compose/chatbox-attachments-no-counter-no-send-button.png +0 -0
  53. package/screenshots/truth/compose/chatbox-counter-and-send-button.png +0 -0
  54. package/screenshots/truth/compose/chatbox-counter-no-send-button.png +0 -0
  55. package/screenshots/truth/compose/chatbox-no-counter-and-send-button.png +0 -0
  56. package/screenshots/truth/compose/chatbox-no-counter-no-send-button.png +0 -0
  57. package/screenshots/truth/compose/chatbox-no-text-attachments-with-all-files-and-click-send.png +0 -0
  58. package/screenshots/truth/compose/chatbox-no-text-attachments-with-all-files.png +0 -0
  59. package/screenshots/truth/compose/chatbox-no-text-attachments-with-failure-files.png +0 -0
  60. package/screenshots/truth/compose/chatbox-no-text-attachments-with-success-files-and-click-send.png +0 -0
  61. package/screenshots/truth/compose/chatbox-no-text-attachments-with-success-files.png +0 -0
  62. package/screenshots/truth/compose/chatbox-with-text-and-click-send.png +0 -0
  63. package/screenshots/truth/compose/chatbox-with-text-and-hit-enter.png +0 -0
  64. package/screenshots/truth/compose/chatbox-with-text-and-spaces.png +0 -0
  65. package/screenshots/truth/compose/chatbox-with-text-and-url.png +0 -0
  66. package/screenshots/truth/compose/chatbox-with-text-attachments-no-files-and-click-send.png +0 -0
  67. package/screenshots/truth/compose/chatbox-with-text-attachments-no-files-and-hit-enter.png +0 -0
  68. package/screenshots/truth/compose/chatbox-with-text-attachments-no-files.png +0 -0
  69. package/screenshots/truth/compose/chatbox-with-text-attachments-with-all-files-and-click-send.png +0 -0
  70. package/screenshots/truth/compose/chatbox-with-text-attachments-with-all-files-and-hit-enter.png +0 -0
  71. package/screenshots/truth/compose/chatbox-with-text-attachments-with-all-files.png +0 -0
  72. package/screenshots/truth/compose/chatbox-with-text-attachments-with-failure-files.png +0 -0
  73. package/screenshots/truth/compose/chatbox-with-text-attachments-with-success-files-and-click-send.png +0 -0
  74. package/screenshots/truth/compose/chatbox-with-text-attachments-with-success-files-and-hit-enter.png +0 -0
  75. package/screenshots/truth/compose/chatbox-with-text-attachments-with-success-files.png +0 -0
  76. package/screenshots/truth/compose/chatbox-with-text-no-spaces.png +0 -0
  77. package/screenshots/truth/compose/chatbox-with-text.png +0 -0
  78. package/screenshots/truth/contacts/compose-attachments-no-text-failure.png +0 -0
  79. package/screenshots/truth/contacts/compose-attachments-no-text-success.png +0 -0
  80. package/screenshots/truth/contacts/compose-text-and-attachments-failure-attachments.png +0 -0
  81. package/screenshots/truth/contacts/compose-text-and-attachments-failure-generic.png +0 -0
  82. package/screenshots/truth/contacts/compose-text-and-attachments-failure-text-and-attachments.png +0 -0
  83. package/screenshots/truth/contacts/compose-text-and-attachments-failure-text.png +0 -0
  84. package/screenshots/truth/contacts/compose-text-and-attachments-success.png +0 -0
  85. package/screenshots/truth/contacts/compose-text-no-attachments-failure.png +0 -0
  86. package/screenshots/truth/contacts/compose-text-no-attachments-success.png +0 -0
  87. package/screenshots/truth/contacts/contact-active-default.png +0 -0
  88. package/screenshots/truth/contacts/contact-active-show-chatbox.png +0 -0
  89. package/src/contacts/ContactChat.ts +21 -10
  90. package/src/contacts/ContactFields.ts +7 -0
  91. package/src/contacts/ContactNotepad.ts +133 -0
  92. package/src/contacts/ContactPending.ts +8 -2
  93. package/src/contacts/ContactStoreElement.ts +12 -2
  94. package/src/contacts/ContactTickets.ts +9 -2
  95. package/src/contacts/events.ts +0 -1
  96. package/src/contacts/helpers.ts +5 -1
  97. package/src/fields/FieldManager.ts +2 -2
  98. package/src/flow/FlowStoreElement.ts +2 -2
  99. package/src/interfaces.ts +13 -1
  100. package/src/list/TembaMenu.ts +12 -0
  101. package/src/store/{StoreElement.ts → EndpointMonitorElement.ts} +1 -4
  102. package/src/store/Store.ts +31 -0
  103. package/src/store/StoreMonitorElement.ts +24 -0
  104. package/src/tabpane/Tab.ts +29 -0
  105. package/src/tabpane/TabPane.ts +54 -15
  106. package/src/vectoricon/index.ts +1 -0
  107. package/temba-modules.ts +2 -0
  108. package/out-tsc/src/store/StoreElement.js.map +0 -1
  109. package/screenshots/truth/contacts/history.png +0 -0
@@ -4,6 +4,7 @@ import { getClasses, postJSON } from '../utils';
4
4
  import { ContactFieldEditor } from './ContactFieldEditor';
5
5
  import { ContactStoreElement } from './ContactStoreElement';
6
6
  import { Checkbox } from '../checkbox/Checkbox';
7
+ import { CustomEventType } from '../interfaces';
7
8
 
8
9
  const MIN_FOR_FILTER = 10;
9
10
 
@@ -104,12 +105,18 @@ export class ContactFields extends ContactStoreElement {
104
105
  if (Object.keys(this.data.fields).length <= MIN_FOR_FILTER) {
105
106
  this.showAll = true;
106
107
  }
108
+
109
+ this.fireCustomEvent(CustomEventType.DetailsChanged, {
110
+ count: Object.values(this.data.fields).filter((value) => !!value).length
111
+ });
107
112
  }
108
113
  }
109
114
 
110
115
  public handleFieldChanged(evt: InputEvent) {
111
116
  const field = evt.currentTarget as ContactFieldEditor;
112
117
  const value = field.value;
118
+
119
+ // TODO: Use contact.postChanges instead of postJSON
113
120
  postJSON('/api/v2/contacts.json?uuid=' + this.data.uuid, {
114
121
  fields: { [field.key]: value }
115
122
  })
@@ -0,0 +1,133 @@
1
+ import { css, html, PropertyValueMap, TemplateResult } from 'lit';
2
+ import { property } from 'lit/decorators.js';
3
+ import { ContactStoreElement } from './ContactStoreElement';
4
+ import { getDisplayName } from './helpers';
5
+ import { ContactNote, CustomEventType } from '../interfaces';
6
+
7
+ export class ContactNotepad extends ContactStoreElement {
8
+ @property({ type: Object, attribute: false })
9
+ note: ContactNote;
10
+
11
+ @property({ type: String })
12
+ dirtyMessage =
13
+ 'You have unsaved changes to the contact notepad. Are you sure you want to contiunue?';
14
+
15
+ static get styles() {
16
+ return css`
17
+ :host {
18
+ height: 100%;
19
+ display: flex;
20
+ }
21
+
22
+ .wrapper {
23
+ flex-grow: 1;
24
+ --color-widget-bg: transparent;
25
+ --color-widget-bg-focused: transparent;
26
+ outline: none;
27
+ border-radius: var(--curvature);
28
+ display: flex;
29
+ flex-direction: column;
30
+ }
31
+
32
+ .notepad {
33
+ flex-grow: 1;
34
+ padding: 1.25rem;
35
+ overflow-y: auto;
36
+ background: transparent;
37
+ border: none;
38
+ outline: none;
39
+ resize: none;
40
+ font-family: monospace;
41
+ }
42
+
43
+ .notepad:focus {
44
+ outline: none;
45
+ }
46
+
47
+ .toolbar {
48
+ background: rgba(0, 0, 0, 0.03);
49
+ padding: 0.25em 0.5em;
50
+ display: flex;
51
+ min-height: 2em;
52
+ align-items: center;
53
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
54
+ }
55
+
56
+ .toolbar temba-button {
57
+ margin-right: 0.5em;
58
+ }
59
+
60
+ .updated {
61
+ font-size: 0.9em;
62
+ flex-grow: 1;
63
+ margin-left: 0.5em;
64
+ }
65
+ `;
66
+ }
67
+
68
+ private handleChange() {
69
+ this.markDirty();
70
+ }
71
+
72
+ private submitChanges() {
73
+ const notepad = this.shadowRoot.querySelector(
74
+ '.notepad'
75
+ ) as HTMLInputElement;
76
+ const note = notepad.value;
77
+ this.postChanges({ note }).then(() => {
78
+ this.markClean();
79
+ });
80
+ }
81
+
82
+ protected updated(
83
+ changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
84
+ ): void {
85
+ super.updated(changes);
86
+
87
+ if (changes.has('data')) {
88
+ this.note =
89
+ this.data?.notes.length > 0
90
+ ? { ...this.data.notes[this.data.notes.length - 1] }
91
+ : null;
92
+ this.fireCustomEvent(CustomEventType.DetailsChanged, {
93
+ count: this.note && this.note.text.length > 0 ? 1 : 0
94
+ });
95
+ this.markClean();
96
+ }
97
+ }
98
+
99
+ public render(): TemplateResult {
100
+ if (this.data) {
101
+ return html`
102
+ <div class="wrapper">
103
+ <!-- prettier-ignore -->
104
+ <textarea class="notepad" @input=${this.handleChange} .value=${this
105
+ .note
106
+ ? this.note.text
107
+ : ''}></textarea>
108
+ <div class="toolbar">
109
+ <div class="updated">
110
+ ${this.note
111
+ ? html`Last updated by ${getDisplayName(this.note.created_by)}
112
+ <temba-date
113
+ value="${this.note.created_on}"
114
+ display="duration"
115
+ ></temba-date>`
116
+ : null}
117
+ </div>
118
+ ${this.dirty
119
+ ? html`
120
+ <temba-button
121
+ name="Save"
122
+ small
123
+ @click=${this.submitChanges}
124
+ ></temba-button>
125
+ `
126
+ : null}
127
+ </div>
128
+ </div>
129
+ `;
130
+ }
131
+ return super.render();
132
+ }
133
+ }
@@ -5,7 +5,7 @@ import {
5
5
  ScheduledEvent,
6
6
  ScheduledEventType
7
7
  } from '../interfaces';
8
- import { StoreElement } from '../store/StoreElement';
8
+ import { EndpointMonitorElement } from '../store/EndpointMonitorElement';
9
9
  import { Icon } from '../vectoricon';
10
10
 
11
11
  const ICONS = {
@@ -14,7 +14,7 @@ const ICONS = {
14
14
  [ScheduledEventType.ScheduledTrigger]: Icon.trigger
15
15
  };
16
16
 
17
- export class ContactPending extends StoreElement {
17
+ export class ContactPending extends EndpointMonitorElement {
18
18
  @property({ type: String })
19
19
  contact: string;
20
20
 
@@ -160,6 +160,12 @@ export class ContactPending extends StoreElement {
160
160
  this.url = null;
161
161
  }
162
162
  }
163
+
164
+ if (changes.has('data')) {
165
+ this.fireCustomEvent(CustomEventType.DetailsChanged, {
166
+ count: this.data.length
167
+ });
168
+ }
163
169
  }
164
170
 
165
171
  public handleEventClicked(event: ScheduledEvent) {
@@ -1,9 +1,9 @@
1
1
  import { PropertyValueMap } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
3
  import { Contact, Group } from '../interfaces';
4
- import { StoreElement } from '../store/StoreElement';
4
+ import { EndpointMonitorElement } from '../store/EndpointMonitorElement';
5
5
 
6
- export class ContactStoreElement extends StoreElement {
6
+ export class ContactStoreElement extends EndpointMonitorElement {
7
7
  @property({ type: String })
8
8
  contact: string;
9
9
 
@@ -39,6 +39,16 @@ export class ContactStoreElement extends StoreElement {
39
39
  return null;
40
40
  }
41
41
 
42
+ public postChanges(payload: any) {
43
+ // clear our cache so we don't have any races
44
+ this.store.removeFromCache(`${this.endpoint}${this.contact}`);
45
+ return this.store
46
+ .postJSON(`${this.endpoint}${this.contact}`, payload)
47
+ .then((response) => {
48
+ this.setContact(response.json);
49
+ });
50
+ }
51
+
42
52
  public setContact(contact: any) {
43
53
  // make sure contact data is properly prepped
44
54
  this.data = this.prepareData([contact]);
@@ -1,14 +1,14 @@
1
1
  import { css, html, PropertyValueMap, TemplateResult } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
3
  import { CustomEventType, Ticket, TicketStatus } from '../interfaces';
4
- import { StoreElement } from '../store/StoreElement';
4
+ import { EndpointMonitorElement } from '../store/EndpointMonitorElement';
5
5
  import { getClasses, postJSON, stopEvent } from '../utils';
6
6
  import { Icon } from '../vectoricon';
7
7
 
8
8
  const dropdownUserScale = 0.7;
9
9
  const inlineUserScale = 0.8;
10
10
 
11
- export class ContactTickets extends StoreElement {
11
+ export class ContactTickets extends EndpointMonitorElement {
12
12
  @property({ type: String })
13
13
  agent: string;
14
14
 
@@ -230,6 +230,13 @@ export class ContactTickets extends StoreElement {
230
230
  changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
231
231
  ): void {
232
232
  super.updated(changes);
233
+
234
+ if (changes.has('data')) {
235
+ this.fireCustomEvent(CustomEventType.DetailsChanged, {
236
+ count: this.data.length
237
+ });
238
+ }
239
+
233
240
  if (changes.has('contact') || changes.has('ticket')) {
234
241
  if (this.contact) {
235
242
  this.url = `/api/v2/tickets.json?contact=${this.contact}${
@@ -73,7 +73,6 @@ export interface TicketEvent extends ContactEvent {
73
73
  assignee?: User;
74
74
  ticket: {
75
75
  uuid: string;
76
- body: string;
77
76
  topic?: ObjectReference;
78
77
  closed_on?: string;
79
78
  opened_on?: string;
@@ -1,4 +1,4 @@
1
- import { Contact, User } from '../interfaces';
1
+ import { Contact, NamedUser, User } from '../interfaces';
2
2
  import { fetchResults, getUrl, postUrl, WebResponse } from '../utils';
3
3
  import { ContactHistoryPage } from './events';
4
4
 
@@ -80,6 +80,10 @@ export const getDisplayName = (user: User) => {
80
80
  return 'Somebody';
81
81
  }
82
82
 
83
+ if ((user as NamedUser).name) {
84
+ return (user as NamedUser).name;
85
+ }
86
+
83
87
  if (user.first_name && user.last_name) {
84
88
  return `${user.first_name} ${user.last_name}`;
85
89
  }
@@ -3,7 +3,7 @@ import { property } from 'lit/decorators.js';
3
3
  import { ContactField, CustomEventType } from '../interfaces';
4
4
 
5
5
  import { SortableList } from '../list/SortableList';
6
- import { StoreElement } from '../store/StoreElement';
6
+ import { EndpointMonitorElement } from '../store/EndpointMonitorElement';
7
7
  import { postJSON } from '../utils';
8
8
 
9
9
  const TYPE_NAMES = {
@@ -31,7 +31,7 @@ const matches = (field: ContactField, query: string): boolean => {
31
31
  return false;
32
32
  };
33
33
 
34
- export class FieldManager extends StoreElement {
34
+ export class FieldManager extends EndpointMonitorElement {
35
35
  static get styles() {
36
36
  return css`
37
37
  :host {
@@ -1,9 +1,9 @@
1
1
  import { html, PropertyValueMap, TemplateResult } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
3
  import { FlowDetails } from '../interfaces';
4
- import { StoreElement } from '../store/StoreElement';
4
+ import { EndpointMonitorElement } from '../store/EndpointMonitorElement';
5
5
 
6
- export class FlowStoreElement extends StoreElement {
6
+ export class FlowStoreElement extends EndpointMonitorElement {
7
7
  @property({ type: String })
8
8
  flow: string;
9
9
 
package/src/interfaces.ts CHANGED
@@ -48,6 +48,10 @@ export interface ScheduledEvent {
48
48
  message?: string;
49
49
  }
50
50
 
51
+ export interface NamedUser extends User {
52
+ name: string;
53
+ }
54
+
51
55
  export interface User {
52
56
  id?: number;
53
57
  first_name?: string;
@@ -136,6 +140,12 @@ export interface Group {
136
140
  is_dynamic?: boolean;
137
141
  }
138
142
 
143
+ export interface ContactNote {
144
+ text: string;
145
+ created_on: string;
146
+ created_by: NamedUser;
147
+ }
148
+
139
149
  export interface ContactTicket {
140
150
  name: string;
141
151
  uuid: string;
@@ -158,6 +168,7 @@ export interface Contact {
158
168
  language?: string;
159
169
  fields: { [key: string]: string };
160
170
  groups: Group[];
171
+ notes: ContactNote[];
161
172
  modified_on: string;
162
173
  created_on: string;
163
174
  last_seen_on: string;
@@ -260,5 +271,6 @@ export enum CustomEventType {
260
271
  OrderChanged = 'temba-order-changed',
261
272
  DragStart = 'temba-drag-start',
262
273
  DragStop = 'temba-drag-stop',
263
- Resized = 'temba-resized'
274
+ Resized = 'temba-resized',
275
+ DetailsChanged = 'temba-details-changed'
264
276
  }
@@ -7,6 +7,7 @@ import { Icon } from '../vectoricon';
7
7
  import { Dropdown } from '../dropdown/Dropdown';
8
8
  import { NotificationList } from './NotificationList';
9
9
  import { ResizeElement } from '../ResizeElement';
10
+ import { Store } from '../store/Store';
10
11
  export interface MenuItem {
11
12
  id?: string;
12
13
  vanity_id?: string;
@@ -820,6 +821,17 @@ export class TembaMenu extends ResizeElement {
820
821
  menuItem: MenuItem,
821
822
  parent: MenuItem = null
822
823
  ) {
824
+ const store = document.querySelector('temba-store') as Store;
825
+ if (store) {
826
+ const unsavedMessage = store.getDirtyMessage();
827
+ if (unsavedMessage) {
828
+ if (!confirm(unsavedMessage)) {
829
+ return;
830
+ }
831
+ }
832
+ store.cleanAll();
833
+ }
834
+
823
835
  if (parent && parent.popup) {
824
836
  const dropdown = this.shadowRoot.querySelector(
825
837
  'temba-dropdown'
@@ -2,14 +2,13 @@ import { PropertyValueMap } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
3
  import { CustomEventType } from '../interfaces';
4
4
 
5
- import { Store } from './Store';
6
5
  import { StoreMonitorElement } from './StoreMonitorElement';
7
6
 
8
7
  /**
9
8
  * StoreElement is a listener for a given endpoint that re-renders
10
9
  * when the underlying store element changes
11
10
  */
12
- export class StoreElement extends StoreMonitorElement {
11
+ export class EndpointMonitorElement extends StoreMonitorElement {
13
12
  @property({ type: String })
14
13
  url: string;
15
14
 
@@ -19,8 +18,6 @@ export class StoreElement extends StoreMonitorElement {
19
18
  @property({ type: Object, attribute: false })
20
19
  data: any;
21
20
 
22
- store: Store;
23
-
24
21
  prepareData(data: any): any {
25
22
  return data;
26
23
  }
@@ -26,6 +26,7 @@ import { DateTime } from 'luxon';
26
26
  import { css, html } from 'lit';
27
27
  import { configureLocalization } from '@lit/localize';
28
28
  import { sourceLocale, targetLocales } from '../locales/locale-codes';
29
+ import { StoreMonitorElement } from './StoreMonitorElement';
29
30
 
30
31
  const { setLocale } = configureLocalization({
31
32
  sourceLocale,
@@ -105,6 +106,32 @@ export class Store extends RapidElement {
105
106
  // http promise to monitor for completeness
106
107
  public initialHttpComplete: Promise<void | WebResponse[]>;
107
108
 
109
+ private dirtyElements: StoreMonitorElement[] = [];
110
+
111
+ public markDirty(ele: StoreMonitorElement) {
112
+ if (!this.dirtyElements.includes(ele)) {
113
+ this.dirtyElements.push(ele);
114
+ }
115
+ }
116
+
117
+ public cleanAll() {
118
+ this.dirtyElements.forEach((ele) => ele.markClean());
119
+ this.dirtyElements = [];
120
+ }
121
+
122
+ public markClean(ele: StoreMonitorElement) {
123
+ this.dirtyElements = this.dirtyElements.filter((el) => el !== ele);
124
+ }
125
+
126
+ public getDirtyMessage() {
127
+ if (this.dirtyElements.length > 0) {
128
+ return (
129
+ this.dirtyElements[0].dirtyMessage ||
130
+ 'You have unsaved changes, are you sure you want to continue?'
131
+ );
132
+ }
133
+ }
134
+
108
135
  private cache: any;
109
136
  public getLocale() {
110
137
  return this.locale[0];
@@ -436,6 +463,10 @@ export class Store extends RapidElement {
436
463
  this.fireCustomEvent(CustomEventType.StoreUpdated, { url, data });
437
464
  }
438
465
 
466
+ public removeFromCache(url: string) {
467
+ this.cache.delete(url);
468
+ }
469
+
439
470
  public makeRequest(
440
471
  url: string,
441
472
  options?: { force?: boolean; prepareData?: (data: any) => any }
@@ -14,8 +14,30 @@ export class StoreMonitorElement extends RapidElement {
14
14
  @property({ type: Boolean })
15
15
  showLoading = false;
16
16
 
17
+ @property({ type: Boolean })
18
+ dirty = false;
19
+
20
+ @property({ type: String })
21
+ dirtyMessage: string;
22
+
17
23
  store: Store;
18
24
 
25
+ markDirty() {
26
+ this.dirty = true;
27
+ this.store.markDirty(this);
28
+ this.fireCustomEvent(CustomEventType.DetailsChanged, {
29
+ dirty: true
30
+ });
31
+ }
32
+
33
+ markClean() {
34
+ this.dirty = false;
35
+ this.store.markClean(this);
36
+ this.fireCustomEvent(CustomEventType.DetailsChanged, {
37
+ dirty: false
38
+ });
39
+ }
40
+
19
41
  private handleStoreUpdated(event: CustomEvent) {
20
42
  this.store.initialHttpComplete.then(() => {
21
43
  this.storeUpdated(event);
@@ -50,6 +72,8 @@ export class StoreMonitorElement extends RapidElement {
50
72
  CustomEventType.StoreUpdated,
51
73
  this.handleStoreUpdated
52
74
  );
75
+
76
+ this.store.markClean(this);
53
77
  }
54
78
  }
55
79
 
@@ -31,6 +31,12 @@ export class Tab extends RapidElement {
31
31
  @property({ type: String })
32
32
  selectionBackground: string;
33
33
 
34
+ @property({ type: String })
35
+ borderColor: string = 'var(--color-widget-border)';
36
+
37
+ @property({ type: String })
38
+ activityColor: string = `var(--color-link-primary)`;
39
+
34
40
  @property({ type: Boolean })
35
41
  selected = false;
36
42
 
@@ -43,12 +49,22 @@ export class Tab extends RapidElement {
43
49
  @property({ type: Boolean })
44
50
  hidden = false;
45
51
 
52
+ @property({ type: Boolean })
53
+ hideEmpty = false;
54
+
55
+ // show just that there is activity instead of count
56
+ @property({ type: Boolean })
57
+ activity = false;
58
+
46
59
  @property({ type: Number })
47
60
  count = 0;
48
61
 
49
62
  @property({ type: Boolean })
50
63
  checked = false;
51
64
 
65
+ @property({ type: Boolean })
66
+ dirty = false;
67
+
52
68
  public updated(
53
69
  changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
54
70
  ): void {
@@ -62,8 +78,21 @@ export class Tab extends RapidElement {
62
78
  return this.count > 0;
63
79
  }
64
80
 
81
+ public handleDetailsChanged(event: CustomEvent) {
82
+ if ('dirty' in event.detail) {
83
+ this.dirty = event.detail.dirty;
84
+ }
85
+ if ('count' in event.detail) {
86
+ this.count = event.detail.count;
87
+ if (this.hideEmpty) {
88
+ this.hidden = this.count === 0;
89
+ }
90
+ }
91
+ }
92
+
65
93
  public render(): TemplateResult {
66
94
  return html`<slot
95
+ @temba-details-changed=${this.handleDetailsChanged}
67
96
  class="${getClasses({ selected: this.selected })}"
68
97
  ></slot> `;
69
98
  }