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
22 changes: 11 additions & 11 deletions docs/architecture/collection-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ change forward or stops at the conflict.

## Smart rules and the live preview

A Smart collection's Contents is the Home rows rule sentence (`RuleBuilder`), with the libraries
A Smart collection's Contents is Add row's rule sentence (`RuleBuilder`), with the libraries
inside it: a server collection needs at least one, and a personal one with none matches every
library the profile can see. Rules about the viewer (Watched and the like) are offered only on a
personal collection. A rule the builder can't show stays as a locked line and is saved unchanged
Expand Down Expand Up @@ -190,9 +190,9 @@ personal collection searches every library the profile can see. The search is a
not the access check: the personal item route refuses a title the acting profile can't see, and a
server collection's members are filtered per viewer when they are read.

## Links into Home rows
## Links into the row pages

Both Home rows pages (`/admin/home-rows` and `/settings/home-screen`) take link parameters next to
Both row pages (admin Sections at `/admin/sections`, and `/settings/home-screen`) take link parameters next to
`?page=`, read by `web/src/lib/homeRows/rowLinks.ts` and followed by
`web/src/components/homeRows/useRowLinks.ts`. Each page reads them once and drops them from the
address, so a reload doesn't repeat them.
Expand All @@ -210,9 +210,9 @@ address, so a reload doesn't repeat them.
- `?edit=<rowId>` opens that row in Edit row; a row that is gone gets a toast.
- `?return=<path>` makes the dialog's back link "Back to *collection*" and, after Add row, goes to
that path with a toast that offers to move the new row (`?edit=` on the same page). Both replace
the Home rows history entry. Settings > Home Screen queues its saves, so there it goes back only
the row page's history entry. Settings > Home Screen queues its saves, so there it goes back only
once the save lands and the row is on the page, even when that takes a later read because the
one after the save failed; a failed save keeps the viewer on Home rows with the save's error
one after the save failed; a failed save keeps the viewer on Home Screen with the save's error
toast. The toast's position counts the page's rows when the add lands, not when the dialog
opened. A link that arrives while the dialog is open starts a fresh dialog on its target. After Add row the navigation state carries `addedRow` (the
`AddedRowState` type: new row id, surface, page and position) so
Expand All @@ -230,19 +230,19 @@ its "On Home and the _Kids_ page" (turned-off rows left out). Rows profiles add
are not listed, because the route reads the administrator page layouts only. When the collections
capabilities don't report `section_references`, the list is left out and Add as a row stays.

