@aaqu/fromcubes-portal-react 0.1.0-alpha.12 → 0.1.0-alpha.14

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.
@@ -56,7 +56,7 @@
56
56
  var libContent = [
57
57
  "declare var React: any;",
58
58
  "declare var ReactDOM: any;",
59
- "declare function useNodeRed(): { data: any; send: (payload: any, topic?: string) => void; user: { userId?: string; userName?: string; username?: string; email?: string; role?: string; groups?: any[] } | null };",
59
+ "declare function useNodeRed(): { 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 };",
60
60
  ].join("\n");
61
61
  console.log(PREFIX, "addExtraLib globals.d.ts");
62
62
  jsDef.addExtraLib(libContent, "file:///globals.d.ts");
@@ -920,7 +920,7 @@
920
920
  var headEditorInstance = null;
921
921
 
922
922
  var STARTER = [
923
- "// useNodeRed() \u2192 { data, send }",
923
+ "// useNodeRed() \u2192 { data, send, portalClient }",
924
924
  "// data = last msg.payload from input wire",
925
925
  "// send(payload, topic?) = push msg to output wire",
926
926
  "// Components from fc-portal-component nodes are available by name.",
@@ -1347,7 +1347,7 @@
1347
1347
  <script type="text/html" data-help-name="portal-react">
1348
1348
  <h3>Quick Reference</h3>
1349
1349
  <ul>
1350
- <li><code>useNodeRed()</code> &rarr; <code>{ data, send, user }</code></li>
1350
+ <li><code>useNodeRed()</code> &rarr; <code>{ data, send, user, portalClient }</code></li>
1351
1351
  <li>Components from <code>fc-portal-component</code> nodes are auto-imported</li>
1352
1352
  <li>Must export <code>&lt;App /&gt;</code></li>
1353
1353
  <li>JSX is transpiled server-side at deploy</li>
@@ -1363,13 +1363,15 @@
1363
1363
 
1364
1364
  <h3>Inputs</h3>
1365
1365
  <p>
1366
- <code>msg.payload</code> is pushed to all connected clients via WebSocket.
1366
+ <code>msg.payload</code> is pushed via WebSocket. Set <code>msg._client</code>
1367
+ to target specific clients, or omit it to broadcast to all.
1367
1368
  </p>
1368
1369
  <pre>
1369
- const { data, send, user } = useNodeRed();
1370
- // data = last msg.payload (reactive)
1370
+ const { data, send, user, portalClient } = useNodeRed();
1371
+ // data = last msg.payload (reactive)
1371
1372
  // send(payload, topic?) — emit msg on output wire
1372
- // user = portal auth data or null</pre
1373
+ // user = portal auth data or null
1374
+ // portalClient = unique session/tab ID (assigned by server)</pre
1373
1375
  >
1374
1376
 
1375
1377
  <h3>Outputs</h3>
@@ -1422,9 +1424,21 @@ const { data, send, user } = useNodeRed();
1422
1424
  a reverse proxy (e.g. Nginx) and exposes user data:
1423
1425
  </p>
1424
1426
  <ul>
1425
- <li><code>useNodeRed()</code> returns <code>{ data, send, user }</code></li>
1427
+ <li><code>useNodeRed()</code> returns <code>{ data, send, user, portalClient }</code></li>
1426
1428
  <li>
1427
- Messages from WebSocket include <code>msg._client</code> with user info
1429
+ All WebSocket messages include <code>msg._client</code> with
1430
+ <code>{ portalClient, ...userFields }</code>
1431
+ </li>
1432
+ <li>
1433
+ To target a specific tab: keep <code>msg._client</code> (or set
1434
+ <code>msg._client = { portalClient: "..." }</code>)
1435
+ </li>
1436
+ <li>
1437
+ To target all tabs of a user: set
1438
+ <code>msg._client = { userId: "..." }</code> (no portalClient)
1439
+ </li>
1440
+ <li>
1441
+ To broadcast to all: remove <code>msg._client</code>
1428
1442
  </li>
1429
1443
  </ul>
1430
1444
 
@@ -1444,3 +1458,488 @@ const { data, send, user } = useNodeRed();
1444
1458
  indicator</strong> checkbox (off by default).
1445
1459
  </p>
1446
1460
  </script>
1461
+
1462
+ <!-- ── Assets sidebar tab ──────────────────────────────────────── -->
1463
+ <script type="text/javascript">
1464
+ (function () {
1465
+ // Resolve httpNodeRoot for public URL prefix
1466
+ var nodeRoot = (RED.settings.httpNodeRoot || "/").replace(/\/$/, "");
1467
+ var publicBase = nodeRoot + "/fromcubes/public/";
1468
+
1469
+ function formatSize(bytes) {
1470
+ if (bytes < 1024) return bytes + " B";
1471
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
1472
+ return (bytes / (1024 * 1024)).toFixed(1) + " MB";
1473
+ }
1474
+
1475
+ var allEntries = [];
1476
+ var collapsed = {};
1477
+
1478
+ var content = $('<div class="red-ui-sidebar-info" style="height:100%;overflow:auto;padding:0;"></div>');
1479
+ var fileList = $('<div style="padding:0;"></div>').appendTo(content);
1480
+
1481
+ // ── Toolbar ──
1482
+ var toolbar = $('<div style="display:flex;align-items:center;gap:4px;"></div>');
1483
+ var fileInput = $('<input type="file" multiple style="display:none;">').appendTo(toolbar);
1484
+ $('<a class="red-ui-sidebar-header-button" href="#"><i class="fa fa-upload"></i> Upload</a>')
1485
+ .on("click", function (e) { e.preventDefault(); fileInput.trigger("click"); })
1486
+ .appendTo(toolbar);
1487
+ $('<a class="red-ui-sidebar-header-button" href="#"><i class="fa fa-folder-open"></i> New folder</a>')
1488
+ .on("click", function (e) { e.preventDefault(); showNewFolderInput(""); })
1489
+ .appendTo(toolbar);
1490
+
1491
+ // ── Helpers ──
1492
+ function pathExists(p) {
1493
+ for (var i = 0; i < allEntries.length; i++) {
1494
+ if (allEntries[i].name === p) return true;
1495
+ }
1496
+ return false;
1497
+ }
1498
+
1499
+ function uploadFiles(files, targetDir) {
1500
+ if (!files || files.length === 0) return;
1501
+ var toUpload = [];
1502
+ var duplicates = [];
1503
+ for (var i = 0; i < files.length; i++) {
1504
+ var uploadPath = targetDir ? targetDir + "/" + files[i].name : files[i].name;
1505
+ if (pathExists(uploadPath)) {
1506
+ duplicates.push({ file: files[i], path: uploadPath });
1507
+ } else {
1508
+ toUpload.push({ file: files[i], path: uploadPath });
1509
+ }
1510
+ }
1511
+ if (duplicates.length > 0) {
1512
+ var names = duplicates.map(function (d) { return d.file.name; }).join(", ");
1513
+ if (confirm("These files already exist: " + names + "\nOverwrite?")) {
1514
+ toUpload = toUpload.concat(duplicates);
1515
+ }
1516
+ }
1517
+ if (toUpload.length === 0) return;
1518
+ var pending = toUpload.length;
1519
+ toUpload.forEach(function (item) {
1520
+ var reader = new FileReader();
1521
+ reader.onload = function () {
1522
+ $.ajax({
1523
+ type: "POST",
1524
+ url: "portal-react/assets/upload/" + item.path.split("/").map(encodeURIComponent).join("/"),
1525
+ data: new Uint8Array(reader.result),
1526
+ contentType: "application/octet-stream",
1527
+ processData: false,
1528
+ success: function () { if (--pending === 0) refreshList(); },
1529
+ error: function () {
1530
+ RED.notify("Upload failed: " + item.file.name, "error");
1531
+ if (--pending === 0) refreshList();
1532
+ },
1533
+ });
1534
+ };
1535
+ reader.readAsArrayBuffer(item.file);
1536
+ });
1537
+ }
1538
+
1539
+ fileInput.on("change", function () {
1540
+ uploadFiles(this.files, "");
1541
+ fileInput.val("");
1542
+ });
1543
+
1544
+ // External file drag & drop on content area (root upload)
1545
+ content.on("dragover", function (e) {
1546
+ if (e.originalEvent.dataTransfer.types.indexOf("Files") >= 0) {
1547
+ e.preventDefault();
1548
+ e.stopPropagation();
1549
+ content.css("background", "rgba(34,211,238,0.05)");
1550
+ }
1551
+ });
1552
+ content.on("dragleave", function (e) {
1553
+ e.preventDefault();
1554
+ content.css("background", "");
1555
+ });
1556
+ content.on("drop", function (e) {
1557
+ e.preventDefault();
1558
+ e.stopPropagation();
1559
+ content.css("background", "");
1560
+ var dt = e.originalEvent.dataTransfer;
1561
+ if (dt.files && dt.files.length > 0) {
1562
+ uploadFiles(dt.files, "");
1563
+ }
1564
+ });
1565
+
1566
+ function moveItem(fromPath, toDir) {
1567
+ var filename = fromPath.split("/").pop();
1568
+ var newPath = toDir ? toDir + "/" + filename : filename;
1569
+ if (fromPath === newPath) return;
1570
+ if (pathExists(newPath)) {
1571
+ if (!confirm("'" + filename + "' already exists in this folder. Overwrite?")) return;
1572
+ }
1573
+ $.ajax({
1574
+ type: "POST", url: "portal-react/assets/move",
1575
+ contentType: "application/json",
1576
+ data: JSON.stringify({ from: fromPath, to: newPath }),
1577
+ success: function () { refreshList(); },
1578
+ error: function (xhr) {
1579
+ var msg = xhr.responseJSON ? xhr.responseJSON.error : "move failed";
1580
+ RED.notify("Move failed: " + msg, "error");
1581
+ },
1582
+ });
1583
+ }
1584
+
1585
+ // ── Inline new-folder input ──
1586
+ function showNewFolderInput(parentDir) {
1587
+ var existingInput = fileList.find(".fc-new-folder-row");
1588
+ if (existingInput.length) existingInput.remove();
1589
+
1590
+ var depth = parentDir ? parentDir.split("/").length : 0;
1591
+ var indent = 8 + depth * 18;
1592
+ 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>');
1593
+ row.css("padding-left", indent + "px");
1594
+ $('<i class="fa fa-folder" style="color:#fbbf24;font-size:13px;width:16px;text-align:center;"></i>').appendTo(row);
1595
+ var inp = $('<input type="text" placeholder="folder name" class="red-ui-searchBox-input" style="flex:1;padding:3px 6px;font-size:12px;">');
1596
+ inp.appendTo(row);
1597
+ 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>');
1598
+ var cancelBtn = $('<button class="red-ui-button red-ui-button-small" title="Cancel"><i class="fa fa-times"></i></button>');
1599
+ function submit() {
1600
+ var name = inp.val().trim();
1601
+ if (!name) { row.remove(); return; }
1602
+ var p = parentDir ? parentDir + "/" + name : name;
1603
+ if (pathExists(p)) {
1604
+ RED.notify("Folder '" + name + "' already exists", "warning");
1605
+ return;
1606
+ }
1607
+ $.ajax({ type: "POST", url: "portal-react/assets/mkdir", contentType: "application/json", data: JSON.stringify({ path: p }),
1608
+ success: function () { row.remove(); refreshList(); },
1609
+ error: function () { RED.notify("Failed to create folder", "error"); },
1610
+ });
1611
+ }
1612
+ okBtn.on("click", function (e) { e.preventDefault(); submit(); });
1613
+ cancelBtn.on("click", function (e) { e.preventDefault(); row.remove(); });
1614
+ inp.on("keydown", function (e) {
1615
+ if (e.key === "Enter") submit();
1616
+ if (e.key === "Escape") row.remove();
1617
+ });
1618
+ okBtn.appendTo(row);
1619
+ cancelBtn.appendTo(row);
1620
+
1621
+ // Insert after the parent folder row, or at top
1622
+ if (parentDir) {
1623
+ var parentRow = fileList.find('[data-path="' + parentDir + '"]');
1624
+ if (parentRow.length) { row.insertAfter(parentRow); } else { fileList.prepend(row); }
1625
+ } else {
1626
+ fileList.prepend(row);
1627
+ }
1628
+ inp.focus();
1629
+ }
1630
+
1631
+ // ── Context menu (native Node-RED style) ──
1632
+ var activeMenu = null;
1633
+ function closeMenu() {
1634
+ if (activeMenu) { activeMenu.remove(); activeMenu = null; }
1635
+ }
1636
+ $(document).on("click", closeMenu);
1637
+
1638
+ function showMenu(anchor, items) {
1639
+ closeMenu();
1640
+ var menu = $('<ul class="red-ui-menu red-ui-menu-dropdown" style="position:absolute;z-index:10000;display:block;"></ul>');
1641
+ items.forEach(function (item) {
1642
+ if (item.divider) {
1643
+ menu.append('<li class="red-ui-menu-divider"></li>');
1644
+ return;
1645
+ }
1646
+ var li = $('<li></li>');
1647
+ var a = $('<a href="#"></a>');
1648
+ if (item.danger) a.css("color", "var(--red-ui-text-color-error)");
1649
+ a.append('<i class="fa ' + item.icon + '" style="width:18px;text-align:center;"></i> ');
1650
+ a.append($('<span class="red-ui-menu-label"></span>').text(item.label));
1651
+ a.on("click", function (e) {
1652
+ e.preventDefault();
1653
+ e.stopPropagation();
1654
+ closeMenu();
1655
+ item.action();
1656
+ });
1657
+ li.append(a);
1658
+ menu.append(li);
1659
+ });
1660
+
1661
+ $("body").append(menu);
1662
+ activeMenu = menu;
1663
+
1664
+ // Position near the anchor
1665
+ var off = anchor.offset();
1666
+ var top = off.top + anchor.outerHeight() + 2;
1667
+ var left = off.left - menu.outerWidth() + anchor.outerWidth();
1668
+ if (left < 0) left = off.left;
1669
+ if (top + menu.outerHeight() > $(window).height()) top = off.top - menu.outerHeight() - 2;
1670
+ menu.css({ top: top, left: left });
1671
+ }
1672
+
1673
+ var adminRoot = (RED.settings.httpAdminRoot || "/").replace(/\/$/, "");
1674
+
1675
+ function showRenameInput(rowEl, fullPath, currentName) {
1676
+ var nameSpan = rowEl.find("span").filter(function () { return $(this).text() === currentName; }).first();
1677
+ if (!nameSpan.length) return;
1678
+ var origText = nameSpan.text();
1679
+ var inp = $('<input type="text" class="red-ui-searchBox-input" style="flex:1;padding:3px 6px;font-size:12px;">').val(origText);
1680
+ 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>');
1681
+ var cancelBtn = $('<button class="red-ui-button red-ui-button-small" title="Cancel"><i class="fa fa-times"></i></button>');
1682
+ var dotIdx = origText.lastIndexOf(".");
1683
+ var hasExt = dotIdx > 0; // has extension (not hidden file)
1684
+ var origExt = hasExt ? origText.slice(dotIdx) : "";
1685
+
1686
+ nameSpan.replaceWith(inp);
1687
+ inp.after(cancelBtn).after(okBtn);
1688
+ inp.focus();
1689
+ // Select only the name part before extension
1690
+ var el = inp[0];
1691
+ if (hasExt && el.setSelectionRange) {
1692
+ el.setSelectionRange(0, dotIdx);
1693
+ } else {
1694
+ inp.select();
1695
+ }
1696
+
1697
+ function restore() {
1698
+ okBtn.remove();
1699
+ cancelBtn.remove();
1700
+ inp.replaceWith($('<span style="flex:1;font-size:12px;word-break:break-all;"></span>').text(origText));
1701
+ }
1702
+ function submit() {
1703
+ var newName = inp.val().trim();
1704
+ if (!newName) {
1705
+ RED.notify("Name cannot be empty", "warning");
1706
+ return;
1707
+ }
1708
+ // Warn if extension changed or removed
1709
+ if (hasExt) {
1710
+ var newDot = newName.lastIndexOf(".");
1711
+ var newExt = newDot > 0 ? newName.slice(newDot) : "";
1712
+ if (newExt.toLowerCase() !== origExt.toLowerCase()) {
1713
+ if (!confirm("Extension changed from '" + origExt + "' to '" + (newExt || "none") + "'. Continue?")) return;
1714
+ }
1715
+ }
1716
+ if (newName === origText) { restore(); return; }
1717
+ var parentDir = fullPath.indexOf("/") >= 0 ? fullPath.slice(0, fullPath.lastIndexOf("/")) : "";
1718
+ var newPath = parentDir ? parentDir + "/" + newName : newName;
1719
+ if (pathExists(newPath)) {
1720
+ RED.notify("'" + newName + "' already exists", "warning");
1721
+ return;
1722
+ }
1723
+ $.ajax({
1724
+ type: "POST", url: "portal-react/assets/move",
1725
+ contentType: "application/json",
1726
+ data: JSON.stringify({ from: fullPath, to: newPath }),
1727
+ success: function () { refreshList(); },
1728
+ error: function (xhr) {
1729
+ var msg = xhr.responseJSON ? xhr.responseJSON.error : "rename failed";
1730
+ RED.notify("Rename failed: " + msg, "error");
1731
+ restore();
1732
+ },
1733
+ });
1734
+ }
1735
+ okBtn.on("click", function (e) { e.preventDefault(); e.stopPropagation(); submit(); });
1736
+ cancelBtn.on("click", function (e) { e.preventDefault(); e.stopPropagation(); restore(); });
1737
+ inp.on("keydown", function (e) {
1738
+ if (e.key === "Enter") submit();
1739
+ if (e.key === "Escape") restore();
1740
+ });
1741
+ }
1742
+
1743
+ // ── Tree rendering ──
1744
+ function buildTree(entries) {
1745
+ // Build nested structure: { children: { name: { type, children, entry } } }
1746
+ var root = { children: {} };
1747
+ entries.forEach(function (e) {
1748
+ var parts = e.name.split("/");
1749
+ var node = root;
1750
+ for (var i = 0; i < parts.length; i++) {
1751
+ if (!node.children[parts[i]]) {
1752
+ node.children[parts[i]] = { children: {} };
1753
+ }
1754
+ node = node.children[parts[i]];
1755
+ }
1756
+ node.entry = e;
1757
+ });
1758
+ return root;
1759
+ }
1760
+
1761
+ function renderTree() {
1762
+ fileList.empty();
1763
+ if (allEntries.length === 0) {
1764
+ 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>'));
1765
+ return;
1766
+ }
1767
+ var tree = buildTree(allEntries);
1768
+ renderNode(tree, "", 0);
1769
+ }
1770
+
1771
+ function renderNode(node, parentPath, depth) {
1772
+ // Collect and sort: dirs first, then files
1773
+ var names = Object.keys(node.children).sort(function (a, b) {
1774
+ var aIsDir = node.children[a].entry && node.children[a].entry.type === "dir";
1775
+ var bIsDir = node.children[b].entry && node.children[b].entry.type === "dir";
1776
+ if (aIsDir && !bIsDir) return -1;
1777
+ if (!aIsDir && bIsDir) return 1;
1778
+ return a.localeCompare(b);
1779
+ });
1780
+
1781
+ names.forEach(function (name) {
1782
+ var child = node.children[name];
1783
+ var e = child.entry;
1784
+ if (!e) return;
1785
+ var fullPath = e.name;
1786
+ var indent = 8 + depth * 18;
1787
+ var isDir = e.type === "dir";
1788
+ var isOpen = !collapsed[fullPath];
1789
+
1790
+ 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>');
1791
+ row.css("padding-left", indent + "px");
1792
+
1793
+ if (isDir) {
1794
+ // Expand/collapse arrow
1795
+ 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>');
1796
+ arrow.on("click", function (e) {
1797
+ e.stopPropagation();
1798
+ collapsed[fullPath] = isOpen;
1799
+ renderTree();
1800
+ });
1801
+ row.append(arrow);
1802
+ $('<i class="fa ' + (isOpen ? 'fa-folder-open' : 'fa-folder') + '" style="color:#fbbf24;font-size:12px;width:16px;text-align:center;"></i>').appendTo(row);
1803
+ $('<span style="flex:1;font-size:12px;cursor:pointer;"></span>').text(name)
1804
+ .on("click", function () { collapsed[fullPath] = isOpen; renderTree(); })
1805
+ .appendTo(row);
1806
+
1807
+ // Context menu trigger
1808
+ var dirMenuBtn = $('<button class="red-ui-button red-ui-button-small" style="padding:1px 5px;"><i class="fa fa-ellipsis-v"></i></button>');
1809
+ (function (fp, nm) {
1810
+ dirMenuBtn.on("click", function (ev) {
1811
+ ev.preventDefault();
1812
+ ev.stopPropagation();
1813
+ showMenu(dirMenuBtn, [
1814
+ { icon: "fa-pencil", label: "Rename", action: function () {
1815
+ showRenameInput(row, fp, nm);
1816
+ }},
1817
+ { icon: "fa-plus", label: "New subfolder", action: function () {
1818
+ if (collapsed[fp]) { collapsed[fp] = false; renderTree(); }
1819
+ setTimeout(function () { showNewFolderInput(fp); }, 50);
1820
+ }},
1821
+ { divider: true },
1822
+ { icon: "fa-trash", label: "Delete folder", danger: true, action: function () {
1823
+ if (!confirm("Delete folder '" + nm + "' and all contents?")) return;
1824
+ $.ajax({
1825
+ type: "DELETE",
1826
+ url: "portal-react/assets/" + fp.split("/").map(encodeURIComponent).join("/"),
1827
+ success: function () { refreshList(); },
1828
+ error: function () { RED.notify("Delete failed", "error"); },
1829
+ });
1830
+ }},
1831
+ ]);
1832
+ });
1833
+ })(fullPath, name);
1834
+ dirMenuBtn.appendTo(row);
1835
+
1836
+ // Drop target
1837
+ row.on("dragover", function (ev) {
1838
+ ev.preventDefault();
1839
+ ev.stopPropagation();
1840
+ row.css("background", "rgba(251,191,36,0.08)");
1841
+ });
1842
+ row.on("dragleave", function () { row.css("background", ""); });
1843
+ row.on("drop", function (ev) {
1844
+ ev.preventDefault();
1845
+ ev.stopPropagation();
1846
+ row.css("background", "");
1847
+ var srcPath = ev.originalEvent.dataTransfer.getData("text/x-asset-path");
1848
+ if (srcPath) {
1849
+ moveItem(srcPath, fullPath);
1850
+ } else if (ev.originalEvent.dataTransfer.files && ev.originalEvent.dataTransfer.files.length > 0) {
1851
+ uploadFiles(ev.originalEvent.dataTransfer.files, fullPath);
1852
+ }
1853
+ });
1854
+ fileList.append(row);
1855
+
1856
+ // Render children if open
1857
+ if (isOpen) {
1858
+ renderNode(child, fullPath, depth + 1);
1859
+ }
1860
+ } else {
1861
+ // File
1862
+ row.attr("draggable", "true").css("cursor", "grab");
1863
+ $('<span style="width:10px;"></span>').appendTo(row); // spacer for arrow alignment
1864
+ $('<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);
1865
+ $('<span style="flex:1;font-size:12px;word-break:break-all;"></span>').text(name).appendTo(row);
1866
+ $('<span style="font-size:10px;color:var(--red-ui-tertiary-text-color);white-space:nowrap;"></span>').text(formatSize(e.size)).appendTo(row);
1867
+
1868
+ row.on("dragstart", function (ev) {
1869
+ ev.originalEvent.dataTransfer.setData("text/x-asset-path", fullPath);
1870
+ ev.originalEvent.dataTransfer.effectAllowed = "move";
1871
+ row.css("opacity", "0.4");
1872
+ });
1873
+ row.on("dragend", function () { row.css("opacity", "1"); });
1874
+
1875
+ 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>');
1876
+ var fileMenuBtn = $('<button class="red-ui-button red-ui-button-small" style="padding:1px 5px;"><i class="fa fa-ellipsis-v"></i></button>');
1877
+ (function (fp, nm) {
1878
+ copyBtn.on("click", function (ev) {
1879
+ ev.preventDefault();
1880
+ ev.stopPropagation();
1881
+ var url = publicBase + fp;
1882
+ navigator.clipboard.writeText(url).then(function () {
1883
+ RED.notify("Copied: " + url, { type: "success", timeout: 2000 });
1884
+ });
1885
+ });
1886
+ fileMenuBtn.on("click", function (ev) {
1887
+ ev.preventDefault();
1888
+ ev.stopPropagation();
1889
+ showMenu(fileMenuBtn, [
1890
+ { icon: "fa-pencil", label: "Rename", action: function () {
1891
+ showRenameInput(row, fp, nm);
1892
+ }},
1893
+ { icon: "fa-download", label: "Download", action: function () {
1894
+ var a = document.createElement("a");
1895
+ a.href = adminRoot + "/portal-react/assets/download/" + fp.split("/").map(encodeURIComponent).join("/");
1896
+ a.download = nm;
1897
+ document.body.appendChild(a);
1898
+ a.click();
1899
+ document.body.removeChild(a);
1900
+ }},
1901
+ { divider: true },
1902
+ { icon: "fa-trash", label: "Delete", danger: true, action: function () {
1903
+ $.ajax({
1904
+ type: "DELETE",
1905
+ url: "portal-react/assets/" + fp.split("/").map(encodeURIComponent).join("/"),
1906
+ success: function () { refreshList(); },
1907
+ error: function () { RED.notify("Delete failed: " + nm, "error"); },
1908
+ });
1909
+ }},
1910
+ ]);
1911
+ });
1912
+ })(fullPath, name);
1913
+ copyBtn.appendTo(row);
1914
+ fileMenuBtn.appendTo(row);
1915
+ fileList.append(row);
1916
+ }
1917
+ });
1918
+ }
1919
+
1920
+ function refreshList() {
1921
+ $.getJSON("portal-react/assets", function (entries) {
1922
+ allEntries = entries || [];
1923
+ renderTree();
1924
+ });
1925
+ }
1926
+
1927
+ RED.sidebar.addTab({
1928
+ id: "fromcubes-public",
1929
+ label: "Portal Assets",
1930
+ name: "fromcubes portal assets",
1931
+ iconClass: "fa fa-desktop",
1932
+ content: content[0],
1933
+ toolbar: toolbar[0],
1934
+ pinned: false,
1935
+ enableOnEdit: true,
1936
+ });
1937
+
1938
+ RED.actions.add("fromcubes:show-assets", function () {
1939
+ RED.sidebar.show("fromcubes-public");
1940
+ });
1941
+
1942
+ RED.events.on("sidebar:resize", function () { refreshList(); });
1943
+ setTimeout(refreshList, 1000);
1944
+ })();
1945
+ </script>