Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed archive/folderview.plus-2026.07.24.02.txz
Binary file not shown.
1 change: 0 additions & 1 deletion archive/folderview.plus-2026.07.24.02.txz.sha256

This file was deleted.

Binary file added archive/folderview.plus-2026.07.28.07.txz
Binary file not shown.
1 change: 1 addition & 0 deletions archive/folderview.plus-2026.07.28.07.txz.sha256
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
169b45ebdf1143c7c4f86d12703727d4c8db3dab1c637b1434c8ddb2dcf03c03 folderview.plus-2026.07.28.07.txz
20 changes: 20 additions & 0 deletions docs/releases/2026.07.28.07.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# FolderView Plus 2026.07.28.07

This focused reliability update restores first-click folder menu behavior after
the shared row-action security migration.

## Folder interactions

- Fix: Prepare Docker and VM folder context menus during the capture phase so
the initiating icon click reaches Unraid's newly attached menu opener.
- Fix: Keep folder edit and expand controls on the existing delegated action
path, and avoid cancelling or double-dispatching context-menu clicks.
- Maintenance: Preserve idempotent binding and teardown for both native capture
and delegated folder-row listeners across repeated page navigation.

## Regression coverage

- Test: Verify the shared row-action controller dispatches each action once,
does not cancel context-menu clicks, and releases both listener types.
- Test: Add a Chromium fixture proving one folder-icon click prepares and opens
the context menu without requiring a second click.
2 changes: 1 addition & 1 deletion docs/sbom.cdx.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"component": {
"type": "application",
"name": "FolderView Plus",
"version": "2026.07.28.06",
"version": "2026.07.28.07",
"properties": [
{
"name": "folderview-plus:runtime-dependencies",
Expand Down
24 changes: 21 additions & 3 deletions folderview.plus.plg
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
<!ENTITY launch "Settings/FolderViewPlus">
<!ENTITY plugdir "/usr/local/emhttp/plugins/&name;">
<!ENTITY pluginURL "https://raw.githubusercontent.com/&github;/dev/folderview.plus.plg">
<!ENTITY version "2026.07.28.06">
<!ENTITY md5 "1e64448a5d593649ac77f0e7a234e82c">
<!ENTITY sha256 "e78b2e2f7d662058c6b3f603f7ab79783a3b0691f7a624be65180a37e389d3e2">
<!ENTITY version "2026.07.28.07">
<!ENTITY md5 "fb77af5943983dd6984d0c5f8eeb3b6a">
<!ENTITY sha256 "169b45ebdf1143c7c4f86d12703727d4c8db3dab1c637b1434c8ddb2dcf03c03">
<!ENTITY iconPackVersion "1.0.0">
<!ENTITY iconPackMd5 "a149c36b41339949bb5c1eecad550704">
<!ENTITY iconPackSha256 "992f6c3544a8a3c1db80b861472fdd8b3d499f20f81796ed71405a10beb750bd">
Expand All @@ -18,6 +18,24 @@
<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">
<CHANGES>

###2026.07.28.07
# FolderView Plus 2026.07.28.07
This focused reliability update restores first-click folder menu behavior after
the shared row-action security migration.
## Folder interactions
- Fix: Prepare Docker and VM folder context menus during the capture phase so
the initiating icon click reaches Unraid's newly attached menu opener.
- Fix: Keep folder edit and expand controls on the existing delegated action
path, and avoid cancelling or double-dispatching context-menu clicks.
- Maintenance: Preserve idempotent binding and teardown for both native capture
and delegated folder-row listeners across repeated page navigation.
## Regression coverage
- Test: Verify the shared row-action controller dispatches each action once,
does not cancel context-menu clicks, and releases both listener types.
- Test: Add a Chromium fixture proving one folder-icon click prepares and opens
the context menu without requiring a second click.


###2026.07.28.06
# FolderView Plus 2026.07.28.06
This reliability update restores folder interactions after the declarative
Expand Down
8 changes: 8 additions & 0 deletions scripts/fixture_browser_tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,14 @@ test('Docker preview hydration and cached-width bootstrap preserve first-frame g
assert.equal(result.settledWidth, result.firstVisibleWidth);
});

test('Docker folder context menu opens from the first folder-icon click', async ({ page }) => {
await page.goto(`${baseUrl}/docker-layout-stability`, { waitUntil: 'load' });
const result = await page.evaluate(() => window.fixtureFolderContextFirstClick.run());
assert.equal(result.attachCount, 1, 'the first click must prepare the folder context menu once');
assert.equal(result.openCount, 1, 'the first click must reach the newly attached context-menu opener');
assert.equal(result.defaultPrevented, false, 'menu preparation must not cancel the opening click');
});

test('Docker and VM host adapters share row, structure, and idempotent hook contracts', async ({ page }) => {
await page.goto(`${baseUrl}/runtime`, { waitUntil: 'load' });
const result = await page.evaluate(() => window.fixtureRuntime.exerciseHostAdapters());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,40 @@
decorateTarget($row, '.folder-appname', 'edit', true);
decorateTarget($row, '.folder-dropdown', 'toggle', true).attr('type', 'button');
};
const dispatch = (element, event) => {
const dispatch = (element, event, options = {}) => {
const action = String(element?.getAttribute?.(actionAttribute) || '').trim();
const id = String(element?.getAttribute?.('data-fv-folder-id') || '').trim();
const handler = handlers[action];
if (!action || !id || typeof handler !== 'function') {
return false;
}
event?.preventDefault?.();
if (options.preventDefault !== false) {
event?.preventDefault?.();
}
handler(id, event);
return true;
};
const handleContextCapture = (event) => {
const target = event?.target?.closest?.(selector);
if (String(target?.getAttribute?.(actionAttribute) || '') !== 'context') {
return;
}
dispatch(target, event, { preventDefault: false });
};
const bind = () => {
doc.removeEventListener?.('click', handleContextCapture, true);
doc.addEventListener?.('click', handleContextCapture, true);
jq(doc)
.off(eventName, selector)
.on(eventName, selector, function(event) {
if (String(this?.getAttribute?.(actionAttribute) || '') === 'context') {
return;
}
dispatch(this, event);
});
};
const destroy = () => {
doc.removeEventListener?.('click', handleContextCapture, true);
jq(doc).off(eventName, selector);
};

Expand Down
49 changes: 49 additions & 0 deletions tests/browser/fixtures/docker-layout-stability.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,20 @@
<span class="appname">Container</span>
<span id="action-target"></span>
</div>
<div id="folder-row" data-fv-folder-id="fixture-folder">
<span id="folder-context-target" class="folder-hand">
<span id="folder-context-child">Folder</span>
</span>
<a class="folder-appname">Edit</a>
<button class="folder-dropdown">Toggle</button>
</div>
<div id="width-probe"></div>

<script src="/vendor/jquery.js"></script>
<script src="/plugin/scripts/runtime.column-layout.js"></script>
<script src="/plugin/scripts/docker.runtime.preview-actions.js"></script>
<script src="/plugin/scripts/docker.runtime.shared.js"></script>
<script src="/plugin/scripts/folder.runtime.row-actions.js"></script>
<script>
const actionSettings = {
preview: 1,
Expand Down Expand Up @@ -130,6 +139,46 @@
}, 120);
})
};

