Skip to content

Commit 109d475

Browse files
author
FolderView Plus Test
committed
Use native docker menu in command view
1 parent dc70a2c commit 109d475

8 files changed

Lines changed: 101 additions & 143 deletions

File tree

archive/folderview.plus-2026.04.14.07.txz.sha256

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3c40dbd80b2d598f020c9e072ef7cc55bf96c5eca36611098d36efbfadbe600e folderview.plus-2026.04.15.20.txz

docs/releases/2026.04.15.20.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- UX: Docker command-view member tiles no longer render the boxed orange tile chrome; the member strip now stays visually closer to the normal FolderView preview surface.
2+
- UX: Command-view WebUI, console, and log actions now use the regular Docker preview action classes so their icons match the standard FolderView quick actions instead of a custom button style.
3+
- Fix: Clicking or right-clicking a command-view container now proxies Unraid's native Docker row trigger, so the tile opens the same native Unraid container menu path the regular FolderView preview uses instead of a custom command-view menu.

folderview.plus.plg

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@
66
<!ENTITY launch "Settings/FolderViewPlus">
77
<!ENTITY plugdir "/usr/local/emhttp/plugins/&name;">
88
<!ENTITY pluginURL "https://raw.githubusercontent.com/&github;/dev/folderview.plus.plg">
9-
<!ENTITY version "2026.04.15.19">
10-
<!ENTITY md5 "914c1a027767564a695bd0b5508376cd">
9+
<!ENTITY version "2026.04.15.20">
10+
<!ENTITY md5 "cb5c9cfdb68417db5a05e88ac358d01d">
1111
]>
1212

1313
<PLUGIN name="&name;" author="&author;" version="&version;" launch="&launch;" pluginURL="&pluginURL;" icon="folder-icon.png" support="https://forums.unraid.net/topic/197631-plugin-folderview-plus/" min="7.0.0">
1414
<CHANGES>
1515

16+
###2026.04.15.20
17+
- UX: Docker command-view member tiles no longer render the boxed orange tile chrome; the member strip now stays visually closer to the normal FolderView preview surface.
18+
- UX: Command-view WebUI, console, and log actions now use the regular Docker preview action classes so their icons match the standard FolderView quick actions instead of a custom button style.
19+
- Fix: Clicking or right-clicking a command-view container now proxies Unraid's native Docker row trigger, so the tile opens the same native Unraid container menu path the regular FolderView preview uses instead of a custom command-view menu.
20+
21+
1622
###2026.04.15.19
1723
- UX: Docker command-view member tiles now keep a consistent fixed width by default so folders do not stretch or compress container cards differently based on row count.
1824
- UX: Command-view container tiles now expose visible WebUI, console, and log quick-action icons, and clicking a tile opens a lightweight per-container action menu for start, stop, pause, resume, and restart.

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.runtime.command-view.js

Lines changed: 66 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@
135135

136136
let rootNode = null;
137137
let clickHandler = null;
138+
let contextMenuHandler = null;
138139
let renderToken = 0;
139-
let expandedMemberKey = '';
140140

141141
const getHostTable = () => doc?.querySelector('table#docker_containers') || null;
142142
const isFolderToken = (value) => String(value || '').trim().startsWith('folder-');
@@ -192,7 +192,11 @@
192192
if (clickHandler && rootNode) {
193193
rootNode.removeEventListener('click', clickHandler);
194194
}
195+
if (contextMenuHandler && rootNode) {
196+
rootNode.removeEventListener('contextmenu', contextMenuHandler);
197+
}
195198
clickHandler = null;
199+
contextMenuHandler = null;
196200
if (rootNode && rootNode.parentNode) {
197201
rootNode.parentNode.removeChild(rootNode);
198202
}
@@ -228,18 +232,6 @@
228232
return { state, label: 'stopped', icon: 'fa-stop' };
229233
};
230234

