@shockers/node-red-plugin-grouped-flows 0.1.26
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/LICENSE +201 -0
- package/README.md +43 -0
- package/grouped-flows.html +1628 -0
- package/package.json +36 -0
|
@@ -0,0 +1,1628 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
(function() {
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
var PLUGIN_ID = "nr-grouped-flows";
|
|
6
|
+
var PLUGIN_VERSION = "0.1.26";
|
|
7
|
+
var ORIGINAL_UL_ID = "red-ui-workspace-tabs";
|
|
8
|
+
var CONTAINER_ID = "nr-grouped-flows";
|
|
9
|
+
var GROUPS_UL_ID = "nr-grouped-flows-groups";
|
|
10
|
+
var FLOWS_UL_ID = "nr-grouped-flows-flows";
|
|
11
|
+
var WORKSPACE_ROOT_ID = "red-ui-workspace";
|
|
12
|
+
var WORKSPACE_ENABLED_CLASS = "nr-grouped-flows-enabled";
|
|
13
|
+
var WORKSPACE_GROUP_PROPERTY = "group";
|
|
14
|
+
var WORKSPACE_SINGLE_ROW_CLASS = "nr-grouped-flows-single-row";
|
|
15
|
+
var DIALOG_GROUP_ROW_CLASS = "nr-grouped-flows-group-row";
|
|
16
|
+
var DIALOG_GROUP_INPUT_ID = "nr-grouped-flows-group-input";
|
|
17
|
+
var DIALOG_GROUP_SAVE_NS = ".nrGroupedFlowsGroupSave";
|
|
18
|
+
var GROUP_PERSISTENCE_KEY = "nr-grouped-flows.groups.v1";
|
|
19
|
+
var BOOT_RETRY_INTERVAL_MS = 250;
|
|
20
|
+
var BOOT_MAX_RETRIES = 120;
|
|
21
|
+
var WARN_CORE_TABS_RETRY_AT = 40;
|
|
22
|
+
var FLOW_DOUBLE_CLICK_WINDOW_MS = 450;
|
|
23
|
+
var EDIT_DIALOG_DEFER_MS = 40;
|
|
24
|
+
var GROUP_REORDER_SETTLE_MS = 500;
|
|
25
|
+
var DEBUG_STORAGE_KEY = "nr-grouped-flows-debug";
|
|
26
|
+
var pluginRegistered = false;
|
|
27
|
+
|
|
28
|
+
function isDebugEnabled() {
|
|
29
|
+
try {
|
|
30
|
+
return window.localStorage && localStorage.getItem(DEBUG_STORAGE_KEY) === "1";
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function setDebugEnabled(enabled) {
|
|
37
|
+
try {
|
|
38
|
+
if (!window.localStorage) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (enabled) {
|
|
42
|
+
localStorage.setItem(DEBUG_STORAGE_KEY, "1");
|
|
43
|
+
} else {
|
|
44
|
+
localStorage.removeItem(DEBUG_STORAGE_KEY);
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
// Ignore storage access failures.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function t(key, fallback, options) {
|
|
52
|
+
try {
|
|
53
|
+
if (window.RED && typeof RED._ === "function") {
|
|
54
|
+
return RED._(key, options || {});
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
// Ignore missing i18n keys and fall back to plain text.
|
|
58
|
+
}
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createController() {
|
|
63
|
+
var debugEnabled = isDebugEnabled();
|
|
64
|
+
var state = {
|
|
65
|
+
originalWrapper: null,
|
|
66
|
+
container: null,
|
|
67
|
+
groupTabs: null,
|
|
68
|
+
flowTabs: null,
|
|
69
|
+
model: {
|
|
70
|
+
groups: [],
|
|
71
|
+
groupByName: {},
|
|
72
|
+
workspaceToGroup: {},
|
|
73
|
+
activeWorkspaceId: null
|
|
74
|
+
},
|
|
75
|
+
activeGroup: null,
|
|
76
|
+
suspendGroupChange: false,
|
|
77
|
+
suspendFlowChange: false,
|
|
78
|
+
retryTimer: null,
|
|
79
|
+
rebuildTimer: null,
|
|
80
|
+
eventBindings: [],
|
|
81
|
+
retryCount: 0,
|
|
82
|
+
didWarnUnavailable: false,
|
|
83
|
+
lastKnownActiveWorkspaceId: null,
|
|
84
|
+
lastWorkspaceByGroup: {},
|
|
85
|
+
groupTabGroupMap: {},
|
|
86
|
+
flowTabWorkspaceMap: {},
|
|
87
|
+
lastFlowPointerWorkspaceId: null,
|
|
88
|
+
lastFlowPointerTime: 0,
|
|
89
|
+
pendingFlowDoubleWorkspaceId: null,
|
|
90
|
+
pendingEditOpenTimer: null,
|
|
91
|
+
applyingFlowReorder: false,
|
|
92
|
+
applyingGroupReorder: false,
|
|
93
|
+
skipNextFlowsReorderRebuild: false,
|
|
94
|
+
lastRenderedGroupSignature: "",
|
|
95
|
+
lastRenderedActiveGroup: null,
|
|
96
|
+
lastRenderedFlowGroup: null,
|
|
97
|
+
lastRenderedFlowSignature: "",
|
|
98
|
+
lastRenderedActiveFlowTab: null,
|
|
99
|
+
wrappedEditFlow: null,
|
|
100
|
+
originalEditFlow: null,
|
|
101
|
+
persistedGroups: null
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function debug(message, data) {
|
|
105
|
+
if (!debugEnabled || !window.console || typeof console.log !== "function") {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (typeof data !== "undefined") {
|
|
109
|
+
console.log("[nr-grouped-workspace-tabs][debug] " + message, data);
|
|
110
|
+
} else {
|
|
111
|
+
console.log("[nr-grouped-workspace-tabs][debug] " + message);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function warn(message) {
|
|
116
|
+
if (window.console && typeof console.warn === "function") {
|
|
117
|
+
console.warn("[nr-grouped-workspace-tabs] " + message);
|
|
118
|
+
}
|
|
119
|
+
if (RED.notify && typeof RED.notify === "function") {
|
|
120
|
+
RED.notify(message, "warning", false, 3000);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function addFlowEnabled() {
|
|
125
|
+
if (RED.settings && typeof RED.settings.theme === "function") {
|
|
126
|
+
return RED.settings.theme("menu.menu-item-workspace-add", true);
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildFlowMenu() {
|
|
132
|
+
return [
|
|
133
|
+
{
|
|
134
|
+
id: "nr-grouped-tabs-list-flows",
|
|
135
|
+
label: t("workspace.listFlows", "List flows"),
|
|
136
|
+
onselect: "core:list-flows"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "nr-grouped-tabs-list-subflows",
|
|
140
|
+
label: t("workspace.listSubflows", "List subflows"),
|
|
141
|
+
onselect: "core:list-subflows"
|
|
142
|
+
},
|
|
143
|
+
null,
|
|
144
|
+
{
|
|
145
|
+
id: "nr-grouped-tabs-add-flow",
|
|
146
|
+
label: t("workspace.addFlow", "Add flow"),
|
|
147
|
+
onselect: "core:add-flow"
|
|
148
|
+
}
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeGroupName(value) {
|
|
153
|
+
return String(value || "").trim();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getWorkspaceById(workspaceId) {
|
|
157
|
+
if (!workspaceId || !RED.nodes || typeof RED.nodes.workspace !== "function") {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return RED.nodes.workspace(workspaceId);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function loadPersistedGroups() {
|
|
164
|
+
try {
|
|
165
|
+
if (!window.localStorage) {
|
|
166
|
+
return {};
|
|
167
|
+
}
|
|
168
|
+
var raw = localStorage.getItem(GROUP_PERSISTENCE_KEY);
|
|
169
|
+
if (!raw) {
|
|
170
|
+
return {};
|
|
171
|
+
}
|
|
172
|
+
var parsed = JSON.parse(raw);
|
|
173
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
174
|
+
return {};
|
|
175
|
+
}
|
|
176
|
+
var groups = {};
|
|
177
|
+
Object.keys(parsed).forEach(function(workspaceId) {
|
|
178
|
+
var groupName = normalizeGroupName(parsed[workspaceId]);
|
|
179
|
+
if (workspaceId && groupName) {
|
|
180
|
+
groups[workspaceId] = groupName;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return groups;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function ensurePersistedGroupsLoaded() {
|
|
190
|
+
if (!state.persistedGroups) {
|
|
191
|
+
state.persistedGroups = loadPersistedGroups();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function savePersistedGroups() {
|
|
196
|
+
try {
|
|
197
|
+
if (!window.localStorage) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
localStorage.setItem(GROUP_PERSISTENCE_KEY, JSON.stringify(state.persistedGroups || {}));
|
|
201
|
+
} catch (err) {
|
|
202
|
+
// Ignore storage write failures.
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getPersistedGroup(workspaceId) {
|
|
207
|
+
ensurePersistedGroupsLoaded();
|
|
208
|
+
if (!workspaceId || !state.persistedGroups) {
|
|
209
|
+
return "";
|
|
210
|
+
}
|
|
211
|
+
return normalizeGroupName(state.persistedGroups[workspaceId]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function setPersistedGroup(workspaceId, groupName) {
|
|
215
|
+
if (!workspaceId) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
ensurePersistedGroupsLoaded();
|
|
219
|
+
var normalized = normalizeGroupName(groupName);
|
|
220
|
+
var oldValue = normalizeGroupName(state.persistedGroups[workspaceId]);
|
|
221
|
+
if (normalized) {
|
|
222
|
+
if (oldValue === normalized) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
state.persistedGroups[workspaceId] = normalized;
|
|
226
|
+
} else if (!oldValue) {
|
|
227
|
+
return;
|
|
228
|
+
} else {
|
|
229
|
+
delete state.persistedGroups[workspaceId];
|
|
230
|
+
}
|
|
231
|
+
savePersistedGroups();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function prunePersistedGroups(validWorkspaceIds) {
|
|
235
|
+
ensurePersistedGroupsLoaded();
|
|
236
|
+
if (!state.persistedGroups) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
var changed = false;
|
|
240
|
+
Object.keys(state.persistedGroups).forEach(function(workspaceId) {
|
|
241
|
+
if (!validWorkspaceIds[workspaceId]) {
|
|
242
|
+
delete state.persistedGroups[workspaceId];
|
|
243
|
+
changed = true;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
return changed;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function makeStableGroupTabId(groupName) {
|
|
250
|
+
return "nr-group-tab-" + encodeURIComponent(String(groupName || ""));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function makeStableTopFlowTabId(workspaceId) {
|
|
254
|
+
return "nr-top-flow-tab-" + String(workspaceId || "");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function makeStableFlowTabId(workspaceId) {
|
|
258
|
+
return "nr-flow-tab-" + String(workspaceId || "");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function getActiveWorkspaceIdFromCore() {
|
|
262
|
+
if (RED.workspaces && typeof RED.workspaces.active === "function") {
|
|
263
|
+
var active = RED.workspaces.active();
|
|
264
|
+
if (typeof active === "string" && active) {
|
|
265
|
+
return active;
|
|
266
|
+
}
|
|
267
|
+
if (active && typeof active.id === "string" && active.id) {
|
|
268
|
+
return active.id;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
var activeLink = $("#" + ORIGINAL_UL_ID + " li.red-ui-tab.active > a.red-ui-tab-label").first();
|
|
273
|
+
if (!activeLink.length) {
|
|
274
|
+
return state.lastKnownActiveWorkspaceId;
|
|
275
|
+
}
|
|
276
|
+
var href = activeLink.attr("href") || "";
|
|
277
|
+
if (!href || href.charAt(0) !== "#") {
|
|
278
|
+
return state.lastKnownActiveWorkspaceId;
|
|
279
|
+
}
|
|
280
|
+
return href.slice(1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function readCoreTabs() {
|
|
284
|
+
var items = [];
|
|
285
|
+
var originalUl = $("#" + ORIGINAL_UL_ID);
|
|
286
|
+
|
|
287
|
+
if (!originalUl.length) {
|
|
288
|
+
return items;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
originalUl.children("li.red-ui-tab").each(function(index, el) {
|
|
292
|
+
var li = $(el);
|
|
293
|
+
if (li.hasClass("hide-tab") || li.hasClass("hide")) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
var link = li.children("a.red-ui-tab-label[href^='#']").first();
|
|
298
|
+
if (!link.length) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
var href = link.attr("href") || "";
|
|
303
|
+
if (href.charAt(0) !== "#") {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
var workspaceId = href.slice(1);
|
|
308
|
+
var fullLabel = link.find("span.red-ui-text-bidi-aware").first().text().trim();
|
|
309
|
+
if (!fullLabel) {
|
|
310
|
+
fullLabel = link.text().trim() || workspaceId;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
var workspace = getWorkspaceById(workspaceId);
|
|
314
|
+
|
|
315
|
+
items.push({
|
|
316
|
+
workspaceId: workspaceId,
|
|
317
|
+
fullLabel: fullLabel,
|
|
318
|
+
group: normalizeGroupName(workspace ? workspace[WORKSPACE_GROUP_PROPERTY] : ""),
|
|
319
|
+
disabled: li.hasClass("red-ui-workspace-disabled"),
|
|
320
|
+
locked: li.hasClass("red-ui-workspace-locked"),
|
|
321
|
+
changed: li.hasClass("red-ui-workspace-changed"),
|
|
322
|
+
active: li.hasClass("active")
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return items;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function buildModel() {
|
|
330
|
+
ensurePersistedGroupsLoaded();
|
|
331
|
+
var groups = [];
|
|
332
|
+
var groupByName = {};
|
|
333
|
+
var workspaceToGroup = {};
|
|
334
|
+
var activeWorkspaceId = null;
|
|
335
|
+
var coreTabs = readCoreTabs();
|
|
336
|
+
var explicitActiveWorkspaceId = getActiveWorkspaceIdFromCore();
|
|
337
|
+
var validWorkspaceIds = {};
|
|
338
|
+
var persistedChanged = false;
|
|
339
|
+
|
|
340
|
+
coreTabs.forEach(function(item) {
|
|
341
|
+
validWorkspaceIds[item.workspaceId] = true;
|
|
342
|
+
if (item.group && state.persistedGroups[item.workspaceId] !== item.group) {
|
|
343
|
+
state.persistedGroups[item.workspaceId] = item.group;
|
|
344
|
+
persistedChanged = true;
|
|
345
|
+
}
|
|
346
|
+
var flow = {
|
|
347
|
+
workspaceId: item.workspaceId,
|
|
348
|
+
flowTabId: makeStableFlowTabId(item.workspaceId),
|
|
349
|
+
label: item.fullLabel,
|
|
350
|
+
fullLabel: item.fullLabel,
|
|
351
|
+
disabled: item.disabled,
|
|
352
|
+
locked: item.locked,
|
|
353
|
+
changed: item.changed
|
|
354
|
+
};
|
|
355
|
+
var groupName = normalizeGroupName(item.group || getPersistedGroup(item.workspaceId));
|
|
356
|
+
var group;
|
|
357
|
+
|
|
358
|
+
if (groupName) {
|
|
359
|
+
var groupKey = "group:" + groupName;
|
|
360
|
+
group = groupByName[groupKey];
|
|
361
|
+
if (!group) {
|
|
362
|
+
group = {
|
|
363
|
+
key: groupKey,
|
|
364
|
+
name: groupName,
|
|
365
|
+
tabId: makeStableGroupTabId(groupName),
|
|
366
|
+
flows: [],
|
|
367
|
+
standalone: false
|
|
368
|
+
};
|
|
369
|
+
groupByName[group.key] = group;
|
|
370
|
+
groups.push(group);
|
|
371
|
+
}
|
|
372
|
+
group.flows.push(flow);
|
|
373
|
+
workspaceToGroup[flow.workspaceId] = group.key;
|
|
374
|
+
} else {
|
|
375
|
+
group = {
|
|
376
|
+
key: "flow:" + flow.workspaceId,
|
|
377
|
+
name: flow.label,
|
|
378
|
+
tabId: makeStableTopFlowTabId(flow.workspaceId),
|
|
379
|
+
flows: [flow],
|
|
380
|
+
standalone: true,
|
|
381
|
+
standaloneWorkspaceId: flow.workspaceId
|
|
382
|
+
};
|
|
383
|
+
groupByName[group.key] = group;
|
|
384
|
+
groups.push(group);
|
|
385
|
+
workspaceToGroup[flow.workspaceId] = group.key;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (item.active || flow.workspaceId === explicitActiveWorkspaceId) {
|
|
389
|
+
activeWorkspaceId = item.workspaceId;
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
if (coreTabs.length) {
|
|
394
|
+
if (prunePersistedGroups(validWorkspaceIds)) {
|
|
395
|
+
persistedChanged = true;
|
|
396
|
+
}
|
|
397
|
+
if (persistedChanged) {
|
|
398
|
+
savePersistedGroups();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
groups: groups,
|
|
404
|
+
groupByName: groupByName,
|
|
405
|
+
workspaceToGroup: workspaceToGroup,
|
|
406
|
+
activeWorkspaceId: activeWorkspaceId || explicitActiveWorkspaceId || state.lastKnownActiveWorkspaceId
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function clearTabs(tabApi) {
|
|
411
|
+
if (!tabApi || typeof tabApi.listTabs !== "function") {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
var existing = tabApi.listTabs() || [];
|
|
416
|
+
existing.slice().forEach(function(id) {
|
|
417
|
+
tabApi.removeTab(id);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function arraysEqual(a, b) {
|
|
422
|
+
if (!a || !b || a.length !== b.length) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
for (var i = 0; i < a.length; i += 1) {
|
|
426
|
+
if (a[i] !== b[i]) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function getGroupSignature(groups) {
|
|
434
|
+
if (!Array.isArray(groups) || !groups.length) {
|
|
435
|
+
return "";
|
|
436
|
+
}
|
|
437
|
+
return groups.map(function(group) {
|
|
438
|
+
return [
|
|
439
|
+
group.key || "",
|
|
440
|
+
group.name || "",
|
|
441
|
+
group.standalone ? "1" : "0"
|
|
442
|
+
].join("\u001e");
|
|
443
|
+
}).join("\u001f");
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function getFlowSignature(flows) {
|
|
447
|
+
if (!Array.isArray(flows) || !flows.length) {
|
|
448
|
+
return "";
|
|
449
|
+
}
|
|
450
|
+
return flows.map(function(flow) {
|
|
451
|
+
return [
|
|
452
|
+
flow.workspaceId,
|
|
453
|
+
flow.label || "",
|
|
454
|
+
flow.fullLabel || "",
|
|
455
|
+
flow.disabled ? "1" : "0",
|
|
456
|
+
flow.locked ? "1" : "0",
|
|
457
|
+
flow.changed ? "1" : "0"
|
|
458
|
+
].join("\u001e");
|
|
459
|
+
}).join("\u001f");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function rememberWorkspaceForGroup(workspaceId) {
|
|
463
|
+
if (!workspaceId) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
var groupKey = state.model.workspaceToGroup[workspaceId];
|
|
467
|
+
if (!groupKey) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
state.lastWorkspaceByGroup[groupKey] = workspaceId;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function pruneGroupWorkspaceMemory() {
|
|
474
|
+
Object.keys(state.lastWorkspaceByGroup).forEach(function(groupKey) {
|
|
475
|
+
var rememberedWorkspaceId = state.lastWorkspaceByGroup[groupKey];
|
|
476
|
+
var group = state.model.groupByName[groupKey];
|
|
477
|
+
if (!group || !rememberedWorkspaceId) {
|
|
478
|
+
delete state.lastWorkspaceByGroup[groupKey];
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
var existsInGroup = group.flows.some(function(flow) {
|
|
482
|
+
return flow.workspaceId === rememberedWorkspaceId;
|
|
483
|
+
});
|
|
484
|
+
if (!existsInGroup) {
|
|
485
|
+
delete state.lastWorkspaceByGroup[groupKey];
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function applyFlowReorder(newOrder) {
|
|
491
|
+
if (state.applyingFlowReorder) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (!Array.isArray(newOrder) || !newOrder.length) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
var activeGroupKey = state.activeGroup;
|
|
499
|
+
var activeGroup = activeGroupKey ? state.model.groupByName[activeGroupKey] : null;
|
|
500
|
+
if (!activeGroup || !activeGroup.flows || activeGroup.flows.length < 2) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
var oldGroupWorkspaceOrder = activeGroup.flows.map(function(flow) {
|
|
505
|
+
return flow.workspaceId;
|
|
506
|
+
});
|
|
507
|
+
var newGroupWorkspaceOrder = newOrder.map(function(tabId) {
|
|
508
|
+
return state.flowTabWorkspaceMap[tabId];
|
|
509
|
+
}).filter(function(workspaceId) {
|
|
510
|
+
return !!workspaceId;
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
if (newGroupWorkspaceOrder.length !== oldGroupWorkspaceOrder.length) {
|
|
514
|
+
debug("flow reorder ignored: mapping mismatch", {
|
|
515
|
+
group: activeGroupKey,
|
|
516
|
+
oldOrder: oldGroupWorkspaceOrder,
|
|
517
|
+
newOrder: newOrder,
|
|
518
|
+
mapped: newGroupWorkspaceOrder
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (arraysEqual(oldGroupWorkspaceOrder, newGroupWorkspaceOrder)) {
|
|
524
|
+
debug("flow reorder ignored: no effective change", {
|
|
525
|
+
group: activeGroupKey,
|
|
526
|
+
order: newGroupWorkspaceOrder
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
var coreWorkspaceOrder = readCoreTabs().map(function(item) {
|
|
532
|
+
return item.workspaceId;
|
|
533
|
+
});
|
|
534
|
+
if (!coreWorkspaceOrder.length) {
|
|
535
|
+
debug("flow reorder ignored: no core workspace order available");
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
var inGroup = {};
|
|
540
|
+
oldGroupWorkspaceOrder.forEach(function(workspaceId) {
|
|
541
|
+
inGroup[workspaceId] = true;
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
var queue = newGroupWorkspaceOrder.slice();
|
|
545
|
+
var updatedWorkspaceOrder = coreWorkspaceOrder.map(function(workspaceId) {
|
|
546
|
+
if (inGroup[workspaceId]) {
|
|
547
|
+
return queue.shift();
|
|
548
|
+
}
|
|
549
|
+
return workspaceId;
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
if (arraysEqual(coreWorkspaceOrder, updatedWorkspaceOrder)) {
|
|
553
|
+
debug("flow reorder ignored: computed global order unchanged", {
|
|
554
|
+
order: updatedWorkspaceOrder
|
|
555
|
+
});
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (!RED.workspaces || typeof RED.workspaces.order !== "function") {
|
|
560
|
+
warn("Grouped tabs cannot reorder flows because RED.workspaces.order is unavailable.");
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
state.applyingFlowReorder = true;
|
|
565
|
+
debug("applying flow reorder via RED.workspaces.order", {
|
|
566
|
+
group: activeGroupKey,
|
|
567
|
+
oldGroupOrder: oldGroupWorkspaceOrder,
|
|
568
|
+
newGroupOrder: newGroupWorkspaceOrder,
|
|
569
|
+
globalOrder: updatedWorkspaceOrder
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
RED.workspaces.order(updatedWorkspaceOrder);
|
|
574
|
+
} finally {
|
|
575
|
+
setTimeout(function() {
|
|
576
|
+
state.applyingFlowReorder = false;
|
|
577
|
+
scheduleRebuild();
|
|
578
|
+
}, 0);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function applyGroupReorder(newOrder) {
|
|
583
|
+
if (state.applyingGroupReorder) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (!Array.isArray(newOrder) || !newOrder.length) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (!state.model.groups || state.model.groups.length < 2) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
var oldGroupOrder = state.model.groups.map(function(group) {
|
|
595
|
+
return group.key;
|
|
596
|
+
});
|
|
597
|
+
var newGroupOrder = newOrder.map(function(tabId) {
|
|
598
|
+
return state.groupTabGroupMap[tabId];
|
|
599
|
+
}).filter(function(groupKey) {
|
|
600
|
+
return !!groupKey;
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
if (newGroupOrder.length !== oldGroupOrder.length) {
|
|
604
|
+
debug("group reorder ignored: mapping mismatch", {
|
|
605
|
+
oldOrder: oldGroupOrder,
|
|
606
|
+
newOrder: newOrder,
|
|
607
|
+
mapped: newGroupOrder
|
|
608
|
+
});
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (arraysEqual(oldGroupOrder, newGroupOrder)) {
|
|
613
|
+
debug("group reorder ignored: no effective change", {
|
|
614
|
+
order: newGroupOrder
|
|
615
|
+
});
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
var groupFlowsByName = {};
|
|
620
|
+
state.model.groups.forEach(function(group) {
|
|
621
|
+
groupFlowsByName[group.key] = group.flows.map(function(flow) {
|
|
622
|
+
return flow.workspaceId;
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
var reorderedWorkspaceOrder = [];
|
|
627
|
+
newGroupOrder.forEach(function(groupKey) {
|
|
628
|
+
reorderedWorkspaceOrder = reorderedWorkspaceOrder.concat(groupFlowsByName[groupKey] || []);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
var coreWorkspaceOrder = readCoreTabs().map(function(item) {
|
|
632
|
+
return item.workspaceId;
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
if (!coreWorkspaceOrder.length) {
|
|
636
|
+
debug("group reorder ignored: no core workspace order available");
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (reorderedWorkspaceOrder.length !== coreWorkspaceOrder.length) {
|
|
641
|
+
debug("group reorder ignored: computed workspace size mismatch", {
|
|
642
|
+
coreOrder: coreWorkspaceOrder,
|
|
643
|
+
reordered: reorderedWorkspaceOrder
|
|
644
|
+
});
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (arraysEqual(coreWorkspaceOrder, reorderedWorkspaceOrder)) {
|
|
649
|
+
debug("group reorder ignored: global order unchanged", {
|
|
650
|
+
order: reorderedWorkspaceOrder
|
|
651
|
+
});
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (!RED.workspaces || typeof RED.workspaces.order !== "function") {
|
|
656
|
+
warn("Grouped tabs cannot reorder groups because RED.workspaces.order is unavailable.");
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
var groupsByName = {};
|
|
661
|
+
state.model.groups.forEach(function(group) {
|
|
662
|
+
groupsByName[group.key] = group;
|
|
663
|
+
});
|
|
664
|
+
state.model.groups = newGroupOrder.map(function(groupKey) {
|
|
665
|
+
return groupsByName[groupKey];
|
|
666
|
+
}).filter(function(group) {
|
|
667
|
+
return !!group;
|
|
668
|
+
});
|
|
669
|
+
state.lastRenderedGroupSignature = getGroupSignature(state.model.groups);
|
|
670
|
+
|
|
671
|
+
state.applyingGroupReorder = true;
|
|
672
|
+
state.skipNextFlowsReorderRebuild = true;
|
|
673
|
+
debug("applying group reorder via RED.workspaces.order", {
|
|
674
|
+
oldGroupOrder: oldGroupOrder,
|
|
675
|
+
newGroupOrder: newGroupOrder,
|
|
676
|
+
globalOrder: reorderedWorkspaceOrder
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
RED.workspaces.order(reorderedWorkspaceOrder);
|
|
681
|
+
} finally {
|
|
682
|
+
setTimeout(function() {
|
|
683
|
+
state.applyingGroupReorder = false;
|
|
684
|
+
state.skipNextFlowsReorderRebuild = false;
|
|
685
|
+
}, GROUP_REORDER_SETTLE_MS);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function activateCoreWorkspace(workspaceId) {
|
|
690
|
+
if (!workspaceId) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (workspaceId === getActiveWorkspaceIdFromCore()) {
|
|
695
|
+
debug("activateCoreWorkspace skipped: already active", { workspaceId: workspaceId });
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
state.lastKnownActiveWorkspaceId = workspaceId;
|
|
700
|
+
debug("activateCoreWorkspace", { workspaceId: workspaceId });
|
|
701
|
+
|
|
702
|
+
if (RED.workspaces && typeof RED.workspaces.show === "function") {
|
|
703
|
+
debug("activateCoreWorkspace using RED.workspaces.show", { workspaceId: workspaceId });
|
|
704
|
+
RED.workspaces.show(workspaceId);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
var coreLink = $("#" + ORIGINAL_UL_ID + " a.red-ui-tab-label").filter(function() {
|
|
709
|
+
return ($(this).attr("href") || "") === ("#" + workspaceId);
|
|
710
|
+
}).first();
|
|
711
|
+
|
|
712
|
+
if (!coreLink.length) {
|
|
713
|
+
if (RED.workspaces && typeof RED.workspaces.show === "function") {
|
|
714
|
+
debug("activateCoreWorkspace fallback RED.workspaces.show", { workspaceId: workspaceId });
|
|
715
|
+
RED.workspaces.show(workspaceId);
|
|
716
|
+
}
|
|
717
|
+
debug("activateCoreWorkspace failed: core link not found", { workspaceId: workspaceId });
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
debug("activateCoreWorkspace fallback synthetic click", { workspaceId: workspaceId });
|
|
722
|
+
coreLink.trigger($.Event("mousedown", { which: 1 }));
|
|
723
|
+
coreLink.trigger($.Event("mouseup", { which: 1 }));
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function performOpenWorkspaceEditDialog(workspaceId) {
|
|
727
|
+
if (!workspaceId) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
var handled = false;
|
|
732
|
+
var workspace = null;
|
|
733
|
+
var subflow = null;
|
|
734
|
+
|
|
735
|
+
if (RED.workspaces && typeof RED.workspaces.edit === "function") {
|
|
736
|
+
try {
|
|
737
|
+
debug("openWorkspaceEditDialog via RED.workspaces.edit", { workspaceId: workspaceId });
|
|
738
|
+
RED.workspaces.edit(workspaceId);
|
|
739
|
+
return;
|
|
740
|
+
} catch (err) {
|
|
741
|
+
debug("openWorkspaceEditDialog RED.workspaces.edit failed", {
|
|
742
|
+
workspaceId: workspaceId,
|
|
743
|
+
error: err && err.message ? err.message : err
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
workspace = getWorkspaceById(workspaceId);
|
|
749
|
+
|
|
750
|
+
if (!workspace) {
|
|
751
|
+
if (RED.nodes && typeof RED.nodes.subflow === "function") {
|
|
752
|
+
subflow = RED.nodes.subflow(workspaceId);
|
|
753
|
+
}
|
|
754
|
+
if (subflow && RED.editor && typeof RED.editor.editSubflow === "function") {
|
|
755
|
+
debug("openWorkspaceEditDialog via RED.editor.editSubflow", { workspaceId: workspaceId });
|
|
756
|
+
RED.editor.editSubflow(subflow);
|
|
757
|
+
handled = true;
|
|
758
|
+
}
|
|
759
|
+
} else if (!workspace.locked && RED.editor && typeof RED.editor.editFlow === "function") {
|
|
760
|
+
debug("openWorkspaceEditDialog via RED.editor.editFlow", { workspaceId: workspaceId });
|
|
761
|
+
RED.editor.editFlow(workspace);
|
|
762
|
+
handled = true;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (handled) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
var coreLink = $("#" + ORIGINAL_UL_ID + " a.red-ui-tab-label").filter(function() {
|
|
770
|
+
return ($(this).attr("href") || "") === ("#" + workspaceId);
|
|
771
|
+
}).first();
|
|
772
|
+
|
|
773
|
+
if (coreLink.length) {
|
|
774
|
+
debug("openWorkspaceEditDialog via core link dblclick fallback", { workspaceId: workspaceId });
|
|
775
|
+
coreLink.trigger($.Event("dblclick", { which: 1 }));
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
debug("openWorkspaceEditDialog failed: no edit path available", {
|
|
780
|
+
workspaceId: workspaceId,
|
|
781
|
+
hasWorkspace: !!workspace,
|
|
782
|
+
hasSubflow: !!subflow,
|
|
783
|
+
hasWorkspacesEdit: !!(RED.workspaces && typeof RED.workspaces.edit === "function"),
|
|
784
|
+
hasEditorEditFlow: !!(RED.editor && typeof RED.editor.editFlow === "function"),
|
|
785
|
+
hasEditorEditSubflow: !!(RED.editor && typeof RED.editor.editSubflow === "function")
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function openWorkspaceEditDialog(workspaceId) {
|
|
790
|
+
if (!workspaceId) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (state.pendingEditOpenTimer) {
|
|
795
|
+
clearTimeout(state.pendingEditOpenTimer);
|
|
796
|
+
state.pendingEditOpenTimer = null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Defer edit opening slightly so tab activation/click handlers finish first.
|
|
800
|
+
state.pendingEditOpenTimer = setTimeout(function() {
|
|
801
|
+
state.pendingEditOpenTimer = null;
|
|
802
|
+
performOpenWorkspaceEditDialog(workspaceId);
|
|
803
|
+
}, EDIT_DIALOG_DEFER_MS);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function markFlowTabWorkspaces() {
|
|
807
|
+
var flowsUl = $("#" + FLOWS_UL_ID);
|
|
808
|
+
if (!flowsUl.length) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
flowsUl.children("li.red-ui-tab").each(function(index, el) {
|
|
813
|
+
var li = $(el);
|
|
814
|
+
var href = $(this).children("a.red-ui-tab-label").attr("href") || "";
|
|
815
|
+
if (href.charAt(0) !== "#") {
|
|
816
|
+
li.removeAttr("data-workspace-id");
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
var tabId = href.slice(1);
|
|
820
|
+
var workspaceId = state.flowTabWorkspaceMap[tabId];
|
|
821
|
+
if (workspaceId) {
|
|
822
|
+
li.attr("data-workspace-id", workspaceId);
|
|
823
|
+
} else {
|
|
824
|
+
li.removeAttr("data-workspace-id");
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function resetFlowTabsState() {
|
|
830
|
+
state.suspendFlowChange = true;
|
|
831
|
+
clearTabs(state.flowTabs);
|
|
832
|
+
state.suspendFlowChange = false;
|
|
833
|
+
state.flowTabWorkspaceMap = {};
|
|
834
|
+
state.lastRenderedFlowGroup = null;
|
|
835
|
+
state.lastRenderedFlowSignature = "";
|
|
836
|
+
state.lastRenderedActiveFlowTab = null;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function setFlowRowVisible(visible) {
|
|
840
|
+
var flowWrapper = $("#" + FLOWS_UL_ID).closest(".red-ui-tabs");
|
|
841
|
+
if (flowWrapper.length) {
|
|
842
|
+
if (visible) {
|
|
843
|
+
flowWrapper.show();
|
|
844
|
+
} else {
|
|
845
|
+
flowWrapper.hide();
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
$("#" + WORKSPACE_ROOT_ID).toggleClass(WORKSPACE_SINGLE_ROW_CLASS, !visible);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function findVisibleDialogForm() {
|
|
852
|
+
var form = $("#dialog-form");
|
|
853
|
+
if (form.length && form.is(":visible")) {
|
|
854
|
+
return form.first();
|
|
855
|
+
}
|
|
856
|
+
form = $(".ui-dialog-content:visible form").first();
|
|
857
|
+
if (form.length) {
|
|
858
|
+
return form;
|
|
859
|
+
}
|
|
860
|
+
return $();
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function persistDialogGroupValue(workspaceId) {
|
|
864
|
+
var workspace = getWorkspaceById(workspaceId);
|
|
865
|
+
var input = $("#" + DIALOG_GROUP_INPUT_ID);
|
|
866
|
+
if (!workspace || !input.length) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
var oldValue = normalizeGroupName(workspace[WORKSPACE_GROUP_PROPERTY]);
|
|
871
|
+
var newValue = normalizeGroupName(input.val());
|
|
872
|
+
if (newValue === oldValue) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (newValue) {
|
|
877
|
+
workspace[WORKSPACE_GROUP_PROPERTY] = newValue;
|
|
878
|
+
} else {
|
|
879
|
+
delete workspace[WORKSPACE_GROUP_PROPERTY];
|
|
880
|
+
}
|
|
881
|
+
setPersistedGroup(workspaceId, newValue);
|
|
882
|
+
if (RED.nodes && typeof RED.nodes.dirty === "function") {
|
|
883
|
+
RED.nodes.dirty(true);
|
|
884
|
+
}
|
|
885
|
+
scheduleRebuild();
|
|
886
|
+
debug("workspace group updated", {
|
|
887
|
+
workspaceId: workspaceId,
|
|
888
|
+
oldGroup: oldValue,
|
|
889
|
+
newGroup: newValue
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function bindDialogGroupSave(workspaceId, form) {
|
|
894
|
+
if (!workspaceId || !form || !form.length) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
var saveButtons = $("#node-dialog-ok, #node-dialog-ok-save, .red-ui-tray-footer button.red-ui-button.primary");
|
|
899
|
+
saveButtons.off(DIALOG_GROUP_SAVE_NS).on("click" + DIALOG_GROUP_SAVE_NS, function() {
|
|
900
|
+
setTimeout(function() {
|
|
901
|
+
persistDialogGroupValue(workspaceId);
|
|
902
|
+
}, 0);
|
|
903
|
+
});
|
|
904
|
+
form.off("submit" + DIALOG_GROUP_SAVE_NS).on("submit" + DIALOG_GROUP_SAVE_NS, function() {
|
|
905
|
+
setTimeout(function() {
|
|
906
|
+
persistDialogGroupValue(workspaceId);
|
|
907
|
+
}, 0);
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function ensureDialogGroupField(workspaceId) {
|
|
912
|
+
var workspace = getWorkspaceById(workspaceId);
|
|
913
|
+
if (!workspace) {
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
var form = findVisibleDialogForm();
|
|
918
|
+
if (!form.length) {
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
var row = form.find("." + DIALOG_GROUP_ROW_CLASS).first();
|
|
923
|
+
if (!row.length) {
|
|
924
|
+
row = $('<div class="form-row ' + DIALOG_GROUP_ROW_CLASS + '"></div>');
|
|
925
|
+
row.append('<label for="' + DIALOG_GROUP_INPUT_ID + '"><i class="fa fa-folder-open"></i> ' + t("Group", "Group") + "</label>");
|
|
926
|
+
row.append('<input type="text" id="' + DIALOG_GROUP_INPUT_ID + '" placeholder="' + t("Optional group", "Optional group") + '">');
|
|
927
|
+
|
|
928
|
+
var infoRow = form.find("#node-input-info").closest(".form-row");
|
|
929
|
+
if (infoRow.length) {
|
|
930
|
+
infoRow.before(row);
|
|
931
|
+
} else {
|
|
932
|
+
var labelRow = form.find("#node-input-label").closest(".form-row");
|
|
933
|
+
if (labelRow.length) {
|
|
934
|
+
labelRow.after(row);
|
|
935
|
+
} else {
|
|
936
|
+
form.append(row);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
row.find("#" + DIALOG_GROUP_INPUT_ID).val(normalizeGroupName(workspace[WORKSPACE_GROUP_PROPERTY]));
|
|
942
|
+
bindDialogGroupSave(workspaceId, form);
|
|
943
|
+
return true;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function scheduleEnsureDialogGroupField(workspaceId) {
|
|
947
|
+
if (!workspaceId) {
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
var attempts = 0;
|
|
952
|
+
function attempt() {
|
|
953
|
+
if (ensureDialogGroupField(workspaceId)) {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
attempts += 1;
|
|
957
|
+
if (attempts < 20) {
|
|
958
|
+
setTimeout(attempt, 25);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
setTimeout(attempt, 0);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function patchEditFlowDialog() {
|
|
965
|
+
if (state.wrappedEditFlow || !RED.editor || typeof RED.editor.editFlow !== "function") {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
state.originalEditFlow = RED.editor.editFlow;
|
|
970
|
+
state.wrappedEditFlow = function(workspace) {
|
|
971
|
+
var result = state.originalEditFlow.apply(this, arguments);
|
|
972
|
+
var workspaceId = workspace && typeof workspace === "object" ? workspace.id : workspace;
|
|
973
|
+
if (workspaceId) {
|
|
974
|
+
scheduleEnsureDialogGroupField(workspaceId);
|
|
975
|
+
}
|
|
976
|
+
return result;
|
|
977
|
+
};
|
|
978
|
+
RED.editor.editFlow = state.wrappedEditFlow;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function unpatchEditFlowDialog() {
|
|
982
|
+
if (!state.wrappedEditFlow || !state.originalEditFlow || !RED.editor) {
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (RED.editor.editFlow === state.wrappedEditFlow) {
|
|
986
|
+
RED.editor.editFlow = state.originalEditFlow;
|
|
987
|
+
}
|
|
988
|
+
state.wrappedEditFlow = null;
|
|
989
|
+
state.originalEditFlow = null;
|
|
990
|
+
$("#node-dialog-ok, #node-dialog-ok-save, .red-ui-tray-footer button.red-ui-button.primary").off(DIALOG_GROUP_SAVE_NS);
|
|
991
|
+
$("#dialog-form").off(DIALOG_GROUP_SAVE_NS);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function bindTabDomEvents() {
|
|
995
|
+
var groupsUl = $("#" + GROUPS_UL_ID);
|
|
996
|
+
var flowsUl = $("#" + FLOWS_UL_ID);
|
|
997
|
+
|
|
998
|
+
groupsUl.off(".nrGroupedTabs");
|
|
999
|
+
flowsUl.off(".nrGroupedTabs");
|
|
1000
|
+
|
|
1001
|
+
groupsUl.on("mousedown.nrGroupedTabs", "li.red-ui-tab", function() {
|
|
1002
|
+
state.lastFlowPointerWorkspaceId = null;
|
|
1003
|
+
state.lastFlowPointerTime = 0;
|
|
1004
|
+
state.pendingFlowDoubleWorkspaceId = null;
|
|
1005
|
+
debug("group mousedown cleared pending double-click state");
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
flowsUl.on("mousedown.nrGroupedTabs", "li.red-ui-tab", function() {
|
|
1009
|
+
var li = $(this);
|
|
1010
|
+
var tabId = li.data("tabId");
|
|
1011
|
+
if (!tabId) {
|
|
1012
|
+
var href = li.children("a.red-ui-tab-label").attr("href") || "";
|
|
1013
|
+
if (href.charAt(0) === "#") {
|
|
1014
|
+
tabId = href.slice(1);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
var workspaceId = li.attr("data-workspace-id") || state.flowTabWorkspaceMap[tabId];
|
|
1019
|
+
if (!workspaceId) {
|
|
1020
|
+
debug("flow mousedown without workspace mapping", { tabId: tabId });
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
var now = Date.now();
|
|
1025
|
+
if (
|
|
1026
|
+
state.lastFlowPointerWorkspaceId === workspaceId &&
|
|
1027
|
+
(now - state.lastFlowPointerTime) < FLOW_DOUBLE_CLICK_WINDOW_MS
|
|
1028
|
+
) {
|
|
1029
|
+
state.pendingFlowDoubleWorkspaceId = workspaceId;
|
|
1030
|
+
debug("flow mousedown potential double-click armed", { workspaceId: workspaceId });
|
|
1031
|
+
} else {
|
|
1032
|
+
state.pendingFlowDoubleWorkspaceId = null;
|
|
1033
|
+
debug("flow mousedown single click", { workspaceId: workspaceId });
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
state.lastFlowPointerWorkspaceId = workspaceId;
|
|
1037
|
+
state.lastFlowPointerTime = now;
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function unbindTabDomEvents() {
|
|
1042
|
+
$("#" + GROUPS_UL_ID).off(".nrGroupedTabs");
|
|
1043
|
+
$("#" + FLOWS_UL_ID).off(".nrGroupedTabs");
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function renderGroupTabs() {
|
|
1047
|
+
if (!state.groupTabs) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
var currentSignature = getGroupSignature(state.model.groups);
|
|
1052
|
+
var groupsChanged = currentSignature !== state.lastRenderedGroupSignature;
|
|
1053
|
+
|
|
1054
|
+
if (groupsChanged) {
|
|
1055
|
+
state.suspendGroupChange = true;
|
|
1056
|
+
clearTabs(state.groupTabs);
|
|
1057
|
+
state.groupTabGroupMap = {};
|
|
1058
|
+
|
|
1059
|
+
state.model.groups.forEach(function(group) {
|
|
1060
|
+
state.groupTabs.addTab({
|
|
1061
|
+
id: group.tabId,
|
|
1062
|
+
label: group.name,
|
|
1063
|
+
name: group.name,
|
|
1064
|
+
groupKey: group.key
|
|
1065
|
+
});
|
|
1066
|
+
state.groupTabGroupMap[group.tabId] = group.key;
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
state.lastRenderedGroupSignature = currentSignature;
|
|
1070
|
+
state.lastRenderedActiveGroup = null;
|
|
1071
|
+
state.suspendGroupChange = false;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (
|
|
1075
|
+
state.activeGroup &&
|
|
1076
|
+
state.model.groupByName[state.activeGroup] &&
|
|
1077
|
+
state.lastRenderedActiveGroup !== state.activeGroup
|
|
1078
|
+
) {
|
|
1079
|
+
state.suspendGroupChange = true;
|
|
1080
|
+
state.groupTabs.activateTab(state.model.groupByName[state.activeGroup].tabId);
|
|
1081
|
+
state.suspendGroupChange = false;
|
|
1082
|
+
state.lastRenderedActiveGroup = state.activeGroup;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function renderFlowTabs(groupKey, activateFirstIfNeeded) {
|
|
1087
|
+
if (!state.flowTabs) {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
var selectedGroup = state.model.groupByName[groupKey];
|
|
1092
|
+
if (!selectedGroup) {
|
|
1093
|
+
setFlowRowVisible(false);
|
|
1094
|
+
resetFlowTabsState();
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (selectedGroup.standalone) {
|
|
1099
|
+
setFlowRowVisible(false);
|
|
1100
|
+
resetFlowTabsState();
|
|
1101
|
+
|
|
1102
|
+
var standaloneFlow = selectedGroup.flows[0];
|
|
1103
|
+
if (!standaloneFlow) {
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
var activeStandaloneWorkspaceId = state.model.activeWorkspaceId || getActiveWorkspaceIdFromCore();
|
|
1108
|
+
if (
|
|
1109
|
+
activateFirstIfNeeded &&
|
|
1110
|
+
standaloneFlow.workspaceId &&
|
|
1111
|
+
standaloneFlow.workspaceId !== activeStandaloneWorkspaceId
|
|
1112
|
+
) {
|
|
1113
|
+
setTimeout(function() {
|
|
1114
|
+
activateCoreWorkspace(standaloneFlow.workspaceId);
|
|
1115
|
+
}, 0);
|
|
1116
|
+
}
|
|
1117
|
+
state.lastWorkspaceByGroup[groupKey] = standaloneFlow.workspaceId;
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
setFlowRowVisible(true);
|
|
1122
|
+
|
|
1123
|
+
var currentFlowSignature = getFlowSignature(selectedGroup.flows);
|
|
1124
|
+
var flowsChanged = (
|
|
1125
|
+
state.lastRenderedFlowGroup !== groupKey ||
|
|
1126
|
+
state.lastRenderedFlowSignature !== currentFlowSignature
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
if (flowsChanged) {
|
|
1130
|
+
state.suspendFlowChange = true;
|
|
1131
|
+
clearTabs(state.flowTabs);
|
|
1132
|
+
state.flowTabWorkspaceMap = {};
|
|
1133
|
+
|
|
1134
|
+
selectedGroup.flows.forEach(function(flow) {
|
|
1135
|
+
state.flowTabs.addTab({
|
|
1136
|
+
id: flow.flowTabId,
|
|
1137
|
+
label: flow.label,
|
|
1138
|
+
name: flow.fullLabel,
|
|
1139
|
+
fullLabel: flow.fullLabel,
|
|
1140
|
+
workspaceId: flow.workspaceId,
|
|
1141
|
+
disabled: flow.disabled,
|
|
1142
|
+
locked: flow.locked,
|
|
1143
|
+
changed: flow.changed
|
|
1144
|
+
});
|
|
1145
|
+
state.flowTabWorkspaceMap[flow.flowTabId] = flow.workspaceId;
|
|
1146
|
+
});
|
|
1147
|
+
markFlowTabWorkspaces();
|
|
1148
|
+
|
|
1149
|
+
state.lastRenderedFlowGroup = groupKey;
|
|
1150
|
+
state.lastRenderedFlowSignature = currentFlowSignature;
|
|
1151
|
+
state.lastRenderedActiveFlowTab = null;
|
|
1152
|
+
state.suspendFlowChange = false;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
var activeWorkspaceId = state.model.activeWorkspaceId || getActiveWorkspaceIdFromCore();
|
|
1156
|
+
var selectedFlow = null;
|
|
1157
|
+
var activeFlowInGroup = null;
|
|
1158
|
+
var rememberedWorkspaceId = state.lastWorkspaceByGroup[groupKey];
|
|
1159
|
+
|
|
1160
|
+
if (activeWorkspaceId) {
|
|
1161
|
+
activeFlowInGroup = selectedGroup.flows.find(function(flow) {
|
|
1162
|
+
return flow.workspaceId === activeWorkspaceId;
|
|
1163
|
+
}) || null;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
if (activeFlowInGroup) {
|
|
1167
|
+
selectedFlow = activeFlowInGroup;
|
|
1168
|
+
} else if (rememberedWorkspaceId) {
|
|
1169
|
+
selectedFlow = selectedGroup.flows.find(function(flow) {
|
|
1170
|
+
return flow.workspaceId === rememberedWorkspaceId;
|
|
1171
|
+
}) || null;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (!selectedFlow && selectedGroup.flows.length) {
|
|
1175
|
+
selectedFlow = selectedGroup.flows[0];
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (
|
|
1179
|
+
activateFirstIfNeeded &&
|
|
1180
|
+
selectedFlow &&
|
|
1181
|
+
selectedFlow.workspaceId !== activeWorkspaceId
|
|
1182
|
+
) {
|
|
1183
|
+
setTimeout(function() {
|
|
1184
|
+
activateCoreWorkspace(selectedFlow.workspaceId);
|
|
1185
|
+
}, 0);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (selectedFlow) {
|
|
1189
|
+
if (state.lastRenderedActiveFlowTab !== selectedFlow.flowTabId) {
|
|
1190
|
+
state.suspendFlowChange = true;
|
|
1191
|
+
state.flowTabs.activateTab(selectedFlow.flowTabId);
|
|
1192
|
+
state.suspendFlowChange = false;
|
|
1193
|
+
state.lastRenderedActiveFlowTab = selectedFlow.flowTabId;
|
|
1194
|
+
}
|
|
1195
|
+
state.lastWorkspaceByGroup[groupKey] = selectedFlow.workspaceId;
|
|
1196
|
+
} else {
|
|
1197
|
+
state.lastRenderedActiveFlowTab = null;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function onGroupChange(groupKey) {
|
|
1202
|
+
state.activeGroup = groupKey;
|
|
1203
|
+
state.lastRenderedActiveGroup = groupKey;
|
|
1204
|
+
|
|
1205
|
+
// Group click shows and activates the group's currently remembered flow (or first flow).
|
|
1206
|
+
renderFlowTabs(groupKey, true);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function rebuild() {
|
|
1210
|
+
if (!mount()) {
|
|
1211
|
+
waitForCoreTabs();
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
state.model = buildModel();
|
|
1216
|
+
state.lastKnownActiveWorkspaceId = state.model.activeWorkspaceId || state.lastKnownActiveWorkspaceId;
|
|
1217
|
+
rememberWorkspaceForGroup(state.model.activeWorkspaceId);
|
|
1218
|
+
pruneGroupWorkspaceMemory();
|
|
1219
|
+
|
|
1220
|
+
if (state.model.activeWorkspaceId) {
|
|
1221
|
+
var groupFromActive = state.model.workspaceToGroup[state.model.activeWorkspaceId];
|
|
1222
|
+
if (groupFromActive) {
|
|
1223
|
+
state.activeGroup = groupFromActive;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (!state.activeGroup || !state.model.groupByName[state.activeGroup]) {
|
|
1228
|
+
state.activeGroup = state.model.groups.length ? state.model.groups[0].key : null;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
renderGroupTabs();
|
|
1232
|
+
renderFlowTabs(state.activeGroup, false);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function scheduleRebuild() {
|
|
1236
|
+
if (state.rebuildTimer) {
|
|
1237
|
+
clearTimeout(state.rebuildTimer);
|
|
1238
|
+
}
|
|
1239
|
+
state.rebuildTimer = setTimeout(rebuild, 0);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function bindEvents() {
|
|
1243
|
+
if (!RED.events || state.eventBindings.length) {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
[
|
|
1248
|
+
"flows:add",
|
|
1249
|
+
"flows:remove",
|
|
1250
|
+
"flows:change",
|
|
1251
|
+
"flows:reorder",
|
|
1252
|
+
"workspace:change",
|
|
1253
|
+
"workspace:show",
|
|
1254
|
+
"workspace:hide"
|
|
1255
|
+
].forEach(function(name) {
|
|
1256
|
+
var handler = function(activeWorkspaceId) {
|
|
1257
|
+
if (name === "flows:reorder" && state.skipNextFlowsReorderRebuild) {
|
|
1258
|
+
state.skipNextFlowsReorderRebuild = false;
|
|
1259
|
+
debug("flows:reorder rebuild skipped (internal group reorder)");
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
if (name === "workspace:change") {
|
|
1264
|
+
if (typeof activeWorkspaceId === "string" && activeWorkspaceId) {
|
|
1265
|
+
state.lastKnownActiveWorkspaceId = activeWorkspaceId;
|
|
1266
|
+
rememberWorkspaceForGroup(activeWorkspaceId);
|
|
1267
|
+
} else if (
|
|
1268
|
+
activeWorkspaceId &&
|
|
1269
|
+
typeof activeWorkspaceId === "object" &&
|
|
1270
|
+
typeof activeWorkspaceId.id === "string" &&
|
|
1271
|
+
activeWorkspaceId.id
|
|
1272
|
+
) {
|
|
1273
|
+
state.lastKnownActiveWorkspaceId = activeWorkspaceId.id;
|
|
1274
|
+
rememberWorkspaceForGroup(activeWorkspaceId.id);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
scheduleRebuild();
|
|
1278
|
+
};
|
|
1279
|
+
RED.events.on(name, handler);
|
|
1280
|
+
state.eventBindings.push({ name: name, handler: handler });
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function unbindEvents() {
|
|
1285
|
+
if (!RED.events || !state.eventBindings.length) {
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
var off = RED.events.off || RED.events.removeListener;
|
|
1290
|
+
if (typeof off === "function") {
|
|
1291
|
+
state.eventBindings.forEach(function(binding) {
|
|
1292
|
+
off.call(RED.events, binding.name, binding.handler);
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
state.eventBindings = [];
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function mount() {
|
|
1300
|
+
if (!RED.tabs || typeof RED.tabs.create !== "function") {
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
var originalUl = $("#" + ORIGINAL_UL_ID);
|
|
1305
|
+
if (!originalUl.length) {
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
var originalWrapper = originalUl.closest(".red-ui-tabs");
|
|
1310
|
+
if (!originalWrapper.length) {
|
|
1311
|
+
return false;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
state.originalWrapper = originalWrapper;
|
|
1315
|
+
|
|
1316
|
+
if (!state.container || !state.container.length) {
|
|
1317
|
+
state.container = $('<div id="' + CONTAINER_ID + '" class="nr-grouped-flows"></div>');
|
|
1318
|
+
state.container.append('<ul id="' + GROUPS_UL_ID + '"></ul>');
|
|
1319
|
+
state.container.append('<ul id="' + FLOWS_UL_ID + '"></ul>');
|
|
1320
|
+
|
|
1321
|
+
originalWrapper.after(state.container);
|
|
1322
|
+
|
|
1323
|
+
state.groupTabs = RED.tabs.create({
|
|
1324
|
+
id: GROUPS_UL_ID,
|
|
1325
|
+
scrollable: true,
|
|
1326
|
+
sortable: true,
|
|
1327
|
+
addButton: addFlowEnabled() ? "core:add-flow" : undefined,
|
|
1328
|
+
addButtonCaption: t("workspace.addFlow", "Add flow"),
|
|
1329
|
+
menu: buildFlowMenu,
|
|
1330
|
+
onreorder: function(oldOrder, newOrder) {
|
|
1331
|
+
debug("group onreorder", {
|
|
1332
|
+
oldOrder: oldOrder,
|
|
1333
|
+
newOrder: newOrder
|
|
1334
|
+
});
|
|
1335
|
+
applyGroupReorder(newOrder);
|
|
1336
|
+
},
|
|
1337
|
+
onchange: function(tab) {
|
|
1338
|
+
if (state.suspendGroupChange || !tab) {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
onGroupChange(tab.groupKey);
|
|
1342
|
+
},
|
|
1343
|
+
ondblclick: function(tab) {
|
|
1344
|
+
if (!tab || !tab.groupKey) {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
var entry = state.model.groupByName[tab.groupKey];
|
|
1348
|
+
if (!entry || !entry.standalone || !entry.standaloneWorkspaceId) {
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
openWorkspaceEditDialog(entry.standaloneWorkspaceId);
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
state.flowTabs = RED.tabs.create({
|
|
1356
|
+
id: FLOWS_UL_ID,
|
|
1357
|
+
scrollable: true,
|
|
1358
|
+
sortable: true,
|
|
1359
|
+
onreorder: function(oldOrder, newOrder) {
|
|
1360
|
+
debug("flow onreorder", {
|
|
1361
|
+
oldOrder: oldOrder,
|
|
1362
|
+
newOrder: newOrder
|
|
1363
|
+
});
|
|
1364
|
+
applyFlowReorder(newOrder);
|
|
1365
|
+
},
|
|
1366
|
+
onchange: function(tab) {
|
|
1367
|
+
if (state.suspendFlowChange || !tab || !tab.workspaceId) {
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
activateCoreWorkspace(tab.workspaceId);
|
|
1371
|
+
},
|
|
1372
|
+
ondblclick: function(tab) {
|
|
1373
|
+
if (!tab || !tab.workspaceId) {
|
|
1374
|
+
debug("flow ondblclick ignored: missing tab/workspace");
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
debug("flow ondblclick received", {
|
|
1378
|
+
workspaceId: tab.workspaceId,
|
|
1379
|
+
pending: state.pendingFlowDoubleWorkspaceId
|
|
1380
|
+
});
|
|
1381
|
+
if (state.pendingFlowDoubleWorkspaceId !== tab.workspaceId) {
|
|
1382
|
+
debug("flow ondblclick blocked by guard", {
|
|
1383
|
+
workspaceId: tab.workspaceId,
|
|
1384
|
+
pending: state.pendingFlowDoubleWorkspaceId
|
|
1385
|
+
});
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
state.pendingFlowDoubleWorkspaceId = null;
|
|
1389
|
+
debug("flow ondblclick accepted", { workspaceId: tab.workspaceId });
|
|
1390
|
+
openWorkspaceEditDialog(tab.workspaceId);
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
bindTabDomEvents();
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
patchEditFlowDialog();
|
|
1398
|
+
state.originalWrapper.hide();
|
|
1399
|
+
state.container.show();
|
|
1400
|
+
$("#" + WORKSPACE_ROOT_ID).addClass(WORKSPACE_ENABLED_CLASS);
|
|
1401
|
+
return true;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function waitForCoreTabs() {
|
|
1405
|
+
if (state.retryTimer) {
|
|
1406
|
+
clearTimeout(state.retryTimer);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
if (mount()) {
|
|
1410
|
+
state.retryCount = 0;
|
|
1411
|
+
bindEvents();
|
|
1412
|
+
rebuild();
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
state.retryCount += 1;
|
|
1417
|
+
if (!state.didWarnUnavailable && state.retryCount >= WARN_CORE_TABS_RETRY_AT) {
|
|
1418
|
+
state.didWarnUnavailable = true;
|
|
1419
|
+
warn("Grouped tabs plugin loaded, but core workspace tabs were not found.");
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
state.retryTimer = setTimeout(waitForCoreTabs, BOOT_RETRY_INTERVAL_MS);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function start() {
|
|
1426
|
+
waitForCoreTabs();
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function stop() {
|
|
1430
|
+
if (state.retryTimer) {
|
|
1431
|
+
clearTimeout(state.retryTimer);
|
|
1432
|
+
state.retryTimer = null;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (state.rebuildTimer) {
|
|
1436
|
+
clearTimeout(state.rebuildTimer);
|
|
1437
|
+
state.rebuildTimer = null;
|
|
1438
|
+
}
|
|
1439
|
+
if (state.pendingEditOpenTimer) {
|
|
1440
|
+
clearTimeout(state.pendingEditOpenTimer);
|
|
1441
|
+
state.pendingEditOpenTimer = null;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
unbindEvents();
|
|
1445
|
+
unbindTabDomEvents();
|
|
1446
|
+
unpatchEditFlowDialog();
|
|
1447
|
+
|
|
1448
|
+
if (state.container && state.container.length) {
|
|
1449
|
+
state.container.remove();
|
|
1450
|
+
state.container = null;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
if (state.originalWrapper && state.originalWrapper.length) {
|
|
1454
|
+
state.originalWrapper.show();
|
|
1455
|
+
}
|
|
1456
|
+
$("#" + WORKSPACE_ROOT_ID).removeClass(WORKSPACE_ENABLED_CLASS);
|
|
1457
|
+
$("#" + WORKSPACE_ROOT_ID).removeClass(WORKSPACE_SINGLE_ROW_CLASS);
|
|
1458
|
+
|
|
1459
|
+
state.groupTabs = null;
|
|
1460
|
+
state.flowTabs = null;
|
|
1461
|
+
state.model = {
|
|
1462
|
+
groups: [],
|
|
1463
|
+
groupByName: {},
|
|
1464
|
+
workspaceToGroup: {},
|
|
1465
|
+
activeWorkspaceId: null
|
|
1466
|
+
};
|
|
1467
|
+
state.activeGroup = null;
|
|
1468
|
+
state.lastWorkspaceByGroup = {};
|
|
1469
|
+
state.groupTabGroupMap = {};
|
|
1470
|
+
state.flowTabWorkspaceMap = {};
|
|
1471
|
+
state.lastFlowPointerWorkspaceId = null;
|
|
1472
|
+
state.lastFlowPointerTime = 0;
|
|
1473
|
+
state.pendingFlowDoubleWorkspaceId = null;
|
|
1474
|
+
state.pendingEditOpenTimer = null;
|
|
1475
|
+
state.applyingFlowReorder = false;
|
|
1476
|
+
state.applyingGroupReorder = false;
|
|
1477
|
+
state.skipNextFlowsReorderRebuild = false;
|
|
1478
|
+
state.lastRenderedGroupSignature = "";
|
|
1479
|
+
state.lastRenderedActiveGroup = null;
|
|
1480
|
+
state.lastRenderedFlowGroup = null;
|
|
1481
|
+
state.lastRenderedFlowSignature = "";
|
|
1482
|
+
state.lastRenderedActiveFlowTab = null;
|
|
1483
|
+
state.wrappedEditFlow = null;
|
|
1484
|
+
state.originalEditFlow = null;
|
|
1485
|
+
state.persistedGroups = null;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
return {
|
|
1489
|
+
start: start,
|
|
1490
|
+
stop: stop
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
var controller;
|
|
1495
|
+
|
|
1496
|
+
function startController() {
|
|
1497
|
+
if (controller) {
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
try {
|
|
1501
|
+
controller = createController();
|
|
1502
|
+
controller.start();
|
|
1503
|
+
} catch (err) {
|
|
1504
|
+
controller = null;
|
|
1505
|
+
if (window.console && typeof console.error === "function") {
|
|
1506
|
+
console.error("[nr-grouped-workspace-tabs] failed to start", err);
|
|
1507
|
+
}
|
|
1508
|
+
if (RED.notify && typeof RED.notify === "function") {
|
|
1509
|
+
RED.notify("Grouped tabs plugin failed to start. Check browser console.", "error");
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function stopController() {
|
|
1515
|
+
if (!controller) {
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
controller.stop();
|
|
1519
|
+
controller = null;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function registerPlugin() {
|
|
1523
|
+
if (pluginRegistered) {
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
if (!window.RED || !RED.plugins || typeof RED.plugins.registerPlugin !== "function") {
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
RED.plugins.registerPlugin(PLUGIN_ID, {
|
|
1531
|
+
onadd: function() {
|
|
1532
|
+
startController();
|
|
1533
|
+
},
|
|
1534
|
+
onremove: function() {
|
|
1535
|
+
stopController();
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
pluginRegistered = true;
|
|
1539
|
+
|
|
1540
|
+
// Keep a direct startup path for environments where plugin onadd does not fire.
|
|
1541
|
+
if (document.readyState === "loading") {
|
|
1542
|
+
document.addEventListener("DOMContentLoaded", startController);
|
|
1543
|
+
} else {
|
|
1544
|
+
startController();
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function boot() {
|
|
1549
|
+
var tries = 0;
|
|
1550
|
+
|
|
1551
|
+
function attemptRegister() {
|
|
1552
|
+
registerPlugin();
|
|
1553
|
+
if (pluginRegistered) {
|
|
1554
|
+
if (window.console && typeof console.info === "function") {
|
|
1555
|
+
console.info("[nr-grouped-workspace-tabs] registered v" + PLUGIN_VERSION);
|
|
1556
|
+
if (isDebugEnabled()) {
|
|
1557
|
+
console.info("[nr-grouped-workspace-tabs] debug mode active");
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
tries += 1;
|
|
1563
|
+
if (tries >= BOOT_MAX_RETRIES) {
|
|
1564
|
+
if (window.console && typeof console.warn === "function") {
|
|
1565
|
+
console.warn("[nr-grouped-workspace-tabs] failed to register plugin API");
|
|
1566
|
+
}
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
setTimeout(attemptRegister, BOOT_RETRY_INTERVAL_MS);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
attemptRegister();
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
if (typeof window !== "undefined") {
|
|
1576
|
+
window.nrGroupedTabsDebug = function(enabled) {
|
|
1577
|
+
setDebugEnabled(!!enabled);
|
|
1578
|
+
if (window.console && typeof console.info === "function") {
|
|
1579
|
+
console.info("[nr-grouped-workspace-tabs] debug " + (enabled ? "enabled" : "disabled") + "; reload the page");
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
boot();
|
|
1585
|
+
})();
|
|
1586
|
+
</script>
|
|
1587
|
+
|
|
1588
|
+
<style>
|
|
1589
|
+
#nr-grouped-flows {
|
|
1590
|
+
display: none;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
#red-ui-workspace.nr-grouped-flows-enabled #nr-grouped-flows {
|
|
1594
|
+
position: absolute;
|
|
1595
|
+
top: 0;
|
|
1596
|
+
left: 0;
|
|
1597
|
+
right: 0;
|
|
1598
|
+
z-index: 3;
|
|
1599
|
+
display: flex;
|
|
1600
|
+
flex-direction: column;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
#nr-grouped-flows > .red-ui-tabs {
|
|
1604
|
+
margin-bottom: 0;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
#nr-grouped-flows > .red-ui-tabs + .red-ui-tabs {
|
|
1608
|
+
margin-top: 2px;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
#red-ui-workspace.nr-grouped-flows-enabled #red-ui-workspace-chart {
|
|
1612
|
+
top: 72px;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
#red-ui-workspace.nr-grouped-flows-enabled #red-ui-workspace-tabs-shade {
|
|
1616
|
+
height: 72px;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
#red-ui-workspace.nr-grouped-flows-enabled.nr-grouped-flows-single-row #red-ui-workspace-chart {
|
|
1620
|
+
top: 36px;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
#red-ui-workspace.nr-grouped-flows-enabled.nr-grouped-flows-single-row #red-ui-workspace-tabs-shade {
|
|
1624
|
+
height: 36px;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
</style>
|
|
1628
|
+
|