diff --git a/archive/folderview.plus-2026.07.24.02.txz b/archive/folderview.plus-2026.07.24.02.txz deleted file mode 100644 index 9e183fb0..00000000 Binary files a/archive/folderview.plus-2026.07.24.02.txz and /dev/null differ diff --git a/archive/folderview.plus-2026.07.24.02.txz.sha256 b/archive/folderview.plus-2026.07.24.02.txz.sha256 deleted file mode 100644 index 5fb0f16a..00000000 --- a/archive/folderview.plus-2026.07.24.02.txz.sha256 +++ /dev/null @@ -1 +0,0 @@ -18ddcd827fc86899b9dc6590298ebeb3d95e37442ba43d7a22b240ee36d21736 folderview.plus-2026.07.24.02.txz diff --git a/archive/folderview.plus-2026.07.28.07.txz b/archive/folderview.plus-2026.07.28.07.txz new file mode 100644 index 00000000..6f974d35 Binary files /dev/null and b/archive/folderview.plus-2026.07.28.07.txz differ diff --git a/archive/folderview.plus-2026.07.28.07.txz.sha256 b/archive/folderview.plus-2026.07.28.07.txz.sha256 new file mode 100644 index 00000000..3f500cee --- /dev/null +++ b/archive/folderview.plus-2026.07.28.07.txz.sha256 @@ -0,0 +1 @@ +169b45ebdf1143c7c4f86d12703727d4c8db3dab1c637b1434c8ddb2dcf03c03 folderview.plus-2026.07.28.07.txz diff --git a/docs/releases/2026.07.28.07.md b/docs/releases/2026.07.28.07.md new file mode 100644 index 00000000..d8e58dc8 --- /dev/null +++ b/docs/releases/2026.07.28.07.md @@ -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. diff --git a/docs/sbom.cdx.json b/docs/sbom.cdx.json index 13eeaa71..c195454f 100644 --- a/docs/sbom.cdx.json +++ b/docs/sbom.cdx.json @@ -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", diff --git a/folderview.plus.plg b/folderview.plus.plg index 5d201706..fbb34599 100644 --- a/folderview.plus.plg +++ b/folderview.plus.plg @@ -6,9 +6,9 @@ - - - + + + @@ -18,6 +18,24 @@ +###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 diff --git a/scripts/fixture_browser_tests.mjs b/scripts/fixture_browser_tests.mjs index ea04422e..b614f4c2 100644 --- a/scripts/fixture_browser_tests.mjs +++ b/scripts/fixture_browser_tests.mjs @@ -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()); diff --git a/src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folder.runtime.row-actions.js b/src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folder.runtime.row-actions.js index 3a253da7..bc609e63 100644 --- a/src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folder.runtime.row-actions.js +++ b/src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folder.runtime.row-actions.js @@ -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); }; diff --git a/tests/browser/fixtures/docker-layout-stability.html b/tests/browser/fixtures/docker-layout-stability.html index 7755f3e4..26d26e84 100644 --- a/tests/browser/fixtures/docker-layout-stability.html +++ b/tests/browser/fixtures/docker-layout-stability.html @@ -35,11 +35,20 @@ Container +
+ + Folder + + Edit + +
+ + diff --git a/tests/folder-row-actions.test.mjs b/tests/folder-row-actions.test.mjs index 49ab2515..3c7b2870 100644 --- a/tests/folder-row-actions.test.mjs +++ b/tests/folder-row-actions.test.mjs @@ -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) { @@ -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) { @@ -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); });