--- /* --------------------------------------------------------------------------- * ResourceListView — shared list rendering for every Backstage resource. * * Reads URL state (?filter, ?q, ?page) and derives: * - active filter (with isDefault fallback) * - active column set (columnsByFilter override → columns) * - filtered + searched + sorted + paginated row set * * Rows are full anchor elements pointing at ?edit= so the table is * fully keyboard-navigable and works without JS. The panel that consumes * the edit param ships in step 6. * ------------------------------------------------------------------------- */ import ListCell from './ListCell.astro'; import type { Column, Resource, ResourceGroup } from '../resource-types'; interface Props { resource: Resource; groups: ResourceGroup[]; } const { resource, groups } = Astro.props; const url = Astro.url; // ── Resolve state from URL ──────────────────────────────────────────────── const filters = resource.list.filters ?? []; const defaultFilterKey = filters.find((f) => f.isDefault)?.key ?? filters[0]?.key ?? 'all'; const filterKey = url.searchParams.get('filter') ?? defaultFilterKey; const activeFilter = filters.find((f) => f.key === filterKey); const search = (url.searchParams.get('q') ?? '').trim(); const pageParam = Number(url.searchParams.get('page') ?? '1'); const requestedPage = Number.isFinite(pageParam) && pageParam > 0 ? Math.floor(pageParam) : 1; const pageSize = resource.list.pageSize ?? 25; // ── Load + transform ────────────────────────────────────────────────────── const queried = await resource.list.queryFn(); const allItems = (Array.isArray(queried) ? queried : []) as Record[]; const filtered = activeFilter ? allItems.filter((item) => activeFilter.predicate(item)) : allItems; const searched = search && resource.list.search ? filtered.filter((item) => { const q = search.toLowerCase(); return resource.list.search!.fields.some((field) => { const v = item[field as string]; return typeof v === 'string' && v.toLowerCase().includes(q); }); }) : filtered; const sort = resource.list.defaultSort; const sorted = sort ? [...searched].sort((a, b) => { const av = a[sort.key]; const bv = b[sort.key]; if (av === bv) return 0; if (av == null) return 1; if (bv == null) return -1; const cmp = av < bv ? -1 : 1; return sort.direction === 'desc' ? -cmp : cmp; }) : searched; const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize)); const page = Math.min(requestedPage, totalPages); const start = (page - 1) * pageSize; const pageItems = sorted.slice(start, start + pageSize); // ── Resolve column set (columnsByFilter override → columns) ─────────────── const columns: Column>[] = (resource.list.columnsByFilter && resource.list.columnsByFilter[filterKey]) ?? resource.list.columns; const gridTemplate = columns.map((c) => c.width ?? '1fr').join(' '); // ── Group eyebrow ───────────────────────────────────────────────────────── const group = groups.find((g) => g.key === resource.groupKey); // ── Helper: build a query string preserving the other params ────────────── function withParams(overrides: Record): string { const next = new URLSearchParams(url.searchParams); for (const [k, v] of Object.entries(overrides)) { if (v == null) next.delete(k); else next.set(k, String(v)); } const s = next.toString(); return s ? `${url.pathname}?${s}` : url.pathname; } const showNewButton = resource.form !== null && resource.ops.create !== undefined; const hasItems = allItems.length > 0; const hasMatches = pageItems.length > 0; ---
{group &&

{group.label}

}

{resource.pluralLabel}

{resource.description &&

{resource.description}

}
{showNewButton && ( + New {resource.singularLabel.toLowerCase()} )}
{(resource.list.search || filters.length > 0) && (
{resource.list.search && ( )} {filters.length > 0 && (
{filters.map((f) => { const isActive = f.key === filterKey; const href = withParams({ filter: f.key === defaultFilterKey ? null : f.key, page: null }); return ( {f.label} ); })}
)}
)} {hasMatches ? (
{columns.map((col) => (
{col.label}
))}
{pageItems.map((item) => { const id = Number(item.id); return ( {columns.map((col) => (
))}
); })}
) : (

{hasItems ? 'No items match the current filters.' : `No ${resource.pluralLabel.toLowerCase()} yet.`}

)} {totalPages > 1 && ( )}