231-
const getMemberKey = (folderId, memberName) => `${String(folderId || '').trim()}::${String(memberName || '').trim()}`;
232-
233-
const dispatchContainerControl = (action, containerId) => {
234-
const safeAction = String(action || '').trim();
235-
const safeContainerId = String(containerId || '').trim();
236-
if (!safeAction || !safeContainerId || typeof win?.eventControl !== 'function') {
237-
return false;
238-
}
239-
win.eventControl({ action: safeAction, container: safeContainerId }, 'loadlist');
240-
return true;
241-
};
242-
243235
const openFolderCardWebuis = (folderCard) => {
244236
if (!(folderCard instanceof HTMLElement)) {
245237
return false;
@@ -263,25 +255,33 @@
263255
return true;
264256
};
265257

266-
const buildMemberMenuButtons = (member) => {
267-
const actions = [];
268-
if (member.stateMeta.state === 'stopped') {
269-
actions.push({ action: 'start', label: 'Start' });
270-
} else if (member.stateMeta.state === 'paused') {
271-
actions.push({ action: 'resume', label: 'Resume' });
272-
actions.push({ action: 'stop', label: 'Stop' });
273-
} else {
274-
actions.push({ action: 'stop', label: 'Stop' });
275-
actions.push({ action: 'pause', label: 'Pause' });
258+
const getNativeMemberTrigger = (containerName) => {
259+
const safeName = String(containerName || '').trim();
260+
if (!safeName || !doc) {
261+
return null;
276262
}
277-
if (member.id) {
278-
actions.push({ action: 'restart', label: 'Restart' });
263+
const sourceRow = doc.getElementById(`ct-${safeName}`);
264+
if (!(sourceRow instanceof HTMLElement)) {
265+
return null;
279266
}
280-
return actions.map((entry) => `
281-
<button type="button" class="fv-docker-command-member-menu-button" data-fv-command-member-action="${escapeHtml(entry.action)}" data-member-name="${escapeHtml(member.name)}">
282-
${escapeHtml(entry.label)}
283-
</button>
284-
`).join('');
267+
return sourceRow.querySelector('td.ct-name > span.outer > span.hand')
268+
|| sourceRow.querySelector('td.ct-name > span.outer > span.inner > span.appname > a.exec');
269+
};
270+
271+
const proxyNativeMemberTrigger = (containerName, eventType = 'click') => {
272+
const trigger = getNativeMemberTrigger(containerName);
273+
if (!(trigger instanceof HTMLElement)) {
274+
return false;
275+
}
276+
const safeType = eventType === 'contextmenu' ? 'contextmenu' : 'click';
277+
const event = new MouseEvent(safeType, {
278+
bubbles: true,
279+
cancelable: true,
280+
view: win || undefined,
281+
button: safeType === 'contextmenu' ? 2 : 0,
282+
buttons: safeType === 'contextmenu' ? 2 : 1
283+
});
284+
return trigger.dispatchEvent(event);
285285
};
286286

287287
const computeOrderedFolderIds = (folders, prefs, hostOrder, unraidOrder) => {
@@ -426,8 +426,8 @@
426426
card.childCount > 0 ? `${card.childCount} child folders` : ''
427427
].filter(Boolean).join(' • ');
428428
const memberTiles = card.members.map((member) => `
429-
<div class="fv-docker-command-member-tile ${escapeHtml(member.stateMeta.state)}${member.updateReady ? ' has-update' : ''}${expandedMemberKey === getMemberKey(card.folderId, member.name) ? ' is-expanded' : ''}" data-member-name="${escapeHtml(member.name)}" data-member-id="${escapeHtml(member.id)}" data-member-webui-url="${escapeHtml(member.webuiUrl)}" data-member-shell="${escapeHtml(member.shell)}">
430-
<button type="button" class="fv-docker-command-member-surface" data-fv-command-member-action="toggle-menu" data-member-name="${escapeHtml(member.name)}">
429+
<div class="fv-docker-command-member-tile ${escapeHtml(member.stateMeta.state)}${member.updateReady ? ' has-update' : ''}" data-member-name="${escapeHtml(member.name)}" data-member-id="${escapeHtml(member.id)}" data-member-webui-url="${escapeHtml(member.webuiUrl)}" data-member-shell="${escapeHtml(member.shell)}">
430+
<div class="fv-docker-command-member-surface hand" data-fv-command-member-surface="true" data-member-name="${escapeHtml(member.name)}">
431431
<span class="fv-docker-command-member-icon-wrap">
432432
<img src="${member.icon}" class="fv-docker-command-member-icon" alt="" loading="lazy" onerror='this.src="${DOCKER_ICON_FALLBACK}"'>
433433
</span>
@@ -441,14 +441,11 @@
441441
${member.updateReady ? '<span class="fv-docker-command-member-update">update ready</span>' : ''}
442442
</span>
443443
</span>
444-
<span class="fv-docker-command-member-quick-actions">
445-
${member.webuiUrl ? `<button type="button" class="fv-docker-command-member-icon-button" title="Open WebUI" aria-label="Open WebUI" data-fv-command-member-action="webui" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-globe" aria-hidden="true"></i></button>` : ''}
446-
<button type="button" class="fv-docker-command-member-icon-button" title="Open console" aria-label="Open console" data-fv-command-member-action="console" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-terminal" aria-hidden="true"></i></button>
447-
<button type="button" class="fv-docker-command-member-icon-button" title="Open logs" aria-label="Open logs" data-fv-command-member-action="logs" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-bars" aria-hidden="true"></i></button>
444+
<span class="fv-docker-command-member-actions">
445+
${member.webuiUrl ? `<span class="folder-element-custom-btn folder-element-webui"><a href="${escapeHtml(member.webuiUrl)}" title="Open WebUI" aria-label="Open WebUI" data-fv-command-member-action="webui" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-globe" aria-hidden="true"></i></a></span>` : ''}
446+
<span class="folder-element-custom-btn folder-element-console"><a href="#" title="Open console" aria-label="Open console" data-fv-command-member-action="console" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-terminal" aria-hidden="true"></i></a></span>
447+
<span class="folder-element-custom-btn folder-element-logs"><a href="#" title="Open logs" aria-label="Open logs" data-fv-command-member-action="logs" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-bars" aria-hidden="true"></i></a></span>
448448
</span>
449-
</button>
450-
<div class="fv-docker-command-member-menu">
451-
${buildMemberMenuButtons(member)}
452449
</div>
453450
</div>
454451
`).join('');
@@ -517,25 +514,11 @@
517514
const folderCard = memberButton.closest('[data-folder-id]');
518515
const folderId = folderCard instanceof HTMLElement ? String(folderCard.getAttribute('data-folder-id') || '').trim() : '';
519516
const memberName = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-name') || '').trim() : '';
520-
const memberId = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-id') || '').trim() : '';
521517
const memberWebuiUrl = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-webui-url') || '').trim() : '';
522518
const memberShell = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-shell') || '').trim() || '/bin/sh' : '/bin/sh';
523519
if (!folderId || !memberName) {
524520
return;
525521
}
526-
const memberKey = getMemberKey(folderId, memberName);
527-
if (memberAction === 'toggle-menu') {
528-
expandedMemberKey = expandedMemberKey === memberKey ? '' : memberKey;
529-
rootNode.querySelectorAll('.fv-docker-command-member-tile.is-expanded').forEach((node) => {
530-
if (node !== memberTile) {
531-
node.classList.remove('is-expanded');
532-
}
533-
});
534-
if (memberTile instanceof HTMLElement) {
535-
memberTile.classList.toggle('is-expanded', expandedMemberKey === memberKey);
536-
}
537-
return;
538-
}
539522
if (memberAction === 'webui') {
540523
openWebuiInNewTab(memberWebuiUrl);
541524
return;
@@ -548,12 +531,18 @@
548531
openTerminal('docker', memberName, '.log');
549532
return;
550533
}
551-
if (memberAction === 'start' || memberAction === 'stop' || memberAction === 'pause' || memberAction === 'resume' || memberAction === 'restart') {
552-
if (dispatchContainerControl(memberAction, memberId)) {
553-
queueLoadlistRefresh({ suppressLoadingUi: true });
554-
}
555-
return;
534+
}
535+
const memberSurface = event.target instanceof Element
536+
? event.target.closest('[data-fv-command-member-surface="true"]')
537+
: null;
538+
if (memberSurface instanceof HTMLElement) {
539+
const memberName = String(memberSurface.getAttribute('data-member-name') || '').trim();
540+
if (memberName) {
541+
event.preventDefault();
542+
event.stopPropagation();
543+
proxyNativeMemberTrigger(memberName, 'click');
556544
}
545+
return;
557546
}
558547
const button = event.target instanceof Element
559548
? event.target.closest('[data-fv-command-action]')
@@ -619,6 +608,25 @@
619608
}
620609
};
621610
rootNode.addEventListener('click', clickHandler);
611+
if (contextMenuHandler) {
612+
rootNode.removeEventListener('contextmenu', contextMenuHandler);
613+
}
614+
contextMenuHandler = (event) => {
615+
const memberSurface = event.target instanceof Element
616+
? event.target.closest('[data-fv-command-member-surface="true"]')
617+
: null;
618+
if (!(memberSurface instanceof HTMLElement)) {
619+
return;
620+
}
621+
const memberName = String(memberSurface.getAttribute('data-member-name') || '').trim();
622+
if (!memberName) {
623+
return;
624+
}
625+
event.preventDefault();
626+
event.stopPropagation();
627+
proxyNativeMemberTrigger(memberName, 'contextmenu');
628+
};
629+
rootNode.addEventListener('contextmenu', contextMenuHandler);
622630
};
623631

624632
const resolveSnapshot = async (options = {}) => {

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/styles/docker.command-view.css

Lines changed: 16 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -241,53 +241,26 @@ body[data-fvplus-docker-command-view-mounted="true"] table#docker_containers {
241241
width: 220px;
242242
max-width: 220px;
243243
min-width: 220px;
244-
min-height: 60px;
244+
min-height: 48px;
245245
display: flex;
246-
flex-direction: column;
247-
border-radius: 12px;
248-
border: 1px solid rgba(255, 255, 255, 0.08);
249-
background: rgba(255, 255, 255, 0.04);
250-
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
251-
overflow: hidden;
252-
}
253-
254-
.fv-docker-command-member-tile.running {
255-
border-color: rgba(122, 208, 90, 0.26);
256-
}
257-
258-
.fv-docker-command-member-tile.paused {
259-
border-color: rgba(215, 164, 24, 0.24);
260-
}
261-
262-
.fv-docker-command-member-tile.stopped {
263-
border-color: rgba(255, 99, 99, 0.22);
264-
}
265-
266-
.fv-docker-command-member-tile.has-update {
267-
box-shadow: inset 0 0 0 1px rgba(255, 154, 60, 0.18);
246+
border: 0;
247+
background: transparent;
248+
box-shadow: none;
268249
}
269250

270251
.fv-docker-command-member-surface {
271-
appearance: none;
272252
width: 100%;
273-
min-height: 58px;
253+
min-height: 48px;
274254
display: flex;
275255
align-items: center;
276256
gap: 0.58rem;
277-
padding: 0.62rem 0.7rem;
278-
border: 0;
257+
padding: 0;
279258
background: transparent;
280259
color: inherit;
281260
text-align: left;
282261
cursor: pointer;
283262
}
284263

285-
.fv-docker-command-member-surface:hover,
286-
.fv-docker-command-member-surface:focus-visible,
287-
.fv-docker-command-member-tile.is-expanded .fv-docker-command-member-surface {
288-
background: rgba(255, 255, 255, 0.03);
289-
}
290-
291264
.fv-docker-command-member-icon-wrap {
292265
flex: 0 0 auto;
293266
width: 36px;
@@ -358,63 +331,24 @@ body[data-fvplus-docker-command-view-mounted="true"] table#docker_containers {
358331
color: #ff9a3c;
359332
}
360333

361-
.fv-docker-command-member-quick-actions {
334+
.fv-docker-command-member-actions {
362335
display: inline-flex;
363336
align-items: center;
364-
gap: 0.18rem;
337+
gap: 5px;
365338
margin-left: auto;
366339
}
367340

368-
.fv-docker-command-member-icon-button {
369-
appearance: none;
370-
width: 28px;
371-
height: 28px;
341+
.fv-docker-command-member-actions .folder-element-custom-btn {
342+
margin-left: 0;
343+
}
344+
345+
.fv-docker-command-member-actions .folder-element-custom-btn > a {
372346
display: inline-flex;
373347
align-items: center;
374348
justify-content: center;
375-
border: 1px solid rgba(255, 255, 255, 0.08);
376-
border-radius: 8px;
377-
background: rgba(255, 255, 255, 0.04);
378-
color: rgba(255, 255, 255, 0.78);
379-
cursor: pointer;
380-
transition: border-color 120ms ease, background 120ms ease, color 120ms ease;
381-
}
382-
383-
.fv-docker-command-member-icon-button:hover,
384-
.fv-docker-command-member-icon-button:focus-visible {
385-
border-color: rgba(255, 154, 60, 0.42);
386-
background: rgba(255, 154, 60, 0.12);
387-
color: #ffb56d;
388-
}
389-
390-
.fv-docker-command-member-menu {
391-
display: none;
392-
flex-wrap: wrap;
393-
gap: 0.4rem;
394-
padding: 0 0.7rem 0.7rem;
395-
border-top: 1px solid rgba(255, 255, 255, 0.06);
396-
}
397-
398-
.fv-docker-command-member-tile.is-expanded .fv-docker-command-member-menu {
399-
display: flex;
400-
}
401-
402-
.fv-docker-command-member-menu-button {
403-
appearance: none;
404-
border: 0;
405-
border-radius: 999px;
406-
padding: 0.22rem 0.58rem;
407-
background: rgba(255, 255, 255, 0.06);
408-
color: rgba(255, 255, 255, 0.84);
409-
font-size: 0.74rem;
410-
font-weight: 600;
411-
cursor: pointer;
412-
}
413-
414-
.fv-docker-command-member-menu-button:hover,
415-
.fv-docker-command-member-menu-button:focus-visible {
416-
background: rgba(255, 154, 60, 0.12);
417-
color: #ffb56d;
349+
width: 13px;
350+
height: 13px;
351+
font-size: 0.96em;
418352
}
419353

420354
.fv-docker-command-member-empty {

0 commit comments

Comments
 (0)