fix(pad): keep menu_right visible on readonly pads by default (#7783)

PR #X (issue #5182) added a client-side `$('#editbar .menu_right').hide()`
for readonly pads, opt-out via `?showMenuRight=true`. The intent — clean
chrome for iframe-embedded readonly announcement pads — was good but the
implementation hid the userlist toggle along with import/export.

That has two unwanted effects on non-embed deployments:

  * Plugins like ep_guest inject their "Log In" button into `#myuser`
    inside the userlist popup, which lives under `.menu_right`. When
    the guest user (readOnly: true) lands on a readonly pad, the button
    they need to escape readonly is hidden. Chicken-and-egg.
  * Settings, embed, home, timeslider, showusers are all legitimately
    useful for readonly viewers and got removed alongside the actual
    write-only controls.

The server already does the right thing without help from the client:

  * src/node/utils/toolbar.ts:282-290 strips `savedrevision` from the
    right toolbar when isReadOnly is true.
  * src/static/css/pad/popup_import_export.css:1 has
    `.readonly .acl-write { display: none }`, which hides the Import
    column of the import/export popup. Export stays visible (and is
    legitimately useful in readonly).

Drop the client-side blanket hide. The iframe-embed use case from #5182
is still served by `?showMenuRight=false` (the existing handler at
src/static/js/pad.ts:91-107), and `?showControls=false` continues to
hide the entire editbar for callers who want even the left menu gone.

Tests: rewrite `hide_menu_right.spec.ts`. The "readonly pad hides
.menu_right by default" assertion is inverted; a new test confirms
`?showMenuRight=false` still hides on readonly pads.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McLear 2026-05-16 17:53:20 +01:00 committed by GitHub
parent 294158e773
commit 8c4f974122
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 34 additions and 13 deletions

View file

@ -91,10 +91,12 @@ 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.
// right-side toolbar (import/export/timeslider/settings/embed/home/
// users) — useful for iframe-embedded readonly pads where the menu
// is just chrome (issue #5182). Explicit 'true' forces the menu
// visible (a no-op against the default, kept for symmetry and for
// overriding any future caller-applied hide). Any other value is a
// no-op — the menu stays in its default state.
name: 'showMenuRight',
checkVal: null,
callback: (val) => {
@ -787,14 +789,18 @@ 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();
}
// The right-side toolbar stays visible on readonly pads. The
// server-side `toolbar.menu(buttons, isReadOnly)` (see
// src/node/utils/toolbar.ts) already strips `savedrevision`, and
// `.readonly .acl-write { display: none }` hides the Import column
// inside the import/export popup, so the remaining controls
// (export, timeslider, settings, embed, home, showusers) are all
// safe for readonly viewers — and the userlist is the surface that
// plugins like ep_guest hang their "Log In" button off, so hiding
// it traps guests in readonly with no way out. Iframe-embed use
// cases that want a clean look (issue #5182) opt in to the hide
// via `?showMenuRight=false`, or hide the whole editbar via
// `?showControls=false`.
} else if (!settings.hideChat) { $('#chaticon').show(); }
$('body').addClass(window.clientVars.readonly ? 'readonly' : 'readwrite');

View file

@ -44,10 +44,25 @@ test.describe('showMenuRight URL parameter', function () {
return url;
};
test('readonly pad hides .menu_right by default', async function ({page}) {
test('readonly pad shows .menu_right by default', async function ({page}) {
// The server-side toolbar.menu(..., isReadOnly) strips `savedrevision`
// and `.readonly .acl-write { display: none }` hides the Import
// column of the import/export popup, so the remaining controls
// (export, timeslider, settings, embed, home, showusers) are safe
// for readonly viewers — and ep_guest / other auth plugins depend
// on the userlist being reachable to surface their Log In button.
const readonlyUrl = await getReadonlyUrl(page);
await page.goto(readonlyUrl);
await page.waitForSelector('#editorcontainer.initialized');
await expect(page.locator('#editbar .menu_right')).toBeVisible();
});
test('readonly pad with showMenuRight=false hides the menu', async function ({page}) {
// Iframe-embed use case (issue #5182): the embedder opts in to the
// hide via the URL parameter when they want a clean look.
const readonlyUrl = await getReadonlyUrl(page);
await page.goto(`${readonlyUrl}?showMenuRight=false`);
await page.waitForSelector('#editorcontainer.initialized');
await expect(page.locator('#editbar .menu_right')).toBeHidden();
});