From 86d4bbab98a45053b4c89f6eee139004364c5ed6 Mon Sep 17 00:00:00 2001 From: lrnselfreliance <33263708+lrnselfreliance@users.noreply.github.com> Date: Sat, 26 Sep 2026 13:11:24 -0600 Subject: [PATCH] Bookmarks: a nav menu of user links, stored in bookmarks.yaml (#729) * bookmarks: user bookmarks in the navigation bar, stored in bookmarks.yaml A "Bookmarks" tab between Calculators and Flasher opens on hover or click and lists the user's bookmarks, with directories as flyout submenus (Menu.Sub). The mobile hamburger shows directories as labelled groups instead. The tab always shows, with an "Edit bookmarks" item, so the feature can be found before any bookmark exists. Bookmarks live in a new config-only bookmarks.yaml: a nested tree where a node with `children` is a directory and a node with `url` is a bookmark. Every node carries an integer id so the API can rename, move and delete without relying on names. A URL may be a path on this WROLPi, a port on this host (`:8096/` or `http://:8096/`, resolved against whatever hostname the browser used, so it works over LAN IP, mDNS and the hotspot alike), or an absolute URL. Each bookmark chooses whether to open in a new tab. The editor at /more/bookmarks follows the playlist items table: numbered rows, up/down/edit/delete, and inline "Add a bookmark" / "Add a directory" forms with a Location picker. Deleting a directory asks first, since its contents go with it. The menu and the editor are in the component gallery. Co-Authored-By: Claude Fable 5.1 * bookmarks: mark More current when the editor is folded inside it; rem indents in the mobile menu Found on the QA Pi at a width where Bookmarks overflows into More: the More button carried no mark on /more/bookmarks, because the bookmarks link has no `to` for isLinkActive to match. isLinkActive now knows the bookmarks link owns its editor page, and the Bookmarks tab uses the same rule. The mobile menu's directory labels sat a few pixels left of the labels around them: the indent was in em, and a Menu.Label is set smaller than a Menu.Item. Co-Authored-By: Claude Fable 5.1 * bookmarks: parse URLs instead of matching a prefix; validate the whole tree on every write The URL contract was checked by prefix on both sides, and a prefix is not a form: `//evil.com` starts like a path and `:8096@evil.com` starts like a port, and a browser takes both off this host. A path is now a single leading slash that resolves to this origin, a port form is an optional scheme, `:`, digits and then only a path, query or fragment (no `@`, no backslash, no control characters), and anything else must parse as http(s). The same rules live in wrolpi/bookmarks.py and BookmarksContext.js, with the same bypass strings in both test suites. The config's dataclass validator only saw that `bookmarks` was a list, so a hand-edited file or the generic config API could store a `javascript:` URL that the bookmarks API would refuse, and the nav rendered it. The config now validates the tree itself on every write and import: URL forms, exactly one of url/children, unique integer ids, boolean new_tab, and no cycles (a YAML anchor can build one). An invalid file is not imported and, since it was never successfully imported, is never overwritten. On the page, a stored URL that does not resolve is rendered as text, never as an href. A refused add keeps what was typed instead of clearing the form, and an edit whose move is refused still refreshes the nav (the rename was committed) and keeps the modal open. An IPv6 host is bracketed before a port. Co-Authored-By: Claude Fable 5.1 * help: document Bookmarks Co-Authored-By: Claude Fable 5.1 --------- Co-authored-by: Claude Fable 5.1 --- app/src/App.js | 17 +- app/src/api.js | 43 +++ app/src/components/Apps.js | 2 + app/src/components/Bookmarks.js | 453 ++++++++++++++++++++++++++ app/src/components/Bookmarks.test.js | 199 +++++++++++ app/src/components/Nav.js | 60 +++- app/src/components/Nav.test.js | 88 +++++ app/src/components/ThemeSamplePage.js | 49 +++ app/src/contexts/BookmarksContext.js | 114 +++++++ docker/help/wrolpi-help | 2 +- wrolpi/bookmarks.py | 278 ++++++++++++++++ wrolpi/bookmarks_api.py | 86 +++++ wrolpi/common.py | 14 + wrolpi/contexts.py | 7 + wrolpi/root_api.py | 2 + wrolpi/test/test_bookmarks.py | 226 +++++++++++++ 16 files changed, 1626 insertions(+), 14 deletions(-) create mode 100644 app/src/components/Bookmarks.js create mode 100644 app/src/components/Bookmarks.test.js create mode 100644 app/src/contexts/BookmarksContext.js create mode 100644 wrolpi/bookmarks.py create mode 100644 wrolpi/bookmarks_api.py create mode 100644 wrolpi/test/test_bookmarks.py diff --git a/app/src/App.js b/app/src/App.js index 1e372a53..25881b13 100644 --- a/app/src/App.js +++ b/app/src/App.js @@ -17,6 +17,7 @@ import {ArchiveRoute} from "./components/Archive"; import {FilesRoute} from "./components/Files"; import {QueryProvider, StatusProvider} from "./hooks/customHooks"; import {FileWorkerStatusProvider} from "./contexts/FileWorkerStatusContext"; +import {BookmarksProvider} from "./contexts/BookmarksContext"; import {ChannelEditPage, ChannelNewPage, ChannelsPage} from "./components/Channels"; import {MapRoute} from "./components/Map"; import {MediaContextProvider, mediaStyles, StatusContext} from "./contexts/contexts"; @@ -69,13 +70,15 @@ function Root() { -
- -
- - - -