- **Add as a row** goes to admin Home rows with `?page=…&add=collection:library:<id>` and, from the
editor, `?return=` set to the editor's own URL (see Links into Home rows). The List's Add to Home
- **Add as a row** goes to admin Sections with `?page=…&add=collection:library:<id>` and, from the
editor, `?return=` set to the editor's own URL (see Links into the row pages). The List's Add to Home
and Add to a library page send no `return`, because the List's address is not a collection page
the allowlist accepts; Home rows keeps its own after-add step. With unsaved changes the editor
the allowlist accepts; Sections keeps its own after-add step. With unsaved changes the editor
asks first: Save and continue saves and goes only once nothing is left unsaved (artwork that
failed to upload stays in the editor with its error), Discard changes drops the draft (titles
stay), Cancel stays. Discard changes is off when the collection is saved as hidden, since only
saving would show it. Save and continue is off whenever Save is, including while a field
changed in both places waits for Keep mine or Use theirs. A collection not created yet, or one hidden from its Collections tab,
can't be added, because a row couldn't open its See all; the button says why.
- **Back from Home rows**, the `addedRow` state names the new row, which is highlighted once the
rows list includes it. The editor sends Home rows the List view it was opened from as history
state, and Home rows hands that state back with `addedRow`, so the editor's Back still reaches
- **Back from Sections**, the `addedRow` state names the new row, which is highlighted once the
rows list includes it. The editor sends Sections the List view it was opened from as history
state, and Sections hands that state back with `addedRow`, so the editor's Back still reaches
that view.
- **Hiding** a collection rows show asks first, naming the pages the rows are on, as the List does
with its `row_count`. While the rows list hasn't loaded (or failed), the List's `row_count` for
Expand Down
18 changes: 9 additions & 9 deletions web/src/App.adminRoutes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ vi.mock("@/hooks/useAuth", async () => {
vi.mock("@/hooks/useIsActingAdmin", () => ({ useIsActingAdmin: () => true }));

vi.mock("@/components/AdminLayout", () => ({ default: () => <Outlet /> }));
vi.mock("@/pages/AdminHomeRows", () => ({ default: () => <div>Home rows page</div> }));
vi.mock("@/pages/AdminHomeRows", () => ({ default: () => <div>Sections page</div> }));

import App from "@/App";

Expand All @@ -51,23 +51,23 @@ describe("App admin Home rows routes", () => {
appRouter = null;
});

it("serves the Home rows page at /admin/home-rows", async () => {
initialEntry = "/admin/home-rows";
it("serves the Sections page at /admin/sections", async () => {
initialEntry = "/admin/sections";

render(<App />);

expect(await screen.findByText("Home rows page")).toBeInTheDocument();
expect(appRouter?.state.location.pathname).toBe("/admin/home-rows");
expect(await screen.findByText("Sections page")).toBeInTheDocument();
expect(appRouter?.state.location.pathname).toBe("/admin/sections");
});

it("sends the old /admin/sections links to Home rows, keeping the query", async () => {
initialEntry = "/admin/sections?page=page-2#rows";
it("sends /admin/home-rows links to Sections, keeping the query", async () => {
initialEntry = "/admin/home-rows?page=page-2#rows";

render(<App />);

expect(await screen.findByText("Home rows page")).toBeInTheDocument();
expect(await screen.findByText("Sections page")).toBeInTheDocument();
expect(appRouter?.state.location).toMatchObject({
pathname: "/admin/home-rows",
pathname: "/admin/sections",
search: "?page=page-2",
hash: "#rows",
});
Expand Down
6 changes: 3 additions & 3 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import {
buildUserCollectionCatalogHref,
} from "@/pages/catalogSearchParams";
import { buildLegacyAutoscanRedirectTarget } from "@/pages/autoscanSearchParams";
import LegacyAdminSectionsRedirect from "@/pages/LegacyAdminSectionsRedirect";
import LegacyAdminHomeRowsRedirect from "@/pages/LegacyAdminHomeRowsRedirect";
import LegacyRequestDetailRedirect from "@/pages/LegacyRequestDetailRedirect";
import { buildLegacyWebhookSyncRedirectTarget } from "@/lib/webhookSync";
import { guardRedirectTarget } from "@/lib/authRedirect";
Expand Down Expand Up @@ -540,8 +540,8 @@ function AppRoutes() {
<Route path="devices" element={<AdminDevices />} />
<Route path="devices/:userId/:deviceId" element={<AdminDevices />} />
<Route path="nodes" element={<AdminNodes />} />
<Route path="home-rows" element={<AdminHomeRows />} />
<Route path="sections" element={<LegacyAdminSectionsRedirect />} />
<Route path="sections" element={<AdminHomeRows />} />
<Route path="home-rows" element={<LegacyAdminHomeRowsRedirect />} />
<Route path="plugins" element={<AdminPlugins />} />
<Route path="plugins/:pluginId" element={<AdminPluginDetail />} />
<Route path="settings/*" element={<AdminSettingsLayout />} />
Expand Down
10 changes: 5 additions & 5 deletions web/src/components/AdminLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function renderAdmin(initialPath = "/admin") {
children: [
{ index: true, element: <h1>Admin dashboard</h1> },
{ path: "users", element: <h1>Admin users</h1> },
{ path: "home-rows", element: <h1>Admin home rows</h1> },
{ path: "sections", element: <h1>Admin sections</h1> },
],
},
],
Expand Down Expand Up @@ -122,11 +122,11 @@ describe("AdminLayout page title", () => {
// The tab title, the route-change announcement and the phone header all read
// this name, so a renamed route must not fall back to the generic "Admin"
// (which the phone header shows as "Dashboard").
it("names the Home rows page in the tab and the phone header", () => {
renderAdmin("/admin/home-rows");
it("names the Sections page in the tab and the phone header", () => {
renderAdmin("/admin/sections");

expect(document.title).toBe("Admin Home rows · Silo");
expect(screen.getByText("Home rows")).toBeInTheDocument();
expect(document.title).toBe("Admin Sections · Silo");
expect(screen.getByText("Sections")).toBeInTheDocument();
expect(screen.queryByText("Dashboard")).not.toBeInTheDocument();
});
});
Expand Down
8 changes: 4 additions & 4 deletions web/src/components/AdminSidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ describe("AdminSidebar", () => {
expect(markup).not.toContain("/admin/settings?tab=");
});

it("includes a Home rows link in the content navigation", () => {
it("includes a Sections link in the content navigation", () => {
const markup = renderSidebar();

expect(markup).toContain('href="/admin/home-rows"');
expect(markup).toContain(">Home rows<");
expect(markup).not.toContain('href="/admin/sections"');
expect(markup).toContain('href="/admin/sections"');
expect(markup).toContain(">Sections<");
expect(markup).not.toContain('href="/admin/home-rows"');
});

it("hides Policy navigation when the editor capability is unavailable", () => {
Expand Down
4 changes: 2 additions & 2 deletions web/src/components/collections/StarterPacksDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,11 @@ describe("Starter packs", () => {
expect(screen.getByRole("button", { name: "Add 4 collections" })).toBeEnabled();
});

it("says where new lists land and that no Home rows are added", async () => {
it("says where new lists land and that no sections are added", async () => {
renderDialog();
await checked();
expect(screen.getByText(/New lists land under/)).toHaveTextContent(
"New lists land under No heading on each library's Collections tab, ready to move into a shelf in Arrange. “Already there” lists aren't touched. No Home rows are added unless you turn on hero banners.",
"New lists land under No heading on each library's Collections tab, ready to move into a shelf in Arrange. “Already there” lists aren't touched. No sections are added unless you turn on hero banners.",
);
});

Expand Down
2 changes: 1 addition & 1 deletion web/src/components/collections/StarterPacksDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ function WhatWillHappen({
aren't touched.{" "}
{heroesOn
? "Hero banners are added to the pages you picked below."
: "No Home rows are added unless you turn on hero banners."}
: "No sections are added unless you turn on hero banners."}
</span>
</p>
</div>
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/homeRows/HomeRowsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ describe("HomeRowsPage", () => {
it("offers Reload rows when the rows changed elsewhere", async () => {
render(<FakePage initialRows={[makeRow("a")]} conflict={{ scope: "page" }} />);
const banner = screen.getByRole("alert");
expect(banner).toHaveTextContent("Home rows changed since you opened this page.");
expect(banner).toHaveTextContent("These rows changed since you opened this page.");
await userEvent.click(within(banner).getByRole("button", { name: "Reload rows" }));
expect(harness.reload).toHaveBeenCalled();
});
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/homeRows/RestoreDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ describe("RestoreDialog", () => {

rerender(<Harness onConfirm={onConfirm} onReload={onReload} conflict />);
expect(screen.getByRole("alert")).toHaveTextContent(
"Home rows changed since you opened this. Reload to see the current rows, then try again.",
"These rows changed since you opened this. Reload to see the current rows, then try again.",
);
expect(screen.queryByRole("button", { name: "Restore defaults" })).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Reload rows" }));
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/homeRows/RestoreDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export function RestoreDialog({
>
<CircleAlert aria-hidden className="text-warning size-[18px] shrink-0" />
<p className="min-w-0 flex-1">
Home rows changed since you opened this. Reload to see the current rows, then try
These rows changed since you opened this. Reload to see the current rows, then try
again.
</p>
<Button size="sm" variant="outline" disabled={busy} onClick={onReload}>
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/homeRows/notes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function ConflictBanner({ onReload, busy }: { onReload: () => void; busy?
>
<CircleAlert aria-hidden className="text-warning size-[18px] shrink-0" />
<p className="min-w-0 flex-1">
Home rows changed since you opened this page. Reload to see the current rows, then try
These rows changed since you opened this page. Reload to see the current rows, then try
again.
</p>
<Button size="sm" variant="outline" disabled={busy} onClick={onReload}>
Expand Down
22 changes: 11 additions & 11 deletions web/src/hooks/queries/homeRows/useAdminHomeRows.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ afterEach(() => {
vi.unstubAllGlobals();
});

function setup(initialEntry = "/admin/home-rows") {
function setup(initialEntry = "/admin/sections") {
const client = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
Expand Down Expand Up @@ -193,19 +193,19 @@ describe("useAdminHomeRows", () => {
});

it("reads the page from ?page= and falls back to Home for an unknown page", async () => {
const library = setup("/admin/home-rows?page=7");
const library = setup("/admin/sections?page=7");
await ready(library.result);
expect(library.result.current.adapter.page).toEqual({ kind: "library", libraryId: 7 });
expect(library.result.current.adapter.rows.map((entry) => entry.id)).toEqual(["lib"]);
library.unmount();

const unknown = setup("/admin/home-rows?page=99");
const unknown = setup("/admin/sections?page=99");
await ready(unknown.result);
expect(unknown.result.current.adapter.page).toEqual({ kind: "home" });
});

it("writes the page to ?page= when the admin switches pages", async () => {
const { result } = setup("/admin/home-rows?keep=1");
const { result } = setup("/admin/sections?keep=1");
await ready(result);
act(() => result.current.adapter.setPage({ kind: "library", libraryId: 7 }));
await waitFor(() => expect(result.current.params.get("page")).toBe("7"));
Expand Down Expand Up @@ -547,7 +547,7 @@ describe("useAdminHomeRows", () => {
error: new Error("Libraries unavailable"),
refetch,
});
const { result } = setup("/admin/home-rows?page=7");
const { result } = setup("/admin/sections?page=7");
await waitFor(() => expect(result.current.adapter.status).toBe("error"));
expect(result.current.adapter.error).toBe("Libraries unavailable");
expect(result.current.adapter.canEdit).toBe(false);
Expand Down Expand Up @@ -581,13 +581,13 @@ describe("useAdminHomeRows", () => {
expect(home.result.current.adapter.capabilities.libraryCopies).toBe(false);
home.unmount();

const library = setup("/admin/home-rows?page=7");
const library = setup("/admin/sections?page=7");
await ready(library.result);
expect(library.result.current.adapter.capabilities.libraryCopies).toBe(true);
});

it("adds a new row to this page and the others in one request, and finds it here", async () => {
const { result } = setup("/admin/home-rows?page=7");
const { result } = setup("/admin/sections?page=7");
await ready(result);
let created!: { newIds: string[] };
await act(async () => {
Expand Down Expand Up @@ -617,7 +617,7 @@ describe("useAdminHomeRows", () => {
});

it("adds a hero row to this page only", async () => {
const { result } = setup("/admin/home-rows?page=7");
const { result } = setup("/admin/sections?page=7");
await ready(result);
mocks.request.mockImplementationOnce(async (operation: string, args: Args) => {
calls.push({ operation, args });
Expand All @@ -643,7 +643,7 @@ describe("useAdminHomeRows", () => {
config: { window: "7d" },
}),
];
const { result } = setup("/admin/home-rows?page=7");
const { result } = setup("/admin/sections?page=7");
await ready(result);
await act(async () => {
await result.current.adapter.copyToLibraries("lib", [7, 8, 9]);
Expand All @@ -664,7 +664,7 @@ describe("useAdminHomeRows", () => {
});

it("refuses to copy a row that changed since the page loaded", async () => {
const { result } = setup("/admin/home-rows?page=7");
const { result } = setup("/admin/sections?page=7");
await ready(result);
libraryRows = libraryRows.map((entry) => ({ ...entry, title: "Renamed elsewhere" }));
await act(async () => {
Expand All @@ -684,7 +684,7 @@ describe("useAdminHomeRows", () => {
config: { filter_library_ids: [7] },
}),
];
const { result } = setup("/admin/home-rows?page=7");
const { result } = setup("/admin/sections?page=7");
await ready(result);
await act(async () => {
await expect(result.current.adapter.copyToLibraries("lib", [8])).rejects.toThrow(
Expand Down
4 changes: 2 additions & 2 deletions web/src/lib/adminNavigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ describe("admin command palette", () => {
},
);

it.each(["home rows", "sections", "rows", "rails"])("finds Home rows for %s", (query) => {
expect(hrefsFor(query)).toContain("/admin/home-rows");
it.each(["home rows", "sections", "rows", "rails"])("finds Sections for %s", (query) => {
expect(hrefsFor(query)).toContain("/admin/sections");
});

it("still finds the request queue", () => {
Expand Down
6 changes: 3 additions & 3 deletions web/src/lib/adminNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@ export const ADMIN_NAV_SECTIONS: AdminNavGroup[] = [
href: "/admin/collections",
},
{
label: "Home rows",
label: "Sections",
description: "Rows on Home and on library pages.",
keywords: ["sections", "rows", "rails", "featured sections"],
keywords: ["home rows", "rows", "rails", "featured sections"],
icon: Rows2,
href: "/admin/home-rows",
href: "/admin/sections",
},
{
label: "Requests",
Expand Down
10 changes: 4 additions & 6 deletions web/src/lib/collections/copy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,24 @@ import { unshareConsequence, unshareWarning } from "./copy";

describe("unshareConsequence", () => {
it("agrees with one other profile", () => {
expect(unshareConsequence(["Leo"])).toBe(
"Leo loses it, including Home rows they made from it.",
);
expect(unshareConsequence(["Leo"])).toBe("Leo loses it, including rows they made from it.");
});

it("agrees with several other profiles", () => {
expect(unshareConsequence(["Maya", "Leo"])).toBe(
"Maya and Leo lose it, including Home rows they made from it.",
"Maya and Leo lose it, including rows they made from it.",
);
});

it("falls back to other profiles when it has no names", () => {
expect(unshareConsequence([])).toBe(
"Other profiles lose it, including Home rows they made from it.",
"Other profiles lose it, including rows they made from it.",
);
});

it("carries the agreement into the editor's save warning", () => {
expect(unshareWarning(["Leo"])).toBe(
"When you save, Leo loses it, including Home rows they made from it.",
"When you save, Leo loses it, including rows they made from it.",
);
});
});
Loading
Loading