@nyaruka/temba-components 0.156.6 → 0.156.8
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.
- package/CHANGELOG.md +17 -0
- package/dist/temba-components.js +817 -889
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/flow/DragManager.ts +1239 -0
- package/src/flow/Editor.ts +1643 -4148
- package/src/flow/IssuesWindow.ts +73 -0
- package/src/flow/RevisionsWindow.ts +274 -0
- package/src/flow/ZoomManager.ts +544 -0
- package/src/form/select/Select.ts +27 -0
- package/src/interfaces.ts +8 -1
- package/temba-modules.ts +4 -0
- package/run.sh +0 -19
- package/setup-worktree.sh +0 -53
|
@@ -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
|
+
}
|