@aaqu/fromcubes-portal-react 0.1.0-alpha.2 → 0.1.0-alpha.21
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 +314 -0
- package/nodes/lib/hooks.js +82 -0
- package/nodes/lib/page-builder.js +347 -0
- package/nodes/lib/router.js +56 -0
- package/nodes/portal-react.html +1143 -196
- package/nodes/portal-react.js +911 -353
- 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();
|
|
@@ -372,15 +499,26 @@
|
|
|
372
499
|
|
|
373
500
|
var diagOpts = {
|
|
374
501
|
noSemanticValidation: true,
|
|
375
|
-
|
|
502
|
+
// Server-side esbuild does the real syntax check at deploy time;
|
|
503
|
+
// Monaco's TS parser produces noisy false positives on raw JSX
|
|
504
|
+
// (1109, 1005, 1128 etc.), so we silence its squiggles entirely.
|
|
505
|
+
noSyntaxValidation: true,
|
|
376
506
|
noSuggestionDiagnostics: true,
|
|
377
507
|
diagnosticCodesToIgnore: [17004],
|
|
378
508
|
};
|
|
379
509
|
console.log(PREFIX, "setDiagnosticsOptions:", JSON.stringify(diagOpts));
|
|
380
510
|
jsDef.setDiagnosticsOptions(diagOpts);
|
|
381
511
|
|
|
382
|
-
console.log(
|
|
383
|
-
|
|
512
|
+
console.log(
|
|
513
|
+
PREFIX,
|
|
514
|
+
"readback compilerOptions:",
|
|
515
|
+
JSON.stringify(jsDef.getCompilerOptions()),
|
|
516
|
+
);
|
|
517
|
+
console.log(
|
|
518
|
+
PREFIX,
|
|
519
|
+
"readback diagnosticsOptions:",
|
|
520
|
+
JSON.stringify(jsDef.getDiagnosticsOptions()),
|
|
521
|
+
);
|
|
384
522
|
}
|
|
385
523
|
|
|
386
524
|
// Exported for oneditprepare to call before model creation
|
|
@@ -391,7 +529,10 @@
|
|
|
391
529
|
window.__fcLoadMonaco = function (cb) {
|
|
392
530
|
console.log(PREFIX, "loadMonaco called, monaco exists:", !!window.monaco);
|
|
393
531
|
if (window.monaco) {
|
|
394
|
-
console.log(
|
|
532
|
+
console.log(
|
|
533
|
+
PREFIX,
|
|
534
|
+
"Monaco already loaded (by Node-RED?), running setup inline",
|
|
535
|
+
);
|
|
395
536
|
ensureJsxSetup();
|
|
396
537
|
cb();
|
|
397
538
|
return;
|
|
@@ -429,26 +570,6 @@
|
|
|
429
570
|
<label for="node-input-compName"><i class="fa fa-cube"></i> JSX Tag</label>
|
|
430
571
|
<input type="text" id="node-input-compName" placeholder="MyComponent" />
|
|
431
572
|
</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
573
|
<div class="form-row" style="margin-bottom:4px;">
|
|
453
574
|
<label><i class="fa fa-code"></i> JSX Code</label>
|
|
454
575
|
</div>
|
|
@@ -491,13 +612,11 @@
|
|
|
491
612
|
name: { value: "" },
|
|
492
613
|
compName: { value: "StatusCard", required: true },
|
|
493
614
|
compCode: { value: COMP_STARTER },
|
|
494
|
-
compInputs: { value: "label,value,unit" },
|
|
495
|
-
compOutputs: { value: "" },
|
|
496
615
|
},
|
|
497
616
|
inputs: 0,
|
|
498
617
|
outputs: 0,
|
|
499
618
|
icon: "font-awesome/fa-cube",
|
|
500
|
-
paletteLabel: "component",
|
|
619
|
+
paletteLabel: "fromcubes component",
|
|
501
620
|
label: function () {
|
|
502
621
|
return this.name || this.compName || "component";
|
|
503
622
|
},
|
|
@@ -510,22 +629,32 @@
|
|
|
510
629
|
if (!window.__fcTwClasses) {
|
|
511
630
|
console.log("[FC-Monaco] loading tw-classes...");
|
|
512
631
|
$.getJSON("portal-react/tw-classes", function (classes) {
|
|
513
|
-
console.log(
|
|
632
|
+
console.log(
|
|
633
|
+
"[FC-Monaco] tw-classes loaded, count=" + classes.length,
|
|
634
|
+
);
|
|
514
635
|
window.__fcTwClasses = classes;
|
|
515
636
|
}).fail(function (xhr) {
|
|
516
|
-
console.error(
|
|
637
|
+
console.error(
|
|
638
|
+
"[FC-Monaco] tw-classes FAILED:",
|
|
639
|
+
xhr.status,
|
|
640
|
+
xhr.statusText,
|
|
641
|
+
);
|
|
517
642
|
});
|
|
518
643
|
}
|
|
519
644
|
|
|
520
645
|
window.__fcLoadMonaco(function (failed) {
|
|
521
646
|
if (failed) {
|
|
522
|
-
console.error(
|
|
647
|
+
console.error(
|
|
648
|
+
"[FC-Monaco] COMP: Monaco load failed, using fallback textarea",
|
|
649
|
+
);
|
|
523
650
|
$("#fcc-monaco").hide();
|
|
524
651
|
$("#fcc-fallback").show().val(code);
|
|
525
652
|
return;
|
|
526
653
|
}
|
|
527
654
|
|
|
528
|
-
console.log(
|
|
655
|
+
console.log(
|
|
656
|
+
"[FC-Monaco] COMP: applying JSX defaults before model creation",
|
|
657
|
+
);
|
|
529
658
|
window.__fcApplyJsxDefaults();
|
|
530
659
|
|
|
531
660
|
var compUri = monaco.Uri.parse("file:///fc-comp-" + node.id + ".jsx");
|
|
@@ -535,8 +664,15 @@
|
|
|
535
664
|
console.log("[FC-Monaco] COMP: disposing existing model");
|
|
536
665
|
existingModel.dispose();
|
|
537
666
|
}
|
|
538
|
-
var compModel = monaco.editor.createModel(
|
|
539
|
-
|
|
667
|
+
var compModel = monaco.editor.createModel(
|
|
668
|
+
code,
|
|
669
|
+
"javascript",
|
|
670
|
+
compUri,
|
|
671
|
+
);
|
|
672
|
+
console.log(
|
|
673
|
+
"[FC-Monaco] COMP: model created, language=" +
|
|
674
|
+
compModel.getLanguageId(),
|
|
675
|
+
);
|
|
540
676
|
|
|
541
677
|
compEditorInstance = monaco.editor.create(
|
|
542
678
|
document.getElementById("fcc-monaco"),
|
|
@@ -547,13 +683,27 @@
|
|
|
547
683
|
// Log markers after a short delay (diagnostics are async)
|
|
548
684
|
setTimeout(function () {
|
|
549
685
|
var markers = monaco.editor.getModelMarkers({ resource: compUri });
|
|
550
|
-
console.log(
|
|
686
|
+
console.log(
|
|
687
|
+
"[FC-Monaco] COMP: markers after 500ms, count=" + markers.length,
|
|
688
|
+
);
|
|
551
689
|
markers.forEach(function (m) {
|
|
552
|
-
console.log(
|
|
690
|
+
console.log(
|
|
691
|
+
"[FC-Monaco] COMP marker: code=" + m.code + " msg=" + m.message,
|
|
692
|
+
);
|
|
553
693
|
});
|
|
554
694
|
// Also log current compiler options state
|
|
555
|
-
console.log(
|
|
556
|
-
|
|
695
|
+
console.log(
|
|
696
|
+
"[FC-Monaco] COMP: current compilerOptions:",
|
|
697
|
+
JSON.stringify(
|
|
698
|
+
monaco.typescript.javascriptDefaults.getCompilerOptions(),
|
|
699
|
+
),
|
|
700
|
+
);
|
|
701
|
+
console.log(
|
|
702
|
+
"[FC-Monaco] COMP: current diagnosticsOptions:",
|
|
703
|
+
JSON.stringify(
|
|
704
|
+
monaco.typescript.javascriptDefaults.getDiagnosticsOptions(),
|
|
705
|
+
),
|
|
706
|
+
);
|
|
557
707
|
}, 500);
|
|
558
708
|
});
|
|
559
709
|
},
|
|
@@ -631,6 +781,34 @@
|
|
|
631
781
|
<ul id="fc-tabs" style="min-width:600px;margin-bottom:0;"></ul>
|
|
632
782
|
</div>
|
|
633
783
|
<div id="fc-tabs-content">
|
|
784
|
+
<!-- ── Tab: JSX ── -->
|
|
785
|
+
<div id="fc-tab-jsx" class="fc-tab-pane">
|
|
786
|
+
<div class="form-row node-text-editor-row">
|
|
787
|
+
<div
|
|
788
|
+
id="fc-editor-wrap"
|
|
789
|
+
style="width:100%;height:420px;border:1px solid var(--red-ui-form-input-border-color,#555);border-radius:4px;overflow:hidden;position:relative;"
|
|
790
|
+
>
|
|
791
|
+
<div id="fc-monaco" style="width:100%;height:100%;"></div>
|
|
792
|
+
<textarea
|
|
793
|
+
id="fc-fallback"
|
|
794
|
+
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;"
|
|
795
|
+
></textarea>
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
<div class="form-row" style="display:flex;gap:8px;margin-top:4px;flex-wrap:wrap;">
|
|
799
|
+
<button type="button" class="red-ui-button" id="fc-btn-starter">
|
|
800
|
+
<i class="fa fa-magic"></i> Default
|
|
801
|
+
</button>
|
|
802
|
+
<button type="button" class="red-ui-button" id="fc-btn-components">
|
|
803
|
+
<i class="fa fa-cube"></i> Components
|
|
804
|
+
</button>
|
|
805
|
+
<span style="flex:1"></span>
|
|
806
|
+
<button type="button" class="red-ui-button" id="fc-btn-preview">
|
|
807
|
+
<i class="fa fa-eye"></i> Preview
|
|
808
|
+
</button>
|
|
809
|
+
</div>
|
|
810
|
+
</div>
|
|
811
|
+
|
|
634
812
|
<!-- ── Tab: Properties ── -->
|
|
635
813
|
<div id="fc-tab-props" class="fc-tab-pane" style="display:none;">
|
|
636
814
|
<div class="form-row">
|
|
@@ -638,10 +816,22 @@
|
|
|
638
816
|
<input type="text" id="node-input-name" placeholder="My Portal" />
|
|
639
817
|
</div>
|
|
640
818
|
<div class="form-row">
|
|
641
|
-
<label for="node-input-
|
|
642
|
-
><i class="fa fa-globe"></i>
|
|
819
|
+
<label for="node-input-subPath"
|
|
820
|
+
><i class="fa fa-globe"></i> Sub-path</label
|
|
643
821
|
>
|
|
644
|
-
<
|
|
822
|
+
<div style="display:inline-flex;align-items:stretch;width:70%;">
|
|
823
|
+
<span
|
|
824
|
+
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;"
|
|
825
|
+
>/fromcubes/</span
|
|
826
|
+
>
|
|
827
|
+
<input
|
|
828
|
+
type="text"
|
|
829
|
+
id="node-input-subPath"
|
|
830
|
+
placeholder="sensors"
|
|
831
|
+
style="flex:1;border-radius:0 4px 4px 0;"
|
|
832
|
+
/>
|
|
833
|
+
</div>
|
|
834
|
+
<input type="hidden" id="node-input-endpoint" />
|
|
645
835
|
<div
|
|
646
836
|
style="font-size:11px;opacity:.5;margin-top:2px;margin-left:105px;"
|
|
647
837
|
id="fc-url-hint"
|
|
@@ -653,41 +843,41 @@
|
|
|
653
843
|
>
|
|
654
844
|
<input type="text" id="node-input-pageTitle" placeholder="Portal" />
|
|
655
845
|
</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">
|
|
846
|
+
<div class="form-row node-input-libs-container-row">
|
|
847
|
+
<label style="width:auto;"><i class="fa fa-archive"></i> Modules</label>
|
|
848
|
+
<ol id="node-input-libs-container"></ol>
|
|
670
849
|
<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;"
|
|
850
|
+
style="font-size:11px;opacity:.5;margin-top:4px;margin-left:4px;"
|
|
673
851
|
>
|
|
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>
|
|
852
|
+
npm packages to bundle. Node-RED auto-installs them at deploy time.
|
|
679
853
|
</div>
|
|
680
854
|
</div>
|
|
681
|
-
<div class="form-row" style="
|
|
682
|
-
<
|
|
683
|
-
<
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
855
|
+
<div class="form-row" style="margin-top:12px;">
|
|
856
|
+
<label style="width:auto;">
|
|
857
|
+
<input
|
|
858
|
+
type="checkbox"
|
|
859
|
+
id="node-input-showWsStatus"
|
|
860
|
+
style="width:auto;margin:0 8px 0 0;vertical-align:middle;"
|
|
861
|
+
/>
|
|
862
|
+
Show WebSocket status indicator
|
|
863
|
+
</label>
|
|
864
|
+
<div style="font-size:11px;opacity:.5;margin-top:4px;margin-left:4px;">
|
|
865
|
+
Displays a small <em>fromcubes</em> badge in the bottom-right corner
|
|
866
|
+
showing connection state.
|
|
867
|
+
</div>
|
|
868
|
+
</div>
|
|
869
|
+
<div class="form-row" style="margin-top:12px;">
|
|
870
|
+
<label style="width:auto;">
|
|
871
|
+
<input
|
|
872
|
+
type="checkbox"
|
|
873
|
+
id="node-input-portalAuth"
|
|
874
|
+
style="width:auto;margin:0 8px 0 0;vertical-align:middle;"
|
|
875
|
+
/>
|
|
876
|
+
Enable Portal Auth headers
|
|
877
|
+
</label>
|
|
878
|
+
<div style="font-size:11px;opacity:.5;margin-top:4px;margin-left:4px;">
|
|
879
|
+
Read <code>X-Portal-*</code> headers from reverse proxy.
|
|
880
|
+
</div>
|
|
691
881
|
</div>
|
|
692
882
|
</div>
|
|
693
883
|
|
|
@@ -723,7 +913,7 @@
|
|
|
723
913
|
var headEditorInstance = null;
|
|
724
914
|
|
|
725
915
|
var STARTER = [
|
|
726
|
-
"// useNodeRed() \u2192 { data, send }",
|
|
916
|
+
"// useNodeRed() \u2192 { data, send, portalClient }",
|
|
727
917
|
"// data = last msg.payload from input wire",
|
|
728
918
|
"// send(payload, topic?) = push msg to output wire",
|
|
729
919
|
"// Components from fc-portal-component nodes are available by name.",
|
|
@@ -734,7 +924,7 @@
|
|
|
734
924
|
"",
|
|
735
925
|
" return (",
|
|
736
926
|
' <div className="min-h-screen bg-zinc-950 p-8">',
|
|
737
|
-
' <h1 className="text-2xl font-light text-cyan-400 mb-6">
|
|
927
|
+
' <h1 className="text-2xl font-light text-cyan-400 mb-6">fromcubes</h1>',
|
|
738
928
|
' <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">',
|
|
739
929
|
' <StatusCard label="Value" value={d.value ?? "—"} unit="" />',
|
|
740
930
|
" </div>",
|
|
@@ -754,17 +944,40 @@
|
|
|
754
944
|
color: "#61dafb",
|
|
755
945
|
defaults: {
|
|
756
946
|
name: { value: "" },
|
|
757
|
-
|
|
758
|
-
|
|
947
|
+
subPath: {
|
|
948
|
+
value: "",
|
|
949
|
+
required: true,
|
|
950
|
+
validate: function (v) {
|
|
951
|
+
if (typeof v !== "string") return false;
|
|
952
|
+
var t = v.trim();
|
|
953
|
+
if (t.length === 0) return false;
|
|
954
|
+
if (/\s/.test(t)) return false;
|
|
955
|
+
if (t.charAt(0) === "/" || t.charAt(t.length - 1) === "/") return false;
|
|
956
|
+
var segs = t.split("/");
|
|
957
|
+
for (var i = 0; i < segs.length; i++) {
|
|
958
|
+
var s = segs[i];
|
|
959
|
+
if (!s || s === "." || s === "..") return false;
|
|
960
|
+
var lower = s.toLowerCase();
|
|
961
|
+
if (lower === "public" || lower === "_ws") return false;
|
|
962
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(s)) return false;
|
|
963
|
+
}
|
|
964
|
+
return true;
|
|
965
|
+
},
|
|
966
|
+
},
|
|
967
|
+
endpoint: { value: "" },
|
|
968
|
+
pageTitle: { value: "fromcubes" },
|
|
759
969
|
componentCode: { value: STARTER },
|
|
760
970
|
customHead: { value: "" },
|
|
971
|
+
portalAuth: { value: false },
|
|
972
|
+
showWsStatus: { value: false },
|
|
973
|
+
libs: { value: [] },
|
|
761
974
|
},
|
|
762
975
|
inputs: 1,
|
|
763
976
|
outputs: 1,
|
|
764
977
|
icon: "font-awesome/fa-desktop",
|
|
765
|
-
paletteLabel: "portal
|
|
978
|
+
paletteLabel: "fromcubes portal",
|
|
766
979
|
label: function () {
|
|
767
|
-
return this.name || this.
|
|
980
|
+
return this.name || ("/fromcubes/" + (this.subPath || "?"));
|
|
768
981
|
},
|
|
769
982
|
|
|
770
983
|
oneditprepare: function () {
|
|
@@ -776,10 +989,16 @@
|
|
|
776
989
|
if (!window.__fcTwClasses) {
|
|
777
990
|
console.log("[FC-Monaco] loading tw-classes...");
|
|
778
991
|
$.getJSON("portal-react/tw-classes", function (classes) {
|
|
779
|
-
console.log(
|
|
992
|
+
console.log(
|
|
993
|
+
"[FC-Monaco] tw-classes loaded, count=" + classes.length,
|
|
994
|
+
);
|
|
780
995
|
window.__fcTwClasses = classes;
|
|
781
996
|
}).fail(function (xhr) {
|
|
782
|
-
console.error(
|
|
997
|
+
console.error(
|
|
998
|
+
"[FC-Monaco] tw-classes FAILED:",
|
|
999
|
+
xhr.status,
|
|
1000
|
+
xhr.statusText,
|
|
1001
|
+
);
|
|
783
1002
|
});
|
|
784
1003
|
}
|
|
785
1004
|
|
|
@@ -800,18 +1019,73 @@
|
|
|
800
1019
|
fcTabs.addTab({ id: "fc-tab-head", label: "Head HTML" });
|
|
801
1020
|
fcTabs.activateTab("fc-tab-jsx");
|
|
802
1021
|
|
|
1022
|
+
// Legacy endpoint detection + convenience pre-fill
|
|
1023
|
+
if (node.endpoint && typeof node.endpoint === "string") {
|
|
1024
|
+
var legacy = node.endpoint;
|
|
1025
|
+
var prefix = "/fromcubes/";
|
|
1026
|
+
if (legacy.indexOf(prefix) === 0) {
|
|
1027
|
+
if (!node.subPath) {
|
|
1028
|
+
$("#node-input-subPath").val(legacy.slice(prefix.length));
|
|
1029
|
+
}
|
|
1030
|
+
} else if (legacy !== "" && legacy !== "/fromcubes") {
|
|
1031
|
+
$("#fc-url-hint")
|
|
1032
|
+
.css("color", "#c00")
|
|
1033
|
+
.text(
|
|
1034
|
+
"Legacy endpoint detected: '" +
|
|
1035
|
+
legacy +
|
|
1036
|
+
"'. Set a Sub-path and redeploy.",
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
803
1041
|
// URL hint
|
|
804
1042
|
function updateHint() {
|
|
805
|
-
var
|
|
806
|
-
|
|
1043
|
+
var sp = ($("#node-input-subPath").val() || "").trim();
|
|
1044
|
+
var root = (RED.settings.httpNodeRoot || "/").replace(/\/$/, "");
|
|
1045
|
+
if (sp) {
|
|
1046
|
+
$("#fc-url-hint")
|
|
1047
|
+
.css("color", "")
|
|
1048
|
+
.text(
|
|
1049
|
+
"Page served at: http://<host>:1880" +
|
|
1050
|
+
root +
|
|
1051
|
+
"/fromcubes/" +
|
|
1052
|
+
sp,
|
|
1053
|
+
);
|
|
1054
|
+
} else if (!node.endpoint || node.endpoint.indexOf("/fromcubes/") === 0) {
|
|
1055
|
+
$("#fc-url-hint")
|
|
1056
|
+
.css("color", "")
|
|
1057
|
+
.text("Sub-path required (will be served under /fromcubes/<sub-path>)");
|
|
1058
|
+
}
|
|
807
1059
|
}
|
|
808
|
-
$("#node-input-
|
|
1060
|
+
$("#node-input-subPath").on("input", updateHint);
|
|
809
1061
|
updateHint();
|
|
810
1062
|
|
|
1063
|
+
// Modules editableList (like function node's libs)
|
|
1064
|
+
var libsList = node.libs || [];
|
|
1065
|
+
$("#node-input-libs-container").css("min-height","68px").editableList({
|
|
1066
|
+
addItem: function(container, i, opt) {
|
|
1067
|
+
var lib = opt || {};
|
|
1068
|
+
var row = $('<div/>',{style:"display:flex;gap:8px;align-items:center;"}).appendTo(container);
|
|
1069
|
+
var modInput = $('<input/>',{type:"text",placeholder:"e.g. chart.js/auto@^4.4.0",style:"flex:1;"}).appendTo(row);
|
|
1070
|
+
var varInput = $('<input/>',{type:"text",placeholder:"Import as (e.g. Chart)",style:"width:140px;"}).appendTo(row);
|
|
1071
|
+
modInput.val(lib.module || "");
|
|
1072
|
+
varInput.val(lib.var || "");
|
|
1073
|
+
container.data("mod", modInput);
|
|
1074
|
+
container.data("var", varInput);
|
|
1075
|
+
},
|
|
1076
|
+
removable: true,
|
|
1077
|
+
sortable: true
|
|
1078
|
+
});
|
|
1079
|
+
libsList.forEach(function(lib) {
|
|
1080
|
+
$("#node-input-libs-container").editableList("addItem", lib);
|
|
1081
|
+
});
|
|
1082
|
+
|
|
811
1083
|
// Monaco
|
|
812
1084
|
window.__fcLoadMonaco(function (failed) {
|
|
813
1085
|
if (failed) {
|
|
814
|
-
console.error(
|
|
1086
|
+
console.error(
|
|
1087
|
+
"[FC-Monaco] PORTAL: Monaco load failed, using fallback textarea",
|
|
1088
|
+
);
|
|
815
1089
|
$("#fc-monaco").hide();
|
|
816
1090
|
$("#fc-fallback").show().val(code);
|
|
817
1091
|
$("#fc-head-monaco").hide();
|
|
@@ -819,11 +1093,15 @@
|
|
|
819
1093
|
return;
|
|
820
1094
|
}
|
|
821
1095
|
|
|
822
|
-
console.log(
|
|
1096
|
+
console.log(
|
|
1097
|
+
"[FC-Monaco] PORTAL: applying JSX defaults before model creation",
|
|
1098
|
+
);
|
|
823
1099
|
window.__fcApplyJsxDefaults();
|
|
824
1100
|
var opts = window.__fcEditorOpts;
|
|
825
1101
|
|
|
826
|
-
var jsxUri = monaco.Uri.parse(
|
|
1102
|
+
var jsxUri = monaco.Uri.parse(
|
|
1103
|
+
"file:///fc-portal-" + node.id + ".jsx",
|
|
1104
|
+
);
|
|
827
1105
|
console.log("[FC-Monaco] PORTAL: JSX model URI=" + jsxUri.toString());
|
|
828
1106
|
var existingJsx = monaco.editor.getModel(jsxUri);
|
|
829
1107
|
if (existingJsx) {
|
|
@@ -831,15 +1109,21 @@
|
|
|
831
1109
|
existingJsx.dispose();
|
|
832
1110
|
}
|
|
833
1111
|
var jsxModel = monaco.editor.createModel(code, "javascript", jsxUri);
|
|
834
|
-
console.log(
|
|
1112
|
+
console.log(
|
|
1113
|
+
"[FC-Monaco] PORTAL: JSX model created, language=" +
|
|
1114
|
+
jsxModel.getLanguageId(),
|
|
1115
|
+
);
|
|
835
1116
|
editorInstance = monaco.editor.create(
|
|
836
1117
|
document.getElementById("fc-monaco"),
|
|
837
1118
|
Object.assign({ model: jsxModel }, opts),
|
|
838
1119
|
);
|
|
839
1120
|
console.log("[FC-Monaco] PORTAL: JSX editor created");
|
|
840
|
-
if (window.__fcAttachSelfClose)
|
|
1121
|
+
if (window.__fcAttachSelfClose)
|
|
1122
|
+
window.__fcAttachSelfClose(editorInstance);
|
|
841
1123
|
|
|
842
|
-
var headUri = monaco.Uri.parse(
|
|
1124
|
+
var headUri = monaco.Uri.parse(
|
|
1125
|
+
"file:///fc-head-" + node.id + ".html",
|
|
1126
|
+
);
|
|
843
1127
|
var existingHead = monaco.editor.getModel(headUri);
|
|
844
1128
|
if (existingHead) existingHead.dispose();
|
|
845
1129
|
var headModel = monaco.editor.createModel(headCode, "html", headUri);
|
|
@@ -852,77 +1136,167 @@
|
|
|
852
1136
|
// Log markers after a short delay (diagnostics are async)
|
|
853
1137
|
setTimeout(function () {
|
|
854
1138
|
var markers = monaco.editor.getModelMarkers({ resource: jsxUri });
|
|
855
|
-
console.log(
|
|
1139
|
+
console.log(
|
|
1140
|
+
"[FC-Monaco] PORTAL: markers after 500ms, count=" +
|
|
1141
|
+
markers.length,
|
|
1142
|
+
);
|
|
856
1143
|
markers.forEach(function (m) {
|
|
857
|
-
console.log(
|
|
1144
|
+
console.log(
|
|
1145
|
+
"[FC-Monaco] PORTAL marker: code=" +
|
|
1146
|
+
m.code +
|
|
1147
|
+
" severity=" +
|
|
1148
|
+
m.severity +
|
|
1149
|
+
" msg=" +
|
|
1150
|
+
m.message,
|
|
1151
|
+
);
|
|
858
1152
|
});
|
|
859
|
-
console.log(
|
|
860
|
-
|
|
1153
|
+
console.log(
|
|
1154
|
+
"[FC-Monaco] PORTAL: current compilerOptions:",
|
|
1155
|
+
JSON.stringify(
|
|
1156
|
+
monaco.typescript.javascriptDefaults.getCompilerOptions(),
|
|
1157
|
+
),
|
|
1158
|
+
);
|
|
1159
|
+
console.log(
|
|
1160
|
+
"[FC-Monaco] PORTAL: current diagnosticsOptions:",
|
|
1161
|
+
JSON.stringify(
|
|
1162
|
+
monaco.typescript.javascriptDefaults.getDiagnosticsOptions(),
|
|
1163
|
+
),
|
|
1164
|
+
);
|
|
861
1165
|
}, 500);
|
|
862
1166
|
});
|
|
863
1167
|
|
|
864
1168
|
// Buttons
|
|
865
1169
|
$("#fc-btn-starter").on("click", function () {
|
|
866
|
-
|
|
867
|
-
|
|
1170
|
+
$("<div>Replace current JSX with default starter code?</div>").dialog({
|
|
1171
|
+
title: "Load Default",
|
|
1172
|
+
modal: true,
|
|
1173
|
+
width: 360,
|
|
1174
|
+
buttons: [
|
|
1175
|
+
{ text: "Cancel", click: function () { $(this).dialog("close"); } },
|
|
1176
|
+
{ text: "Replace", class: "primary", click: function () {
|
|
1177
|
+
if (editorInstance) editorInstance.setValue(STARTER);
|
|
1178
|
+
else $("#fc-fallback").val(STARTER);
|
|
1179
|
+
$(this).dialog("close");
|
|
1180
|
+
}},
|
|
1181
|
+
],
|
|
1182
|
+
close: function () { $(this).remove(); },
|
|
1183
|
+
});
|
|
868
1184
|
});
|
|
869
1185
|
|
|
870
1186
|
$("#fc-btn-preview").on("click", function () {
|
|
871
|
-
var
|
|
872
|
-
|
|
1187
|
+
var sp = ($("#node-input-subPath").val() || "").trim();
|
|
1188
|
+
var root = (RED.settings.httpNodeRoot || "/").replace(/\/$/, "");
|
|
1189
|
+
if (sp) window.open(root + "/fromcubes/" + sp, "_blank");
|
|
873
1190
|
});
|
|
874
1191
|
|
|
875
1192
|
$("#fc-btn-components").on("click", function () {
|
|
876
1193
|
$.getJSON("portal-react/registry", function (reg) {
|
|
877
|
-
var names = Object.keys(reg);
|
|
1194
|
+
var names = Object.keys(reg).sort();
|
|
878
1195
|
if (!names.length) {
|
|
879
1196
|
RED.notify("No component nodes on canvas.", "warning");
|
|
880
1197
|
return;
|
|
881
1198
|
}
|
|
882
|
-
|
|
1199
|
+
|
|
1200
|
+
function extractProps(code) {
|
|
1201
|
+
if (!code) return [];
|
|
1202
|
+
var m = code.match(/function\s+\w+\s*\(\s*\{([^}]*)\}/);
|
|
1203
|
+
if (!m) return [];
|
|
1204
|
+
return m[1].split(",").map(function (s) { return s.trim().split(/\s*=\s*/)[0]; }).filter(Boolean);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
var html =
|
|
1208
|
+
'<div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">' +
|
|
1209
|
+
'<input type="text" id="fc-comp-search" placeholder="Search..." ' +
|
|
1210
|
+
'style="width:100%;box-sizing:border-box;margin-bottom:6px;padding:5px 8px;border:1px solid rgba(128,128,128,.4);border-radius:3px;' +
|
|
1211
|
+
'background:var(--red-ui-form-input-background-color,#fff);color:var(--red-ui-form-text-color,#333);font-size:12px;flex-shrink:0;" />' +
|
|
1212
|
+
'<div id="fc-comp-list" style="flex:1;overflow-y:auto;">';
|
|
1213
|
+
|
|
883
1214
|
names.forEach(function (n) {
|
|
884
1215
|
var c = reg[n];
|
|
1216
|
+
var props = extractProps(c.code);
|
|
1217
|
+
var detailParts = [];
|
|
1218
|
+
if (props.length) {
|
|
1219
|
+
detailParts.push('<div style="margin:4px 0 0 0;font-size:11px;opacity:.6;">' +
|
|
1220
|
+
props.map(function (p) {
|
|
1221
|
+
return '<code style="background:rgba(128,128,128,.15);padding:0 4px;border-radius:2px;margin-right:3px;">' + p + '</code>';
|
|
1222
|
+
}).join("") + '</div>');
|
|
1223
|
+
}
|
|
1224
|
+
var hasDetail = detailParts.length > 0;
|
|
1225
|
+
|
|
885
1226
|
html +=
|
|
886
|
-
'<div
|
|
887
|
-
'
|
|
888
|
-
|
|
889
|
-
'">' +
|
|
890
|
-
n +
|
|
891
|
-
|
|
892
|
-
'
|
|
893
|
-
|
|
894
|
-
"] out:[" +
|
|
895
|
-
(c.outputs || []).join(",") +
|
|
896
|
-
"]</span></div>";
|
|
1227
|
+
'<div class="fc-comp-item" data-name="' + n + '" data-search="' + n.toLowerCase() + '" ' +
|
|
1228
|
+
'style="border-bottom:1px solid rgba(128,128,128,.1);">' +
|
|
1229
|
+
'<div style="display:flex;align-items:center;padding:4px 6px;cursor:pointer;" class="fc-comp-row">' +
|
|
1230
|
+
(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>') +
|
|
1231
|
+
'<span class="fc-comp-name" data-name="' + n + '" style="font-weight:600;font-size:12px;flex:1;">' + n + '</span>' +
|
|
1232
|
+
'</div>' +
|
|
1233
|
+
(hasDetail ? '<div class="fc-comp-detail" style="display:none;padding:0 6px 4px 20px;">' + detailParts.join("") + '</div>' : '') +
|
|
1234
|
+
'</div>';
|
|
897
1235
|
});
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1236
|
+
html += '</div></div>';
|
|
1237
|
+
|
|
1238
|
+
var $dlg = $("<div></div>").html(html).dialog({
|
|
1239
|
+
title: "Components",
|
|
1240
|
+
modal: true,
|
|
1241
|
+
width: 380,
|
|
1242
|
+
buttons: [
|
|
1243
|
+
{ text: "Close", click: function () { $(this).dialog("close"); } },
|
|
1244
|
+
],
|
|
1245
|
+
close: function () { $(this).remove(); },
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
// Search
|
|
1249
|
+
$dlg.find("#fc-comp-search").on("input", function () {
|
|
1250
|
+
var q = $(this).val().toLowerCase();
|
|
1251
|
+
$dlg.find(".fc-comp-item").each(function () {
|
|
1252
|
+
$(this).toggle($(this).data("search").indexOf(q) !== -1);
|
|
915
1253
|
});
|
|
916
|
-
|
|
917
|
-
|
|
1254
|
+
}).focus();
|
|
1255
|
+
|
|
1256
|
+
// Hover
|
|
1257
|
+
$dlg.on("mouseenter", ".fc-comp-row", function () {
|
|
1258
|
+
$(this).css("background", "rgba(128,128,128,.1)");
|
|
1259
|
+
}).on("mouseleave", ".fc-comp-row", function () {
|
|
1260
|
+
$(this).css("background", "");
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
// Arrow toggle detail
|
|
1264
|
+
$dlg.on("click", ".fc-comp-arrow", function (e) {
|
|
1265
|
+
e.stopPropagation();
|
|
1266
|
+
var $item = $(this).closest(".fc-comp-item");
|
|
1267
|
+
var $detail = $item.find(".fc-comp-detail");
|
|
1268
|
+
var open = $detail.is(":visible");
|
|
1269
|
+
$detail.slideToggle(100);
|
|
1270
|
+
$(this).css("transform", open ? "" : "rotate(90deg)");
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
// Click name: delete selection, insert <Tag></Tag> in one line, cursor between tags
|
|
1274
|
+
$dlg.on("click", ".fc-comp-name", function () {
|
|
1275
|
+
var name = $(this).data("name");
|
|
1276
|
+
var openTag = "<" + name + ">";
|
|
1277
|
+
var closeTag = "</" + name + ">";
|
|
1278
|
+
var text = openTag + closeTag;
|
|
1279
|
+
$dlg.dialog("close");
|
|
918
1280
|
if (editorInstance) {
|
|
919
|
-
editorInstance.
|
|
920
|
-
|
|
1281
|
+
var sel = editorInstance.getSelection();
|
|
1282
|
+
var startLine = sel.startLineNumber;
|
|
1283
|
+
var startCol = sel.startColumn;
|
|
1284
|
+
editorInstance.executeEdits("fc-components", [{
|
|
1285
|
+
range: sel,
|
|
1286
|
+
text: text,
|
|
1287
|
+
}]);
|
|
1288
|
+
setTimeout(function () {
|
|
1289
|
+
editorInstance.setPosition({ lineNumber: startLine, column: startCol + openTag.length });
|
|
1290
|
+
editorInstance.focus();
|
|
1291
|
+
}, 50);
|
|
921
1292
|
} else {
|
|
922
1293
|
var ta = $("#fc-fallback")[0];
|
|
923
|
-
var
|
|
924
|
-
|
|
925
|
-
|
|
1294
|
+
var s = ta.selectionStart, e = ta.selectionEnd, v = ta.value;
|
|
1295
|
+
ta.value = v.slice(0, s) + text + v.slice(e);
|
|
1296
|
+
setTimeout(function () {
|
|
1297
|
+
ta.selectionStart = ta.selectionEnd = s + openTag.length;
|
|
1298
|
+
ta.focus();
|
|
1299
|
+
}, 50);
|
|
926
1300
|
}
|
|
927
1301
|
});
|
|
928
1302
|
});
|
|
@@ -931,6 +1305,24 @@
|
|
|
931
1305
|
|
|
932
1306
|
oneditsave: function () {
|
|
933
1307
|
console.log("[FC-Monaco] PORTAL oneditsave");
|
|
1308
|
+
|
|
1309
|
+
// Clear legacy endpoint field + normalize subPath
|
|
1310
|
+
$("#node-input-endpoint").val("");
|
|
1311
|
+
this.endpoint = "";
|
|
1312
|
+
this.subPath = ($("#node-input-subPath").val() || "").trim();
|
|
1313
|
+
|
|
1314
|
+
// Collect libs from editableList
|
|
1315
|
+
var libs = [];
|
|
1316
|
+
var items = $("#node-input-libs-container").editableList("items");
|
|
1317
|
+
items.each(function() {
|
|
1318
|
+
var mod = $(this).data("mod").val().trim();
|
|
1319
|
+
var v = $(this).data("var").val().trim();
|
|
1320
|
+
if (mod) {
|
|
1321
|
+
libs.push({ module: mod, var: v });
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
this.libs = libs;
|
|
1325
|
+
|
|
934
1326
|
var code = editorInstance
|
|
935
1327
|
? editorInstance.getValue()
|
|
936
1328
|
: $("#fc-fallback").val();
|
|
@@ -997,37 +1389,59 @@
|
|
|
997
1389
|
Help
|
|
998
1390
|
============================================================ -->
|
|
999
1391
|
<script type="text/html" data-help-name="portal-react">
|
|
1392
|
+
<h3>Quick Reference</h3>
|
|
1393
|
+
<ul>
|
|
1394
|
+
<li><code>useNodeRed()</code> → <code>{ data, send, user, portalClient }</code></li>
|
|
1395
|
+
<li>Components from <code>fc-portal-component</code> nodes are auto-imported</li>
|
|
1396
|
+
<li>Must export <code><App /></code></li>
|
|
1397
|
+
<li>JSX is transpiled server-side at deploy</li>
|
|
1398
|
+
</ul>
|
|
1399
|
+
|
|
1000
1400
|
<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).
|
|
1401
|
+
Renders a React 19 application on a configurable HTTP endpoint with live
|
|
1402
|
+
WebSocket data binding. JSX is transpiled <strong>server-side at deploy
|
|
1403
|
+
time</strong> using esbuild — browsers receive pre-compiled JS with zero
|
|
1404
|
+
runtime compilation. Tailwind CSS 4 utility classes are generated
|
|
1405
|
+
server-side and served as static CSS.
|
|
1009
1406
|
</p>
|
|
1010
1407
|
|
|
1011
1408
|
<h3>Inputs</h3>
|
|
1012
1409
|
<p>
|
|
1013
|
-
<code>msg.payload</code> is pushed
|
|
1410
|
+
<code>msg.payload</code> is pushed via WebSocket. Set <code>msg._client</code>
|
|
1411
|
+
to target specific clients, or omit it to broadcast to all.
|
|
1014
1412
|
</p>
|
|
1015
1413
|
<pre>
|
|
1016
|
-
const { data, send } = useNodeRed();
|
|
1017
|
-
// data
|
|
1414
|
+
const { data, send, user, portalClient } = useNodeRed();
|
|
1415
|
+
// data = last msg.payload (reactive)
|
|
1416
|
+
// send(payload, topic?) — emit msg on output wire
|
|
1417
|
+
// user = portal auth data or null
|
|
1418
|
+
// portalClient = unique session/tab ID (assigned by server)</pre
|
|
1018
1419
|
>
|
|
1019
1420
|
|
|
1020
1421
|
<h3>Outputs</h3>
|
|
1021
1422
|
<p>
|
|
1022
1423
|
<code>send(payload, topic?)</code> emits a <code>msg</code> on the node's
|
|
1023
|
-
output wire.
|
|
1424
|
+
output wire via WebSocket.
|
|
1425
|
+
</p>
|
|
1426
|
+
|
|
1427
|
+
<h3>npm Packages</h3>
|
|
1428
|
+
<p>
|
|
1429
|
+
Add npm packages in the <strong>Modules</strong> list (e.g.
|
|
1430
|
+
<code>chart.js/auto@^4.4.0</code>, <code>d3</code>, <code>three</code>).
|
|
1431
|
+
Node-RED auto-installs them at deploy time. All packages are bundled with
|
|
1432
|
+
React into a single vendor IIFE via esbuild, cached by hash of installed
|
|
1433
|
+
versions. Use them in JSX via standard imports.
|
|
1024
1434
|
</p>
|
|
1025
1435
|
|
|
1026
1436
|
<h3>Deploy behavior</h3>
|
|
1027
1437
|
<ul>
|
|
1028
1438
|
<li>
|
|
1029
|
-
Each deploy re-transpiles JSX
|
|
1030
|
-
instant)
|
|
1439
|
+
Each deploy re-transpiles JSX via esbuild and rebuilds Tailwind CSS
|
|
1440
|
+
(both cached by content hash — unchanged code is instant)
|
|
1441
|
+
</li>
|
|
1442
|
+
<li>
|
|
1443
|
+
Vendor bundle (React + npm packages) rebuilt only when package versions
|
|
1444
|
+
change
|
|
1031
1445
|
</li>
|
|
1032
1446
|
<li>
|
|
1033
1447
|
Active WebSocket clients receive close code 1001 and auto-reconnect
|
|
@@ -1045,15 +1459,548 @@ const { data, send } = useNodeRed();
|
|
|
1045
1459
|
<p>
|
|
1046
1460
|
Components defined in <strong>fc-portal-component</strong> nodes on the
|
|
1047
1461
|
canvas are auto-injected into every portal-react page. Use them as JSX tags
|
|
1048
|
-
by their component name.
|
|
1462
|
+
by their component name — no imports needed.
|
|
1049
1463
|
</p>
|
|
1050
1464
|
|
|
1465
|
+
<h3>Portal Auth</h3>
|
|
1466
|
+
<p>
|
|
1467
|
+
When enabled in the Auth tab, reads <code>X-Portal-*</code> headers set by
|
|
1468
|
+
a reverse proxy (e.g. Nginx) and exposes user data:
|
|
1469
|
+
</p>
|
|
1470
|
+
<ul>
|
|
1471
|
+
<li><code>useNodeRed()</code> returns <code>{ data, send, user, portalClient }</code></li>
|
|
1472
|
+
<li>
|
|
1473
|
+
All WebSocket messages include <code>msg._client</code> with
|
|
1474
|
+
<code>{ portalClient, ...userFields }</code>
|
|
1475
|
+
</li>
|
|
1476
|
+
<li>
|
|
1477
|
+
To target a specific tab: keep <code>msg._client</code> (or set
|
|
1478
|
+
<code>msg._client = { portalClient: "..." }</code>)
|
|
1479
|
+
</li>
|
|
1480
|
+
<li>
|
|
1481
|
+
To target all tabs of a user: set
|
|
1482
|
+
<code>msg._client = { userId: "..." }</code> (no portalClient)
|
|
1483
|
+
</li>
|
|
1484
|
+
<li>
|
|
1485
|
+
To broadcast to all: remove <code>msg._client</code>
|
|
1486
|
+
</li>
|
|
1487
|
+
</ul>
|
|
1488
|
+
|
|
1051
1489
|
<h3>Custom Head HTML</h3>
|
|
1052
1490
|
<p>
|
|
1053
1491
|
Inject CDN links, fonts, or extra stylesheets into
|
|
1054
1492
|
<code><head></code>. Example:
|
|
1055
1493
|
</p>
|
|
1056
1494
|
<pre>
|
|
1057
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet"></pre
|
|
1495
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet"></pre
|
|
1058
1496
|
>
|
|
1497
|
+
|
|
1498
|
+
<h3>WebSocket Status</h3>
|
|
1499
|
+
<p>
|
|
1500
|
+
Optional <em>fromcubes</em> badge in the bottom-right corner showing
|
|
1501
|
+
connection state. Enable via the <strong>Show WebSocket status
|
|
1502
|
+
indicator</strong> checkbox (off by default).
|
|
1503
|
+
</p>
|
|
1504
|
+
</script>
|
|
1505
|
+
|
|
1506
|
+
<!-- ── Assets sidebar tab ──────────────────────────────────────── -->
|
|
1507
|
+
<script type="text/javascript">
|
|
1508
|
+
(function () {
|
|
1509
|
+
// Resolve httpNodeRoot for public URL prefix
|
|
1510
|
+
var nodeRoot = (RED.settings.httpNodeRoot || "/").replace(/\/$/, "");
|
|
1511
|
+
var publicBase = nodeRoot + "/fromcubes/public/";
|
|
1512
|
+
|
|
1513
|
+
function formatSize(bytes) {
|
|
1514
|
+
if (bytes < 1024) return bytes + " B";
|
|
1515
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
1516
|
+
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
var allEntries = [];
|
|
1520
|
+
var collapsed = {};
|
|
1521
|
+
|
|
1522
|
+
var content = $('<div class="red-ui-sidebar-info" style="height:100%;overflow:auto;padding:0;"></div>');
|
|
1523
|
+
var fileList = $('<div style="padding:0;"></div>').appendTo(content);
|
|
1524
|
+
|
|
1525
|
+
// ── Toolbar ──
|
|
1526
|
+
var toolbar = $('<div style="display:flex;align-items:center;gap:6px;margin:0 6px;padding:2px 0 0;"></div>');
|
|
1527
|
+
var fileInput = $('<input type="file" multiple style="display:none;">').appendTo(toolbar);
|
|
1528
|
+
$('<button class="red-ui-button red-ui-button-small" style="flex-shrink:0;"><i class="fa fa-upload"></i> Upload</button>')
|
|
1529
|
+
.on("click", function (e) { e.preventDefault(); fileInput.trigger("click"); })
|
|
1530
|
+
.appendTo(toolbar);
|
|
1531
|
+
$('<button class="red-ui-button red-ui-button-small" style="flex-shrink:0;"><i class="fa fa-folder-open"></i> New folder</button>')
|
|
1532
|
+
.on("click", function (e) { e.preventDefault(); showNewFolderInput(""); })
|
|
1533
|
+
.appendTo(toolbar);
|
|
1534
|
+
|
|
1535
|
+
// ── Helpers ──
|
|
1536
|
+
function pathExists(p) {
|
|
1537
|
+
for (var i = 0; i < allEntries.length; i++) {
|
|
1538
|
+
if (allEntries[i].name === p) return true;
|
|
1539
|
+
}
|
|
1540
|
+
return false;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function uploadFiles(files, targetDir) {
|
|
1544
|
+
if (!files || files.length === 0) return;
|
|
1545
|
+
var toUpload = [];
|
|
1546
|
+
var duplicates = [];
|
|
1547
|
+
for (var i = 0; i < files.length; i++) {
|
|
1548
|
+
var uploadPath = targetDir ? targetDir + "/" + files[i].name : files[i].name;
|
|
1549
|
+
if (pathExists(uploadPath)) {
|
|
1550
|
+
duplicates.push({ file: files[i], path: uploadPath });
|
|
1551
|
+
} else {
|
|
1552
|
+
toUpload.push({ file: files[i], path: uploadPath });
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
if (duplicates.length > 0) {
|
|
1556
|
+
var names = duplicates.map(function (d) { return d.file.name; }).join(", ");
|
|
1557
|
+
if (confirm("These files already exist: " + names + "\nOverwrite?")) {
|
|
1558
|
+
toUpload = toUpload.concat(duplicates);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
if (toUpload.length === 0) return;
|
|
1562
|
+
var pending = toUpload.length;
|
|
1563
|
+
toUpload.forEach(function (item) {
|
|
1564
|
+
var reader = new FileReader();
|
|
1565
|
+
reader.onload = function () {
|
|
1566
|
+
$.ajax({
|
|
1567
|
+
type: "POST",
|
|
1568
|
+
url: "portal-react/assets/upload/" + item.path.split("/").map(encodeURIComponent).join("/"),
|
|
1569
|
+
data: new Uint8Array(reader.result),
|
|
1570
|
+
contentType: "application/octet-stream",
|
|
1571
|
+
processData: false,
|
|
1572
|
+
success: function () { if (--pending === 0) refreshList(); },
|
|
1573
|
+
error: function () {
|
|
1574
|
+
RED.notify("Upload failed: " + item.file.name, "error");
|
|
1575
|
+
if (--pending === 0) refreshList();
|
|
1576
|
+
},
|
|
1577
|
+
});
|
|
1578
|
+
};
|
|
1579
|
+
reader.readAsArrayBuffer(item.file);
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
fileInput.on("change", function () {
|
|
1584
|
+
uploadFiles(this.files, "");
|
|
1585
|
+
fileInput.val("");
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
// External file drag & drop on content area (root upload)
|
|
1589
|
+
content.on("dragover", function (e) {
|
|
1590
|
+
if (e.originalEvent.dataTransfer.types.indexOf("Files") >= 0) {
|
|
1591
|
+
e.preventDefault();
|
|
1592
|
+
e.stopPropagation();
|
|
1593
|
+
content.css("background", "rgba(34,211,238,0.05)");
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
content.on("dragleave", function (e) {
|
|
1597
|
+
e.preventDefault();
|
|
1598
|
+
content.css("background", "");
|
|
1599
|
+
});
|
|
1600
|
+
content.on("drop", function (e) {
|
|
1601
|
+
e.preventDefault();
|
|
1602
|
+
e.stopPropagation();
|
|
1603
|
+
content.css("background", "");
|
|
1604
|
+
var dt = e.originalEvent.dataTransfer;
|
|
1605
|
+
if (dt.files && dt.files.length > 0) {
|
|
1606
|
+
uploadFiles(dt.files, "");
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
function moveItem(fromPath, toDir) {
|
|
1611
|
+
var filename = fromPath.split("/").pop();
|
|
1612
|
+
var newPath = toDir ? toDir + "/" + filename : filename;
|
|
1613
|
+
if (fromPath === newPath) return;
|
|
1614
|
+
if (pathExists(newPath)) {
|
|
1615
|
+
if (!confirm("'" + filename + "' already exists in this folder. Overwrite?")) return;
|
|
1616
|
+
}
|
|
1617
|
+
$.ajax({
|
|
1618
|
+
type: "POST", url: "portal-react/assets/move",
|
|
1619
|
+
contentType: "application/json",
|
|
1620
|
+
data: JSON.stringify({ from: fromPath, to: newPath }),
|
|
1621
|
+
success: function () { refreshList(); },
|
|
1622
|
+
error: function (xhr) {
|
|
1623
|
+
var msg = xhr.responseJSON ? xhr.responseJSON.error : "move failed";
|
|
1624
|
+
RED.notify("Move failed: " + msg, "error");
|
|
1625
|
+
},
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// ── Inline new-folder input ──
|
|
1630
|
+
function showNewFolderInput(parentDir) {
|
|
1631
|
+
var existingInput = fileList.find(".fc-new-folder-row");
|
|
1632
|
+
if (existingInput.length) existingInput.remove();
|
|
1633
|
+
|
|
1634
|
+
var depth = parentDir ? parentDir.split("/").length : 0;
|
|
1635
|
+
var indent = 8 + depth * 18;
|
|
1636
|
+
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>');
|
|
1637
|
+
row.css("padding-left", indent + "px");
|
|
1638
|
+
$('<i class="fa fa-folder" style="color:#fbbf24;font-size:13px;width:16px;text-align:center;"></i>').appendTo(row);
|
|
1639
|
+
var inp = $('<input type="text" placeholder="folder name" class="red-ui-searchBox-input" style="flex:1;padding:3px 6px;font-size:12px;">');
|
|
1640
|
+
inp.appendTo(row);
|
|
1641
|
+
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>');
|
|
1642
|
+
var cancelBtn = $('<button class="red-ui-button red-ui-button-small" title="Cancel"><i class="fa fa-times"></i></button>');
|
|
1643
|
+
function submit() {
|
|
1644
|
+
var name = inp.val().trim();
|
|
1645
|
+
if (!name) { row.remove(); return; }
|
|
1646
|
+
var p = parentDir ? parentDir + "/" + name : name;
|
|
1647
|
+
if (pathExists(p)) {
|
|
1648
|
+
RED.notify("Folder '" + name + "' already exists", "warning");
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
$.ajax({ type: "POST", url: "portal-react/assets/mkdir", contentType: "application/json", data: JSON.stringify({ path: p }),
|
|
1652
|
+
success: function () { row.remove(); refreshList(); },
|
|
1653
|
+
error: function () { RED.notify("Failed to create folder", "error"); },
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
okBtn.on("click", function (e) { e.preventDefault(); submit(); });
|
|
1657
|
+
cancelBtn.on("click", function (e) { e.preventDefault(); row.remove(); });
|
|
1658
|
+
inp.on("keydown", function (e) {
|
|
1659
|
+
if (e.key === "Enter") submit();
|
|
1660
|
+
if (e.key === "Escape") row.remove();
|
|
1661
|
+
});
|
|
1662
|
+
okBtn.appendTo(row);
|
|
1663
|
+
cancelBtn.appendTo(row);
|
|
1664
|
+
|
|
1665
|
+
// Insert after the parent folder row, or at top
|
|
1666
|
+
if (parentDir) {
|
|
1667
|
+
var parentRow = fileList.find('[data-path="' + parentDir + '"]');
|
|
1668
|
+
if (parentRow.length) { row.insertAfter(parentRow); } else { fileList.prepend(row); }
|
|
1669
|
+
} else {
|
|
1670
|
+
fileList.prepend(row);
|
|
1671
|
+
}
|
|
1672
|
+
inp.focus();
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// ── Context menu (native Node-RED style) ──
|
|
1676
|
+
var activeMenu = null;
|
|
1677
|
+
function closeMenu() {
|
|
1678
|
+
if (activeMenu) { activeMenu.remove(); activeMenu = null; }
|
|
1679
|
+
}
|
|
1680
|
+
$(document).on("click", closeMenu);
|
|
1681
|
+
|
|
1682
|
+
function showMenu(anchor, items) {
|
|
1683
|
+
closeMenu();
|
|
1684
|
+
var menu = $('<ul class="red-ui-menu red-ui-menu-dropdown" style="position:absolute;z-index:10000;display:block;"></ul>');
|
|
1685
|
+
items.forEach(function (item) {
|
|
1686
|
+
if (item.divider) {
|
|
1687
|
+
menu.append('<li class="red-ui-menu-divider"></li>');
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
var li = $('<li></li>');
|
|
1691
|
+
var a = $('<a href="#"></a>');
|
|
1692
|
+
if (item.danger) a.css("color", "var(--red-ui-text-color-error)");
|
|
1693
|
+
a.append('<i class="fa ' + item.icon + '" style="width:18px;text-align:center;"></i> ');
|
|
1694
|
+
a.append($('<span class="red-ui-menu-label"></span>').text(item.label));
|
|
1695
|
+
a.on("click", function (e) {
|
|
1696
|
+
e.preventDefault();
|
|
1697
|
+
e.stopPropagation();
|
|
1698
|
+
closeMenu();
|
|
1699
|
+
item.action();
|
|
1700
|
+
});
|
|
1701
|
+
li.append(a);
|
|
1702
|
+
menu.append(li);
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
$("body").append(menu);
|
|
1706
|
+
activeMenu = menu;
|
|
1707
|
+
|
|
1708
|
+
// Position near the anchor
|
|
1709
|
+
var off = anchor.offset();
|
|
1710
|
+
var top = off.top + anchor.outerHeight() + 2;
|
|
1711
|
+
var left = off.left - menu.outerWidth() + anchor.outerWidth();
|
|
1712
|
+
if (left < 0) left = off.left;
|
|
1713
|
+
if (top + menu.outerHeight() > $(window).height()) top = off.top - menu.outerHeight() - 2;
|
|
1714
|
+
menu.css({ top: top, left: left });
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
var adminRoot = (RED.settings.httpAdminRoot || "/").replace(/\/$/, "");
|
|
1718
|
+
|
|
1719
|
+
function showRenameInput(rowEl, fullPath, currentName) {
|
|
1720
|
+
var nameSpan = rowEl.find("span").filter(function () { return $(this).text() === currentName; }).first();
|
|
1721
|
+
if (!nameSpan.length) return;
|
|
1722
|
+
var origText = nameSpan.text();
|
|
1723
|
+
var inp = $('<input type="text" class="red-ui-searchBox-input" style="flex:1;padding:3px 6px;font-size:12px;">').val(origText);
|
|
1724
|
+
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>');
|
|
1725
|
+
var cancelBtn = $('<button class="red-ui-button red-ui-button-small" title="Cancel"><i class="fa fa-times"></i></button>');
|
|
1726
|
+
var dotIdx = origText.lastIndexOf(".");
|
|
1727
|
+
var hasExt = dotIdx > 0; // has extension (not hidden file)
|
|
1728
|
+
var origExt = hasExt ? origText.slice(dotIdx) : "";
|
|
1729
|
+
|
|
1730
|
+
nameSpan.replaceWith(inp);
|
|
1731
|
+
inp.after(cancelBtn).after(okBtn);
|
|
1732
|
+
inp.focus();
|
|
1733
|
+
// Select only the name part before extension
|
|
1734
|
+
var el = inp[0];
|
|
1735
|
+
if (hasExt && el.setSelectionRange) {
|
|
1736
|
+
el.setSelectionRange(0, dotIdx);
|
|
1737
|
+
} else {
|
|
1738
|
+
inp.select();
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
function restore() {
|
|
1742
|
+
okBtn.remove();
|
|
1743
|
+
cancelBtn.remove();
|
|
1744
|
+
inp.replaceWith($('<span style="flex:1;font-size:12px;word-break:break-all;"></span>').text(origText));
|
|
1745
|
+
}
|
|
1746
|
+
function submit() {
|
|
1747
|
+
var newName = inp.val().trim();
|
|
1748
|
+
if (!newName) {
|
|
1749
|
+
RED.notify("Name cannot be empty", "warning");
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
// Warn if extension changed or removed
|
|
1753
|
+
if (hasExt) {
|
|
1754
|
+
var newDot = newName.lastIndexOf(".");
|
|
1755
|
+
var newExt = newDot > 0 ? newName.slice(newDot) : "";
|
|
1756
|
+
if (newExt.toLowerCase() !== origExt.toLowerCase()) {
|
|
1757
|
+
if (!confirm("Extension changed from '" + origExt + "' to '" + (newExt || "none") + "'. Continue?")) return;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
if (newName === origText) { restore(); return; }
|
|
1761
|
+
var parentDir = fullPath.indexOf("/") >= 0 ? fullPath.slice(0, fullPath.lastIndexOf("/")) : "";
|
|
1762
|
+
var newPath = parentDir ? parentDir + "/" + newName : newName;
|
|
1763
|
+
if (pathExists(newPath)) {
|
|
1764
|
+
RED.notify("'" + newName + "' already exists", "warning");
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
$.ajax({
|
|
1768
|
+
type: "POST", url: "portal-react/assets/move",
|
|
1769
|
+
contentType: "application/json",
|
|
1770
|
+
data: JSON.stringify({ from: fullPath, to: newPath }),
|
|
1771
|
+
success: function () { refreshList(); },
|
|
1772
|
+
error: function (xhr) {
|
|
1773
|
+
var msg = xhr.responseJSON ? xhr.responseJSON.error : "rename failed";
|
|
1774
|
+
RED.notify("Rename failed: " + msg, "error");
|
|
1775
|
+
restore();
|
|
1776
|
+
},
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
okBtn.on("click", function (e) { e.preventDefault(); e.stopPropagation(); submit(); });
|
|
1780
|
+
cancelBtn.on("click", function (e) { e.preventDefault(); e.stopPropagation(); restore(); });
|
|
1781
|
+
inp.on("keydown", function (e) {
|
|
1782
|
+
if (e.key === "Enter") submit();
|
|
1783
|
+
if (e.key === "Escape") restore();
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// ── Tree rendering ──
|
|
1788
|
+
function buildTree(entries) {
|
|
1789
|
+
// Build nested structure: { children: { name: { type, children, entry } } }
|
|
1790
|
+
var root = { children: {} };
|
|
1791
|
+
entries.forEach(function (e) {
|
|
1792
|
+
var parts = e.name.split("/");
|
|
1793
|
+
var node = root;
|
|
1794
|
+
for (var i = 0; i < parts.length; i++) {
|
|
1795
|
+
if (!node.children[parts[i]]) {
|
|
1796
|
+
node.children[parts[i]] = { children: {} };
|
|
1797
|
+
}
|
|
1798
|
+
node = node.children[parts[i]];
|
|
1799
|
+
}
|
|
1800
|
+
node.entry = e;
|
|
1801
|
+
});
|
|
1802
|
+
return root;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
var ROOT_KEY = "__root__";
|
|
1806
|
+
|
|
1807
|
+
function renderTree() {
|
|
1808
|
+
fileList.empty();
|
|
1809
|
+
var isOpen = !collapsed[ROOT_KEY];
|
|
1810
|
+
|
|
1811
|
+
// Root folder row — always visible
|
|
1812
|
+
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>');
|
|
1813
|
+
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>');
|
|
1814
|
+
rootArrow.on("click", function () { collapsed[ROOT_KEY] = isOpen; renderTree(); });
|
|
1815
|
+
rootRow.append(rootArrow);
|
|
1816
|
+
$('<i class="fa ' + (isOpen ? 'fa-folder-open' : 'fa-folder') + '" style="color:#fbbf24;font-size:12px;width:16px;text-align:center;"></i>').appendTo(rootRow);
|
|
1817
|
+
$('<span style="flex:1;font-size:12px;cursor:pointer;opacity:0.8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>').text(publicBase.replace(/\/$/, ""))
|
|
1818
|
+
.on("click", function () { collapsed[ROOT_KEY] = isOpen; renderTree(); })
|
|
1819
|
+
.appendTo(rootRow);
|
|
1820
|
+
fileList.append(rootRow);
|
|
1821
|
+
|
|
1822
|
+
if (!isOpen) return;
|
|
1823
|
+
|
|
1824
|
+
if (allEntries.length === 0) {
|
|
1825
|
+
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>'));
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
var tree = buildTree(allEntries);
|
|
1829
|
+
renderNode(tree, "", 1);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function renderNode(node, parentPath, depth) {
|
|
1833
|
+
// Collect and sort: dirs first, then files
|
|
1834
|
+
var names = Object.keys(node.children).sort(function (a, b) {
|
|
1835
|
+
var aIsDir = node.children[a].entry && node.children[a].entry.type === "dir";
|
|
1836
|
+
var bIsDir = node.children[b].entry && node.children[b].entry.type === "dir";
|
|
1837
|
+
if (aIsDir && !bIsDir) return -1;
|
|
1838
|
+
if (!aIsDir && bIsDir) return 1;
|
|
1839
|
+
return a.localeCompare(b);
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
names.forEach(function (name) {
|
|
1843
|
+
var child = node.children[name];
|
|
1844
|
+
var e = child.entry;
|
|
1845
|
+
if (!e) return;
|
|
1846
|
+
var fullPath = e.name;
|
|
1847
|
+
var indent = 8 + depth * 18;
|
|
1848
|
+
var isDir = e.type === "dir";
|
|
1849
|
+
var isOpen = !collapsed[fullPath];
|
|
1850
|
+
|
|
1851
|
+
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>');
|
|
1852
|
+
row.css("padding-left", indent + "px");
|
|
1853
|
+
|
|
1854
|
+
if (isDir) {
|
|
1855
|
+
// Expand/collapse arrow
|
|
1856
|
+
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>');
|
|
1857
|
+
arrow.on("click", function (e) {
|
|
1858
|
+
e.stopPropagation();
|
|
1859
|
+
collapsed[fullPath] = isOpen;
|
|
1860
|
+
renderTree();
|
|
1861
|
+
});
|
|
1862
|
+
row.append(arrow);
|
|
1863
|
+
$('<i class="fa ' + (isOpen ? 'fa-folder-open' : 'fa-folder') + '" style="color:#fbbf24;font-size:12px;width:16px;text-align:center;"></i>').appendTo(row);
|
|
1864
|
+
$('<span style="flex:1;font-size:12px;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + fullPath + '"></span>').text(name)
|
|
1865
|
+
.on("click", function () { collapsed[fullPath] = isOpen; renderTree(); })
|
|
1866
|
+
.appendTo(row);
|
|
1867
|
+
|
|
1868
|
+
// Context menu trigger
|
|
1869
|
+
var dirMenuBtn = $('<button class="red-ui-button red-ui-button-small" style="padding:1px 5px;"><i class="fa fa-ellipsis-v"></i></button>');
|
|
1870
|
+
(function (fp, nm) {
|
|
1871
|
+
dirMenuBtn.on("click", function (ev) {
|
|
1872
|
+
ev.preventDefault();
|
|
1873
|
+
ev.stopPropagation();
|
|
1874
|
+
showMenu(dirMenuBtn, [
|
|
1875
|
+
{ icon: "fa-pencil", label: "Rename", action: function () {
|
|
1876
|
+
showRenameInput(row, fp, nm);
|
|
1877
|
+
}},
|
|
1878
|
+
{ icon: "fa-plus", label: "New subfolder", action: function () {
|
|
1879
|
+
if (collapsed[fp]) { collapsed[fp] = false; renderTree(); }
|
|
1880
|
+
setTimeout(function () { showNewFolderInput(fp); }, 50);
|
|
1881
|
+
}},
|
|
1882
|
+
{ divider: true },
|
|
1883
|
+
{ icon: "fa-trash", label: "Delete folder", danger: true, action: function () {
|
|
1884
|
+
if (!confirm("Delete folder '" + nm + "' and all contents?")) return;
|
|
1885
|
+
$.ajax({
|
|
1886
|
+
type: "DELETE",
|
|
1887
|
+
url: "portal-react/assets/" + fp.split("/").map(encodeURIComponent).join("/"),
|
|
1888
|
+
success: function () { refreshList(); },
|
|
1889
|
+
error: function () { RED.notify("Delete failed", "error"); },
|
|
1890
|
+
});
|
|
1891
|
+
}},
|
|
1892
|
+
]);
|
|
1893
|
+
});
|
|
1894
|
+
})(fullPath, name);
|
|
1895
|
+
dirMenuBtn.appendTo(row);
|
|
1896
|
+
|
|
1897
|
+
// Drop target
|
|
1898
|
+
row.on("dragover", function (ev) {
|
|
1899
|
+
ev.preventDefault();
|
|
1900
|
+
ev.stopPropagation();
|
|
1901
|
+
row.css("background", "rgba(251,191,36,0.08)");
|
|
1902
|
+
});
|
|
1903
|
+
row.on("dragleave", function () { row.css("background", ""); });
|
|
1904
|
+
row.on("drop", function (ev) {
|
|
1905
|
+
ev.preventDefault();
|
|
1906
|
+
ev.stopPropagation();
|
|
1907
|
+
row.css("background", "");
|
|
1908
|
+
var srcPath = ev.originalEvent.dataTransfer.getData("text/x-asset-path");
|
|
1909
|
+
if (srcPath) {
|
|
1910
|
+
moveItem(srcPath, fullPath);
|
|
1911
|
+
} else if (ev.originalEvent.dataTransfer.files && ev.originalEvent.dataTransfer.files.length > 0) {
|
|
1912
|
+
uploadFiles(ev.originalEvent.dataTransfer.files, fullPath);
|
|
1913
|
+
}
|
|
1914
|
+
});
|
|
1915
|
+
fileList.append(row);
|
|
1916
|
+
|
|
1917
|
+
// Render children if open
|
|
1918
|
+
if (isOpen) {
|
|
1919
|
+
renderNode(child, fullPath, depth + 1);
|
|
1920
|
+
}
|
|
1921
|
+
} else {
|
|
1922
|
+
// File
|
|
1923
|
+
row.attr("draggable", "true").css("cursor", "grab");
|
|
1924
|
+
$('<span style="width:10px;"></span>').appendTo(row); // spacer for arrow alignment
|
|
1925
|
+
$('<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);
|
|
1926
|
+
$('<span style="flex:1;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + fullPath + '"></span>').text(name).appendTo(row);
|
|
1927
|
+
$('<span style="font-size:10px;color:var(--red-ui-tertiary-text-color);white-space:nowrap;"></span>').text(formatSize(e.size)).appendTo(row);
|
|
1928
|
+
|
|
1929
|
+
row.on("dragstart", function (ev) {
|
|
1930
|
+
ev.originalEvent.dataTransfer.setData("text/x-asset-path", fullPath);
|
|
1931
|
+
ev.originalEvent.dataTransfer.effectAllowed = "move";
|
|
1932
|
+
row.css("opacity", "0.4");
|
|
1933
|
+
});
|
|
1934
|
+
row.on("dragend", function () { row.css("opacity", "1"); });
|
|
1935
|
+
|
|
1936
|
+
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>');
|
|
1937
|
+
var fileMenuBtn = $('<button class="red-ui-button red-ui-button-small" style="padding:1px 5px;"><i class="fa fa-ellipsis-v"></i></button>');
|
|
1938
|
+
(function (fp, nm) {
|
|
1939
|
+
copyBtn.on("click", function (ev) {
|
|
1940
|
+
ev.preventDefault();
|
|
1941
|
+
ev.stopPropagation();
|
|
1942
|
+
var url = publicBase + fp;
|
|
1943
|
+
navigator.clipboard.writeText(url).then(function () {
|
|
1944
|
+
RED.notify("Copied: " + url, { type: "success", timeout: 2000 });
|
|
1945
|
+
});
|
|
1946
|
+
});
|
|
1947
|
+
fileMenuBtn.on("click", function (ev) {
|
|
1948
|
+
ev.preventDefault();
|
|
1949
|
+
ev.stopPropagation();
|
|
1950
|
+
showMenu(fileMenuBtn, [
|
|
1951
|
+
{ icon: "fa-pencil", label: "Rename", action: function () {
|
|
1952
|
+
showRenameInput(row, fp, nm);
|
|
1953
|
+
}},
|
|
1954
|
+
{ icon: "fa-download", label: "Download", action: function () {
|
|
1955
|
+
var a = document.createElement("a");
|
|
1956
|
+
a.href = adminRoot + "/portal-react/assets/download/" + fp.split("/").map(encodeURIComponent).join("/");
|
|
1957
|
+
a.download = nm;
|
|
1958
|
+
document.body.appendChild(a);
|
|
1959
|
+
a.click();
|
|
1960
|
+
document.body.removeChild(a);
|
|
1961
|
+
}},
|
|
1962
|
+
{ divider: true },
|
|
1963
|
+
{ icon: "fa-trash", label: "Delete", danger: true, action: function () {
|
|
1964
|
+
$.ajax({
|
|
1965
|
+
type: "DELETE",
|
|
1966
|
+
url: "portal-react/assets/" + fp.split("/").map(encodeURIComponent).join("/"),
|
|
1967
|
+
success: function () { refreshList(); },
|
|
1968
|
+
error: function () { RED.notify("Delete failed: " + nm, "error"); },
|
|
1969
|
+
});
|
|
1970
|
+
}},
|
|
1971
|
+
]);
|
|
1972
|
+
});
|
|
1973
|
+
})(fullPath, name);
|
|
1974
|
+
copyBtn.appendTo(row);
|
|
1975
|
+
fileMenuBtn.appendTo(row);
|
|
1976
|
+
fileList.append(row);
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
function refreshList() {
|
|
1982
|
+
$.getJSON("portal-react/assets", function (entries) {
|
|
1983
|
+
allEntries = entries || [];
|
|
1984
|
+
renderTree();
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
RED.sidebar.addTab({
|
|
1989
|
+
id: "fromcubes-public",
|
|
1990
|
+
label: "Portal Assets",
|
|
1991
|
+
name: "fromcubes portal assets",
|
|
1992
|
+
iconClass: "fa fa-desktop",
|
|
1993
|
+
content: content[0],
|
|
1994
|
+
toolbar: toolbar[0],
|
|
1995
|
+
pinned: false,
|
|
1996
|
+
enableOnEdit: true,
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
RED.actions.add("fromcubes:show-assets", function () {
|
|
2000
|
+
RED.sidebar.show("fromcubes-public");
|
|
2001
|
+
});
|
|
2002
|
+
|
|
2003
|
+
RED.events.on("sidebar:resize", function () { refreshList(); });
|
|
2004
|
+
setTimeout(refreshList, 1000);
|
|
2005
|
+
})();
|
|
1059
2006
|
</script>
|