Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions webapp/e2e/studio-nocturne.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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$/);
});
9 changes: 8 additions & 1 deletion webapp/src/components/folder-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function FolderPicker({
rootLabel,
current,
exclude,
nested = true,
onPick,
onClose,
}: {
Expand All @@ -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<void>;
onClose: () => void;
Expand Down Expand Up @@ -128,7 +135,7 @@ export function FolderPicker({
<ul className="folder-pick-list" aria-label={t("filed.destinations")}>
{entry(null, rootLabel, 0)}
{outline.map(({ folder, depth }) =>
entry(folder.id, folder.name, depth + 1),
entry(folder.id, folder.name, depth + (nested ? 1 : 0)),
)}
</ul>
)}
Expand Down
10 changes: 9 additions & 1 deletion webapp/src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,17 @@ export type IconName =
| "radio"
| "info"
| "download"
| "folder";
| "folder"
| "back";

const paths: Record<IconName, ReactNode> = {
// An arrow pointing left: back to where the visitor came from.
back: (
<>
<path d="M19 12H5" />
<path d="m11 18-6-6 6-6" />
</>
),
// A folder with its tab: a place the files are, on disk or filed.
folder: (
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
Expand Down
39 changes: 37 additions & 2 deletions webapp/src/components/track-editor.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<CorrectionField, TranslationKey> = {
Expand All @@ -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 <Loading error={error} />;
if (!value) {
return (
<section className="correction-page">
<BackButton />
<Loading error={error} />
</section>
);
}
return <CorrectionForm key={trackId} initial={value} />;
}

/**
* 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 (
<button
type="button"
className="page-back"
onClick={() =>
router.history.canGoBack()
? router.history.back()
: void navigate({ to: "/songs" })
}
>
<Icon name="back" size={18} />
{t("common.back")}
</button>
);
}

function CorrectionForm({ initial }: { initial: Loaded }) {
const { t } = useI18n();
// Read through the cache, so the rest of the client sees the corrected
Expand Down Expand Up @@ -149,6 +183,7 @@ function CorrectionForm({ initial }: { initial: Loaded }) {

return (
<section className="correction-page">
<BackButton />
<PageHeader
kicker={t("nav.group.library")}
title={t("correction.edit")}
Expand Down
1 change: 1 addition & 0 deletions webapp/src/components/track-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export function useTrackMenu() {
libraryId={moving.song.library_id}
title={t("filed.moveTrack", { title: moving.song.title })}
rootLabel={t("filed.unfiled")}
nested={false}
onPick={async (folderId) => {
await placeTracks(
moving.song.library_id,
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/state/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -1135,6 +1136,7 @@ const fr: Record<TranslationKey, TranslationValue> = {
"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",
Expand Down
38 changes: 31 additions & 7 deletions webapp/src/styles/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

Expand All @@ -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);
}
Loading