From d48a4f37e58ba08f1f0ab50f1ec410efa8358ded Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 19 Apr 2026 16:46:30 +0100 Subject: [PATCH 1/4] feat(editor): add showMenuRight URL param to hide right-side toolbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a showMenuRight URL/embed parameter. When set to false, the right-side toolbar (.menu_right — import/export, timeslider, settings, share, users) is hidden. Default behavior (menu shown) is unchanged. Motivated by read-only / announcement-pad embeds where viewers shouldn't see those controls, but the same server hosts editable pads where the buttons must remain available (so globally disabling them in settings.json is not a fit). Closes #5182 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/static/js/pad.ts | 7 +++++ .../specs/hide_menu_right.spec.ts | 26 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/tests/frontend-new/specs/hide_menu_right.spec.ts diff --git a/src/static/js/pad.ts b/src/static/js/pad.ts index d9698f5e776..01273fdc457 100644 --- a/src/static/js/pad.ts +++ b/src/static/js/pad.ts @@ -76,6 +76,13 @@ const getParameters = [ $('#editbar').css('display', 'flex'); }, }, + { + name: 'showMenuRight', + checkVal: 'false', + callback: (val) => { + $('#editbar .menu_right').hide(); + }, + }, { name: 'showChat', checkVal: null, diff --git a/src/tests/frontend-new/specs/hide_menu_right.spec.ts b/src/tests/frontend-new/specs/hide_menu_right.spec.ts new file mode 100644 index 00000000000..13bfaecefdd --- /dev/null +++ b/src/tests/frontend-new/specs/hide_menu_right.spec.ts @@ -0,0 +1,26 @@ +import {expect, test} from "@playwright/test"; +import {appendQueryParams, goToNewPad} from "../helper/padHelper"; + +test.beforeEach(async ({page, browser}) => { + const context = await browser.newContext(); + await context.clearCookies(); + await goToNewPad(page); +}); + +test.describe('showMenuRight URL parameter', function () { + test('without the parameter, .menu_right is visible', async function ({page}) { + await expect(page.locator('#editbar .menu_right')).toBeVisible(); + }); + + test('showMenuRight=false hides .menu_right', async function ({page}) { + await appendQueryParams(page, {showMenuRight: 'false'}); + await expect(page.locator('#editbar .menu_right')).toBeHidden(); + // The left menu stays visible so the pad remains navigable. + await expect(page.locator('#editbar .menu_left')).toBeVisible(); + }); + + test('showMenuRight with any other value leaves .menu_right visible', async function ({page}) { + await appendQueryParams(page, {showMenuRight: 'true'}); + await expect(page.locator('#editbar .menu_right')).toBeVisible(); + }); +}); From feae2a6e6cb45b0c4265f779950ef20b0fe55ccd Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 19 Apr 2026 18:37:08 +0100 Subject: [PATCH 2/4] fix(editor): auto-hide menu_right on readonly pads, accept showMenuRight=true override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Qodo review feedback on #7553: 1. Readonly pads now hide the right-side toolbar automatically. The original issue (#5182) was specifically about readonly embeds; the previous implementation only honoured an explicit `?showMenuRight=false` URL parameter, which meant that vanilla readonly pads still showed import/export/timeslider/settings/share/users controls — all noise for viewers who can't interact with the pad anyway. 2. Callers who still want the menu visible on readonly pads can opt back in with `?showMenuRight=true`. The URL-param callback now accepts both values instead of just `false`. 3. The Playwright spec's `browser.newContext() + clearCookies()` pattern was a no-op because the test navigated with the existing `page` fixture (different context). Switch to `page.context().clearCookies()`, and cover both the auto-hide and the explicit-override paths on a readonly-URL navigation. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/static/js/pad.ts | 21 ++++++++++++-- .../specs/hide_menu_right.spec.ts | 28 ++++++++++++++++--- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/static/js/pad.ts b/src/static/js/pad.ts index 01273fdc457..76fc2616bf4 100644 --- a/src/static/js/pad.ts +++ b/src/static/js/pad.ts @@ -77,10 +77,19 @@ const getParameters = [ }, }, { + // showMenuRight accepts 'true' or 'false'. Explicit 'false' hides the + // right-side toolbar (import/export/timeslider/settings/share/users); + // explicit 'true' forces it visible, overriding the readonly + // auto-hide applied further down (issue #5182). Any other value is + // a no-op — the menu stays in its default state. name: 'showMenuRight', - checkVal: 'false', + checkVal: null, callback: (val) => { - $('#editbar .menu_right').hide(); + if (val === 'false') { + $('#editbar .menu_right').hide(); + } else if (val === 'true') { + $('#editbar .menu_right').show(); + } }, }, { @@ -685,6 +694,14 @@ const pad = { $('#chaticon').hide(); $('#options-chatandusers').parent().hide(); $('#options-stickychat').parent().hide(); + // Hide the right-side toolbar on readonly pads — import/export, + // timeslider, settings, share, users are all noise for viewers + // who can't interact with the pad. Callers who need those + // controls visible on a readonly pad can force them back via + // `?showMenuRight=true`, which runs in getParameters() above. + if (getUrlVars().get('showMenuRight') !== 'true') { + $('#editbar .menu_right').hide(); + } } else if (!settings.hideChat) { $('#chaticon').show(); } $('body').addClass(window.clientVars.readonly ? 'readonly' : 'readwrite'); diff --git a/src/tests/frontend-new/specs/hide_menu_right.spec.ts b/src/tests/frontend-new/specs/hide_menu_right.spec.ts index 13bfaecefdd..d6a86b94134 100644 --- a/src/tests/frontend-new/specs/hide_menu_right.spec.ts +++ b/src/tests/frontend-new/specs/hide_menu_right.spec.ts @@ -1,9 +1,11 @@ import {expect, test} from "@playwright/test"; import {appendQueryParams, goToNewPad} from "../helper/padHelper"; -test.beforeEach(async ({page, browser}) => { - const context = await browser.newContext(); - await context.clearCookies(); +test.beforeEach(async ({page}) => { + // clearCookies on the page's own context — creating a separate + // BrowserContext and clearing cookies on it is a no-op for the page + // fixture (Qodo review feedback on #7553). + await page.context().clearCookies(); await goToNewPad(page); }); @@ -19,8 +21,26 @@ test.describe('showMenuRight URL parameter', function () { await expect(page.locator('#editbar .menu_left')).toBeVisible(); }); - test('showMenuRight with any other value leaves .menu_right visible', async function ({page}) { + test('showMenuRight=true keeps .menu_right visible', async function ({page}) { await appendQueryParams(page, {showMenuRight: 'true'}); await expect(page.locator('#editbar .menu_right')).toBeVisible(); }); + + test('readonly pad hides .menu_right by default', async function ({page}) { + // Find the share link which exposes the readonly r.* id, then navigate. + await page.locator('.buttonicon-embed').click(); + const readonlyUrl = await page.locator('#readonlyInput').inputValue(); + expect(readonlyUrl).toMatch(/\/p\/r\./); + await page.goto(readonlyUrl); + await page.waitForSelector('#editorcontainer.initialized'); + await expect(page.locator('#editbar .menu_right')).toBeHidden(); + }); + + test('readonly pad with showMenuRight=true keeps the menu visible', async function ({page}) { + await page.locator('.buttonicon-embed').click(); + const readonlyUrl = await page.locator('#readonlyInput').inputValue(); + await page.goto(`${readonlyUrl}?showMenuRight=true`); + await page.waitForSelector('#editorcontainer.initialized'); + await expect(page.locator('#editbar .menu_right')).toBeVisible(); + }); }); From f2a97a94ba19a0bf8411a4fe6f75c8e312b4fc1b Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 19 Apr 2026 18:55:37 +0100 Subject: [PATCH 3/4] test(7553): use actual readonly-URL selector in Playwright spec The previous test looked up (capital-I) and called inputValue() on it. The real element is (lowercase) and it's a toggle checkbox, not a URL field. The readonly URL itself is in `#linkinput`, updated live when the readonly checkbox is checked. Wire the test to that flow. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../specs/hide_menu_right.spec.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/tests/frontend-new/specs/hide_menu_right.spec.ts b/src/tests/frontend-new/specs/hide_menu_right.spec.ts index d6a86b94134..d1bb7cd7e8d 100644 --- a/src/tests/frontend-new/specs/hide_menu_right.spec.ts +++ b/src/tests/frontend-new/specs/hide_menu_right.spec.ts @@ -1,4 +1,4 @@ -import {expect, test} from "@playwright/test"; +import {expect, Page, test} from "@playwright/test"; import {appendQueryParams, goToNewPad} from "../helper/padHelper"; test.beforeEach(async ({page}) => { @@ -26,19 +26,26 @@ test.describe('showMenuRight URL parameter', function () { await expect(page.locator('#editbar .menu_right')).toBeVisible(); }); - test('readonly pad hides .menu_right by default', async function ({page}) { - // Find the share link which exposes the readonly r.* id, then navigate. + // Helper: open the Share popup, flip it to read-only, read the r.* URL + // back out of #linkinput. The readonly toggle is a checkbox + // (`#readonlyinput`) that rewrites #linkinput's value live. + const getReadonlyUrl = async (page: Page) => { await page.locator('.buttonicon-embed').click(); - const readonlyUrl = await page.locator('#readonlyInput').inputValue(); - expect(readonlyUrl).toMatch(/\/p\/r\./); + await page.locator('#readonlyinput').check(); + const url = await page.locator('#linkinput').inputValue(); + expect(url).toMatch(/\/p\/r\./); + return url; + }; + + test('readonly pad hides .menu_right by default', async function ({page}) { + const readonlyUrl = await getReadonlyUrl(page); await page.goto(readonlyUrl); await page.waitForSelector('#editorcontainer.initialized'); await expect(page.locator('#editbar .menu_right')).toBeHidden(); }); test('readonly pad with showMenuRight=true keeps the menu visible', async function ({page}) { - await page.locator('.buttonicon-embed').click(); - const readonlyUrl = await page.locator('#readonlyInput').inputValue(); + const readonlyUrl = await getReadonlyUrl(page); await page.goto(`${readonlyUrl}?showMenuRight=true`); await page.waitForSelector('#editorcontainer.initialized'); await expect(page.locator('#editbar .menu_right')).toBeVisible(); From c1f4bc2c92942c14fa3931dda8d3391e154e2e12 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 19 Apr 2026 19:05:23 +0100 Subject: [PATCH 4/4] test(7553): wait for share popup before clicking readonly checkbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Playwright's stability check kept retrying the click while the popup was animating open ("element is not stable"). Wait for #embed.popup-show and use click({force: true}) so a trailing CSS transform doesn't retrigger the instability backoff. Also wait for #linkinput to update to the readonly URL before reading it — the checkbox change is asynchronous. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/tests/frontend-new/specs/hide_menu_right.spec.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/tests/frontend-new/specs/hide_menu_right.spec.ts b/src/tests/frontend-new/specs/hide_menu_right.spec.ts index d1bb7cd7e8d..5d09749a27c 100644 --- a/src/tests/frontend-new/specs/hide_menu_right.spec.ts +++ b/src/tests/frontend-new/specs/hide_menu_right.spec.ts @@ -28,10 +28,17 @@ test.describe('showMenuRight URL parameter', function () { // Helper: open the Share popup, flip it to read-only, read the r.* URL // back out of #linkinput. The readonly toggle is a checkbox - // (`#readonlyinput`) that rewrites #linkinput's value live. + // (`#readonlyinput`) that rewrites #linkinput's value live. The popup + // animates open, so the checkbox is briefly "not stable" — wait for + // the popup's show class before interacting, and use force:true so a + // trailing transform doesn't trip Playwright's stability check. const getReadonlyUrl = async (page: Page) => { await page.locator('.buttonicon-embed').click(); - await page.locator('#readonlyinput').check(); + await page.locator('#embed.popup-show').waitFor({state: 'visible'}); + await page.locator('#readonlyinput').check({force: true}); + await page.waitForFunction( + () => (document.querySelector('#linkinput') as HTMLInputElement | null) + ?.value.includes('/p/r.')); const url = await page.locator('#linkinput').inputValue(); expect(url).toMatch(/\/p\/r\./); return url;