@nyaruka/temba-components 0.156.6 → 0.156.7

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.
@@ -0,0 +1,73 @@
1
+ import { html, TemplateResult } from 'lit-html';
2
+ import { css } from 'lit';
3
+ import { property } from 'lit/decorators.js';
4
+ import { RapidElement } from '../RapidElement';
5
+ import { CustomEventType } from '../interfaces';
6
+ import { FlowIssue } from '../store/AppState';
7
+ import { formatIssueMessage } from './utils';
8
+
9
+ export class IssuesWindow extends RapidElement {
10
+ static get styles() {
11
+ return css`
12
+ :host {
13
+ display: contents;
14
+ }
15
+
16
+ .issue-list-item {
17
+ display: flex;
18
+ align-items: center;
19
+ gap: 8px;
20
+ padding: 8px;
21
+ border-radius: 4px;
22
+ cursor: pointer;
23
+ }
24
+
25
+ .issue-list-item:hover {
26
+ background: #fff1f0;
27
+ }
28
+ `;
29
+ }
30
+
31
+ @property({ type: Array })
32
+ issues: FlowIssue[] = [];
33
+
34
+ @property({ type: Boolean })
35
+ hidden = true;
36
+
37
+ private handleItemClick(issue: FlowIssue): void {
38
+ this.fireCustomEvent(CustomEventType.IssueSelected, { issue });
39
+ }
40
+
41
+ public render(): TemplateResult | string {
42
+ if (!this.issues?.length) return '';
43
+ return html`
44
+ <temba-floating-window
45
+ id="issues-window"
46
+ name="issues"
47
+ header="Flow Issues"
48
+ icon="alert_warning"
49
+ .width=${360}
50
+ .maxHeight=${600}
51
+ .top=${120}
52
+ color="tomato"
53
+ .hidden=${this.hidden}
54
+ @temba-dialog-hidden=${() =>
55
+ this.fireCustomEvent(CustomEventType.IssuesClosed)}
56
+ >
57
+ <div style="display:flex; flex-direction:column; gap:2px;">
58
+ ${this.issues.map(
59
+ (issue) => html`
60
+ <div
61
+ class="issue-list-item"
62
+ @click=${() => this.handleItemClick(issue)}
63
+ >
64
+ <temba-icon name="alert_warning" size="1.2"></temba-icon>
65
+ <span>${formatIssueMessage(issue)}</span>
66
+ </div>
67
+ `
68
+ )}
69
+ </div>
70
+ </temba-floating-window>
71
+ `;
72
+ }
73
+ }
@@ -0,0 +1,274 @@
1
+ import { html, TemplateResult } from 'lit-html';
2
+ import { css, PropertyValues } from 'lit';
3
+ import { property, state } from 'lit/decorators.js';
4
+ import { RapidElement } from '../RapidElement';
5
+ import { CustomEventType } from '../interfaces';
6
+ import { getStore } from '../store/Store';
7
+ import { FlowDefinition } from '../store/flow-definition';
8
+ import { fetchResults } from '../utils';
9
+ import { FLOW_SPEC_VERSION } from '../store/AppState';
10
+
11
+ export interface Revision {
12
+ id: number;
13
+ user: {
14
+ id: number;
15
+ username: string;
16
+ first_name: string;
17
+ last_name: string;
18
+ name?: string;
19
+ };
20
+ created_on: string;
21
+ comment?: string;
22
+ }
23
+
24
+ export class RevisionsWindow extends RapidElement {
25
+ static get styles() {
26
+ return css`
27
+ :host {
28
+ display: contents;
29
+ }
30
+ `;
31
+ }
32
+
33
+ @property({ type: String })
34
+ flow = '';
35
+
36
+ @property({ type: Boolean })
37
+ hidden = true;
38
+
39
+ @property({ type: Boolean })
40
+ saving = false;
41
+
42
+ @state()
43
+ private revisions: Revision[] = [];
44
+
45
+ @state()
46
+ private viewingRevision: Revision | null = null;
47
+
48
+ @state()
49
+ private isLoading = false;
50
+
51
+ private preRevertState: {
52
+ definition: FlowDefinition;
53
+ dirtyDate: Date | null;
54
+ } | null = null;
55
+ private browseLanguageCode: string | null = null;
56
+
57
+ public get isViewingRevision(): boolean {
58
+ return this.viewingRevision !== null;
59
+ }
60
+
61
+ protected updated(changes: PropertyValues): void {
62
+ super.updated(changes);
63
+ if (
64
+ changes.has('hidden') &&
65
+ !this.hidden &&
66
+ changes.get('hidden') === true
67
+ ) {
68
+ this.fetchRevisions();
69
+ }
70
+ }
71
+
72
+ public close(): void {
73
+ this.resetScroll();
74
+ if (this.viewingRevision) {
75
+ this.cancelRevisionView();
76
+ }
77
+ this.fireCustomEvent(CustomEventType.RevisionsClosed);
78
+ }
79
+
80
+ public render(): TemplateResult {
81
+ return html`
82
+ <temba-floating-window
83
+ id="revisions-window"
84
+ name="revisions"
85
+ header="Revisions"
86
+ icon="revisions"
87
+ .width=${240}
88
+ .maxHeight=${400}
89
+ .top=${120}
90
+ color="rgb(142, 94, 167)"
91
+ .saving=${this.saving}
92
+ .hidden=${this.hidden}
93
+ @temba-dialog-hidden=${() => this.close()}
94
+ >
95
+ <div class="localization-window-content">
96
+ <div
97
+ class="revisions-list"
98
+ style="display:flex; flex-direction:column; gap:8px; overflow-y:auto; padding-bottom:10px;"
99
+ >
100
+ ${this.isLoading && !this.revisions.length
101
+ ? html`<temba-loading></temba-loading>`
102
+ : this.revisions.map((rev) => {
103
+ const isSelected = this.viewingRevision?.id === rev.id;
104
+ return html`
105
+ <div
106
+ class="revision-item ${isSelected ? 'selected' : ''}"
107
+ style="padding:8px; border-radius:4px; cursor:pointer; background:${isSelected
108
+ ? '#f0f6ff'
109
+ : '#f9fafb'}; border:1px solid ${isSelected
110
+ ? '#a4cafe'
111
+ : '#e5e7eb'}; transition: all 0.2s ease;"
112
+ @click=${() => this.handleRevisionClick(rev)}
113
+ >
114
+ <div
115
+ style="display:flex; justify-content:space-between; align-items:center;"
116
+ >
117
+ <div
118
+ class="revision-header"
119
+ style="margin-bottom: 2px;"
120
+ >
121
+ <div
122
+ style="font-weight:600; font-size:13px; color:#111827;"
123
+ >
124
+ <temba-date
125
+ value=${rev.created_on}
126
+ display="duration"
127
+ ></temba-date>
128
+ </div>
129
+ <div style="font-size:11px; color:#6b7280;">
130
+ ${rev.user.name || rev.user.username}
131
+ </div>
132
+ </div>
133
+ ${isSelected
134
+ ? html`<button
135
+ class="revert-button"
136
+ @click=${(e: Event) => {
137
+ e.stopPropagation();
138
+ this.handleRevertClick();
139
+ }}
140
+ >
141
+ Revert
142
+ </button>`
143
+ : html``}
144
+ </div>
145
+
146
+ ${rev.comment
147
+ ? html`<div
148
+ style="font-size:12px; color:#4b5563; margin-top:4px;"
149
+ >
150
+ ${rev.comment}
151
+ </div>`
152
+ : ''}
153
+ </div>
154
+ `;
155
+ })}
156
+ </div>
157
+ </div>
158
+ </temba-floating-window>
159
+ `;
160
+ }
161
+
162
+ // --- Private ---
163
+
164
+ private async fetchRevisions() {
165
+ this.isLoading = true;
166
+ try {
167
+ const results = await fetchResults(
168
+ `/flow/revisions/${this.flow}/?version=${FLOW_SPEC_VERSION}`
169
+ );
170
+ this.revisions = results.slice(1);
171
+ } catch (e) {
172
+ console.error('Error fetching revisions', e);
173
+ } finally {
174
+ this.isLoading = false;
175
+ }
176
+ }
177
+
178
+ private async handleRevisionClick(revision: Revision) {
179
+ if (this.viewingRevision?.id === revision.id) {
180
+ return;
181
+ }
182
+
183
+ const store = getStore().getState();
184
+
185
+ if (!this.viewingRevision) {
186
+ this.preRevertState = {
187
+ definition: store.flowDefinition,
188
+ dirtyDate: store.dirtyDate
189
+ };
190
+ this.browseLanguageCode = store.languageCode;
191
+ }
192
+
193
+ this.viewingRevision = revision;
194
+ this.isLoading = true;
195
+
196
+ this.fireCustomEvent(CustomEventType.RevisionViewed, { revision });
197
+
198
+ try {
199
+ await store.fetchRevision(
200
+ `/flow/revisions/${this.flow}`,
201
+ revision.id.toString()
202
+ );
203
+ if (this.browseLanguageCode) {
204
+ store.setLanguageCode(this.browseLanguageCode);
205
+ }
206
+ } catch (e) {
207
+ console.error('Error fetching revision details', e);
208
+ this.cancelRevisionView();
209
+ } finally {
210
+ this.isLoading = false;
211
+ }
212
+ }
213
+
214
+ private cancelRevisionView() {
215
+ const store = getStore().getState();
216
+ const preservedLanguageCode =
217
+ this.browseLanguageCode || store.languageCode;
218
+
219
+ if (this.preRevertState) {
220
+ const currentInfo = store.flowInfo;
221
+ store.setFlowContents({
222
+ definition: this.preRevertState.definition,
223
+ info: currentInfo
224
+ });
225
+ if (this.preRevertState.dirtyDate) {
226
+ store.setDirtyDate(this.preRevertState.dirtyDate);
227
+ }
228
+ if (preservedLanguageCode) {
229
+ store.setLanguageCode(preservedLanguageCode);
230
+ }
231
+ } else {
232
+ store.fetchRevision(`/flow/revisions/${this.flow}`).finally(() => {
233
+ if (preservedLanguageCode) {
234
+ store.setLanguageCode(preservedLanguageCode);
235
+ }
236
+ });
237
+ }
238
+
239
+ this.viewingRevision = null;
240
+ this.preRevertState = null;
241
+ this.browseLanguageCode = null;
242
+ this.fireCustomEvent(CustomEventType.RevisionCancelled);
243
+ }
244
+
245
+ private async handleRevertClick() {
246
+ if (!this.viewingRevision || !this.preRevertState) return;
247
+
248
+ const store = getStore().getState();
249
+ const preservedLanguageCode =
250
+ this.browseLanguageCode || store.languageCode;
251
+
252
+ const definitionToSave = {
253
+ ...store.flowDefinition,
254
+ revision: this.preRevertState.definition.revision
255
+ };
256
+
257
+ this.viewingRevision = null;
258
+ this.preRevertState = null;
259
+ this.browseLanguageCode = null;
260
+
261
+ this.fireCustomEvent(CustomEventType.RevisionReverted, {
262
+ definition: definitionToSave,
263
+ languageCode: preservedLanguageCode
264
+ });
265
+ }
266
+
267
+ private resetScroll() {
268
+ const win = this.shadowRoot?.querySelector('temba-floating-window');
269
+ const list = win?.shadowRoot?.querySelector('.body');
270
+ if (list) {
271
+ list.scrollTop = 0;
272
+ }
273
+ }
274
+ }