Skip to content

Commit eec3926

Browse files
author
FolderView Plus Test
committed
Fix docker command view native actions
1 parent 2b47e59 commit eec3926

8 files changed

Lines changed: 92 additions & 86 deletions

File tree

archive/folderview.plus-2026.04.14.09.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+
808f6deb8223c2ed19a2ba23d8f890955f7c17eb7babca864b08b3668c64b2f2 folderview.plus-2026.04.15.22.txz

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.21">
10-
<!ENTITY md5 "c2077648bc18417418c9c310f5bc550b">
9+
<!ENTITY version "2026.04.15.22">
10+
<!ENTITY md5 "b1a1abe75b1562c1bc70afead901aa7b">
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.22
17+
- Fix: Docker support-bundle snapshots, trace storage caps, and rendered-state diagnostics.
18+
- Fix: Docker runtime rows, folder state, and container interactions.
19+
- UX: Settings workspace layout, section flows, and table behavior.
20+
21+
1622
###2026.04.15.21
1723
- UX: Docker command-view container cards now restore the outlined member-tile treatment, but the outline colors are theme-aware and use the plugin runtime status/border tokens instead of hardcoded orange chrome.
1824
- UX: Command-view quick actions now reuse the regular FolderView Docker preview action classes, and the branch action buttons gain a subtle theme-token outline so they sit closer to the rest of the plugin controls.

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/docker.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,7 @@ const getDockerCommandViewApi = () => {
671671
dockerCommandViewApi = dockerCommandViewModule.createApi({
672672
window,
673673
document,
674+
$,
674675
utils,
675676
escapeHtml: (value) => escapeHtml(value),
676677
parseJsonPayloadSafe: (payload) => parseJsonPayloadSafe(payload),
@@ -727,6 +728,8 @@ const getDockerCommandViewApi = () => {
727728
openWebuiInNewTab: (url) => openWebuiInNewTab(url),
728729
openWebuiPopupWindow: (url, targetName = '_blank') => openWebuiPopupWindow(url, targetName),
729730
openTerminal: (type, containerName, shellValue) => openTerminal(type, containerName, shellValue),
731+
appendDockerPreviewActionButtons: ($target, settings = {}, containerName = '', shellValue = '/bin/sh', webuiUrl = '') =>
732+
appendDockerPreviewActionButtons($target, settings, containerName, shellValue, webuiUrl),
730733
toggleFolderPin: (folderId) => toggleDockerFolderPin(folderId),
731734
toggleFolderLock: (folderId) => toggleDockerFolderLock(folderId),
732735
queueLoadlistRefresh: (options = {}) => queueLoadlistRefresh(options),

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

Lines changed: 71 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
const createApi = (deps = {}) => {
1818
const win = deps.window || fallbackWindow;
1919
const doc = deps.document || win?.document || (typeof document !== 'undefined' ? document : null);
20+
const jq = deps.$ || win?.jQuery || win?.$;
2021
const utils = deps.utils && typeof deps.utils === 'object' ? deps.utils : {};
2122
const escapeHtml = typeof deps.escapeHtml === 'function'
2223
? deps.escapeHtml
@@ -132,10 +133,12 @@
132133
const queueLoadlistRefresh = typeof deps.queueLoadlistRefresh === 'function'
133134
? deps.queueLoadlistRefresh
134135
: (() => {});
136+
const appendDockerPreviewActionButtons = typeof deps.appendDockerPreviewActionButtons === 'function'
137+
? deps.appendDockerPreviewActionButtons
138+
: (() => {});
135139

136140
let rootNode = null;
137141
let clickHandler = null;
138-
let contextMenuHandler = null;
139142
let renderToken = 0;
140143

141144
const getHostTable = () => doc?.querySelector('table#docker_containers') || null;
@@ -192,11 +195,7 @@
192195
if (clickHandler && rootNode) {
193196
rootNode.removeEventListener('click', clickHandler);
194197
}
195-
if (contextMenuHandler && rootNode) {
196-
rootNode.removeEventListener('contextmenu', contextMenuHandler);
197-
}
198198
clickHandler = null;
199-
contextMenuHandler = null;
200199
if (rootNode && rootNode.parentNode) {
201200
rootNode.parentNode.removeChild(rootNode);
202201
}
@@ -274,14 +273,34 @@
274273
return false;
275274
}
276275
const safeType = eventType === 'contextmenu' ? 'contextmenu' : 'click';
276+
if (typeof jq === 'function') {
277+
try {
278+
jq(trigger).trigger(safeType);
279+
return true;
280+
} catch (_error) {
281+
// Fall through to native DOM dispatch.
282+
}
283+
}
284+
if (safeType === 'click' && typeof trigger.click === 'function') {
285+
try {
286+
trigger.click();
287+
return true;
288+
} catch (_error) {
289+
// Fall through to synthetic DOM dispatch.
290+
}
291+
}
277292
const event = new MouseEvent(safeType, {
278293
bubbles: true,
279294
cancelable: true,
280295
view: win || undefined,
281296
button: safeType === 'contextmenu' ? 2 : 0,
282297
buttons: safeType === 'contextmenu' ? 2 : 1
283298
});
284-
return trigger.dispatchEvent(event);
299+
try {
300+
return trigger.dispatchEvent(event);
301+
} catch (_error) {
302+
return false;
303+
}
285304
};
286305

287306
const hydrateNativeMemberSurface = (surface, containerName) => {
@@ -293,44 +312,31 @@
293312
return;
294313
}
295314
const trigger = getNativeMemberTrigger(safeName);
296-
if (!(trigger instanceof HTMLElement)) {
297-
surface.addEventListener('click', (event) => {
298-
event.preventDefault();
299-
event.stopPropagation();
300-
proxyNativeMemberTrigger(safeName, 'click');
301-
});
302-
surface.addEventListener('contextmenu', (event) => {
303-
event.preventDefault();
304-
event.stopPropagation();
305-
proxyNativeMemberTrigger(safeName, 'contextmenu');
306-
});
307-
return;
308-
}
309-
const inlineClick = String(trigger.getAttribute('onclick') || '').trim();
310-
const inlineContextMenu = String(trigger.getAttribute('oncontextmenu') || '').trim();
311-
const title = String(trigger.getAttribute('title') || '').trim();
312315
surface.classList.add('hand');
313-
if (inlineClick) {
314-
surface.setAttribute('onclick', inlineClick);
315-
} else {
316-
surface.addEventListener('click', (event) => {
317-
event.preventDefault();
318-
event.stopPropagation();
319-
proxyNativeMemberTrigger(safeName, 'click');
320-
});
321-
}
322-
if (inlineContextMenu) {
323-
surface.setAttribute('oncontextmenu', inlineContextMenu);
324-
} else {
325-
surface.addEventListener('contextmenu', (event) => {
326-
event.preventDefault();
327-
event.stopPropagation();
328-
proxyNativeMemberTrigger(safeName, 'contextmenu');
329-
});
330-
}
316+
surface.setAttribute('role', 'button');
317+
surface.setAttribute('tabindex', '0');
318+
const title = trigger instanceof HTMLElement ? String(trigger.getAttribute('title') || '').trim() : '';
331319
if (title) {
332320
surface.setAttribute('title', title);
333321
}
322+
surface.addEventListener('click', (event) => {
323+
event.preventDefault();
324+
event.stopPropagation();
325+
proxyNativeMemberTrigger(safeName, 'click');
326+
});
327+
surface.addEventListener('contextmenu', (event) => {
328+
event.preventDefault();
329+
event.stopPropagation();
330+
proxyNativeMemberTrigger(safeName, 'contextmenu');
331+
});
332+
surface.addEventListener('keydown', (event) => {
333+
if (event.key !== 'Enter' && event.key !== ' ') {
334+
return;
335+
}
336+
event.preventDefault();
337+
event.stopPropagation();
338+
proxyNativeMemberTrigger(safeName, 'click');
339+
});
334340
};
335341

336342
const computeOrderedFolderIds = (folders, prefs, hostOrder, unraidOrder) => {
@@ -490,12 +496,8 @@
490496
${member.updateReady ? '<span class="fv-docker-command-member-update">update ready</span>' : ''}
491497
</span>
492498
</span>
493-
<span class="fv-docker-command-member-actions">
494-
${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>` : ''}
495-
<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>
496-
<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>
497-
</span>
498499
</div>
500+
<span class="fv-docker-command-member-actions fv-preview-actions-compact" data-fv-command-member-actions-host="true" data-member-name="${escapeHtml(member.name)}" data-member-webui-url="${escapeHtml(member.webuiUrl)}" data-member-shell="${escapeHtml(member.shell)}"></span>
499501
</div>
500502
`).join('');
501503
return `
@@ -547,6 +549,30 @@
547549
}
548550
hydrateNativeMemberSurface(surface, String(surface.getAttribute('data-member-name') || '').trim());
549551
});
552+
if (typeof jq === 'function') {
553+
root.querySelectorAll('[data-fv-command-member-actions-host="true"]').forEach((host) => {
554+
if (!(host instanceof HTMLElement)) {
555+
return;
556+
}
557+
const memberName = String(host.getAttribute('data-member-name') || '').trim();
558+
const memberWebuiUrl = String(host.getAttribute('data-member-webui-url') || '').trim();
559+
const memberShell = String(host.getAttribute('data-member-shell') || '').trim() || '/bin/sh';
560+
if (!memberName) {
561+
return;
562+
}
563+
appendDockerPreviewActionButtons(
564+
jq(host),
565+
{
566+
preview_webui: Boolean(memberWebuiUrl),
567+
preview_console: true,
568+
preview_logs: true
569+
},
570+
memberName,
571+
memberShell,
572+
memberWebuiUrl
573+
);
574+
});
575+
}
550576
return true;
551577
};
552578

@@ -558,35 +584,6 @@
558584
rootNode.removeEventListener('click', clickHandler);
559585
}
560586
clickHandler = (event) => {
561-
const memberButton = event.target instanceof Element
562-
? event.target.closest('[data-fv-command-member-action]')
563-
: null;
564-
if (memberButton instanceof HTMLElement) {
565-
event.preventDefault();
566-
event.stopPropagation();
567-
const memberAction = String(memberButton.getAttribute('data-fv-command-member-action') || '').trim();
568-
const memberTile = memberButton.closest('.fv-docker-command-member-tile');
569-
const folderCard = memberButton.closest('[data-folder-id]');
570-
const folderId = folderCard instanceof HTMLElement ? String(folderCard.getAttribute('data-folder-id') || '').trim() : '';
571-
const memberName = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-name') || '').trim() : '';
572-
const memberWebuiUrl = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-webui-url') || '').trim() : '';
573-
const memberShell = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-shell') || '').trim() || '/bin/sh' : '/bin/sh';
574-
if (!folderId || !memberName) {
575-
return;
576-
}
577-
if (memberAction === 'webui') {
578-
openWebuiInNewTab(memberWebuiUrl);
579-
return;
580-
}
581-
if (memberAction === 'console') {
582-
openTerminal('docker', memberName, memberShell);
583-
return;
584-
}
585-
if (memberAction === 'logs') {
586-
openTerminal('docker', memberName, '.log');
587-
return;
588-
}
589-
}
590587
const button = event.target instanceof Element
591588
? event.target.closest('[data-fv-command-action]')
592589
: null;

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ body[data-fvplus-docker-command-view-mounted="true"] table#docker_containers {
2525
border: 1px solid var(--fv-docker-command-border);
2626
border-radius: 16px;
2727
padding: 1rem;
28-
background:
29-
radial-gradient(circle at top right, var(--fv-docker-command-accent-soft), transparent 26%),
30-
linear-gradient(160deg, var(--fv-docker-command-surface-panel), var(--fv-docker-command-surface-muted));
28+
background: var(--fv-docker-command-surface-panel);
3129
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.2);
3230
}
3331

@@ -255,6 +253,7 @@ body[data-fvplus-docker-command-view-mounted="true"] table#docker_containers {
255253
min-width: 220px;
256254
min-height: 48px;
257255
display: flex;
256+
flex-direction: column;
258257
border: 1px solid var(--fv-docker-command-border-faint);
259258
border-radius: 12px;
260259
background: var(--fv-docker-command-surface-panel);
@@ -372,6 +371,8 @@ body[data-fvplus-docker-command-view-mounted="true"] table#docker_containers {
372371
align-items: center;
373372
gap: 5px;
374373
margin-left: auto;
374+
padding: 0 0.55rem 0.45rem;
375+
align-self: flex-end;
375376
}
376377

377378
.fv-docker-command-member-actions .folder-element-custom-btn {

tests/docker-runtime-shared-architecture.test.mjs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ test('docker command-view stylesheet only hides the host table when the isolated
236236
assert.match(dockerCommandViewCss, /var\(--fvplus-theme-border-subtle/);
237237
assert.match(dockerCommandViewCss, /\.fv-docker-command-member-tile/);
238238
assert.match(dockerCommandViewCss, /flex:\s*0 0 220px/);
239+
assert.match(dockerCommandViewCss, /background:\s*var\(--fv-docker-command-surface-panel\);/);
239240
assert.doesNotMatch(dockerCommandViewCss, /body\[data-fvplus-docker-page-view="command"\] table#docker_containers/);
240241
});
241242

@@ -245,16 +246,14 @@ test('docker command-view renders visible member tiles instead of name-only chip
245246
assert.match(dockerCommandViewJs, /const getNativeMemberTrigger = \(containerName\) =>/);
246247
assert.match(dockerCommandViewJs, /const proxyNativeMemberTrigger = \(containerName,\s*eventType = 'click'\) =>/);
247248
assert.match(dockerCommandViewJs, /const hydrateNativeMemberSurface = \(surface,\s*containerName\) =>/);
249+
assert.match(dockerCommandViewJs, /const appendDockerPreviewActionButtons = typeof deps\.appendDockerPreviewActionButtons === 'function'/);
248250
assert.match(dockerCommandViewJs, /class="fv-docker-command-member-tile/);
249251
assert.match(dockerCommandViewJs, /data-fv-command-member-surface="true"/);
252+
assert.match(dockerCommandViewJs, /data-fv-command-member-actions-host="true"/);
250253
assert.match(dockerCommandViewJs, /class="fv-docker-command-member-pill"/);
251254
assert.match(dockerCommandViewJs, /class="fv-docker-command-member-icon"/);
252-
assert.match(dockerCommandViewJs, /folder-element-custom-btn folder-element-webui/);
253-
assert.match(dockerCommandViewJs, /folder-element-custom-btn folder-element-console/);
254-
assert.match(dockerCommandViewJs, /folder-element-custom-btn folder-element-logs/);
255-
assert.match(dockerCommandViewJs, /data-fv-command-member-action="webui"/);
256-
assert.match(dockerCommandViewJs, /data-fv-command-member-action="console"/);
257-
assert.match(dockerCommandViewJs, /data-fv-command-member-action="logs"/);
255+
assert.match(dockerCommandViewJs, /appendDockerPreviewActionButtons\(/);
256+
assert.match(dockerJs, /appendDockerPreviewActionButtons:\s*\(\$target,\s*settings = \{\},\s*containerName = '',\s*shellValue = '\/bin\/sh',\s*webuiUrl = ''\)\s*=>/);
258257
assert.match(dockerCommandViewJs, /const openFolderCardWebuis = \(folderCard\) =>/);
259258
assert.match(dockerCommandViewJs, /if \(requestBundle\.fullInfo\)/);
260259
assert.doesNotMatch(dockerCommandViewJs, /fv-docker-command-member-menu/);

0 commit comments

Comments
 (0)