Skip to content

πŸ“Š Note details tab in the sidebar - #1974

Open
karlitschek wants to merge 2 commits into
mainfrom
feat/noid/note-info
Open

πŸ“Š Note details tab in the sidebar#1974
karlitschek wants to merge 2 commits into
mainfrom
feat/noid/note-info

Conversation

@karlitschek

@karlitschek karlitschek commented Aug 6, 2026

Copy link
Copy Markdown
Member
2026-08-18 09_37_28-Notes - Nextcloud β€” Mozilla Firefox

The note sidebar hosts Sharing but says nothing about the note itself. This adds a first tab β€” Details β€” with the note's category, a reading estimate, its path, and a read-only marker when the note cannot be written. The file's size and modification date go into the sidebar header instead, since they describe the note as a whole rather than belonging to one of its tabs.

Most of it is free: the sidebar already fetches the note's DAV node for the sharing tab, so the size comes from data it was loading anyway, and everything else is on the note in the store.

The reading estimate shapes the design

The note list payload excludes content, so a note that has never been opened has none client-side. Rather than fetching every body up front, the tab pulls the one note it needs, and only once its tab is actually selected β€” opening the sidebar to share a note does not drag its body down with it. Until then the row shows a placeholder, and a body that cannot be read shows a dash rather than disappearing.

Counting words means ignoring the markup, otherwise # and ** inflate the number. noteStats.js strips the obvious things β€” fenced code, image syntax, link targets while keeping labels, heading, quote and list markers, setext underlines, emphasis β€” and leaves the rest alone. A full parse would be much more code for a number nobody checks to the decimal.

Deliberately no word or character count

