π’ Expose file versions in the sidebar - #1972
Open
karlitschek wants to merge 7 commits into
Open
Conversation
karlitschek
requested review from
enjeck and
silverkszlo
and
a lite review from Copilot
and removed request for
enjeck and
silverkszlo
August 6, 2026 17:26
enjeck
reviewed
Aug 6, 2026
enjeck
left a comment
Contributor
There was a problem hiding this comment.
Pls add screenshots showing what the UI looks like. Maybe significant enough tp require designer input
AndyScherzinger
force-pushed
the
feat/noid/expose-versions
branch
2 times, most recently
from
August 16, 2026 10:27
4c30cea to
71fb7b5
Compare
AndyScherzinger
force-pushed
the
feat/noid/expose-versions
branch
from
August 16, 2026 10:50
71fb7b5 to
f1635de
Compare
AndyScherzinger
force-pushed
the
feat/noid/expose-versions
branch
2 times, most recently
from
August 16, 2026 11:46
ad83197 to
ca9796d
Compare
AndyScherzinger
force-pushed
the
feat/noid/expose-versions
branch
from
August 16, 2026 13:11
ca9796d to
59d192f
Compare
AndyScherzinger
force-pushed
the
feat/noid/expose-versions
branch
2 times, most recently
from
August 16, 2026 23:30
f27f092 to
4a03eb9
Compare
AndyScherzinger
force-pushed
the
feat/noid/expose-versions
branch
from
August 17, 2026 05:10
4a03eb9 to
0d6f281
Compare
AndyScherzinger
force-pushed
the
feat/noid/expose-versions
branch
2 times, most recently
from
August 17, 2026 06:35
687a517 to
9d4047e
Compare
Notes have been versioned all along β they are ordinary files, so files_versions keeps history for them without the app doing anything. There was just no way to see it from Notes. Most of the wiring already existed: * PageController dispatches OCA\Files\Event\LoadSidebar, and files_versions registers a listener on that event which adds its sidebar-tab script. The Versions tab has therefore been registered on every Notes page already, simply never rendered. * NotePlain and NoteRich both already subscribe to files_versions:restore:requested and :restored, showing a loading state and refreshing the note afterwards. The restore path was built and unreachable. * NoteShareSidebar already knew how to mount a registered Files sidebar tab as a custom element with the node/folder/view props it expects. The only thing missing was that the sidebar hard-filtered the tab registry down to `id === 'sharing'`. It now renders every tab from an allow-list, so Sharing and Versions sit side by side. Details: * Tab selection moved to a pure function in sidebarTabs.js. It is an allow-list rather than "everything registered", because LoadSidebar brings in whatever every installed app registers and a note sidebar should not grow new tabs when an unrelated app is installed. A tab's own enabled() predicate still has the final say β the versions tab hides itself on public shares and for non-files β but it needs a node to judge, so while the node is still loading tabs are kept and filtered again once it arrives, and a predicate that throws drops that tab instead of taking the sidebar down. * Tabs initialise independently, so one failing to define its custom element no longer hides the others; only a total failure is reported. * New event notes:sidebar:open carries a tab id. notes:share:open is kept as a thin wrapper so anything already emitting it keeps working. * "Versions" action added to the note's action menu, next to "Share". That menu lives in the note list row, so it is present in every editor mode rather than only the non-default one. * Sidebar copy no longer says "sharing" now that it hosts two tabs. The data-cy-notes-share-sidebar hook is deliberately unchanged, since playwright/e2e/basic.spec.ts asserts on it. Assisted-by: Claude Code:claude-opus-5[1m] Co-Authored-By: Andy Scherzinger <info@andy-scherzinger.de> Signed-off-by: Frank Karlitschek <frank.karlitschek@nextcloud.com>
The note editor's actions menu had no way to reach the sidebar, so sharing and versions were only reachable from the note list row. The entry uses the DockRight icon and the "Open sidebar" label the Viewer app uses for the same purpose, and sits next to "Full screen" as it does there. That menu only exists in the markdown editor β the rich editor brings its own menu bar β so the note list row remains the path that works in every editor mode. The e2e helper that switches the editor mode takes the isolated request fixture rather than page.request: the latter shares the browser cookie jar, so its basic auth replaces the session cookie and logs the page out on the next reload. Assisted-by: Claude Code:claude-opus-5[1m] Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The sidebar header carried only the note title, while the Files sidebar shows the file metadata right below it. Reimplements the subname the Files app renders in its sidebar header (apps/files/src/components/FilesSidebar/FilesSidebarSubname.vue): the formatted file size, the modification date and the owner as a user bubble with avatar and display name, rendered through NcAppSidebar's subname slot. No new data plumbing was needed β fetchDavNode() already uses the default propfind, which asks for getcontentlength, getlastmodified, owner-id and owner-display-name. The metadata is the state of the file at the moment the sidebar was opened; nothing re-fetches the node while it stays open. Assisted-by: Claude Code:claude-opus-5[1m] Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The sidebar kept three collections to answer one question β is this tab's custom element defined? * initializedTabs was only ever written once customElements.whenDefined() had resolved, which is exactly when customElements.get() starts returning the constructor β and that is checked first in the same guard. Redundant, so it is gone. * pendingTabs caches an in-flight side effect on the global custom element registry rather than component state, so it moves to module scope along with the two functions that use it. * loadingTab is raised once per run instead of from the per-tab helper. Guarding it per tab bought nothing, since loadNodeContext() raises loadingContext on every open anyway. The active tab is now clamped by a computed feeding the `active` prop instead of a watcher writing back into activeTab, and the reset shared by onSidebarOpen and onClosed became a single method. tabError is dropped as well: now that a tab failing to define its element is no longer offered, it could only be set when no tab was left at all, where the empty state already carries a message. The failure itself is still logged with the tab id. Assisted-by: Claude Code:claude-opus-5[1m] Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Sidebar tabs should carry outlined icons that fill once the tab is active. The sharing tab now renders ShareVariantOutline while inactive and ShareVariant while active, following the pattern from nextcloud/tables#2672. The switch happens inside the #icon slot rather than through a dedicated slot, as @nextcloud/vue has no #icon-active yet: NcAppSidebarTab exposes renderIcon() without arguments. That is enough here, because the tab button invokes renderIcon() from its own render function, so reading the resolved tab id there tracks it. Only the sharing tab is overridden. Every other tab keeps the icon its app registered, versions included β there is no outlined counterpart of the backup-restore icon to fill in. Assisted-by: Claude Code:claude-opus-5[1m] Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Restoring an older version left the list showing the state from when the sidebar was opened. It took a reload or reopening the sidebar to see the restored version as the current one. The versions tab already reloads itself when the mtime of the node it was handed changes, and emits files:node:updated with a node carrying the restored etag, size and mtime. The Files sidebar closes that loop by swapping its current node whenever such an event names it, which is what the note sidebar now does too β matching on source, as the Files sidebar store does. The subname in the sidebar header picks the update up as well, so size and modification date no longer lag behind a restore either. The test emits the event files_versions sends out after a restore and watches for the reload it triggers, rather than restoring for real: what the sidebar has to do is the same either way, and the outcome then does not hinge on how a server stamps a rollback. Assisted-by: Claude Code:claude-opus-5[1m] Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger
force-pushed
the
feat/noid/expose-versions
branch
from
August 17, 2026 07:09
9d4047e to
f9ffec2
Compare
Restoring a version left the editor showing the old content until its periodic refresh came around, with nothing indicating that anything was going on β and that stale content could be typed into meanwhile. Both editors already handled this, but were never reached: they read a payload files_versions no longer emits β a fileInfo key, and a fileId on the version β so the requested handler threw on the missing key and the restored one always returned early. They take the node from the event now and compare its fileid. That brings back the loading state, which replaces the editor with a spinner and thereby keeps it from being typed into while the content is swapped, along with the immediate refresh once the restore lands. Two things were needed for that state to mean anything: NotePlain's refreshNote() returns its promise now, as it would otherwise be cleared before the new content arrived, and both editors clear it on files_versions:restore:failed, which would leave the editor stuck behind the spinner for good. The test delays the restore request so the window it asserts on is not a race, and opens the note explicitly, as a reload would leave the editor on whichever note was open before. Its revisions are written over WebDAV rather than through the app, which would retitle β and thereby rename β the note from its changed content, and they are spaced out because recent versions are thinned to one per two seconds. Version entries are located from the list rather than from the sidebar: the sharing tab's element reports itself as its own shadow root, which sends a piercing query into a loop. The poll that waited for a conflict button to auto-click goes as well. It looked for data-cy="resolveServerVersion", which exists neither in Notes nor in Text β Text's collision dialog offers useEditorVersion and useReaderVersion β so it never hit and never stopped, and fixing the guard above would have turned it into a timer per restore that runs for as long as the page is open. Pressing that button for the user would mean discarding whatever they had typed but not yet saved, which is the very thing the dialog asks about, so the dialog is left to them. Assisted-by: Claude Code:claude-opus-5[1m] Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AndyScherzinger
force-pushed
the
feat/noid/expose-versions
branch
from
August 17, 2026 08:23
f9ffec2 to
46c0a7c
Compare
AndyScherzinger
approved these changes
Aug 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Notes have been versioned all along β they are ordinary files, so files_versions keeps history for them without the app doing anything. There was just no way to see it from Notes. Exposing it turned the note sidebar into something worth bringing closer to the Files one, so this PR does both.
Most of the wiring already existed:
The only thing missing was that the sidebar hard-filtered the tab registry down to
id === 'sharing'. It now renders every tab from an allow-list, so Sharing and Versions sit side by side.What else is in here
Implementation notes
Tests
New
playwright/e2e/note-sidebar.spec.ts:openNoteActions()moved from note-actions.spec.ts into support/note.ts so both specs share it. Two helpers were added there as well:setNoteMode()for the markdown-editor case, andcreateNoteRevisions(), which creates a note and then rewrites it over WebDAV β going around the app on purpose, as writing through it would retitle and thereby rename the note. Both take Playwright's isolated request fixture rather than page.request, whose basic auth would replace the session cookie the browser is logged in with.πΌοΈ Screenshots
π€ AI (if applicable)