@nyaruka/temba-components 0.137.0 → 0.138.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.
- package/CHANGELOG.md +9 -0
- package/dist/temba-components.js +392 -258
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +2 -2
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +45 -24
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +305 -16
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +110 -64
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +11 -4
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/src/store/AppState.js +12 -2
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/test/temba-flow-editor-node.test.js +2 -1
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor-revisions.test.js +106 -0
- package/out-tsc/test/temba-flow-editor-revisions.test.js.map +1 -0
- package/out-tsc/test/temba-flow-editor.test.js +14 -10
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js +7 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +6 -0
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/package.json +1 -1
- package/src/display/FloatingTab.ts +2 -2
- package/src/flow/CanvasNode.ts +54 -29
- package/src/flow/Editor.ts +357 -17
- package/src/flow/Plumber.ts +123 -69
- package/src/simulator/Simulator.ts +11 -5
- package/src/store/AppState.ts +13 -2
- package/test/temba-flow-editor-node.test.ts +2 -1
- package/test/temba-flow-editor-revisions.test.ts +134 -0
- package/test/temba-flow-editor.test.ts +16 -10
- package/test/temba-flow-plumber-connections.test.ts +7 -1
- package/test/temba-flow-plumber.test.ts +6 -0
package/src/flow/Plumber.ts
CHANGED
|
@@ -89,50 +89,54 @@ export class Plumber {
|
|
|
89
89
|
private showContactsTimeout: number | null = null;
|
|
90
90
|
private editor: any;
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
this.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
connectionOverlays: OVERLAYS_DEFAULTS
|
|
103
|
-
});
|
|
92
|
+
initializeJSPlumb(canvas: HTMLElement) {
|
|
93
|
+
this.jsPlumb = newInstance({
|
|
94
|
+
container: canvas,
|
|
95
|
+
connectionsDetachable: true,
|
|
96
|
+
endpointStyle: {
|
|
97
|
+
fill: 'green'
|
|
98
|
+
},
|
|
99
|
+
connector: CONNECTOR_DEFAULTS,
|
|
100
|
+
connectionOverlays: OVERLAYS_DEFAULTS
|
|
101
|
+
});
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
// Bind to connection events
|
|
104
|
+
this.jsPlumb.bind(EVENT_CONNECTION, (info) => {
|
|
105
|
+
this.connectionDragging = false;
|
|
106
|
+
this.notifyListeners(EVENT_CONNECTION, info);
|
|
107
|
+
});
|
|
110
108
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
// Bind to connection drag events
|
|
110
|
+
this.jsPlumb.bind(EVENT_CONNECTION_DRAG, (info) => {
|
|
111
|
+
this.connectionDragging = true;
|
|
112
|
+
this.notifyListeners(EVENT_CONNECTION_DRAG, info);
|
|
113
|
+
});
|
|
116
114
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
this.jsPlumb.bind(EVENT_CONNECTION_ABORT, (info) => {
|
|
116
|
+
this.connectionDragging = false;
|
|
117
|
+
this.notifyListeners(EVENT_CONNECTION_ABORT, info);
|
|
118
|
+
});
|
|
121
119
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
this.jsPlumb.bind(EVENT_CONNECTION_DETACHED, (info) => {
|
|
121
|
+
this.connectionDragging = false;
|
|
122
|
+
this.notifyListeners(EVENT_CONNECTION_DETACHED, info);
|
|
123
|
+
});
|
|
126
124
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
this.jsPlumb.bind(EVENT_REVERT, (info) => {
|
|
126
|
+
this.notifyListeners(EVENT_REVERT, info);
|
|
127
|
+
});
|
|
130
128
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
this.jsPlumb.bind(INTERCEPT_BEFORE_DROP, () => {
|
|
130
|
+
// we always deny automatic connections
|
|
131
|
+
return false;
|
|
132
|
+
});
|
|
133
|
+
this.jsPlumb.bind(INTERCEPT_BEFORE_DETACH, () => {});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
constructor(canvas: HTMLElement, editor: any) {
|
|
137
|
+
this.editor = editor;
|
|
138
|
+
ready(() => {
|
|
139
|
+
this.initializeJSPlumb(canvas);
|
|
136
140
|
});
|
|
137
141
|
}
|
|
138
142
|
|
|
@@ -159,12 +163,14 @@ export class Plumber {
|
|
|
159
163
|
|
|
160
164
|
public makeTarget(uuid: string) {
|
|
161
165
|
const element = document.getElementById(uuid);
|
|
162
|
-
|
|
166
|
+
if (!element) return;
|
|
167
|
+
return this.jsPlumb.addEndpoint(element, TARGET_DEFAULTS);
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
public makeSource(uuid: string) {
|
|
166
171
|
const element = document.getElementById(uuid);
|
|
167
|
-
|
|
172
|
+
if (!element) return;
|
|
173
|
+
return this.jsPlumb.addEndpoint(element, SOURCE_DEFAULTS);
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
// we'll process our pending connections, but we want to debounce this
|
|
@@ -180,27 +186,40 @@ export class Plumber {
|
|
|
180
186
|
this.jsPlumb.batch(() => {
|
|
181
187
|
this.pendingConnections.forEach((connection) => {
|
|
182
188
|
const { scope, fromId, toId } = connection;
|
|
183
|
-
const fromElement = document.getElementById(fromId);
|
|
184
|
-
const toElement = document.getElementById(toId);
|
|
185
|
-
|
|
186
|
-
// delete any existing endpoints
|
|
187
|
-
this.jsPlumb.selectEndpoints({ source: fromId }).deleteAll();
|
|
188
|
-
|
|
189
|
-
const source = this.jsPlumb.addEndpoint(fromElement, {
|
|
190
|
-
...SOURCE_DEFAULTS,
|
|
191
|
-
endpoint: {
|
|
192
|
-
...SOURCE_DEFAULTS.endpoint,
|
|
193
|
-
options: {
|
|
194
|
-
...SOURCE_DEFAULTS.endpoint.options,
|
|
195
|
-
cssClass: 'plumb-source connected'
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
189
|
|
|
200
|
-
|
|
190
|
+
// sources and targets must exist
|
|
191
|
+
const source = document.getElementById(fromId);
|
|
192
|
+
// const target = document.getElementById(toId);
|
|
193
|
+
|
|
194
|
+
this.revalidate([fromId, toId]);
|
|
195
|
+
|
|
196
|
+
// we need to find the source endpoint
|
|
197
|
+
const sourceEndpoint = this.jsPlumb
|
|
198
|
+
.getEndpoints(source)
|
|
199
|
+
?.find((endpoint) =>
|
|
200
|
+
endpoint.elementId === fromId ? true : false
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// update endpoint have connect css class
|
|
204
|
+
if (sourceEndpoint) {
|
|
205
|
+
sourceEndpoint.addClass('connected');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// each connection needs its own target endpoint
|
|
209
|
+
const targetEndpoint = this.makeTarget(toId);
|
|
210
|
+
|
|
211
|
+
if (!sourceEndpoint || !targetEndpoint) {
|
|
212
|
+
console.warn(
|
|
213
|
+
`Plumber: Cannot connect ${fromId} to ${toId}. Element(s) missing.`
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// delete connections
|
|
219
|
+
this.jsPlumb.select({ source, targetEndpoint }).deleteAll();
|
|
201
220
|
this.jsPlumb.connect({
|
|
202
|
-
source,
|
|
203
|
-
target,
|
|
221
|
+
source: source,
|
|
222
|
+
target: targetEndpoint,
|
|
204
223
|
connector: {
|
|
205
224
|
...CONNECTOR_DEFAULTS,
|
|
206
225
|
options: { ...CONNECTOR_DEFAULTS.options, gap: [0, 5] }
|
|
@@ -212,7 +231,15 @@ export class Plumber {
|
|
|
212
231
|
});
|
|
213
232
|
this.pendingConnections = [];
|
|
214
233
|
});
|
|
215
|
-
|
|
234
|
+
|
|
235
|
+
// Force a repaint to ensure connections are positioned correctly
|
|
236
|
+
// especially after bulk updates or view switching
|
|
237
|
+
window.requestAnimationFrame(() => {
|
|
238
|
+
if (this.jsPlumb) {
|
|
239
|
+
this.jsPlumb.repaintEverything();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}, 0);
|
|
216
243
|
}
|
|
217
244
|
|
|
218
245
|
public connectIds(scope: string, fromId: string, toId: string) {
|
|
@@ -597,20 +624,44 @@ export class Plumber {
|
|
|
597
624
|
});
|
|
598
625
|
}
|
|
599
626
|
|
|
600
|
-
public
|
|
627
|
+
public reset() {
|
|
628
|
+
if (this.connectionWait) {
|
|
629
|
+
clearTimeout(this.connectionWait);
|
|
630
|
+
this.connectionWait = null;
|
|
631
|
+
}
|
|
632
|
+
this.pendingConnections = [];
|
|
633
|
+
this.jsPlumb.select().deleteAll();
|
|
634
|
+
this.jsPlumb._managedElements = {};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
public forgetNode(nodeId: string) {
|
|
638
|
+
if (!this.jsPlumb) return;
|
|
639
|
+
const element = document.getElementById(nodeId);
|
|
640
|
+
if (!element) return;
|
|
641
|
+
|
|
642
|
+
this.jsPlumb.deleteConnectionsForElement(element);
|
|
643
|
+
this.jsPlumb.removeAllEndpoints(element);
|
|
644
|
+
this.jsPlumb.unmanage(element);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
public removeNodeConnections(nodeId: string, exitIds?: string[]) {
|
|
601
648
|
if (!this.jsPlumb) return;
|
|
602
649
|
|
|
603
650
|
const inbound = this.jsPlumb.select({ target: nodeId });
|
|
604
|
-
|
|
651
|
+
|
|
652
|
+
// Use provided exitIds or try to find them in DOM (fallback)
|
|
653
|
+
const exits =
|
|
654
|
+
exitIds ||
|
|
605
655
|
Array.from(
|
|
606
656
|
document.getElementById(nodeId)?.querySelectorAll('.exit') || []
|
|
607
657
|
).map((exit) => {
|
|
608
658
|
return exit.id;
|
|
609
|
-
}) ||
|
|
659
|
+
}) ||
|
|
660
|
+
[];
|
|
610
661
|
|
|
611
662
|
inbound.deleteAll();
|
|
612
|
-
this.jsPlumb.select({ source:
|
|
613
|
-
this.jsPlumb.selectEndpoints({ source:
|
|
663
|
+
this.jsPlumb.select({ source: exits }).deleteAll();
|
|
664
|
+
this.jsPlumb.selectEndpoints({ source: exits }).deleteAll();
|
|
614
665
|
}
|
|
615
666
|
|
|
616
667
|
public removeExitConnection(exitId: string) {
|
|
@@ -627,13 +678,16 @@ export class Plumber {
|
|
|
627
678
|
this.jsPlumb.deleteConnection(connection);
|
|
628
679
|
});
|
|
629
680
|
|
|
630
|
-
// Re-create the source endpoint (now without connection)
|
|
631
|
-
this.jsPlumb.removeAllEndpoints(exitElement);
|
|
632
|
-
this.makeSource(exitId);
|
|
633
|
-
|
|
634
681
|
return connections.length > 0;
|
|
635
682
|
}
|
|
636
683
|
|
|
684
|
+
public removeAllEndpoints(nodeId: string) {
|
|
685
|
+
if (!this.jsPlumb) return;
|
|
686
|
+
const element = document.getElementById(nodeId);
|
|
687
|
+
if (!element) return;
|
|
688
|
+
this.jsPlumb.removeAllEndpoints(element, true);
|
|
689
|
+
}
|
|
690
|
+
|
|
637
691
|
/**
|
|
638
692
|
* Set the removing state for an exit's connection
|
|
639
693
|
* @param exitId The ID of the exit whose connections should be marked as removing
|
|
@@ -183,7 +183,6 @@ export class Simulator extends RapidElement {
|
|
|
183
183
|
backdrop-filter: blur(10px);
|
|
184
184
|
border-radius: 16px;
|
|
185
185
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
186
|
-
pointer-events: all;
|
|
187
186
|
}
|
|
188
187
|
.option-btn {
|
|
189
188
|
background: rgba(255, 255, 255, 0.1);
|
|
@@ -359,6 +358,10 @@ export class Simulator extends RapidElement {
|
|
|
359
358
|
pointer-events: auto;
|
|
360
359
|
}
|
|
361
360
|
|
|
361
|
+
.context-explorer.hidden {
|
|
362
|
+
pointer-events: none !important;
|
|
363
|
+
}
|
|
364
|
+
|
|
362
365
|
.context-item {
|
|
363
366
|
display: flex;
|
|
364
367
|
align-items: flex-start;
|
|
@@ -1319,8 +1322,6 @@ export class Simulator extends RapidElement {
|
|
|
1319
1322
|
const phoneWindow = this.shadowRoot.getElementById(
|
|
1320
1323
|
'phone-window'
|
|
1321
1324
|
) as FloatingWindow;
|
|
1322
|
-
// phoneWindow.hide();
|
|
1323
|
-
|
|
1324
1325
|
phoneWindow.handleClose();
|
|
1325
1326
|
this.isVisible = false;
|
|
1326
1327
|
getStore().getState().setSimulatorActive(false);
|
|
@@ -1753,7 +1754,9 @@ export class Simulator extends RapidElement {
|
|
|
1753
1754
|
>
|
|
1754
1755
|
<div class="phone-simulator" style="${styleVars}">
|
|
1755
1756
|
<div
|
|
1756
|
-
class="context-explorer ${this.contextExplorerOpen
|
|
1757
|
+
class="context-explorer ${this.contextExplorerOpen
|
|
1758
|
+
? 'open'
|
|
1759
|
+
: ''} ${this.isVisible ? 'visible' : 'hidden'}"
|
|
1757
1760
|
>
|
|
1758
1761
|
<div class="context-explorer-scroll">
|
|
1759
1762
|
${this.context
|
|
@@ -1884,7 +1887,10 @@ export class Simulator extends RapidElement {
|
|
|
1884
1887
|
</div>
|
|
1885
1888
|
</div>
|
|
1886
1889
|
</div>
|
|
1887
|
-
<div
|
|
1890
|
+
<div
|
|
1891
|
+
class="option-pane"
|
|
1892
|
+
style="pointer-events:${this.isVisible ? 'all' : 'none'}"
|
|
1893
|
+
>
|
|
1888
1894
|
<button class="option-btn" @click=${this.handleClose} title="Close">
|
|
1889
1895
|
<temba-icon name="x" size="1.5"></temba-icon>
|
|
1890
1896
|
</button>
|
package/src/store/AppState.ts
CHANGED
|
@@ -105,6 +105,7 @@ export interface AppState {
|
|
|
105
105
|
languageNames: { [code: string]: Language };
|
|
106
106
|
workspace: Workspace;
|
|
107
107
|
isTranslating: boolean;
|
|
108
|
+
viewingRevision: boolean;
|
|
108
109
|
|
|
109
110
|
dirtyDate: Date | null;
|
|
110
111
|
|
|
@@ -174,6 +175,7 @@ export const zustand = createStore<AppState>()(
|
|
|
174
175
|
flowDefinition: null,
|
|
175
176
|
flowInfo: null,
|
|
176
177
|
isTranslating: false,
|
|
178
|
+
viewingRevision: false,
|
|
177
179
|
dirtyDate: null,
|
|
178
180
|
activity: null,
|
|
179
181
|
simulatorActivity: null,
|
|
@@ -187,6 +189,7 @@ export const zustand = createStore<AppState>()(
|
|
|
187
189
|
},
|
|
188
190
|
|
|
189
191
|
fetchRevision: async (endpoint: string, id: string = null) => {
|
|
192
|
+
const viewingRevision = !!id && id !== 'latest';
|
|
190
193
|
if (!id) {
|
|
191
194
|
id = 'latest';
|
|
192
195
|
}
|
|
@@ -198,7 +201,11 @@ export const zustand = createStore<AppState>()(
|
|
|
198
201
|
throw new Error('Network response was not ok');
|
|
199
202
|
}
|
|
200
203
|
const data = (await response.json()) as FlowContents;
|
|
201
|
-
set({
|
|
204
|
+
set({
|
|
205
|
+
flowInfo: data.info,
|
|
206
|
+
flowDefinition: data.definition,
|
|
207
|
+
viewingRevision
|
|
208
|
+
});
|
|
202
209
|
},
|
|
203
210
|
|
|
204
211
|
fetchWorkspace: async (endpoint) => {
|
|
@@ -282,7 +289,11 @@ export const zustand = createStore<AppState>()(
|
|
|
282
289
|
setFlowContents: (flow: FlowContents) => {
|
|
283
290
|
set((state: AppState) => {
|
|
284
291
|
const flowLang = flow.definition.language;
|
|
285
|
-
|
|
292
|
+
// Clone to ensure mutable for sorting
|
|
293
|
+
state.flowDefinition = {
|
|
294
|
+
...flow.definition,
|
|
295
|
+
nodes: [...(flow.definition.nodes || [])]
|
|
296
|
+
};
|
|
286
297
|
state.flowInfo = flow.info;
|
|
287
298
|
// Reset to the flow's default language when loading a new flow
|
|
288
299
|
state.languageCode = flowLang;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { html, fixture, expect } from '@open-wc/testing';
|
|
2
|
+
import { Editor } from '../src/flow/Editor';
|
|
3
|
+
import { stub, restore, SinonStub } from 'sinon';
|
|
4
|
+
|
|
5
|
+
customElements.define('temba-flow-editor-revisions', Editor);
|
|
6
|
+
|
|
7
|
+
describe('Editor Revisions', () => {
|
|
8
|
+
let element: Editor;
|
|
9
|
+
let fetchStub: SinonStub;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
restore();
|
|
13
|
+
fetchStub = stub(window, 'fetch');
|
|
14
|
+
// Initialize without 'flow' attribute to prevent firstUpdated from calling getStore().getState()
|
|
15
|
+
element = await fixture(
|
|
16
|
+
html`<temba-flow-editor-revisions></temba-flow-editor-revisions>`
|
|
17
|
+
);
|
|
18
|
+
element.flow = 'test-flow';
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
restore();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should exclude the most recent revision from the list', async () => {
|
|
26
|
+
const mockRevisions = [
|
|
27
|
+
{
|
|
28
|
+
id: 3,
|
|
29
|
+
created_on: '2023-01-03',
|
|
30
|
+
user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 2,
|
|
34
|
+
created_on: '2023-01-02',
|
|
35
|
+
user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 1,
|
|
39
|
+
created_on: '2023-01-01',
|
|
40
|
+
user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Mock the fetch response for the revisions list
|
|
45
|
+
const mockResponse = new Response(
|
|
46
|
+
JSON.stringify({ results: mockRevisions }),
|
|
47
|
+
{
|
|
48
|
+
status: 200,
|
|
49
|
+
headers: { 'Content-Type': 'application/json' }
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
fetchStub.resolves(mockResponse);
|
|
54
|
+
|
|
55
|
+
// Call fetchRevisions (private)
|
|
56
|
+
// Note: fetchRevisions calls fetchResults -> fetchResultsPage -> fetch
|
|
57
|
+
await (element as any).fetchRevisions();
|
|
58
|
+
|
|
59
|
+
const revisions = (element as any).revisions;
|
|
60
|
+
expect(revisions.length).to.equal(2);
|
|
61
|
+
expect(revisions[0].id).to.equal(2);
|
|
62
|
+
expect(revisions[1].id).to.equal(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should handle empty revisions list', async () => {
|
|
66
|
+
const mockResponse = new Response(JSON.stringify({ results: [] }), {
|
|
67
|
+
status: 200
|
|
68
|
+
});
|
|
69
|
+
fetchStub.resolves(mockResponse);
|
|
70
|
+
|
|
71
|
+
await (element as any).fetchRevisions();
|
|
72
|
+
const revisions = (element as any).revisions;
|
|
73
|
+
expect(revisions.length).to.equal(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should handle single revision in list', async () => {
|
|
77
|
+
const mockRevisions = [
|
|
78
|
+
{
|
|
79
|
+
id: 1,
|
|
80
|
+
created_on: '2023-01-01',
|
|
81
|
+
user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
const mockResponse = new Response(
|
|
85
|
+
JSON.stringify({ results: mockRevisions }),
|
|
86
|
+
{ status: 200 }
|
|
87
|
+
);
|
|
88
|
+
fetchStub.resolves(mockResponse);
|
|
89
|
+
|
|
90
|
+
await (element as any).fetchRevisions();
|
|
91
|
+
const revisions = (element as any).revisions;
|
|
92
|
+
expect(revisions.length).to.equal(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should have purple color for revisions tab and blue for selected item', async () => {
|
|
96
|
+
// Force revisions window to show
|
|
97
|
+
(element as any).revisionsWindowHidden = false;
|
|
98
|
+
(element as any).localizationWindowHidden = true;
|
|
99
|
+
|
|
100
|
+
// Mock revisions so we can see list items
|
|
101
|
+
const mockRevisions = [
|
|
102
|
+
{
|
|
103
|
+
id: 2,
|
|
104
|
+
created_on: '2023-01-02',
|
|
105
|
+
user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 1,
|
|
109
|
+
created_on: '2023-01-01',
|
|
110
|
+
user: { id: 1, first_name: 'A', last_name: 'B', username: 'ab' }
|
|
111
|
+
}
|
|
112
|
+
];
|
|
113
|
+
(element as any).revisions = mockRevisions;
|
|
114
|
+
(element as any).viewingRevision = mockRevisions[0]; // Select the first one
|
|
115
|
+
|
|
116
|
+
await element.requestUpdate();
|
|
117
|
+
|
|
118
|
+
// Check tab color
|
|
119
|
+
const tab = element.querySelector('#revisions-tab');
|
|
120
|
+
expect(tab).to.exist;
|
|
121
|
+
expect(tab.getAttribute('color')).to.equal('rgb(142, 94, 167)');
|
|
122
|
+
|
|
123
|
+
// Check selected item styles
|
|
124
|
+
const selectedItem = element.querySelector(
|
|
125
|
+
'.revision-item.selected'
|
|
126
|
+
) as HTMLElement;
|
|
127
|
+
expect(selectedItem).to.exist;
|
|
128
|
+
|
|
129
|
+
// We need to check inline styles because they are set in the template
|
|
130
|
+
const style = selectedItem.getAttribute('style');
|
|
131
|
+
expect(style).to.contain('#f0f6ff'); // Blue background
|
|
132
|
+
expect(style).to.contain('#a4cafe'); // Blue border
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -653,11 +653,14 @@ describe('Editor', () => {
|
|
|
653
653
|
|
|
654
654
|
await editor.updateComplete;
|
|
655
655
|
|
|
656
|
-
// node-2
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
expect(
|
|
656
|
+
// node-2 is at top (top: 100 < top: 200) so it should be flow-start
|
|
657
|
+
const node1 = editor.querySelector('temba-flow-node[uuid="node-1"]');
|
|
658
|
+
const node2 = editor.querySelector('temba-flow-node[uuid="node-2"]');
|
|
659
|
+
|
|
660
|
+
expect(node2).to.exist;
|
|
661
|
+
expect(node1).to.exist;
|
|
662
|
+
expect(node2.classList.contains('flow-start')).to.be.true;
|
|
663
|
+
expect(node1.classList.contains('flow-start')).to.be.false;
|
|
661
664
|
|
|
662
665
|
// move node-1 to the top
|
|
663
666
|
zustand.getState().updateCanvasPositions({
|
|
@@ -720,7 +723,7 @@ describe('Editor', () => {
|
|
|
720
723
|
|
|
721
724
|
await editor.updateComplete;
|
|
722
725
|
|
|
723
|
-
|
|
726
|
+
const flowNodes = editor.querySelectorAll('temba-flow-node');
|
|
724
727
|
expect(flowNodes[0].classList.contains('flow-start')).to.be.true;
|
|
725
728
|
|
|
726
729
|
// add a new node at the top
|
|
@@ -741,10 +744,13 @@ describe('Editor', () => {
|
|
|
741
744
|
await editor.updateComplete;
|
|
742
745
|
|
|
743
746
|
// new node should now be the flow-start
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
expect(
|
|
747
|
+
const node1 = editor.querySelector('temba-flow-node[uuid="node-1"]');
|
|
748
|
+
const node2 = editor.querySelector('temba-flow-node[uuid="node-2"]');
|
|
749
|
+
|
|
750
|
+
expect(node2).to.exist;
|
|
751
|
+
expect(node1).to.exist;
|
|
752
|
+
expect(node2.classList.contains('flow-start')).to.be.true;
|
|
753
|
+
expect(node1.classList.contains('flow-start')).to.be.false;
|
|
748
754
|
});
|
|
749
755
|
|
|
750
756
|
it('should handle flow-start when first node is removed', async () => {
|
|
@@ -29,7 +29,14 @@ describe('Plumber - Connection Management', () => {
|
|
|
29
29
|
removeClass: stub(),
|
|
30
30
|
batch: stub().callsFake((fn: any) => fn()),
|
|
31
31
|
addEndpoint: stub().returns({}),
|
|
32
|
+
revalidate: stub(),
|
|
32
33
|
connect: stub(),
|
|
34
|
+
getEndpoints: stub().returns([
|
|
35
|
+
{ elementId: 'test-from', addClass: stub() }
|
|
36
|
+
]),
|
|
37
|
+
select: stub().returns({
|
|
38
|
+
deleteAll: stub()
|
|
39
|
+
}),
|
|
33
40
|
selectEndpoints: stub().returns({
|
|
34
41
|
deleteAll: stub()
|
|
35
42
|
}),
|
|
@@ -131,7 +138,6 @@ describe('Plumber - Connection Management', () => {
|
|
|
131
138
|
expect(result).to.be.true;
|
|
132
139
|
expect((plumber as any).jsPlumb.deleteConnection).to.have.been
|
|
133
140
|
.calledTwice;
|
|
134
|
-
expect((plumber as any).jsPlumb.removeAllEndpoints).to.have.been.called;
|
|
135
141
|
});
|
|
136
142
|
|
|
137
143
|
it('returns false when no connections exist', () => {
|
|
@@ -31,6 +31,12 @@ describe('Plumber', () => {
|
|
|
31
31
|
batch: stub().callsFake((fn) => fn()),
|
|
32
32
|
addEndpoint: stub().returns({}),
|
|
33
33
|
connect: stub(),
|
|
34
|
+
getEndpoints: stub().returns([
|
|
35
|
+
{ elementId: 'test-from', addClass: stub() }
|
|
36
|
+
]),
|
|
37
|
+
select: stub().returns({
|
|
38
|
+
deleteAll: stub()
|
|
39
|
+
}),
|
|
34
40
|
selectEndpoints: stub().returns({
|
|
35
41
|
deleteAll: stub()
|
|
36
42
|
}),
|