The Text app already shows both for the note open in rich mode, computed from its parsed document rather than from the markdown. The two definitions disagree β€” for **bold** [label](https://example.com) Text counts 10 characters and a markdown-based count sees 37 β€” so a second pair of numbers here would invite the question of which is right. A reading estimate is the one figure Text does not offer.

Making room for a second tab

The sidebar hosted exactly one, so this takes a little with it:

  • A 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, and "Details" is offered in the note's action menu next to Share.
  • The requested tab is clamped to one the sidebar actually renders β€” NcAppSidebar falls back to its first tab on its own, but does not report that back, so active would otherwise reach the wrong element.
  • Both tab icons are outlined until their tab is active, then fill, following the pattern from 🎨 Icons should be outline-variant where possibleΒ tables#2672. The swap happens inside the #icon slot rather than through a dedicated one, since @nextcloud/vue has no #icon-active.
  • The header no longer says "Share", the "not available right now" empty state now also checks for a note so it cannot appear just because the sharing tab failed to register, and the context error copy no longer claims sharing is what failed.

The tab is Notes' own rather than a Files sidebar tab, so it renders outside the registry lookup with order 0 to sit ahead of Sharing, and its rows are inset to the same 8px the Files tabs put their own content at.

Keeping up with the note

The sidebar is opened for one note, but the list keeps navigating. An open sidebar now follows whichever note is being looked at, the way the Files one does, keeping the tab in view β€” this is its own commit, since it is a fix rather than part of the tab. It watches the note id in the route rather than the store's selectedNote, which nothing ever sets.

Within the tab, the category was reactive but the path was not, and needed a full reload. Two separate causes, both required:

  • updateNote() copies noteAttributes only, and internalPath is not one of them, so it was dropped on every update β€” a refetch could not have refreshed it either.
  • The category endpoint answers with the category string alone, so nothing carried the new path back. setCategory() refetches the note now.

Measured on a dev instance: neither fix, the path never updates; the store fix alone, 18.9s (the next background sync); both, 415ms.

Layout

Rows are stacked β€” label above value β€” rather than laid out in two columns. A path is long enough that a two-column row either wraps inside a narrow value column or has to break the rhythm by spanning the full width on its own; stacking every row removes the special case and gives each value the whole sidebar. It also leaves room for translated labels, which run considerably longer than the English ones.

core/css/apps.scss styles dl/dt/dd for prose in a wide content area: 12px of padding on every side, and a fixed 130px label column aligned to its end. In a sidebar that doubled the height of every row and left the labels ragged, so the component resets all three. Worth knowing for anyone else reaching for a description list.

The header subname carries size Β· modified, the latter through NcDateTime so it stays current without a reload. This overlaps #1972, which renders a richer subname of its own including the owner β€” that one should win on rebase. It is duplicated here only so this PR is reviewable on its own.

Tests

New playwright/e2e/note-sidebar.spec.ts:

  • opens the details tab from the actions menu
  • keeps the sharing tab reachable next to the details one
  • fills a tab icon only while its tab is active
  • falls back to the details tab when the requested one is unavailable
  • estimates the reading time from the note body
  • follows the note the list navigates to
  • marks the reading time unavailable when the note body cannot be loaded

src/tests/noteStats.spec.js covers noteStats.js β€” every markup rule stripMarkup() implements, including the non-breaking space a task checkbox may contain, plus a body that is absent or not a string and a reading estimate that neither rounds an existing note down to nothing nor misrounds at the boundaries. It needs the vitest setup from #1989, which should merge first.

src/tests/notesStore.spec.js covers updateNote() propagating internalPath. That one is a unit test rather than an e2e one on purpose: the path refreshes within seconds in the e2e container whether the fix is there or not, so no browser test can fail for it.

πŸ€– AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@karlitschek
karlitschek requested review from enjeck and silverkszlo and a lite review from Copilot and removed request for enjeck and silverkszlo August 6, 2026 18:32

This comment was marked as off-topic.

@AndyScherzinger AndyScherzinger added 3. to review AI assisted This PR contains AI-assisted commits enhancement New feature or request labels Aug 11, 2026
@enjeck
enjeck requested a lite review from Copilot August 16, 2026 07:57

This comment was marked as outdated.

This comment was marked as resolved.

@AndyScherzinger AndyScherzinger changed the title Feat/noid/note info πŸ“Š Note info Aug 16, 2026
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/note-info branch 4 times, most recently from 67be3df to 651ea2a Compare August 17, 2026 21:00
@AndyScherzinger AndyScherzinger changed the title πŸ“Š Note info πŸ“Š Note details tab in the sidebar Aug 17, 2026
@AndyScherzinger AndyScherzinger added this to the 6.1.0 milestone Aug 17, 2026
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/note-info branch 5 times, most recently from e84f6de to 35b117b Compare August 18, 2026 07:41
@AndyScherzinger
AndyScherzinger requested a lite review from Copilot August 18, 2026 07:44
@AndyScherzinger
AndyScherzinger marked this pull request as ready for review August 18, 2026 07:47

This comment was marked as resolved.

This comment was marked as resolved.

Frank Karlitschek and others added 2 commits August 19, 2026 00:56
The sidebar hosts Sharing but says nothing about the note itself. This adds a
first tab with category, a reading estimate, size, created and modified times,
the file path, and a read-only marker when the note cannot be written.

Most of it is free: the sidebar already fetches the note's DAV node for the
sharing tab, so size and creation time come from data it was loading anyway,
and everything else is on the note in the store.

The reading estimate is not free, and that shapes the design. The note list
payload excludes `content`, so a note that has never been opened has none
client-side. Rather than fetching every body up front, the tab pulls the one
note it needs and only once its tab is actually selected β€” opening the sidebar
to share a note does not drag its body down with it. Until then the row shows a
placeholder, and a body that cannot be read shows a dash rather than
disappearing. fetchNote() only rejects on a missing note and reports everything
else itself, so the content is what says whether the fetch worked.

Estimating a reading time means counting words without the markup, otherwise
'#' and '**' inflate the number. noteStats.js strips the obvious things β€”
fenced code, image syntax, link targets while keeping labels, heading, quote
and list markers, setext underlines, emphasis β€” and leaves the rest alone. A
full parse would be much more code for a number nobody checks to the decimal.

Deliberately no word or character count. The Text app shows both for the note
open in rich mode, computed from its parsed document rather than from the
markdown, so a second pair of numbers here would differ from those and invite
the question of which is right. A reading estimate is the one figure Text does
not offer.

The sidebar hosted a single tab, so making room for a second one takes a little
with it:

* A 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, and "Details"
  is offered in the note's action menu next to Share.
* NcAppSidebar falls back to its first tab when the requested one is not there
  but does not report that back, so the id is clamped in resolvedTab() for
  `active` to reach the right element.
* Both tab icons are outlined until their tab is active, following the pattern
  from nextcloud/tables#2672. The switch happens inside the #icon slot as
  @nextcloud/vue has no #icon-active yet.
* The header no longer says "Share", the "not available right now" empty state
  now also checks for a note so it cannot appear just because the sharing tab
  failed to register, and the context error copy no longer claims sharing is
  what failed.

The tab is Notes' own rather than a Files sidebar tab, so it renders outside
the registry lookup with order 0 to sit ahead of Sharing, and its rows are
inset to the same 8px the Files tabs put their own content at.

The body is fetched for one note at a time, and which note that is guards the
result: the sidebar can be sent to another note while a fetch is still on its
way β€” from a row of the list, or by the list navigating β€” and the answer to the
first request must then neither be reported for the second note nor keep its
own fetch from starting.

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 sidebar was opened for one note and then stayed on it. Picking another
note in the list navigated the app but left the sidebar showing the previous
note's sharing, versions and details, which reads as stale data rather than as
a sidebar that belongs to a different note.

It now watches the note id in the route and re-opens itself for whatever the
list navigates to, keeping the tab that is in view β€” the way the Files sidebar
follows the selected file. Note selection is read from the route rather than
from the store: setSelectedNote() is never called anywhere, so
store.notes.selectedNote is always null.

Reusing onSidebarOpen() means the switch goes through the same path as opening
the sidebar in the first place, so the DAV node, the parent folder and the note
body are all reloaded for the new note rather than half-updated.

Assisted-by: Claude Code:claude-opus-5[1m]
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3. to review AI assisted This PR contains AI-assisted commits enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants