app/components/admin/media-grid.tsx
131 lines and 5 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| MediaGrid | 14 to 137 | 5 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:73> | 73 to 97 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:84> | 84 to 95 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:102> | 102 to 132 | 6 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:118> | 118 to 129 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
Imports
It imports app/components/admin/media-bulk-bar.tsx, app/components/admin/media-keyboard.tsx, app/components/admin/media-list-header.tsx, app/components/admin/media-tile.tsx, app/lib/media/view.mjs. Imported by app/routes/admin.media._index.tsx.
Source
import { Form } from "react-router";
import { BulkBar } from "~/components/admin/media-bulk-bar";
import { useGridKeyboard } from "~/components/admin/media-keyboard";
import { MediaListHeader } from "~/components/admin/media-list-header";
import { MediaRow, MediaTile } from "~/components/admin/media-tile";
import { groupRows } from "~/lib/media/view.mjs";
import type { hrefWith, sortHref } from "~/lib/media/view.mjs";
import type { Route } from "../../routes/+types/admin.media._index";
type Listing = Extract<Route.ComponentProps["loaderData"], { detail: unknown }>;
export function MediaGrid({
objects,
chosen,
selectRange,
setSelected,
setConfirmingTrash,
linkTo,
view,
pending,
scanComplete,
tagCounts,
tabStop,
setActive,
}: {
objects: Listing["objects"];
chosen: string[];
selectRange: (key: string, shift: boolean) => void;
setSelected: (keys: string[]) => void;
setConfirmingTrash: (value: boolean) => void;
linkTo: (over?: Parameters<typeof hrefWith>[1]) => string;
view: Parameters<typeof sortHref>[0];
pending: boolean;
scanComplete: boolean;
tagCounts: Listing["tagCounts"];
/** The one tile whose controls take Tab in the grid view. */
tabStop: string;
setActive: (key: string) => void;
}) {
const keys = useGridKeyboard({ setActive, setSelected });
const list = view.view === "list";
const buckets = groupRows(objects, view.group);
return (
// Not role="grid": the column count follows the container width, so positions mean nothing to a screen reader.
<Form method="post">
{/* The form wraps the grid so the checkboxes submit with it; nested in the toolbar it would be
a form inside a form, which the browser drops. */}
{/* In the document before anything is selected, so the first count is announced too; the bar
below mounts with its number. */}
<p className="cap-sr-only" role="status">
{chosen.length > 0 ? `${chosen.length} selected` : ""}
</p>
{chosen.length > 0 ? (
<BulkBar
objects={objects}
chosen={chosen}
setSelected={setSelected}
setConfirmingTrash={setConfirmingTrash}
tagCounts={tagCounts}
/>
) : null}
{list ? (
<div className="cap-table-wrap" role="region" tabIndex={0} aria-label="Files">
<table className="cap-table" data-view="list" aria-busy={pending || undefined}>
<thead>
<MediaListHeader view={view} />
</thead>
{/* Grouped page-locally: a group never spans a page, so the heading counts this page. */}
{buckets.map((bucket) => (
<tbody key={bucket.label || "ungrouped"}>
{bucket.label ? (
<tr>
<th scope="colgroup" colSpan={7}>
{bucket.label}{" "}
<span className="cap-muted">{bucket.rows.length} on this page</span>
{bucket.note ? <span className="cap-muted"> {bucket.note}</span> : null}
</th>
</tr>
) : null}
{bucket.rows.map((object) => (
<MediaRow
key={object.key}
object={object}
chosen={chosen}
selectRange={selectRange}
linkTo={linkTo}
view={view}
scanComplete={scanComplete}
tabStop
/>
))}
</tbody>
))}
</table>
</div>
) : (
<div className="cap-media" data-view="grid" data-size={view.size}>
{buckets.map((bucket) => (
<section key={bucket.label || "ungrouped"} className="cap-media-group">
{bucket.label ? (
<h2 className="cap-media-group-title">
{bucket.label}{" "}
<span className="cap-media-group-count">{bucket.rows.length} on this page</span>
{bucket.note ? <span className="cap-media-group-count"> {bucket.note}</span> : null}
</h2>
) : null}
<ul
className="cap-media-grid"
role="list"
aria-label={bucket.label ? `Files, ${bucket.label}` : "Files"}
aria-busy={pending || undefined}
{...keys}
>
{bucket.rows.map((object) => (
<MediaTile
key={object.key}
object={object}
chosen={chosen}
selectRange={selectRange}
linkTo={linkTo}
view={view}
scanComplete={scanComplete}
tabStop={object.key === tabStop}
/>
))}
</ul>
</section>
))}
</div>
)}
</Form>
);
}