let contextAttachCount = 0;
let contextOpenCount = 0;
let contextDefaultPrevented = false;
let contextListener = null;
const contextTarget = document.querySelector('#folder-context-target');
const rowActionsController = window.FolderViewDockerRuntimeShared.createFolderRowActionsController({
window,
document,
$: window.jQuery,
namespace: 'fvFixtureFolderRowAction',
actionAttribute: 'data-fv-fixture-folder-action',
handlers: {
context: () => {
contextAttachCount += 1;
if (contextListener) {
contextTarget.removeEventListener('click', contextListener);
}
contextListener = (event) => {
contextOpenCount += 1;
contextDefaultPrevented = event.defaultPrevented;
};
contextTarget.addEventListener('click', contextListener);
},
edit: () => {},
toggle: () => {}
}
});
rowActionsController.decorate(window.jQuery('#folder-row'), 'fixture-folder');
rowActionsController.bind();
window.fixtureFolderContextFirstClick = {
run: () => {
document.querySelector('#folder-context-child').click();
return {
attachCount: contextAttachCount,
openCount: contextOpenCount,
defaultPrevented: contextDefaultPrevented
};
}
};
</script>
</body>
</html>
35 changes: 33 additions & 2 deletions tests/folder-row-actions.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,18 @@ const createTarget = () => {
};

test('folder row actions replace declarative handlers and dispatch each action once', () => {
const document = {};
const nativeListeners = new Map();
const document = {
addEventListener(type, handler, capture) {
nativeListeners.set(`${type}|${capture === true}`, handler);
},
removeEventListener(type, handler, capture) {
const key = `${type}|${capture === true}`;
if (nativeListeners.get(key) === handler) {
nativeListeners.delete(key);
}
}
};
const delegated = new Map();
const eventRoot = {
off(eventName, selector) {
Expand Down Expand Up @@ -96,9 +107,28 @@ test('folder row actions replace declarative handlers and dispatch each action o
assert.equal(targets['.folder-dropdown'].attr('type'), 'button');

controller.bind();
controller.bind();
const captureHandler = nativeListeners.get('click|true');
const delegatedHandler = delegated.get('click.fvTestFolderRowAction|[data-fv-test-folder-action]');
assert.equal(typeof captureHandler, 'function');
assert.equal(typeof delegatedHandler, 'function');
let prevented = 0;
const contextElement = {
getAttribute(name) {
return targets['.folder-hand'].attr(name) || '';
}
};
captureHandler({
target: {
closest(selector) {
assert.equal(selector, '[data-fv-test-folder-action]');
return contextElement;
}
},
preventDefault() {
prevented += 1;
}
});
for (const selector of Object.keys(targets)) {
delegatedHandler.call({
getAttribute(name) {
Expand All @@ -111,8 +141,9 @@ test('folder row actions replace declarative handlers and dispatch each action o
});
}
assert.deepEqual(calls, ['context:folder-1', 'edit:folder-1', 'toggle:folder-1']);
assert.equal(prevented, 3);
assert.equal(prevented, 2);

controller.destroy();
assert.equal(nativeListeners.size, 0);
assert.equal(delegated.size, 0);
});