From ab5c9171387a5003e5049ea6f51b818fc74a89a6 Mon Sep 17 00:00:00 2001 From: InstaZDLL Date: Wed, 21 Oct 2026 19:56:09 +0200 Subject: [PATCH] fix: give the folder picker its padding, and the tag editor a way back The picker and the folder cards named --space-5, a token that does not exist, so the dialog had no padding and its rows touched its edges. They now use the tokens that exist, with more room per row. Filing a track no longer indents the root's folders under Unfiled, which is a choice beside them, not their parent. The tag editor gets a Back arrow above its header, back to wherever it was opened from, or to the songs list when there is nowhere to go back to; it shows while the track loads too. Signed-off-by: InstaZDLL --- webapp/e2e/studio-nocturne.spec.ts | 27 +++++++++++++++++ webapp/src/components/folder-picker.tsx | 9 +++++- webapp/src/components/icons.tsx | 10 ++++++- webapp/src/components/track-editor.tsx | 39 +++++++++++++++++++++++-- webapp/src/components/track-menu.tsx | 1 + webapp/src/state/i18n.tsx | 2 ++ webapp/src/styles/styles.css | 38 +++++++++++++++++++----- 7 files changed, 115 insertions(+), 11 deletions(-) diff --git a/webapp/e2e/studio-nocturne.spec.ts b/webapp/e2e/studio-nocturne.spec.ts index 4ff0b4b..2ef799c 100644 --- a/webapp/e2e/studio-nocturne.spec.ts +++ b/webapp/e2e/studio-nocturne.spec.ts @@ -5357,3 +5357,30 @@ test("takes no file while the folder cannot be checked", async ({ page }) => { await expect(page.getByText("Received files go into the folder Rock.")).toBeVisible(); await expect(page.getByLabel("Choose audio files")).toBeEnabled(); }); + +/** + * The tag editor is reached from somewhere — a song row, the track menu — and + * its arrow goes back there. Opened from a typed address, with nowhere to go + * back to, it lands on the songs list rather than off the site. + */ +test("goes back from the tag editor to where it was opened", async ({ + page, +}) => { + // Opened from an album, the arrow goes back to the album, not to a list + // it would also have landed on had it ignored where the visitor came from. + await page.route("**/api/v2/albums/album-2", (route) => + route.fulfill({ json: { ...albumDetail, songs: [correctable.song] } }), + ); + await page.goto("/albums/album-2"); + await page.getByRole("link", { name: "Correct tags: Army Of Me" }).click(); + await expect(page).toHaveURL(/\/tracks\/song-9\/edit$/); + await page.getByRole("button", { name: "Back", exact: true }).click(); + await expect(page).toHaveURL(/\/albums\/album-2$/); + + // A full load starts the router's history afresh: with nowhere to go back + // to, the arrow lands on the songs list, not on the album the browser + // still holds behind this page. + await page.goto("/tracks/song-9/edit"); + await page.getByRole("button", { name: "Back", exact: true }).click(); + await expect(page).toHaveURL(/\/songs$/); +}); diff --git a/webapp/src/components/folder-picker.tsx b/webapp/src/components/folder-picker.tsx index 2938977..e8a757c 100644 --- a/webapp/src/components/folder-picker.tsx +++ b/webapp/src/components/folder-picker.tsx @@ -23,6 +23,7 @@ export function FolderPicker({ rootLabel, current, exclude, + nested = true, onPick, onClose, }: { @@ -35,6 +36,12 @@ export function FolderPicker({ */ current?: string | null; exclude?: string; + /** + * Whether the folders sit beneath the root entry. True for moving a folder, + * where "Top level" holds them; false for filing a track, where "Unfiled" + * is one more choice beside them, not their parent. + */ + nested?: boolean; /** Rejects to keep the dialog open and say the move failed. */ onPick: (folderId: string | null) => Promise; onClose: () => void; @@ -128,7 +135,7 @@ export function FolderPicker({
    {entry(null, rootLabel, 0)} {outline.map(({ folder, depth }) => - entry(folder.id, folder.name, depth + 1), + entry(folder.id, folder.name, depth + (nested ? 1 : 0)), )}
)} diff --git a/webapp/src/components/icons.tsx b/webapp/src/components/icons.tsx index de576c7..6299e94 100644 --- a/webapp/src/components/icons.tsx +++ b/webapp/src/components/icons.tsx @@ -31,9 +31,17 @@ export type IconName = | "radio" | "info" | "download" - | "folder"; + | "folder" + | "back"; const paths: Record = { + // An arrow pointing left: back to where the visitor came from. + back: ( + <> + + + + ), // A folder with its tab: a place the files are, on disk or filed. folder: ( diff --git a/webapp/src/components/track-editor.tsx b/webapp/src/components/track-editor.tsx index d3ddef8..2383b4f 100644 --- a/webapp/src/components/track-editor.tsx +++ b/webapp/src/components/track-editor.tsx @@ -1,5 +1,5 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { Link } from "@tanstack/react-router"; +import { Link, useNavigate, useRouter } from "@tanstack/react-router"; import { type FormEvent, useState } from "react"; import { @@ -24,6 +24,7 @@ import { } from "../lib/track-corrections"; import { type TranslationKey, useI18n } from "../state/i18n"; import { CanvasPanel } from "./canvas-panel"; +import { Icon } from "./icons"; import { Loading, PageHeader } from "./page-shell"; const LABELS: Record = { @@ -50,10 +51,43 @@ type Loaded = { song: SongCredits; tracked: TrackOverrides }; */ export function TrackEditorPage({ trackId }: { trackId: string }) { const { data: value, error } = useQuery(trackEditQuery(trackId)); - if (!value) return ; + if (!value) { + return ( +
+ + +
+ ); + } return ; } +/** + * Back to wherever the editor was opened from — a song row, an album, the + * track menu. Opened from a typed address there is no "where", and the songs + * list is the nearest place that holds the track. Drawn while the track loads + * and when it fails to, so the way out never waits on the answer. + */ +function BackButton() { + const { t } = useI18n(); + const router = useRouter(); + const navigate = useNavigate(); + return ( + + ); +} + function CorrectionForm({ initial }: { initial: Loaded }) { const { t } = useI18n(); // Read through the cache, so the rest of the client sees the corrected @@ -149,6 +183,7 @@ function CorrectionForm({ initial }: { initial: Loaded }) { return (
+ { await placeTracks( moving.song.library_id, diff --git a/webapp/src/state/i18n.tsx b/webapp/src/state/i18n.tsx index 5578d7d..cee4b17 100644 --- a/webapp/src/state/i18n.tsx +++ b/webapp/src/state/i18n.tsx @@ -442,6 +442,7 @@ const en = { "nav.playing": "Now playing", "nav.upload": "Upload", "nav.folders": "Folders", + "common.back": "Back", "folders.received": "Received", "folders.count": { one: "{count} folder", other: "{count} folders" }, "folders.where": "Folder path", @@ -1135,6 +1136,7 @@ const fr: Record = { "nav.playing": "En écoute", "nav.upload": "Téléverser", "nav.folders": "Dossiers", + "common.back": "Retour", "folders.received": "Reçus", "folders.count": { one: "{count} dossier", other: "{count} dossiers" }, "folders.where": "Chemin du dossier", diff --git a/webapp/src/styles/styles.css b/webapp/src/styles/styles.css index 49de1ed..811689f 100644 --- a/webapp/src/styles/styles.css +++ b/webapp/src/styles/styles.css @@ -2898,7 +2898,7 @@ button.danger { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr)); gap: var(--space-2); - margin: 0 0 var(--space-5); + margin: 0 0 var(--space-6); padding: 0; list-style: none; } @@ -2979,12 +2979,14 @@ button.danger { margin: 0 0 var(--space-3); } +/* The spacing tokens go 4 then 6: there is no --space-5, and a padding naming + it is no padding at all — which is how this panel first shipped. */ .folder-picker[open] { display: grid; - gap: var(--space-4); - width: min(28rem, calc(100vw - 2rem)); - max-height: min(36rem, calc(100dvh - 2rem)); - padding: var(--space-5); + gap: var(--space-6); + width: min(34rem, calc(100vw - 2rem)); + max-height: min(40rem, calc(100dvh - 2rem)); + padding: var(--space-6); overflow-y: auto; border: 1px solid var(--line-strong); border-radius: 1rem; @@ -3012,7 +3014,7 @@ button.danger { .folder-pick-list { display: grid; - gap: var(--space-1); + gap: var(--space-2); margin: 0; padding: 0; list-style: none; @@ -3021,8 +3023,11 @@ button.danger { .folder-pick { display: flex; align-items: center; - gap: var(--space-2); + gap: var(--space-3); width: 100%; + min-height: 3rem; + padding-block: var(--space-3); + padding-inline-end: var(--space-4); text-align: start; } @@ -3035,3 +3040,22 @@ button.danger { .upload-into { margin: 0 0 var(--space-3); } + +/* The way back from a page reached from somewhere else, above its header. */ +.page-back { + display: inline-flex; + align-items: center; + gap: var(--space-2); + min-height: 2.25rem; + margin: 0 0 var(--space-3); + padding: var(--space-1) var(--space-2); + border: 0; + border-radius: 0.5rem; + background: transparent; + color: var(--muted); +} + +.page-back:hover { + background: var(--panel-soft); + color: var(--text); +}