@aaqu/fromcubes-portal-react 0.1.0-alpha.2 → 0.1.0-alpha.20
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 +1 -1
- package/README.md +154 -79
- package/examples/001-shared-components-flow.json +68 -0
- package/examples/{sensor-portal-flow.json → 002-sensor-portal-flow.json} +3 -3
- package/examples/003-chart-portal-flow.json +93 -0
- package/examples/004-d3-poland-flow.json +80 -0
- package/examples/005-threejs-portal-flow.json +87 -0
- package/examples/006-pixi-portal-flow.json +86 -0
- package/examples/007-webgpu-tsl-flow.json +85 -0
- package/nodes/lib/assets.js +212 -0
- package/nodes/lib/helpers.js +308 -0
- package/nodes/lib/hooks.js +82 -0
- package/nodes/lib/page-builder.js +219 -0
- package/nodes/lib/router.js +56 -0
- package/nodes/portal-react.html +1139 -195
- package/nodes/portal-react.js +720 -349
- package/package.json +21 -11
- package/nodes/vendor/react-19.production.min.js +0 -55
- package/scripts/bundle-react.js +0 -31
package/nodes/portal-react.html
CHANGED
|
@@ -26,7 +26,10 @@
|
|
|
26
26
|
// Idempotent — safe to call multiple times, only runs setup once.
|
|
27
27
|
function ensureJsxSetup() {
|
|
28
28
|
if (jsxSetupDone) {
|
|
29
|
-
console.log(
|
|
29
|
+
console.log(
|
|
30
|
+
PREFIX,
|
|
31
|
+
"ensureJsxSetup: already done, re-applying compiler+diag only",
|
|
32
|
+
);
|
|
30
33
|
// Always re-apply compiler/diag in case Node-RED reset them
|
|
31
34
|
applyCompilerAndDiag();
|
|
32
35
|
return;
|
|
@@ -36,8 +39,16 @@
|
|
|
36
39
|
var jsDef = monaco.typescript.javascriptDefaults;
|
|
37
40
|
|
|
38
41
|
// Log initial state
|
|
39
|
-
console.log(
|
|
40
|
-
|
|
42
|
+
console.log(
|
|
43
|
+
PREFIX,
|
|
44
|
+
"INITIAL compilerOptions:",
|
|
45
|
+
JSON.stringify(jsDef.getCompilerOptions()),
|
|
46
|
+
);
|
|
47
|
+
console.log(
|
|
48
|
+
PREFIX,
|
|
49
|
+
"INITIAL diagnosticsOptions:",
|
|
50
|
+
JSON.stringify(jsDef.getDiagnosticsOptions()),
|
|
51
|
+
);
|
|
41
52
|
|
|
42
53
|
applyCompilerAndDiag();
|
|
43
54
|
|
|
@@ -45,7 +56,7 @@
|
|
|
45
56
|
var libContent = [
|
|
46
57
|
"declare var React: any;",
|
|
47
58
|
"declare var ReactDOM: any;",
|
|
48
|
-
"declare function useNodeRed(): { data: any; send: (payload: any, topic?: string) => void };",
|
|
59
|
+
"declare function useNodeRed(opts?: { ignoreRecovery?: boolean }): { data: any; send: (payload: any, topic?: string) => void; user: { userId?: string; userName?: string; username?: string; email?: string; role?: string; groups?: any[] } | null; portalClient: string | null };",
|
|
49
60
|
].join("\n");
|
|
50
61
|
console.log(PREFIX, "addExtraLib globals.d.ts");
|
|
51
62
|
jsDef.addExtraLib(libContent, "file:///globals.d.ts");
|
|
@@ -134,7 +145,17 @@
|
|
|
134
145
|
});
|
|
135
146
|
}
|
|
136
147
|
}
|
|
137
|
-
console.log(
|
|
148
|
+
console.log(
|
|
149
|
+
PREFIX,
|
|
150
|
+
"TW completion: ctx=" +
|
|
151
|
+
(isClassName ? "className" : "string") +
|
|
152
|
+
" word='" +
|
|
153
|
+
word +
|
|
154
|
+
"', matched=" +
|
|
155
|
+
suggestions.length +
|
|
156
|
+
"/" +
|
|
157
|
+
classes.length,
|
|
158
|
+
);
|
|
138
159
|
return { suggestions: suggestions };
|
|
139
160
|
},
|
|
140
161
|
});
|
|
@@ -142,19 +163,82 @@
|
|
|
142
163
|
// JSX/HTML tag completion — type tag name, Tab → <tag>|</tag>
|
|
143
164
|
console.log(PREFIX, "registering JSX tag completion provider");
|
|
144
165
|
var HTML_TAGS = [
|
|
145
|
-
"div",
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
166
|
+
"div",
|
|
167
|
+
"span",
|
|
168
|
+
"p",
|
|
169
|
+
"a",
|
|
170
|
+
"button",
|
|
171
|
+
"input",
|
|
172
|
+
"img",
|
|
173
|
+
"form",
|
|
174
|
+
"label",
|
|
175
|
+
"h1",
|
|
176
|
+
"h2",
|
|
177
|
+
"h3",
|
|
178
|
+
"h4",
|
|
179
|
+
"h5",
|
|
180
|
+
"h6",
|
|
181
|
+
"ul",
|
|
182
|
+
"ol",
|
|
183
|
+
"li",
|
|
184
|
+
"dl",
|
|
185
|
+
"dt",
|
|
186
|
+
"dd",
|
|
187
|
+
"table",
|
|
188
|
+
"thead",
|
|
189
|
+
"tbody",
|
|
190
|
+
"tfoot",
|
|
191
|
+
"tr",
|
|
192
|
+
"th",
|
|
193
|
+
"td",
|
|
194
|
+
"section",
|
|
195
|
+
"article",
|
|
196
|
+
"aside",
|
|
197
|
+
"header",
|
|
198
|
+
"footer",
|
|
199
|
+
"nav",
|
|
200
|
+
"main",
|
|
201
|
+
"strong",
|
|
202
|
+
"em",
|
|
203
|
+
"b",
|
|
204
|
+
"i",
|
|
205
|
+
"u",
|
|
206
|
+
"s",
|
|
207
|
+
"small",
|
|
208
|
+
"mark",
|
|
209
|
+
"code",
|
|
210
|
+
"pre",
|
|
211
|
+
"blockquote",
|
|
212
|
+
"br",
|
|
213
|
+
"hr",
|
|
214
|
+
"wbr",
|
|
215
|
+
"select",
|
|
216
|
+
"option",
|
|
217
|
+
"optgroup",
|
|
218
|
+
"textarea",
|
|
219
|
+
"fieldset",
|
|
220
|
+
"legend",
|
|
221
|
+
"details",
|
|
222
|
+
"summary",
|
|
223
|
+
"dialog",
|
|
224
|
+
"canvas",
|
|
225
|
+
"video",
|
|
226
|
+
"audio",
|
|
227
|
+
"source",
|
|
228
|
+
"picture",
|
|
229
|
+
"figure",
|
|
230
|
+
"figcaption",
|
|
231
|
+
"svg",
|
|
232
|
+
"path",
|
|
233
|
+
"circle",
|
|
234
|
+
"rect",
|
|
235
|
+
"line",
|
|
236
|
+
"g",
|
|
237
|
+
"defs",
|
|
238
|
+
"use",
|
|
239
|
+
"text",
|
|
156
240
|
];
|
|
157
|
-
var VOID_TAGS = new Set(["br","hr","img","input","wbr","source"]);
|
|
241
|
+
var VOID_TAGS = new Set(["br", "hr", "img", "input", "wbr", "source"]);
|
|
158
242
|
|
|
159
243
|
monaco.languages.registerCompletionItemProvider("javascript", {
|
|
160
244
|
triggerCharacters: ["<"],
|
|
@@ -165,12 +249,15 @@
|
|
|
165
249
|
// After "<" — suggest tags
|
|
166
250
|
var afterBracket = before.match(/<([a-zA-Z]*)$/);
|
|
167
251
|
// Bare word at line start or after whitespace/{ — Emmet-like (lower or upper)
|
|
168
|
-
var bareWord =
|
|
252
|
+
var bareWord =
|
|
253
|
+
!afterBracket &&
|
|
254
|
+
before.match(/(?:^|[\s{(,])([a-zA-Z][a-zA-Z0-9]*)$/);
|
|
169
255
|
|
|
170
256
|
if (!afterBracket && !bareWord) return { suggestions: [] };
|
|
171
257
|
|
|
172
258
|
var typed = (afterBracket ? afterBracket[1] : bareWord[1]) || "";
|
|
173
|
-
var replaceStart =
|
|
259
|
+
var replaceStart =
|
|
260
|
+
position.column - typed.length - (afterBracket ? 1 : 0);
|
|
174
261
|
|
|
175
262
|
var range = {
|
|
176
263
|
startLineNumber: position.lineNumber,
|
|
@@ -192,7 +279,8 @@
|
|
|
192
279
|
label: tag,
|
|
193
280
|
kind: monaco.languages.CompletionItemKind.Property,
|
|
194
281
|
insertText: "<" + tag + " $0/>",
|
|
195
|
-
insertTextRules:
|
|
282
|
+
insertTextRules:
|
|
283
|
+
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
196
284
|
range: range,
|
|
197
285
|
detail: "<" + tag + " />",
|
|
198
286
|
sortText: "0" + tag,
|
|
@@ -202,7 +290,8 @@
|
|
|
202
290
|
label: tag,
|
|
203
291
|
kind: monaco.languages.CompletionItemKind.Property,
|
|
204
292
|
insertText: "<" + tag + ">$0</" + tag + ">",
|
|
205
|
-
insertTextRules:
|
|
293
|
+
insertTextRules:
|
|
294
|
+
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
206
295
|
range: range,
|
|
207
296
|
detail: "<" + tag + ">…</" + tag + ">",
|
|
208
297
|
sortText: "0" + tag + "a",
|
|
@@ -211,7 +300,8 @@
|
|
|
211
300
|
label: tag,
|
|
212
301
|
kind: monaco.languages.CompletionItemKind.Property,
|
|
213
302
|
insertText: "<" + tag + " $0/>",
|
|
214
|
-
insertTextRules:
|
|
303
|
+
insertTextRules:
|
|
304
|
+
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
215
305
|
range: range,
|
|
216
306
|
detail: "<" + tag + " />",
|
|
217
307
|
sortText: "0" + tag + "b",
|
|
@@ -220,9 +310,15 @@
|
|
|
220
310
|
}
|
|
221
311
|
|
|
222
312
|
// Custom components from registry (PascalCase)
|
|
223
|
-
var upperMatch =
|
|
224
|
-
|
|
225
|
-
var
|
|
313
|
+
var upperMatch =
|
|
314
|
+
afterBracket && before.match(/<([A-Z][a-zA-Z0-9]*)$/);
|
|
315
|
+
var bareUpper =
|
|
316
|
+
!afterBracket && before.match(/(?:^|[\s{(,])([A-Z][a-zA-Z0-9]*)$/);
|
|
317
|
+
var compTyped = upperMatch
|
|
318
|
+
? upperMatch[1]
|
|
319
|
+
: bareUpper
|
|
320
|
+
? bareUpper[1]
|
|
321
|
+
: "";
|
|
226
322
|
|
|
227
323
|
if (compTyped || (!typed && afterBracket)) {
|
|
228
324
|
// Fetch component names from registry (cached on window)
|
|
@@ -231,13 +327,19 @@
|
|
|
231
327
|
var name = reg[c];
|
|
232
328
|
if (seen.has(name)) continue;
|
|
233
329
|
if (compTyped && name.indexOf(compTyped) !== 0) continue;
|
|
234
|
-
if (
|
|
330
|
+
if (
|
|
331
|
+
!compTyped &&
|
|
332
|
+
typed &&
|
|
333
|
+
name.toLowerCase().indexOf(typed) !== 0
|
|
334
|
+
)
|
|
335
|
+
continue;
|
|
235
336
|
seen.add(name);
|
|
236
337
|
suggestions.push({
|
|
237
338
|
label: name,
|
|
238
339
|
kind: monaco.languages.CompletionItemKind.Class,
|
|
239
340
|
insertText: "<" + name + ">$0</" + name + ">",
|
|
240
|
-
insertTextRules:
|
|
341
|
+
insertTextRules:
|
|
342
|
+
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
241
343
|
range: range,
|
|
242
344
|
detail: "<" + name + ">…</" + name + ">",
|
|
243
345
|
sortText: "0" + name + "a",
|
|
@@ -246,7 +348,8 @@
|
|
|
246
348
|
label: name,
|
|
247
349
|
kind: monaco.languages.CompletionItemKind.Class,
|
|
248
350
|
insertText: "<" + name + " $0/>",
|
|
249
|
-
insertTextRules:
|
|
351
|
+
insertTextRules:
|
|
352
|
+
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
250
353
|
range: range,
|
|
251
354
|
detail: "<" + name + " />",
|
|
252
355
|
sortText: "0" + name + "b",
|
|
@@ -258,7 +361,8 @@
|
|
|
258
361
|
label: compTyped,
|
|
259
362
|
kind: monaco.languages.CompletionItemKind.Class,
|
|
260
363
|
insertText: "<" + compTyped + ">$0</" + compTyped + ">",
|
|
261
|
-
insertTextRules:
|
|
364
|
+
insertTextRules:
|
|
365
|
+
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
262
366
|
range: range,
|
|
263
367
|
detail: "<" + compTyped + ">…</" + compTyped + ">",
|
|
264
368
|
sortText: "0" + compTyped + "a",
|
|
@@ -267,7 +371,8 @@
|
|
|
267
371
|
label: compTyped,
|
|
268
372
|
kind: monaco.languages.CompletionItemKind.Class,
|
|
269
373
|
insertText: "<" + compTyped + " $0/>",
|
|
270
|
-
insertTextRules:
|
|
374
|
+
insertTextRules:
|
|
375
|
+
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
271
376
|
range: range,
|
|
272
377
|
detail: "<" + compTyped + " />",
|
|
273
378
|
sortText: "0" + compTyped + "b",
|
|
@@ -275,7 +380,13 @@
|
|
|
275
380
|
}
|
|
276
381
|
}
|
|
277
382
|
|
|
278
|
-
console.log(
|
|
383
|
+
console.log(
|
|
384
|
+
PREFIX,
|
|
385
|
+
"JSX tag completion: typed='" +
|
|
386
|
+
typed +
|
|
387
|
+
"', suggestions=" +
|
|
388
|
+
suggestions.length,
|
|
389
|
+
);
|
|
279
390
|
return { suggestions: suggestions };
|
|
280
391
|
},
|
|
281
392
|
});
|
|
@@ -296,12 +407,14 @@
|
|
|
296
407
|
var m = line.match(/^(.*)<([a-zA-Z][a-zA-Z0-9]*)>\/<\/\2>(.*)$/);
|
|
297
408
|
var matchStr, tag, prefix;
|
|
298
409
|
if (m) {
|
|
299
|
-
prefix = m[1];
|
|
410
|
+
prefix = m[1];
|
|
411
|
+
tag = m[2];
|
|
300
412
|
matchStr = "<" + tag + ">/</" + tag + ">";
|
|
301
413
|
} else {
|
|
302
414
|
m = line.match(/^(.*)<([a-zA-Z][a-zA-Z0-9]*)\/><\/\2>(.*)$/);
|
|
303
415
|
if (!m) return;
|
|
304
|
-
prefix = m[1];
|
|
416
|
+
prefix = m[1];
|
|
417
|
+
tag = m[2];
|
|
305
418
|
matchStr = "<" + tag + "/></" + tag + ">";
|
|
306
419
|
}
|
|
307
420
|
var startCol = prefix.length + 1;
|
|
@@ -309,15 +422,17 @@
|
|
|
309
422
|
var replacement = "<" + tag + " />";
|
|
310
423
|
var cursorCol = startCol + replacement.length - 2;
|
|
311
424
|
setTimeout(function () {
|
|
312
|
-
editor.executeEdits("self-close-collapse", [
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
425
|
+
editor.executeEdits("self-close-collapse", [
|
|
426
|
+
{
|
|
427
|
+
range: {
|
|
428
|
+
startLineNumber: lineNum,
|
|
429
|
+
endLineNumber: lineNum,
|
|
430
|
+
startColumn: startCol,
|
|
431
|
+
endColumn: endCol,
|
|
432
|
+
},
|
|
433
|
+
text: replacement,
|
|
318
434
|
},
|
|
319
|
-
|
|
320
|
-
}]);
|
|
435
|
+
]);
|
|
321
436
|
editor.setPosition({ lineNumber: lineNum, column: cursorCol });
|
|
322
437
|
}, 0);
|
|
323
438
|
});
|
|
@@ -328,7 +443,11 @@
|
|
|
328
443
|
function refreshComponentNames() {
|
|
329
444
|
$.getJSON("portal-react/registry", function (reg) {
|
|
330
445
|
window.__fcComponentNames = Object.keys(reg);
|
|
331
|
-
console.log(
|
|
446
|
+
console.log(
|
|
447
|
+
PREFIX,
|
|
448
|
+
"component names loaded:",
|
|
449
|
+
window.__fcComponentNames,
|
|
450
|
+
);
|
|
332
451
|
}).fail(function () {
|
|
333
452
|
window.__fcComponentNames = [];
|
|
334
453
|
});
|
|
@@ -343,11 +462,19 @@
|
|
|
343
462
|
console.group(PREFIX + " MARKERS on " + uri.toString());
|
|
344
463
|
markers.forEach(function (m) {
|
|
345
464
|
console.log(
|
|
346
|
-
" code=" +
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
465
|
+
" code=" +
|
|
466
|
+
(m.code || "?") +
|
|
467
|
+
" severity=" +
|
|
468
|
+
m.severity +
|
|
469
|
+
" source=" +
|
|
470
|
+
(m.source || "?") +
|
|
471
|
+
" msg=" +
|
|
472
|
+
m.message +
|
|
473
|
+
" [L" +
|
|
474
|
+
m.startLineNumber +
|
|
475
|
+
":" +
|
|
476
|
+
m.startColumn +
|
|
477
|
+
"]",
|
|
351
478
|
);
|
|
352
479
|
});
|
|
353
480
|
console.groupEnd();
|
|
@@ -379,8 +506,16 @@
|
|
|
379
506
|
console.log(PREFIX, "setDiagnosticsOptions:", JSON.stringify(diagOpts));
|
|
380
507
|
jsDef.setDiagnosticsOptions(diagOpts);
|
|
381
508
|
|
|
382
|
-
console.log(
|
|
383
|
-
|
|
509
|
+
console.log(
|
|
510
|
+
PREFIX,
|
|
511
|
+
"readback compilerOptions:",
|
|
512
|
+
JSON.stringify(jsDef.getCompilerOptions()),
|
|
513
|
+
);
|
|
514
|
+
console.log(
|
|
515
|
+
PREFIX,
|
|
516
|
+
"readback diagnosticsOptions:",
|
|
517
|
+
JSON.stringify(jsDef.getDiagnosticsOptions()),
|
|
518
|
+
);
|
|
384
519
|
}
|
|
385
520
|
|
|
386
521
|
// Exported for oneditprepare to call before model creation
|
|
@@ -391,7 +526,10 @@
|
|
|
391
526
|
window.__fcLoadMonaco = function (cb) {
|
|
392
527
|
console.log(PREFIX, "loadMonaco called, monaco exists:", !!window.monaco);
|
|
393
528
|
if (window.monaco) {
|
|
394
|
-
console.log(
|
|
529
|
+
console.log(
|
|
530
|
+
PREFIX,
|
|
531
|
+
"Monaco already loaded (by Node-RED?), running setup inline",
|
|
532
|
+
);
|
|
395
533
|
ensureJsxSetup();
|
|
396
534
|
cb();
|
|
397
535
|
return;
|
|
@@ -429,26 +567,6 @@
|
|
|
429
567
|
<label for="node-input-compName"><i class="fa fa-cube"></i> JSX Tag</label>
|
|
430
568
|
<input type="text" id="node-input-compName" placeholder="MyComponent" />
|
|
431
569
|
</div>
|
|
432
|
-
<div class="form-row" style="display:flex;gap:8px;">
|
|
433
|
-
<div style="flex:1">
|
|
434
|
-
<label><i class="fa fa-sign-in"></i> Input fields</label>
|
|
435
|
-
<input
|
|
436
|
-
type="text"
|
|
437
|
-
id="node-input-compInputs"
|
|
438
|
-
placeholder="payload,topic"
|
|
439
|
-
style="width:100%"
|
|
440
|
-
/>
|
|
441
|
-
</div>
|
|
442
|
-
<div style="flex:1">
|
|
443
|
-
<label><i class="fa fa-sign-out"></i> Output fields</label>
|
|
444
|
-
<input
|
|
445
|
-
type="text"
|
|
446
|
-
id="node-input-compOutputs"
|
|
447
|
-
placeholder="payload,topic"
|
|
448
|
-
style="width:100%"
|
|
449
|
-
/>
|
|
450
|
-
</div>
|
|
451
|
-
</div>
|
|
452
570
|
<div class="form-row" style="margin-bottom:4px;">
|
|
453
571
|
<label><i class="fa fa-code"></i> JSX Code</label>
|
|
454
572
|
</div>
|
|
@@ -491,13 +609,11 @@
|
|
|
491
609
|
name: { value: "" },
|
|
492
610
|
compName: { value: "StatusCard", required: true },
|
|
493
611
|
compCode: { value: COMP_STARTER },
|
|
494
|
-
compInputs: { value: "label,value,unit" },
|
|
495
|
-
compOutputs: { value: "" },
|
|
496
612
|
},
|
|
497
613
|
inputs: 0,
|
|
498
614
|
outputs: 0,
|
|
499
615
|
icon: "font-awesome/fa-cube",
|
|
500
|
-
paletteLabel: "component",
|
|
616
|
+
paletteLabel: "fromcubes component",
|
|
501
617
|
label: function () {
|
|
502
618
|
return this.name || this.compName || "component";
|
|
503
619
|
},
|
|
@@ -510,22 +626,32 @@
|
|
|
510
626
|
if (!window.__fcTwClasses) {
|
|
511
627
|
console.log("[FC-Monaco] loading tw-classes...");
|
|
512
628
|
$.getJSON("portal-react/tw-classes", function (classes) {
|
|
513
|
-
console.log(
|
|
629
|
+
console.log(
|
|
630
|
+
"[FC-Monaco] tw-classes loaded, count=" + classes.length,
|
|
631
|
+
);
|
|
514
632
|
window.__fcTwClasses = classes;
|
|
515
633
|
}).fail(function (xhr) {
|
|
516
|
-
console.error(
|
|
634
|
+
console.error(
|
|
635
|
+
"[FC-Monaco] tw-classes FAILED:",
|
|
636
|
+
xhr.status,
|
|
637
|
+
xhr.statusText,
|
|
638
|
+
);
|
|
517
639
|
});
|
|
518
640
|
}
|
|
519
641
|
|
|
520
642
|
window.__fcLoadMonaco(function (failed) {
|
|
521
643
|
if (failed) {
|
|
522
|
-
console.error(
|
|
644
|
+
console.error(
|
|
645
|
+
"[FC-Monaco] COMP: Monaco load failed, using fallback textarea",
|
|
646
|
+
);
|
|
523
647
|
$("#fcc-monaco").hide();
|
|
524
648
|
$("#fcc-fallback").show().val(code);
|
|
525
649
|
return;
|
|
526
650
|
}
|
|
527
651
|
|
|
528
|
-
console.log(
|
|
652
|
+
console.log(
|
|
653
|
+
"[FC-Monaco] COMP: applying JSX defaults before model creation",
|
|
654
|
+
);
|
|
529
655
|
window.__fcApplyJsxDefaults();
|
|
530
656
|
|
|
531
657
|
var compUri = monaco.Uri.parse("file:///fc-comp-" + node.id + ".jsx");
|
|
@@ -535,8 +661,15 @@
|
|
|
535
661
|
console.log("[FC-Monaco] COMP: disposing existing model");
|
|
536
662
|
existingModel.dispose();
|
|
537
663
|
}
|
|
538
|
-
var compModel = monaco.editor.createModel(
|
|
539
|
-
|
|
664
|
+
var compModel = monaco.editor.createModel(
|
|
665
|
+
code,
|
|
666
|
+
"javascript",
|
|
667
|
+
compUri,
|
|
668
|
+
);
|
|
669
|
+
console.log(
|
|
670
|
+
"[FC-Monaco] COMP: model created, language=" +
|
|
671
|
+
compModel.getLanguageId(),
|
|
672
|
+
);
|
|
540
673
|
|
|
541
674
|
compEditorInstance = monaco.editor.create(
|
|
542
675
|
document.getElementById("fcc-monaco"),
|
|
@@ -547,13 +680,27 @@
|
|
|
547
680
|
// Log markers after a short delay (diagnostics are async)
|
|
548
681
|
setTimeout(function () {
|
|
549
682
|
var markers = monaco.editor.getModelMarkers({ resource: compUri });
|
|
550
|
-
console.log(
|
|
683
|
+
console.log(
|
|
684
|
+
"[FC-Monaco] COMP: markers after 500ms, count=" + markers.length,
|
|
685
|
+
);
|
|
551
686
|
markers.forEach(function (m) {
|
|
552
|
-
console.log(
|
|
687
|
+
console.log(
|
|
688
|
+
"[FC-Monaco] COMP marker: code=" + m.code + " msg=" + m.message,
|
|
689
|
+
);
|
|
553
690
|
});
|
|
554
691
|
// Also log current compiler options state
|
|
555
|
-
console.log(
|
|
556
|
-
|
|
692
|
+
console.log(
|
|
693
|
+
"[FC-Monaco] COMP: current compilerOptions:",
|
|
694
|
+
JSON.stringify(
|
|
695
|
+
monaco.typescript.javascriptDefaults.getCompilerOptions(),
|
|
696
|
+
),
|
|
697
|
+
);
|
|
698
|
+
console.log(
|
|
699
|
+
"[FC-Monaco] COMP: current diagnosticsOptions:",
|
|
700
|
+
JSON.stringify(
|
|
701
|
+
monaco.typescript.javascriptDefaults.getDiagnosticsOptions(),
|
|
702
|
+
),
|
|
703
|
+
);
|
|
557
704
|
}, 500);
|
|
558
705
|
});
|
|
559
706
|
},
|
|
@@ -631,6 +778,34 @@
|
|
|
631
778
|
<ul id="fc-tabs" style="min-width:600px;margin-bottom:0;"></ul>
|
|
632
779
|
</div>
|
|
633
780
|
<div id="fc-tabs-content">
|
|
781
|
+
<!-- ── Tab: JSX ── -->
|
|
782
|
+
<div id="fc-tab-jsx" class="fc-tab-pane">
|
|
783
|
+
<div class="form-row node-text-editor-row">
|
|
784
|
+
<div
|
|
785
|
+
id="fc-editor-wrap"
|
|
786
|
+
style="width:100%;height:420px;border:1px solid var(--red-ui-form-input-border-color,#555);border-radius:4px;overflow:hidden;position:relative;"
|
|
787
|
+
>
|
|
788
|
+
<div id="fc-monaco" style="width:100%;height:100%;"></div>
|
|
789
|
+
<textarea
|
|
790
|
+
id="fc-fallback"
|
|
791
|
+
style="display:none;width:100%;height:100%;font-family:'Cascadia Code',Consolas,monospace;font-size:13px;background:#1e1e1e;color:#d4d4d4;border:none;padding:12px;resize:none;"
|
|
792
|
+
></textarea>
|
|
793
|
+
</div>
|
|
794
|
+
</div>
|
|
795
|
+
<div class="form-row" style="display:flex;gap:8px;margin-top:4px;flex-wrap:wrap;">
|
|
796
|
+
<button type="button" class="red-ui-button" id="fc-btn-starter">
|
|
797
|
+
<i class="fa fa-magic"></i> Default
|
|
798
|
+
</button>
|
|
799
|
+
<button type="button" class="red-ui-button" id="fc-btn-components">
|
|
800
|
+
<i class="fa fa-cube"></i> Components
|
|
801
|
+
</button>
|
|
802
|
+
<span style="flex:1"></span>
|
|
803
|
+
<button type="button" class="red-ui-button" id="fc-btn-preview">
|
|
804
|
+
<i class="fa fa-eye"></i> Preview
|
|
805
|
+
</button>
|
|
806
|
+
</div>
|
|
807
|
+
</div>
|
|
808
|
+
|
|
634
809
|
<!-- ── Tab: Properties ── -->
|
|
635
810
|
<div id="fc-tab-props" class="fc-tab-pane" style="display:none;">
|
|
636
811
|
<div class="form-row">
|
|
@@ -638,10 +813,22 @@
|
|
|
638
813
|
<input type="text" id="node-input-name" placeholder="My Portal" />
|
|
639
814
|
</div>
|
|
640
815
|
<div class="form-row">
|
|
641
|
-
<label for="node-input-
|
|
642
|
-
><i class="fa fa-globe"></i>
|
|
816
|
+
<label for="node-input-subPath"
|
|
817
|
+
><i class="fa fa-globe"></i> Sub-path</label
|
|
643
818
|
>
|
|
644
|
-
<
|
|
819
|
+
<div style="display:inline-flex;align-items:stretch;width:70%;">
|
|
820
|
+
<span
|
|
821
|
+
style="display:inline-flex;align-items:center;padding:0 8px;background:var(--red-ui-secondary-background,#f3f3f3);border:1px solid var(--red-ui-form-input-border-color,#ccc);border-right:none;border-radius:4px 0 0 4px;color:var(--red-ui-secondary-text-color,#888);font-family:monospace;user-select:none;"
|
|
822
|
+
>/fromcubes/</span
|
|
823
|
+
>
|
|
824
|
+
<input
|
|
825
|
+
type="text"
|
|
826
|
+
id="node-input-subPath"
|
|
827
|
+
placeholder="sensors"
|
|
828
|
+
style="flex:1;border-radius:0 4px 4px 0;"
|
|
829
|
+
/>
|
|
830
|
+
</div>
|
|
831
|
+
<input type="hidden" id="node-input-endpoint" />
|
|
645
832
|
<div
|
|
646
833
|
style="font-size:11px;opacity:.5;margin-top:2px;margin-left:105px;"
|
|
647
834
|
id="fc-url-hint"
|
|
@@ -653,41 +840,41 @@
|
|
|
653
840
|
>
|
|
654
841
|
<input type="text" id="node-input-pageTitle" placeholder="Portal" />
|
|
655
842
|
</div>
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
<div id="fc-tab-jsx" class="fc-tab-pane">
|
|
660
|
-
<div class="form-row" style="margin-bottom:0;">
|
|
661
|
-
<div style="font-size:11px;opacity:.6;margin-bottom:4px;">
|
|
662
|
-
<code>useNodeRed()</code> →
|
|
663
|
-
<code>{ data, send }</code> | Components from
|
|
664
|
-
<code>fc-portal-component</code> nodes auto-imported |
|
|
665
|
-
Must export <code><App /></code> |
|
|
666
|
-
<strong>Transpiled server-side at deploy</strong>
|
|
667
|
-
</div>
|
|
668
|
-
</div>
|
|
669
|
-
<div class="form-row node-text-editor-row">
|
|
843
|
+
<div class="form-row node-input-libs-container-row">
|
|
844
|
+
<label style="width:auto;"><i class="fa fa-archive"></i> Modules</label>
|
|
845
|
+
<ol id="node-input-libs-container"></ol>
|
|
670
846
|
<div
|
|
671
|
-
|
|
672
|
-
style="width:100%;height:420px;border:1px solid var(--red-ui-form-input-border-color,#555);border-radius:4px;overflow:hidden;position:relative;"
|
|
847
|
+
style="font-size:11px;opacity:.5;margin-top:4px;margin-left:4px;"
|
|
673
848
|
>
|
|
674
|
-
|
|
675
|
-
<textarea
|
|
676
|
-
id="fc-fallback"
|
|
677
|
-
style="display:none;width:100%;height:100%;font-family:'Cascadia Code',Consolas,monospace;font-size:13px;background:#1e1e1e;color:#d4d4d4;border:none;padding:12px;resize:none;"
|
|
678
|
-
></textarea>
|
|
849
|
+
npm packages to bundle. Node-RED auto-installs them at deploy time.
|
|
679
850
|
</div>
|
|
680
851
|
</div>
|
|
681
|
-
<div class="form-row" style="
|
|
682
|
-
<
|
|
683
|
-
<
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
852
|
+
<div class="form-row" style="margin-top:12px;">
|
|
853
|
+
<label style="width:auto;">
|
|
854
|
+
<input
|
|
855
|
+
type="checkbox"
|
|
856
|
+
id="node-input-showWsStatus"
|
|
857
|
+
style="width:auto;margin:0 8px 0 0;vertical-align:middle;"
|
|
858
|
+
/>
|
|
859
|
+
Show WebSocket status indicator
|
|
860
|
+
</label>
|
|
861
|
+
<div style="font-size:11px;opacity:.5;margin-top:4px;margin-left:4px;">
|
|
862
|
+
Displays a small <em>fromcubes</em> badge in the bottom-right corner
|
|
863
|
+
showing connection state.
|
|
864
|
+
</div>
|
|
865
|
+
</div>
|
|
866
|
+
<div class="form-row" style="margin-top:12px;">
|
|
867
|
+
<label style="width:auto;">
|
|
868
|
+
<input
|
|
869
|
+
type="checkbox"
|
|
870
|
+
id="node-input-portalAuth"
|
|
871
|
+
style="width:auto;margin:0 8px 0 0;vertical-align:middle;"
|
|
872
|
+
/>
|
|
873
|
+
Enable Portal Auth headers
|
|
874
|
+
</label>
|
|
875
|
+
<div style="font-size:11px;opacity:.5;margin-top:4px;margin-left:4px;">
|
|
876
|
+
Read <code>X-Portal-*</code> headers from reverse proxy.
|
|
877
|
+
</div>
|
|
691
878
|
</div>
|
|
692
879
|
</div>
|
|
693
880
|
|
|
@@ -723,7 +910,7 @@
|
|
|
723
910
|
var headEditorInstance = null;
|
|
724
911
|
|
|
725
912
|
var STARTER = [
|
|
726
|
-
"// useNodeRed() \u2192 { data, send }",
|
|
913
|
+
"// useNodeRed() \u2192 { data, send, portalClient }",
|
|
727
914
|
"// data = last msg.payload from input wire",
|
|
728
915
|
"// send(payload, topic?) = push msg to output wire",
|
|
729
916
|
"// Components from fc-portal-component nodes are available by name.",
|
|
@@ -734,7 +921,7 @@
|
|
|
734
921
|
"",
|
|
735
922
|
" return (",
|
|
736
923
|
' <div className="min-h-screen bg-zinc-950 p-8">',
|
|
737
|
-
' <h1 className="text-2xl font-light text-cyan-400 mb-6">
|
|
924
|
+
' <h1 className="text-2xl font-light text-cyan-400 mb-6">fromcubes</h1>',
|
|
738
925
|
' <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">',
|
|
739
926
|
' <StatusCard label="Value" value={d.value ?? "—"} unit="" />',
|
|
740
927
|
" </div>",
|
|
@@ -754,17 +941,40 @@
|
|
|
754
941
|
color: "#61dafb",
|
|
755
942
|
defaults: {
|
|
756
943
|
name: { value: "" },
|
|
757
|
-
|
|
758
|
-
|
|
944
|
+
subPath: {
|
|
945
|
+
value: "",
|
|
946
|
+
required: true,
|
|
947
|
+
validate: function (v) {
|
|
948
|
+
if (typeof v !== "string") return false;
|
|
949
|
+
var t = v.trim();
|
|
950
|
+
if (t.length === 0) return false;
|
|
951
|
+
if (/\s/.test(t)) return false;
|
|
952
|
+
if (t.charAt(0) === "/" || t.charAt(t.length - 1) === "/") return false;
|
|
953
|
+
var segs = t.split("/");
|
|
954
|
+
for (var i = 0; i < segs.length; i++) {
|
|
955
|
+
var s = segs[i];
|
|
956
|
+
if (!s || s === "." || s === "..") return false;
|
|
957
|
+
var lower = s.toLowerCase();
|
|
958
|
+
if (lower === "public" || lower === "_ws") return false;
|
|
959
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(s)) return false;
|
|
960
|
+
}
|
|
961
|
+
return true;
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
endpoint: { value: "" },
|
|
965
|
+
pageTitle: { value: "fromcubes" },
|
|
759
966
|
componentCode: { value: STARTER },
|
|
760
967
|
customHead: { value: "" },
|
|
968
|
+
portalAuth: { value: false },
|
|
969
|
+
showWsStatus: { value: false },
|
|
970
|
+
libs: { value: [] },
|
|
761
971
|
},
|
|
762
972
|
inputs: 1,
|
|
763
973
|
outputs: 1,
|
|
764
974
|
icon: "font-awesome/fa-desktop",
|
|
765
|
-
paletteLabel: "portal
|
|
975
|
+
paletteLabel: "fromcubes portal",
|
|
766
976
|
label: function () {
|
|
767
|
-
return this.name || this.
|
|
977
|
+
return this.name || ("/fromcubes/" + (this.subPath || "?"));
|
|
768
978
|
},
|
|
769
979
|
|
|
770
980
|
oneditprepare: function () {
|
|
@@ -776,10 +986,16 @@
|
|
|
776
986
|
if (!window.__fcTwClasses) {
|
|
777
987
|
console.log("[FC-Monaco] loading tw-classes...");
|
|
778
988
|
$.getJSON("portal-react/tw-classes", function (classes) {
|
|
779
|
-
console.log(
|
|
989
|
+
console.log(
|
|
990
|
+
"[FC-Monaco] tw-classes loaded, count=" + classes.length,
|
|
991
|
+
);
|
|
780
992
|
window.__fcTwClasses = classes;
|
|
781
993
|
}).fail(function (xhr) {
|
|
782
|
-
console.error(
|
|
994
|
+
console.error(
|
|
995
|
+
"[FC-Monaco] tw-classes FAILED:",
|
|
996
|
+
xhr.status,
|
|
997
|
+
xhr.statusText,
|
|
998
|
+
);
|
|
783
999
|
});
|
|
784
1000
|
}
|
|
785
1001
|
|
|
@@ -800,18 +1016,73 @@
|
|
|
800
1016
|
fcTabs.addTab({ id: "fc-tab-head", label: "Head HTML" });
|
|
801
1017
|
fcTabs.activateTab("fc-tab-jsx");
|
|
802
1018
|
|
|
1019
|
+
// Legacy endpoint detection + convenience pre-fill
|
|
1020
|
+
if (node.endpoint && typeof node.endpoint === "string") {
|
|
1021
|
+
var legacy = node.endpoint;
|
|
1022
|
+
var prefix = "/fromcubes/";
|
|
1023
|
+
if (legacy.indexOf(prefix) === 0) {
|
|
1024
|
+
if (!node.subPath) {
|
|
1025
|
+
$("#node-input-subPath").val(legacy.slice(prefix.length));
|
|
1026
|
+
}
|
|
1027
|
+
} else if (legacy !== "" && legacy !== "/fromcubes") {
|
|
1028
|
+
$("#fc-url-hint")
|
|
1029
|
+
.css("color", "#c00")
|
|
1030
|
+
.text(
|
|
1031
|
+
"Legacy endpoint detected: '" +
|
|
1032
|
+
legacy +
|
|
1033
|
+
"'. Set a Sub-path and redeploy.",
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
803
1038
|
// URL hint
|
|
804
1039
|
function updateHint() {
|
|
805
|
-
var
|
|
806
|
-
|
|
1040
|
+
var sp = ($("#node-input-subPath").val() || "").trim();
|
|
1041
|
+
var root = (RED.settings.httpNodeRoot || "/").replace(/\/$/, "");
|
|
1042
|
+
if (sp) {
|
|
1043
|
+
$("#fc-url-hint")
|
|
1044
|
+
.css("color", "")
|
|
1045
|
+
.text(
|
|
1046
|
+
"Page served at: http://<host>:1880" +
|
|
1047
|
+
root +
|
|
1048
|
+
"/fromcubes/" +
|
|
1049
|
+
sp,
|
|
1050
|
+
);
|
|
1051
|
+
} else if (!node.endpoint || node.endpoint.indexOf("/fromcubes/") === 0) {
|
|
1052
|
+
$("#fc-url-hint")
|
|
1053
|
+
.css("color", "")
|
|
1054
|
+
.text("Sub-path required (will be served under /fromcubes/<sub-path>)");
|
|
1055
|
+
}
|
|
807
1056
|
}
|
|
808
|
-
$("#node-input-
|
|
1057
|
+
$("#node-input-subPath").on("input", updateHint);
|
|
809
1058
|
updateHint();
|
|
810
1059
|
|
|
1060
|
+
// Modules editableList (like function node's libs)
|
|
1061
|
+
var libsList = node.libs || [];
|
|
1062
|
+
$("#node-input-libs-container").css("min-height","68px").editableList({
|
|
1063
|
+
addItem: function(container, i, opt) {
|
|
1064
|
+
var lib = opt || {};
|
|
1065
|
+
var row = $('<div/>',{style:"display:flex;gap:8px;align-items:center;"}).appendTo(container);
|
|
1066
|
+
var modInput = $('<input/>',{type:"text",placeholder:"e.g. chart.js/auto@^4.4.0",style:"flex:1;"}).appendTo(row);
|
|
1067
|
+
var varInput = $('<input/>',{type:"text",placeholder:"Import as (e.g. Chart)",style:"width:140px;"}).appendTo(row);
|
|
1068
|
+
modInput.val(lib.module || "");
|
|
1069
|
+
varInput.val(lib.var || "");
|
|
1070
|
+
container.data("mod", modInput);
|
|
1071
|
+
container.data("var", varInput);
|
|
1072
|
+
},
|
|
1073
|
+
removable: true,
|
|
1074
|
+
sortable: true
|
|
1075
|
+
});
|
|
1076
|
+
libsList.forEach(function(lib) {
|
|
1077
|
+
$("#node-input-libs-container").editableList("addItem", lib);
|
|
1078
|
+
});
|
|
1079
|
+
|
|
811
1080
|
// Monaco
|
|
812
1081
|
window.__fcLoadMonaco(function (failed) {
|
|
813
1082
|
if (failed) {
|
|
814
|
-
console.error(
|
|
1083
|
+
console.error(
|
|
1084
|
+
"[FC-Monaco] PORTAL: Monaco load failed, using fallback textarea",
|
|
1085
|
+
);
|
|
815
1086
|
$("#fc-monaco").hide();
|
|
816
1087
|
$("#fc-fallback").show().val(code);
|
|
817
1088
|
$("#fc-head-monaco").hide();
|
|
@@ -819,11 +1090,15 @@
|
|
|
819
1090
|
return;
|
|
820
1091
|
}
|
|
821
1092
|
|
|
822
|
-
console.log(
|
|
1093
|
+
console.log(
|
|
1094
|
+
"[FC-Monaco] PORTAL: applying JSX defaults before model creation",
|
|
1095
|
+
);
|
|
823
1096
|
window.__fcApplyJsxDefaults();
|
|
824
1097
|
var opts = window.__fcEditorOpts;
|
|
825
1098
|
|
|
826
|
-
var jsxUri = monaco.Uri.parse(
|
|
1099
|
+
var jsxUri = monaco.Uri.parse(
|
|
1100
|
+
"file:///fc-portal-" + node.id + ".jsx",
|
|
1101
|
+
);
|
|
827
1102
|
console.log("[FC-Monaco] PORTAL: JSX model URI=" + jsxUri.toString());
|
|
828
1103
|
var existingJsx = monaco.editor.getModel(jsxUri);
|
|
829
1104
|
if (existingJsx) {
|
|
@@ -831,15 +1106,21 @@
|
|
|
831
1106
|
existingJsx.dispose();
|
|
832
1107
|
}
|
|
833
1108
|
var jsxModel = monaco.editor.createModel(code, "javascript", jsxUri);
|
|
834
|
-
console.log(
|
|
1109
|
+
console.log(
|
|
1110
|
+
"[FC-Monaco] PORTAL: JSX model created, language=" +
|
|
1111
|
+
jsxModel.getLanguageId(),
|
|
1112
|
+
);
|
|
835
1113
|
editorInstance = monaco.editor.create(
|
|
836
1114
|
document.getElementById("fc-monaco"),
|
|
837
1115
|
Object.assign({ model: jsxModel }, opts),
|
|
838
1116
|
);
|
|
839
1117
|
console.log("[FC-Monaco] PORTAL: JSX editor created");
|
|
840
|
-
if (window.__fcAttachSelfClose)
|
|
1118
|
+
if (window.__fcAttachSelfClose)
|
|
1119
|
+
window.__fcAttachSelfClose(editorInstance);
|
|
841
1120
|
|
|
842
|
-
var headUri = monaco.Uri.parse(
|
|
1121
|
+
var headUri = monaco.Uri.parse(
|
|
1122
|
+
"file:///fc-head-" + node.id + ".html",
|
|
1123
|
+
);
|
|
843
1124
|
var existingHead = monaco.editor.getModel(headUri);
|
|
844
1125
|
if (existingHead) existingHead.dispose();
|
|
845
1126
|
var headModel = monaco.editor.createModel(headCode, "html", headUri);
|
|
@@ -852,77 +1133,167 @@
|
|
|
852
1133
|
// Log markers after a short delay (diagnostics are async)
|
|
853
1134
|
setTimeout(function () {
|
|
854
1135
|
var markers = monaco.editor.getModelMarkers({ resource: jsxUri });
|
|
855
|
-
console.log(
|
|
1136
|
+
console.log(
|
|
1137
|
+
"[FC-Monaco] PORTAL: markers after 500ms, count=" +
|
|
1138
|
+
markers.length,
|
|
1139
|
+
);
|
|
856
1140
|
markers.forEach(function (m) {
|
|
857
|
-
console.log(
|
|
1141
|
+
console.log(
|
|
1142
|
+
"[FC-Monaco] PORTAL marker: code=" +
|
|
1143
|
+
m.code +
|
|
1144
|
+
" severity=" +
|
|
1145
|
+
m.severity +
|
|
1146
|
+
" msg=" +
|
|
1147
|
+
m.message,
|
|
1148
|
+
);
|
|
858
1149
|
});
|
|
859
|
-
console.log(
|
|
860
|
-
|
|
1150
|
+
console.log(
|
|
1151
|
+
"[FC-Monaco] PORTAL: current compilerOptions:",
|
|
1152
|
+
JSON.stringify(
|
|
1153
|
+
monaco.typescript.javascriptDefaults.getCompilerOptions(),
|
|
1154
|
+
),
|
|
1155
|
+
);
|
|
1156
|
+
console.log(
|
|
1157
|
+
"[FC-Monaco] PORTAL: current diagnosticsOptions:",
|
|
1158
|
+
JSON.stringify(
|
|
1159
|
+
monaco.typescript.javascriptDefaults.getDiagnosticsOptions(),
|
|
1160
|
+
),
|
|
1161
|
+
);
|
|
861
1162
|
}, 500);
|
|
862
1163
|
});
|
|
863
1164
|
|
|
864
1165
|
// Buttons
|
|
865
1166
|
$("#fc-btn-starter").on("click", function () {
|
|
866
|
-
|
|
867
|
-
|
|
1167
|
+
$("<div>Replace current JSX with default starter code?</div>").dialog({
|
|
1168
|
+
title: "Load Default",
|
|
1169
|
+
modal: true,
|
|
1170
|
+
width: 360,
|
|
1171
|
+
buttons: [
|
|
1172
|
+
{ text: "Cancel", click: function () { $(this).dialog("close"); } },
|
|
1173
|
+
{ text: "Replace", class: "primary", click: function () {
|
|
1174
|
+
if (editorInstance) editorInstance.setValue(STARTER);
|
|
1175
|
+
else $("#fc-fallback").val(STARTER);
|
|
1176
|
+
$(this).dialog("close");
|
|
1177
|
+
}},
|
|
1178
|
+
],
|
|
1179
|
+
close: function () { $(this).remove(); },
|
|
1180
|
+
});
|
|
868
1181
|
});
|
|
869
1182
|
|
|
870
1183
|
$("#fc-btn-preview").on("click", function () {
|
|
871
|
-
var
|
|
872
|
-
|
|
1184
|
+
var sp = ($("#node-input-subPath").val() || "").trim();
|
|
1185
|
+
var root = (RED.settings.httpNodeRoot || "/").replace(/\/$/, "");
|
|
1186
|
+
if (sp) window.open(root + "/fromcubes/" + sp, "_blank");
|
|
873
1187
|
});
|
|
874
1188
|
|
|
875
1189
|
$("#fc-btn-components").on("click", function () {
|
|
876
1190
|
$.getJSON("portal-react/registry", function (reg) {
|
|
877
|
-
var names = Object.keys(reg);
|
|
1191
|
+
var names = Object.keys(reg).sort();
|
|
878
1192
|
if (!names.length) {
|
|
879
1193
|
RED.notify("No component nodes on canvas.", "warning");
|
|
880
1194
|
return;
|
|
881
1195
|
}
|
|
882
|
-
|
|
1196
|
+
|
|
1197
|
+
function extractProps(code) {
|
|
1198
|
+
if (!code) return [];
|
|
1199
|
+
var m = code.match(/function\s+\w+\s*\(\s*\{([^}]*)\}/);
|
|
1200
|
+
if (!m) return [];
|
|
1201
|
+
return m[1].split(",").map(function (s) { return s.trim().split(/\s*=\s*/)[0]; }).filter(Boolean);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
var html =
|
|
1205
|
+
'<div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">' +
|
|
1206
|
+
'<input type="text" id="fc-comp-search" placeholder="Search..." ' +
|
|
1207
|
+
'style="width:100%;box-sizing:border-box;margin-bottom:6px;padding:5px 8px;border:1px solid rgba(128,128,128,.4);border-radius:3px;' +
|
|
1208
|
+
'background:var(--red-ui-form-input-background-color,#fff);color:var(--red-ui-form-text-color,#333);font-size:12px;flex-shrink:0;" />' +
|
|
1209
|
+
'<div id="fc-comp-list" style="flex:1;overflow-y:auto;">';
|
|
1210
|
+
|
|
883
1211
|
names.forEach(function (n) {
|
|
884
1212
|
var c = reg[n];
|
|
1213
|
+
var props = extractProps(c.code);
|
|
1214
|
+
var detailParts = [];
|
|
1215
|
+
if (props.length) {
|
|
1216
|
+
detailParts.push('<div style="margin:4px 0 0 0;font-size:11px;opacity:.6;">' +
|
|
1217
|
+
props.map(function (p) {
|
|
1218
|
+
return '<code style="background:rgba(128,128,128,.15);padding:0 4px;border-radius:2px;margin-right:3px;">' + p + '</code>';
|
|
1219
|
+
}).join("") + '</div>');
|
|
1220
|
+
}
|
|
1221
|
+
var hasDetail = detailParts.length > 0;
|
|
1222
|
+
|
|
885
1223
|
html +=
|
|
886
|
-
'<div
|
|
887
|
-
'
|
|
888
|
-
|
|
889
|
-
'">' +
|
|
890
|
-
n +
|
|
891
|
-
|
|
892
|
-
'
|
|
893
|
-
|
|
894
|
-
"] out:[" +
|
|
895
|
-
(c.outputs || []).join(",") +
|
|
896
|
-
"]</span></div>";
|
|
1224
|
+
'<div class="fc-comp-item" data-name="' + n + '" data-search="' + n.toLowerCase() + '" ' +
|
|
1225
|
+
'style="border-bottom:1px solid rgba(128,128,128,.1);">' +
|
|
1226
|
+
'<div style="display:flex;align-items:center;padding:4px 6px;cursor:pointer;" class="fc-comp-row">' +
|
|
1227
|
+
(hasDetail ? '<i class="fa fa-caret-right fc-comp-arrow" style="width:14px;opacity:.4;font-size:12px;transition:transform .15s;"></i>' : '<span style="width:14px;"></span>') +
|
|
1228
|
+
'<span class="fc-comp-name" data-name="' + n + '" style="font-weight:600;font-size:12px;flex:1;">' + n + '</span>' +
|
|
1229
|
+
'</div>' +
|
|
1230
|
+
(hasDetail ? '<div class="fc-comp-detail" style="display:none;padding:0 6px 4px 20px;">' + detailParts.join("") + '</div>' : '') +
|
|
1231
|
+
'</div>';
|
|
897
1232
|
});
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1233
|
+
html += '</div></div>';
|
|
1234
|
+
|
|
1235
|
+
var $dlg = $("<div></div>").html(html).dialog({
|
|
1236
|
+
title: "Components",
|
|
1237
|
+
modal: true,
|
|
1238
|
+
width: 380,
|
|
1239
|
+
buttons: [
|
|
1240
|
+
{ text: "Close", click: function () { $(this).dialog("close"); } },
|
|
1241
|
+
],
|
|
1242
|
+
close: function () { $(this).remove(); },
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
// Search
|
|
1246
|
+
$dlg.find("#fc-comp-search").on("input", function () {
|
|
1247
|
+
var q = $(this).val().toLowerCase();
|
|
1248
|
+
$dlg.find(".fc-comp-item").each(function () {
|
|
1249
|
+
$(this).toggle($(this).data("search").indexOf(q) !== -1);
|
|
915
1250
|
});
|
|
916
|
-
|
|
917
|
-
|
|
1251
|
+
}).focus();
|
|
1252
|
+
|
|
1253
|
+
// Hover
|
|
1254
|
+
$dlg.on("mouseenter", ".fc-comp-row", function () {
|
|
1255
|
+
$(this).css("background", "rgba(128,128,128,.1)");
|
|
1256
|
+
}).on("mouseleave", ".fc-comp-row", function () {
|
|
1257
|
+
$(this).css("background", "");
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
// Arrow toggle detail
|
|
1261
|
+
$dlg.on("click", ".fc-comp-arrow", function (e) {
|
|
1262
|
+
e.stopPropagation();
|
|
1263
|
+
var $item = $(this).closest(".fc-comp-item");
|
|
1264
|
+
var $detail = $item.find(".fc-comp-detail");
|
|
1265
|
+
var open = $detail.is(":visible");
|
|
1266
|
+
$detail.slideToggle(100);
|
|
1267
|
+
$(this).css("transform", open ? "" : "rotate(90deg)");
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
// Click name: delete selection, insert <Tag></Tag> in one line, cursor between tags
|
|
1271
|
+
$dlg.on("click", ".fc-comp-name", function () {
|
|
1272
|
+
var name = $(this).data("name");
|
|
1273
|
+
var openTag = "<" + name + ">";
|
|
1274
|
+
var closeTag = "</" + name + ">";
|
|
1275
|
+
var text = openTag + closeTag;
|
|
1276
|
+
$dlg.dialog("close");
|
|
918
1277
|
if (editorInstance) {
|
|
919
|
-
editorInstance.
|
|
920
|
-
|
|
1278
|
+
var sel = editorInstance.getSelection();
|
|
1279
|
+
var startLine = sel.startLineNumber;
|
|
1280
|
+
var startCol = sel.startColumn;
|
|
1281
|
+
editorInstance.executeEdits("fc-components", [{
|
|
1282
|
+
range: sel,
|
|
1283
|
+
text: text,
|
|
1284
|
+
}]);
|
|
1285
|
+
setTimeout(function () {
|
|
1286
|
+
editorInstance.setPosition({ lineNumber: startLine, column: startCol + openTag.length });
|
|
1287
|
+
editorInstance.focus();
|
|
1288
|
+
}, 50);
|
|
921
1289
|
} else {
|
|
922
1290
|
var ta = $("#fc-fallback")[0];
|
|
923
|
-
var
|
|
924
|
-
|
|
925
|
-
|
|
1291
|
+
var s = ta.selectionStart, e = ta.selectionEnd, v = ta.value;
|
|
1292
|
+
ta.value = v.slice(0, s) + text + v.slice(e);
|
|
1293
|
+
setTimeout(function () {
|
|
1294
|
+
ta.selectionStart = ta.selectionEnd = s + openTag.length;
|
|
1295
|
+
ta.focus();
|
|
1296
|
+
}, 50);
|
|
926
1297
|
}
|
|
927
1298
|
});
|
|
928
1299
|
});
|
|
@@ -931,6 +1302,24 @@
|
|
|
931
1302
|
|
|
932
1303
|
oneditsave: function () {
|
|
933
1304
|
console.log("[FC-Monaco] PORTAL oneditsave");
|
|
1305
|
+
|
|
1306
|
+
// Clear legacy endpoint field + normalize subPath
|
|
1307
|
+
$("#node-input-endpoint").val("");
|
|
1308
|
+
this.endpoint = "";
|
|
1309
|
+
this.subPath = ($("#node-input-subPath").val() || "").trim();
|
|
1310
|
+
|
|
1311
|
+
// Collect libs from editableList
|
|
1312
|
+
var libs = [];
|
|
1313
|
+
var items = $("#node-input-libs-container").editableList("items");
|
|
1314
|
+
items.each(function() {
|
|
1315
|
+
var mod = $(this).data("mod").val().trim();
|
|
1316
|
+
var v = $(this).data("var").val().trim();
|
|
1317
|
+
if (mod) {
|
|
1318
|
+
libs.push({ module: mod, var: v });
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
this.libs = libs;
|
|
1322
|
+
|
|
934
1323
|
var code = editorInstance
|
|
935
1324
|
? editorInstance.getValue()
|
|
936
1325
|
: $("#fc-fallback").val();
|
|
@@ -997,37 +1386,59 @@
|
|
|
997
1386
|
Help
|
|
998
1387
|
============================================================ -->
|
|
999
1388
|
<script type="text/html" data-help-name="portal-react">
|
|
1389
|
+
<h3>Quick Reference</h3>
|
|
1390
|
+
<ul>
|
|
1391
|
+
<li><code>useNodeRed()</code> → <code>{ data, send, user, portalClient }</code></li>
|
|
1392
|
+
<li>Components from <code>fc-portal-component</code> nodes are auto-imported</li>
|
|
1393
|
+
<li>Must export <code><App /></code></li>
|
|
1394
|
+
<li>JSX is transpiled server-side at deploy</li>
|
|
1395
|
+
</ul>
|
|
1396
|
+
|
|
1000
1397
|
<p>
|
|
1001
|
-
Renders a React application on a configurable HTTP endpoint with live
|
|
1002
|
-
WebSocket data binding.
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
JSX is transpiled <strong>server-side at deploy time</strong> using Sucrase.
|
|
1007
|
-
The browser receives plain JS — no Babel, no runtime compilation. Page
|
|
1008
|
-
weight: ~45 KB (React production).
|
|
1398
|
+
Renders a React 19 application on a configurable HTTP endpoint with live
|
|
1399
|
+
WebSocket data binding. JSX is transpiled <strong>server-side at deploy
|
|
1400
|
+
time</strong> using esbuild — browsers receive pre-compiled JS with zero
|
|
1401
|
+
runtime compilation. Tailwind CSS 4 utility classes are generated
|
|
1402
|
+
server-side and served as static CSS.
|
|
1009
1403
|
</p>
|
|
1010
1404
|
|
|
1011
1405
|
<h3>Inputs</h3>
|
|
1012
1406
|
<p>
|
|
1013
|
-
<code>msg.payload</code> is pushed
|
|
1407
|
+
<code>msg.payload</code> is pushed via WebSocket. Set <code>msg._client</code>
|
|
1408
|
+
to target specific clients, or omit it to broadcast to all.
|
|
1014
1409
|
</p>
|
|
1015
1410
|
<pre>
|
|
1016
|
-
const { data, send } = useNodeRed();
|
|
1017
|
-
// data
|
|
1411
|
+
const { data, send, user, portalClient } = useNodeRed();
|
|
1412
|
+
// data = last msg.payload (reactive)
|
|
1413
|
+
// send(payload, topic?) — emit msg on output wire
|
|
1414
|
+
// user = portal auth data or null
|
|
1415
|
+
// portalClient = unique session/tab ID (assigned by server)</pre
|
|
1018
1416
|
>
|
|
1019
1417
|
|
|
1020
1418
|
<h3>Outputs</h3>
|
|
1021
1419
|
<p>
|
|
1022
1420
|
<code>send(payload, topic?)</code> emits a <code>msg</code> on the node's
|
|
1023
|
-
output wire.
|
|
1421
|
+
output wire via WebSocket.
|
|
1422
|
+
</p>
|
|
1423
|
+
|
|
1424
|
+
<h3>npm Packages</h3>
|
|
1425
|
+
<p>
|
|
1426
|
+
Add npm packages in the <strong>Modules</strong> list (e.g.
|
|
1427
|
+
<code>chart.js/auto@^4.4.0</code>, <code>d3</code>, <code>three</code>).
|
|
1428
|
+
Node-RED auto-installs them at deploy time. All packages are bundled with
|
|
1429
|
+
React into a single vendor IIFE via esbuild, cached by hash of installed
|
|
1430
|
+
versions. Use them in JSX via standard imports.
|
|
1024
1431
|
</p>
|
|
1025
1432
|
|
|
1026
1433
|
<h3>Deploy behavior</h3>
|
|
1027
1434
|
<ul>
|
|
1028
1435
|
<li>
|
|
1029
|
-
Each deploy re-transpiles JSX
|
|
1030
|
-
instant)
|
|
1436
|
+
Each deploy re-transpiles JSX via esbuild and rebuilds Tailwind CSS
|
|
1437
|
+
(both cached by content hash — unchanged code is instant)
|
|
1438
|
+
</li>
|
|
1439
|
+
<li>
|
|
1440
|
+
Vendor bundle (React + npm packages) rebuilt only when package versions
|
|
1441
|
+
change
|
|
1031
1442
|
</li>
|
|
1032
1443
|
<li>
|
|
1033
1444
|
Active WebSocket clients receive close code 1001 and auto-reconnect
|
|
@@ -1045,15 +1456,548 @@ const { data, send } = useNodeRed();
|
|
|
1045
1456
|
<p>
|
|
1046
1457
|
Components defined in <strong>fc-portal-component</strong> nodes on the
|
|
1047
1458
|
canvas are auto-injected into every portal-react page. Use them as JSX tags
|
|
1048
|
-
by their component name.
|
|
1459
|
+
by their component name — no imports needed.
|
|
1049
1460
|
</p>
|
|
1050
1461
|
|
|
1462
|
+
<h3>Portal Auth</h3>
|
|
1463
|
+
<p>
|
|
1464
|
+
When enabled in the Auth tab, reads <code>X-Portal-*</code> headers set by
|
|
1465
|
+
a reverse proxy (e.g. Nginx) and exposes user data:
|
|
1466
|
+
</p>
|
|
1467
|
+
<ul>
|
|
1468
|
+
<li><code>useNodeRed()</code> returns <code>{ data, send, user, portalClient }</code></li>
|
|
1469
|
+
<li>
|
|
1470
|
+
All WebSocket messages include <code>msg._client</code> with
|
|
1471
|
+
<code>{ portalClient, ...userFields }</code>
|
|
1472
|
+
</li>
|
|
1473
|
+
<li>
|
|
1474
|
+
To target a specific tab: keep <code>msg._client</code> (or set
|
|
1475
|
+
<code>msg._client = { portalClient: "..." }</code>)
|
|
1476
|
+
</li>
|
|
1477
|
+
<li>
|
|
1478
|
+
To target all tabs of a user: set
|
|
1479
|
+
<code>msg._client = { userId: "..." }</code> (no portalClient)
|
|
1480
|
+
</li>
|
|
1481
|
+
<li>
|
|
1482
|
+
To broadcast to all: remove <code>msg._client</code>
|
|
1483
|
+
</li>
|
|
1484
|
+
</ul>
|
|
1485
|
+
|
|
1051
1486
|
<h3>Custom Head HTML</h3>
|
|
1052
1487
|
<p>
|
|
1053
1488
|
Inject CDN links, fonts, or extra stylesheets into
|
|
1054
1489
|
<code><head></code>. Example:
|
|
1055
1490
|
</p>
|
|
1056
1491
|
<pre>
|
|
1057
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet"></pre
|
|
1492
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet"></pre
|
|
1058
1493
|
>
|
|
1494
|
+
|
|
1495
|
+
<h3>WebSocket Status</h3>
|
|
1496
|
+
<p>
|
|
1497
|
+
Optional <em>fromcubes</em> badge in the bottom-right corner showing
|
|
1498
|
+
connection state. Enable via the <strong>Show WebSocket status
|
|
1499
|
+
indicator</strong> checkbox (off by default).
|
|
1500
|
+
</p>
|
|
1501
|
+
</script>
|
|
1502
|
+
|
|
1503
|
+
<!-- ── Assets sidebar tab ──────────────────────────────────────── -->
|
|
1504
|
+
<script type="text/javascript">
|
|
1505
|
+
(function () {
|
|
1506
|
+
// Resolve httpNodeRoot for public URL prefix
|
|
1507
|
+
var nodeRoot = (RED.settings.httpNodeRoot || "/").replace(/\/$/, "");
|
|
1508
|
+
var publicBase = nodeRoot + "/fromcubes/public/";
|
|
1509
|
+
|
|
1510
|
+
function formatSize(bytes) {
|
|
1511
|
+
if (bytes < 1024) return bytes + " B";
|
|
1512
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
1513
|
+
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
var allEntries = [];
|
|
1517
|
+
var collapsed = {};
|
|
1518
|
+
|
|
1519
|
+
var content = $('<div class="red-ui-sidebar-info" style="height:100%;overflow:auto;padding:0;"></div>');
|
|
1520
|
+
var fileList = $('<div style="padding:0;"></div>').appendTo(content);
|
|
1521
|
+
|
|
1522
|
+
// ── Toolbar ──
|
|
1523
|
+
var toolbar = $('<div style="display:flex;align-items:center;gap:6px;margin:0 6px;padding:2px 0 0;"></div>');
|
|
1524
|
+
var fileInput = $('<input type="file" multiple style="display:none;">').appendTo(toolbar);
|
|
1525
|
+
$('<button class="red-ui-button red-ui-button-small" style="flex-shrink:0;"><i class="fa fa-upload"></i> Upload</button>')
|
|
1526
|
+
.on("click", function (e) { e.preventDefault(); fileInput.trigger("click"); })
|
|
1527
|
+
.appendTo(toolbar);
|
|
1528
|
+
$('<button class="red-ui-button red-ui-button-small" style="flex-shrink:0;"><i class="fa fa-folder-open"></i> New folder</button>')
|
|
1529
|
+
.on("click", function (e) { e.preventDefault(); showNewFolderInput(""); })
|
|
1530
|
+
.appendTo(toolbar);
|
|
1531
|
+
|
|
1532
|
+
// ── Helpers ──
|
|
1533
|
+
function pathExists(p) {
|
|
1534
|
+
for (var i = 0; i < allEntries.length; i++) {
|
|
1535
|
+
if (allEntries[i].name === p) return true;
|
|
1536
|
+
}
|
|
1537
|
+
return false;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function uploadFiles(files, targetDir) {
|
|
1541
|
+
if (!files || files.length === 0) return;
|
|
1542
|
+
var toUpload = [];
|
|
1543
|
+
var duplicates = [];
|
|
1544
|
+
for (var i = 0; i < files.length; i++) {
|
|
1545
|
+
var uploadPath = targetDir ? targetDir + "/" + files[i].name : files[i].name;
|
|
1546
|
+
if (pathExists(uploadPath)) {
|
|
1547
|
+
duplicates.push({ file: files[i], path: uploadPath });
|
|
1548
|
+
} else {
|
|
1549
|
+
toUpload.push({ file: files[i], path: uploadPath });
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
if (duplicates.length > 0) {
|
|
1553
|
+
var names = duplicates.map(function (d) { return d.file.name; }).join(", ");
|
|
1554
|
+
if (confirm("These files already exist: " + names + "\nOverwrite?")) {
|
|
1555
|
+
toUpload = toUpload.concat(duplicates);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
if (toUpload.length === 0) return;
|
|
1559
|
+
var pending = toUpload.length;
|
|
1560
|
+
toUpload.forEach(function (item) {
|
|
1561
|
+
var reader = new FileReader();
|
|
1562
|
+
reader.onload = function () {
|
|
1563
|
+
$.ajax({
|
|
1564
|
+
type: "POST",
|
|
1565
|
+
url: "portal-react/assets/upload/" + item.path.split("/").map(encodeURIComponent).join("/"),
|
|
1566
|
+
data: new Uint8Array(reader.result),
|
|
1567
|
+
contentType: "application/octet-stream",
|
|
1568
|
+
processData: false,
|
|
1569
|
+
success: function () { if (--pending === 0) refreshList(); },
|
|
1570
|
+
error: function () {
|
|
1571
|
+
RED.notify("Upload failed: " + item.file.name, "error");
|
|
1572
|
+
if (--pending === 0) refreshList();
|
|
1573
|
+
},
|
|
1574
|
+
});
|
|
1575
|
+
};
|
|
1576
|
+
reader.readAsArrayBuffer(item.file);
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
fileInput.on("change", function () {
|
|
1581
|
+
uploadFiles(this.files, "");
|
|
1582
|
+
fileInput.val("");
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
// External file drag & drop on content area (root upload)
|
|
1586
|
+
content.on("dragover", function (e) {
|
|
1587
|
+
if (e.originalEvent.dataTransfer.types.indexOf("Files") >= 0) {
|
|
1588
|
+
e.preventDefault();
|
|
1589
|
+
e.stopPropagation();
|
|
1590
|
+
content.css("background", "rgba(34,211,238,0.05)");
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
content.on("dragleave", function (e) {
|
|
1594
|
+
e.preventDefault();
|
|
1595
|
+
content.css("background", "");
|
|
1596
|
+
});
|
|
1597
|
+
content.on("drop", function (e) {
|
|
1598
|
+
e.preventDefault();
|
|
1599
|
+
e.stopPropagation();
|
|
1600
|
+
content.css("background", "");
|
|
1601
|
+
var dt = e.originalEvent.dataTransfer;
|
|
1602
|
+
if (dt.files && dt.files.length > 0) {
|
|
1603
|
+
uploadFiles(dt.files, "");
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
function moveItem(fromPath, toDir) {
|
|
1608
|
+
var filename = fromPath.split("/").pop();
|
|
1609
|
+
var newPath = toDir ? toDir + "/" + filename : filename;
|
|
1610
|
+
if (fromPath === newPath) return;
|
|
1611
|
+
if (pathExists(newPath)) {
|
|
1612
|
+
if (!confirm("'" + filename + "' already exists in this folder. Overwrite?")) return;
|
|
1613
|
+
}
|
|
1614
|
+
$.ajax({
|
|
1615
|
+
type: "POST", url: "portal-react/assets/move",
|
|
1616
|
+
contentType: "application/json",
|
|
1617
|
+
data: JSON.stringify({ from: fromPath, to: newPath }),
|
|
1618
|
+
success: function () { refreshList(); },
|
|
1619
|
+
error: function (xhr) {
|
|
1620
|
+
var msg = xhr.responseJSON ? xhr.responseJSON.error : "move failed";
|
|
1621
|
+
RED.notify("Move failed: " + msg, "error");
|
|
1622
|
+
},
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// ── Inline new-folder input ──
|
|
1627
|
+
function showNewFolderInput(parentDir) {
|
|
1628
|
+
var existingInput = fileList.find(".fc-new-folder-row");
|
|
1629
|
+
if (existingInput.length) existingInput.remove();
|
|
1630
|
+
|
|
1631
|
+
var depth = parentDir ? parentDir.split("/").length : 0;
|
|
1632
|
+
var indent = 8 + depth * 18;
|
|
1633
|
+
var row = $('<div class="fc-new-folder-row" style="display:flex;align-items:center;gap:6px;padding:8px;border-bottom:1px solid var(--red-ui-secondary-border-color);background:var(--red-ui-tertiary-background);"></div>');
|
|
1634
|
+
row.css("padding-left", indent + "px");
|
|
1635
|
+
$('<i class="fa fa-folder" style="color:#fbbf24;font-size:13px;width:16px;text-align:center;"></i>').appendTo(row);
|
|
1636
|
+
var inp = $('<input type="text" placeholder="folder name" class="red-ui-searchBox-input" style="flex:1;padding:3px 6px;font-size:12px;">');
|
|
1637
|
+
inp.appendTo(row);
|
|
1638
|
+
var okBtn = $('<button class="red-ui-button red-ui-button-small" style="color:var(--red-ui-text-color-success);" title="Create"><i class="fa fa-check"></i></button>');
|
|
1639
|
+
var cancelBtn = $('<button class="red-ui-button red-ui-button-small" title="Cancel"><i class="fa fa-times"></i></button>');
|
|
1640
|
+
function submit() {
|
|
1641
|
+
var name = inp.val().trim();
|
|
1642
|
+
if (!name) { row.remove(); return; }
|
|
1643
|
+
var p = parentDir ? parentDir + "/" + name : name;
|
|
1644
|
+
if (pathExists(p)) {
|
|
1645
|
+
RED.notify("Folder '" + name + "' already exists", "warning");
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
$.ajax({ type: "POST", url: "portal-react/assets/mkdir", contentType: "application/json", data: JSON.stringify({ path: p }),
|
|
1649
|
+
success: function () { row.remove(); refreshList(); },
|
|
1650
|
+
error: function () { RED.notify("Failed to create folder", "error"); },
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
okBtn.on("click", function (e) { e.preventDefault(); submit(); });
|
|
1654
|
+
cancelBtn.on("click", function (e) { e.preventDefault(); row.remove(); });
|
|
1655
|
+
inp.on("keydown", function (e) {
|
|
1656
|
+
if (e.key === "Enter") submit();
|
|
1657
|
+
if (e.key === "Escape") row.remove();
|
|
1658
|
+
});
|
|
1659
|
+
okBtn.appendTo(row);
|
|
1660
|
+
cancelBtn.appendTo(row);
|
|
1661
|
+
|
|
1662
|
+
// Insert after the parent folder row, or at top
|
|
1663
|
+
if (parentDir) {
|
|
1664
|
+
var parentRow = fileList.find('[data-path="' + parentDir + '"]');
|
|
1665
|
+
if (parentRow.length) { row.insertAfter(parentRow); } else { fileList.prepend(row); }
|
|
1666
|
+
} else {
|
|
1667
|
+
fileList.prepend(row);
|
|
1668
|
+
}
|
|
1669
|
+
inp.focus();
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// ── Context menu (native Node-RED style) ──
|
|
1673
|
+
var activeMenu = null;
|
|
1674
|
+
function closeMenu() {
|
|
1675
|
+
if (activeMenu) { activeMenu.remove(); activeMenu = null; }
|
|
1676
|
+
}
|
|
1677
|
+
$(document).on("click", closeMenu);
|
|
1678
|
+
|
|
1679
|
+
function showMenu(anchor, items) {
|
|
1680
|
+
closeMenu();
|
|
1681
|
+
var menu = $('<ul class="red-ui-menu red-ui-menu-dropdown" style="position:absolute;z-index:10000;display:block;"></ul>');
|
|
1682
|
+
items.forEach(function (item) {
|
|
1683
|
+
if (item.divider) {
|
|
1684
|
+
menu.append('<li class="red-ui-menu-divider"></li>');
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
var li = $('<li></li>');
|
|
1688
|
+
var a = $('<a href="#"></a>');
|
|
1689
|
+
if (item.danger) a.css("color", "var(--red-ui-text-color-error)");
|
|
1690
|
+
a.append('<i class="fa ' + item.icon + '" style="width:18px;text-align:center;"></i> ');
|
|
1691
|
+
a.append($('<span class="red-ui-menu-label"></span>').text(item.label));
|
|
1692
|
+
a.on("click", function (e) {
|
|
1693
|
+
e.preventDefault();
|
|
1694
|
+
e.stopPropagation();
|
|
1695
|
+
closeMenu();
|
|
1696
|
+
item.action();
|
|
1697
|
+
});
|
|
1698
|
+
li.append(a);
|
|
1699
|
+
menu.append(li);
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
$("body").append(menu);
|
|
1703
|
+
activeMenu = menu;
|
|
1704
|
+
|
|
1705
|
+
// Position near the anchor
|
|
1706
|
+
var off = anchor.offset();
|
|
1707
|
+
var top = off.top + anchor.outerHeight() + 2;
|
|
1708
|
+
var left = off.left - menu.outerWidth() + anchor.outerWidth();
|
|
1709
|
+
if (left < 0) left = off.left;
|
|
1710
|
+
if (top + menu.outerHeight() > $(window).height()) top = off.top - menu.outerHeight() - 2;
|
|
1711
|
+
menu.css({ top: top, left: left });
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
var adminRoot = (RED.settings.httpAdminRoot || "/").replace(/\/$/, "");
|
|
1715
|
+
|
|
1716
|
+
function showRenameInput(rowEl, fullPath, currentName) {
|
|
1717
|
+
var nameSpan = rowEl.find("span").filter(function () { return $(this).text() === currentName; }).first();
|
|
1718
|
+
if (!nameSpan.length) return;
|
|
1719
|
+
var origText = nameSpan.text();
|
|
1720
|
+
var inp = $('<input type="text" class="red-ui-searchBox-input" style="flex:1;padding:3px 6px;font-size:12px;">').val(origText);
|
|
1721
|
+
var okBtn = $('<button class="red-ui-button red-ui-button-small" style="color:var(--red-ui-text-color-success);" title="Save"><i class="fa fa-check"></i></button>');
|
|
1722
|
+
var cancelBtn = $('<button class="red-ui-button red-ui-button-small" title="Cancel"><i class="fa fa-times"></i></button>');
|
|
1723
|
+
var dotIdx = origText.lastIndexOf(".");
|
|
1724
|
+
var hasExt = dotIdx > 0; // has extension (not hidden file)
|
|
1725
|
+
var origExt = hasExt ? origText.slice(dotIdx) : "";
|
|
1726
|
+
|
|
1727
|
+
nameSpan.replaceWith(inp);
|
|
1728
|
+
inp.after(cancelBtn).after(okBtn);
|
|
1729
|
+
inp.focus();
|
|
1730
|
+
// Select only the name part before extension
|
|
1731
|
+
var el = inp[0];
|
|
1732
|
+
if (hasExt && el.setSelectionRange) {
|
|
1733
|
+
el.setSelectionRange(0, dotIdx);
|
|
1734
|
+
} else {
|
|
1735
|
+
inp.select();
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
function restore() {
|
|
1739
|
+
okBtn.remove();
|
|
1740
|
+
cancelBtn.remove();
|
|
1741
|
+
inp.replaceWith($('<span style="flex:1;font-size:12px;word-break:break-all;"></span>').text(origText));
|
|
1742
|
+
}
|
|
1743
|
+
function submit() {
|
|
1744
|
+
var newName = inp.val().trim();
|
|
1745
|
+
if (!newName) {
|
|
1746
|
+
RED.notify("Name cannot be empty", "warning");
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
// Warn if extension changed or removed
|
|
1750
|
+
if (hasExt) {
|
|
1751
|
+
var newDot = newName.lastIndexOf(".");
|
|
1752
|
+
var newExt = newDot > 0 ? newName.slice(newDot) : "";
|
|
1753
|
+
if (newExt.toLowerCase() !== origExt.toLowerCase()) {
|
|
1754
|
+
if (!confirm("Extension changed from '" + origExt + "' to '" + (newExt || "none") + "'. Continue?")) return;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
if (newName === origText) { restore(); return; }
|
|
1758
|
+
var parentDir = fullPath.indexOf("/") >= 0 ? fullPath.slice(0, fullPath.lastIndexOf("/")) : "";
|
|
1759
|
+
var newPath = parentDir ? parentDir + "/" + newName : newName;
|
|
1760
|
+
if (pathExists(newPath)) {
|
|
1761
|
+
RED.notify("'" + newName + "' already exists", "warning");
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
$.ajax({
|
|
1765
|
+
type: "POST", url: "portal-react/assets/move",
|
|
1766
|
+
contentType: "application/json",
|
|
1767
|
+
data: JSON.stringify({ from: fullPath, to: newPath }),
|
|
1768
|
+
success: function () { refreshList(); },
|
|
1769
|
+
error: function (xhr) {
|
|
1770
|
+
var msg = xhr.responseJSON ? xhr.responseJSON.error : "rename failed";
|
|
1771
|
+
RED.notify("Rename failed: " + msg, "error");
|
|
1772
|
+
restore();
|
|
1773
|
+
},
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
okBtn.on("click", function (e) { e.preventDefault(); e.stopPropagation(); submit(); });
|
|
1777
|
+
cancelBtn.on("click", function (e) { e.preventDefault(); e.stopPropagation(); restore(); });
|
|
1778
|
+
inp.on("keydown", function (e) {
|
|
1779
|
+
if (e.key === "Enter") submit();
|
|
1780
|
+
if (e.key === "Escape") restore();
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// ── Tree rendering ──
|
|
1785
|
+
function buildTree(entries) {
|
|
1786
|
+
// Build nested structure: { children: { name: { type, children, entry } } }
|
|
1787
|
+
var root = { children: {} };
|
|
1788
|
+
entries.forEach(function (e) {
|
|
1789
|
+
var parts = e.name.split("/");
|
|
1790
|
+
var node = root;
|
|
1791
|
+
for (var i = 0; i < parts.length; i++) {
|
|
1792
|
+
if (!node.children[parts[i]]) {
|
|
1793
|
+
node.children[parts[i]] = { children: {} };
|
|
1794
|
+
}
|
|
1795
|
+
node = node.children[parts[i]];
|
|
1796
|
+
}
|
|
1797
|
+
node.entry = e;
|
|
1798
|
+
});
|
|
1799
|
+
return root;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
var ROOT_KEY = "__root__";
|
|
1803
|
+
|
|
1804
|
+
function renderTree() {
|
|
1805
|
+
fileList.empty();
|
|
1806
|
+
var isOpen = !collapsed[ROOT_KEY];
|
|
1807
|
+
|
|
1808
|
+
// Root folder row — always visible
|
|
1809
|
+
var rootRow = $('<div style="display:flex;align-items:center;gap:5px;padding:4px 8px;border-bottom:1px solid var(--red-ui-secondary-border-color);"></div>');
|
|
1810
|
+
var rootArrow = $('<i class="fa ' + (isOpen ? 'fa-caret-down' : 'fa-caret-right') + '" style="color:var(--red-ui-secondary-text-color);font-size:11px;width:10px;text-align:center;cursor:pointer;"></i>');
|
|
1811
|
+
rootArrow.on("click", function () { collapsed[ROOT_KEY] = isOpen; renderTree(); });
|
|
1812
|
+
rootRow.append(rootArrow);
|
|
1813
|
+
$('<i class="fa ' + (isOpen ? 'fa-folder-open' : 'fa-folder') + '" style="color:#fbbf24;font-size:12px;width:16px;text-align:center;"></i>').appendTo(rootRow);
|
|
1814
|
+
$('<span style="flex:1;font-size:12px;cursor:pointer;opacity:0.8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>').text(publicBase.replace(/\/$/, ""))
|
|
1815
|
+
.on("click", function () { collapsed[ROOT_KEY] = isOpen; renderTree(); })
|
|
1816
|
+
.appendTo(rootRow);
|
|
1817
|
+
fileList.append(rootRow);
|
|
1818
|
+
|
|
1819
|
+
if (!isOpen) return;
|
|
1820
|
+
|
|
1821
|
+
if (allEntries.length === 0) {
|
|
1822
|
+
fileList.append($('<div style="padding:16px;color:var(--red-ui-secondary-text-color);text-align:center;">No files uploaded.<br><span style="font-size:11px;">Drop files here or click Upload.</span></div>'));
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
var tree = buildTree(allEntries);
|
|
1826
|
+
renderNode(tree, "", 1);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function renderNode(node, parentPath, depth) {
|
|
1830
|
+
// Collect and sort: dirs first, then files
|
|
1831
|
+
var names = Object.keys(node.children).sort(function (a, b) {
|
|
1832
|
+
var aIsDir = node.children[a].entry && node.children[a].entry.type === "dir";
|
|
1833
|
+
var bIsDir = node.children[b].entry && node.children[b].entry.type === "dir";
|
|
1834
|
+
if (aIsDir && !bIsDir) return -1;
|
|
1835
|
+
if (!aIsDir && bIsDir) return 1;
|
|
1836
|
+
return a.localeCompare(b);
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
names.forEach(function (name) {
|
|
1840
|
+
var child = node.children[name];
|
|
1841
|
+
var e = child.entry;
|
|
1842
|
+
if (!e) return;
|
|
1843
|
+
var fullPath = e.name;
|
|
1844
|
+
var indent = 8 + depth * 18;
|
|
1845
|
+
var isDir = e.type === "dir";
|
|
1846
|
+
var isOpen = !collapsed[fullPath];
|
|
1847
|
+
|
|
1848
|
+
var row = $('<div data-path="' + fullPath + '" style="display:flex;align-items:center;gap:5px;padding:4px 8px;border-bottom:1px solid var(--red-ui-secondary-border-color);"></div>');
|
|
1849
|
+
row.css("padding-left", indent + "px");
|
|
1850
|
+
|
|
1851
|
+
if (isDir) {
|
|
1852
|
+
// Expand/collapse arrow
|
|
1853
|
+
var arrow = $('<i class="fa ' + (isOpen ? 'fa-caret-down' : 'fa-caret-right') + '" style="color:var(--red-ui-secondary-text-color);font-size:11px;width:10px;text-align:center;cursor:pointer;"></i>');
|
|
1854
|
+
arrow.on("click", function (e) {
|
|
1855
|
+
e.stopPropagation();
|
|
1856
|
+
collapsed[fullPath] = isOpen;
|
|
1857
|
+
renderTree();
|
|
1858
|
+
});
|
|
1859
|
+
row.append(arrow);
|
|
1860
|
+
$('<i class="fa ' + (isOpen ? 'fa-folder-open' : 'fa-folder') + '" style="color:#fbbf24;font-size:12px;width:16px;text-align:center;"></i>').appendTo(row);
|
|
1861
|
+
$('<span style="flex:1;font-size:12px;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + fullPath + '"></span>').text(name)
|
|
1862
|
+
.on("click", function () { collapsed[fullPath] = isOpen; renderTree(); })
|
|
1863
|
+
.appendTo(row);
|
|
1864
|
+
|
|
1865
|
+
// Context menu trigger
|
|
1866
|
+
var dirMenuBtn = $('<button class="red-ui-button red-ui-button-small" style="padding:1px 5px;"><i class="fa fa-ellipsis-v"></i></button>');
|
|
1867
|
+
(function (fp, nm) {
|
|
1868
|
+
dirMenuBtn.on("click", function (ev) {
|
|
1869
|
+
ev.preventDefault();
|
|
1870
|
+
ev.stopPropagation();
|
|
1871
|
+
showMenu(dirMenuBtn, [
|
|
1872
|
+
{ icon: "fa-pencil", label: "Rename", action: function () {
|
|
1873
|
+
showRenameInput(row, fp, nm);
|
|
1874
|
+
}},
|
|
1875
|
+
{ icon: "fa-plus", label: "New subfolder", action: function () {
|
|
1876
|
+
if (collapsed[fp]) { collapsed[fp] = false; renderTree(); }
|
|
1877
|
+
setTimeout(function () { showNewFolderInput(fp); }, 50);
|
|
1878
|
+
}},
|
|
1879
|
+
{ divider: true },
|
|
1880
|
+
{ icon: "fa-trash", label: "Delete folder", danger: true, action: function () {
|
|
1881
|
+
if (!confirm("Delete folder '" + nm + "' and all contents?")) return;
|
|
1882
|
+
$.ajax({
|
|
1883
|
+
type: "DELETE",
|
|
1884
|
+
url: "portal-react/assets/" + fp.split("/").map(encodeURIComponent).join("/"),
|
|
1885
|
+
success: function () { refreshList(); },
|
|
1886
|
+
error: function () { RED.notify("Delete failed", "error"); },
|
|
1887
|
+
});
|
|
1888
|
+
}},
|
|
1889
|
+
]);
|
|
1890
|
+
});
|
|
1891
|
+
})(fullPath, name);
|
|
1892
|
+
dirMenuBtn.appendTo(row);
|
|
1893
|
+
|
|
1894
|
+
// Drop target
|
|
1895
|
+
row.on("dragover", function (ev) {
|
|
1896
|
+
ev.preventDefault();
|
|
1897
|
+
ev.stopPropagation();
|
|
1898
|
+
row.css("background", "rgba(251,191,36,0.08)");
|
|
1899
|
+
});
|
|
1900
|
+
row.on("dragleave", function () { row.css("background", ""); });
|
|
1901
|
+
row.on("drop", function (ev) {
|
|
1902
|
+
ev.preventDefault();
|
|
1903
|
+
ev.stopPropagation();
|
|
1904
|
+
row.css("background", "");
|
|
1905
|
+
var srcPath = ev.originalEvent.dataTransfer.getData("text/x-asset-path");
|
|
1906
|
+
if (srcPath) {
|
|
1907
|
+
moveItem(srcPath, fullPath);
|
|
1908
|
+
} else if (ev.originalEvent.dataTransfer.files && ev.originalEvent.dataTransfer.files.length > 0) {
|
|
1909
|
+
uploadFiles(ev.originalEvent.dataTransfer.files, fullPath);
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
fileList.append(row);
|
|
1913
|
+
|
|
1914
|
+
// Render children if open
|
|
1915
|
+
if (isOpen) {
|
|
1916
|
+
renderNode(child, fullPath, depth + 1);
|
|
1917
|
+
}
|
|
1918
|
+
} else {
|
|
1919
|
+
// File
|
|
1920
|
+
row.attr("draggable", "true").css("cursor", "grab");
|
|
1921
|
+
$('<span style="width:10px;"></span>').appendTo(row); // spacer for arrow alignment
|
|
1922
|
+
$('<i class="fa fa-file-o" style="color:var(--red-ui-secondary-text-color);font-size:11px;width:16px;text-align:center;"></i>').appendTo(row);
|
|
1923
|
+
$('<span style="flex:1;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + fullPath + '"></span>').text(name).appendTo(row);
|
|
1924
|
+
$('<span style="font-size:10px;color:var(--red-ui-tertiary-text-color);white-space:nowrap;"></span>').text(formatSize(e.size)).appendTo(row);
|
|
1925
|
+
|
|
1926
|
+
row.on("dragstart", function (ev) {
|
|
1927
|
+
ev.originalEvent.dataTransfer.setData("text/x-asset-path", fullPath);
|
|
1928
|
+
ev.originalEvent.dataTransfer.effectAllowed = "move";
|
|
1929
|
+
row.css("opacity", "0.4");
|
|
1930
|
+
});
|
|
1931
|
+
row.on("dragend", function () { row.css("opacity", "1"); });
|
|
1932
|
+
|
|
1933
|
+
var copyBtn = $('<button class="red-ui-button red-ui-button-small" style="padding:1px 5px;" title="Copy public path"><i class="fa fa-clipboard"></i></button>');
|
|
1934
|
+
var fileMenuBtn = $('<button class="red-ui-button red-ui-button-small" style="padding:1px 5px;"><i class="fa fa-ellipsis-v"></i></button>');
|
|
1935
|
+
(function (fp, nm) {
|
|
1936
|
+
copyBtn.on("click", function (ev) {
|
|
1937
|
+
ev.preventDefault();
|
|
1938
|
+
ev.stopPropagation();
|
|
1939
|
+
var url = publicBase + fp;
|
|
1940
|
+
navigator.clipboard.writeText(url).then(function () {
|
|
1941
|
+
RED.notify("Copied: " + url, { type: "success", timeout: 2000 });
|
|
1942
|
+
});
|
|
1943
|
+
});
|
|
1944
|
+
fileMenuBtn.on("click", function (ev) {
|
|
1945
|
+
ev.preventDefault();
|
|
1946
|
+
ev.stopPropagation();
|
|
1947
|
+
showMenu(fileMenuBtn, [
|
|
1948
|
+
{ icon: "fa-pencil", label: "Rename", action: function () {
|
|
1949
|
+
showRenameInput(row, fp, nm);
|
|
1950
|
+
}},
|
|
1951
|
+
{ icon: "fa-download", label: "Download", action: function () {
|
|
1952
|
+
var a = document.createElement("a");
|
|
1953
|
+
a.href = adminRoot + "/portal-react/assets/download/" + fp.split("/").map(encodeURIComponent).join("/");
|
|
1954
|
+
a.download = nm;
|
|
1955
|
+
document.body.appendChild(a);
|
|
1956
|
+
a.click();
|
|
1957
|
+
document.body.removeChild(a);
|
|
1958
|
+
}},
|
|
1959
|
+
{ divider: true },
|
|
1960
|
+
{ icon: "fa-trash", label: "Delete", danger: true, action: function () {
|
|
1961
|
+
$.ajax({
|
|
1962
|
+
type: "DELETE",
|
|
1963
|
+
url: "portal-react/assets/" + fp.split("/").map(encodeURIComponent).join("/"),
|
|
1964
|
+
success: function () { refreshList(); },
|
|
1965
|
+
error: function () { RED.notify("Delete failed: " + nm, "error"); },
|
|
1966
|
+
});
|
|
1967
|
+
}},
|
|
1968
|
+
]);
|
|
1969
|
+
});
|
|
1970
|
+
})(fullPath, name);
|
|
1971
|
+
copyBtn.appendTo(row);
|
|
1972
|
+
fileMenuBtn.appendTo(row);
|
|
1973
|
+
fileList.append(row);
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
function refreshList() {
|
|
1979
|
+
$.getJSON("portal-react/assets", function (entries) {
|
|
1980
|
+
allEntries = entries || [];
|
|
1981
|
+
renderTree();
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
RED.sidebar.addTab({
|
|
1986
|
+
id: "fromcubes-public",
|
|
1987
|
+
label: "Portal Assets",
|
|
1988
|
+
name: "fromcubes portal assets",
|
|
1989
|
+
iconClass: "fa fa-desktop",
|
|
1990
|
+
content: content[0],
|
|
1991
|
+
toolbar: toolbar[0],
|
|
1992
|
+
pinned: false,
|
|
1993
|
+
enableOnEdit: true,
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
RED.actions.add("fromcubes:show-assets", function () {
|
|
1997
|
+
RED.sidebar.show("fromcubes-public");
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
RED.events.on("sidebar:resize", function () { refreshList(); });
|
|
2001
|
+
setTimeout(refreshList, 1000);
|
|
2002
|
+
})();
|
|
1059
2003
|
</script>
|