project-bifrost-platform/src/admin/components/ListCell.astro
Jonathan Hvid dd9ea68fab feat(admin): publishing-group resources (dispatches, roadmap, events)
The Backstage rebuild's first three resource configs. /admin/dispatches,
/admin/roadmap, and /admin/events now resolve through the dynamic route
with full list views, edit panels, and the publish/archive actions.

- src/admin/resources/dispatches.ts — kind/status/author/excerpt/body
  fields, embedded pulse sub-form (pulse_question + multi-text options +
  opens/closes datetimes), publish/archive actions, notifyCount on
  drafts so the sidebar lights up terracotta until they ship.
- src/admin/resources/roadmap.ts — title/description/status/target/
  display_order/metadata_text plus a multi-select-async for attributed
  members. ops.update writes via setRoadmapAttributions() after the
  basic save so the pivot table stays in sync.
- src/admin/resources/events.ts — full event fields; ops.create
  auto-generates a unique slug from the title when blank.
- src/admin/embeds/PulseSubForm.astro — reads the dispatch's current
  pulse via getPulseById(), renders question + options + opens/closes.
  Pulses follow their parent dispatch's lifecycle (draft → open on
  publish, → closed on archive); no status field of their own.
- src/admin/components/ResourceEditPanel.astro — dispatches on
  embed.component, renders PulseSubForm for 'pulse-sub-form'.
- src/admin/resource-types.ts — renamed column .valueOf to .value
  (collision with Object.prototype.valueOf was breaking TS structural
  matching); OpContext now optionally carries the raw FormData so
  resources with sub-forms can read embed fields.
- src/pages/admin/[resource].astro — passes formData into opCtx.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:24:13 +02:00

112 lines
4 KiB
Text

---
/* ---------------------------------------------------------------------------
* Internal cell renderer for ResourceListView.
*
* One Column kind per branch. Branches must stay exhaustive over Column<T>
* — if a new kind is added to resource-types.ts, TypeScript will warn here
* via the `never` fallback.
* ------------------------------------------------------------------------- */
import { relativeTime } from '../../lib/format';
import type { Column } from '../resource-types';
interface Props {
column: Column<Record<string, unknown>>;
item: Record<string, unknown>;
}
const { column, item } = Astro.props;
const kind = column.kind ?? 'text';
// Text column — default rendering ─────────────────────────────────────────
let textTitle: string | null = null;
let textSubtitle: string | null = null;
if (kind === 'text') {
const col = column as Extract<typeof column, { kind?: 'text' }>;
if (col.render) {
const r = col.render(item);
textTitle = r.title;
textSubtitle = r.subtitle ?? null;
} else {
const v = item[column.key];
textTitle = v == null ? '' : String(v);
}
}
// Pill column ─────────────────────────────────────────────────────────────
let pillLabel: string | null = null;
let pillClass: string | null = null;
if (kind === 'pill') {
const col = column as Extract<typeof column, { kind: 'pill' }>;
const raw = col.value ? col.value(item) : (item[column.key] as string | undefined);
if (raw) {
const variant = col.pillVariants[raw];
if (variant) {
pillLabel = variant.label;
pillClass = variant.class;
} else {
pillLabel = raw;
pillClass = 'pill-draft'; // graceful fallback
}
}
}
// Relative-date column ────────────────────────────────────────────────────
let relText: string | null = null;
let relEmpty: string | null = null;
if (kind === 'relative-date') {
const col = column as Extract<typeof column, { kind: 'relative-date' }>;
const raw = col.value ? col.value(item) : (item[column.key] as string | null | undefined);
if (raw) {
relText = relativeTime(raw);
} else {
relEmpty = col.emptyFallback ?? '—';
}
}
// Number column ───────────────────────────────────────────────────────────
let numberText: string | null = null;
if (kind === 'number') {
const col = column as Extract<typeof column, { kind: 'number' }>;
const raw = col.value ? col.value(item) : (item[column.key] as number | null | undefined);
numberText = raw == null ? '—' : String(raw);
}
// Tag-list column ─────────────────────────────────────────────────────────
let tags: string[] = [];
if (kind === 'tag-list') {
const col = column as Extract<typeof column, { kind: 'tag-list' }>;
tags = col.value(item);
}
---
{kind === 'text' && (
<div class="bs-cell-text">
<span class:list={['bs-cell-title', { primary: column.primary }]}>{textTitle}</span>
{textSubtitle && <span class="bs-cell-subtitle">{textSubtitle}</span>}
</div>
)}
{kind === 'pill' && (
pillLabel
? <span class:list={['bs-pill', pillClass]}>{pillLabel}</span>
: <span class="bs-cell-empty">—</span>
)}
{kind === 'relative-date' && (
relText
? <span class="bs-cell-rel">{relText}</span>
: <span class="bs-cell-empty">{relEmpty}</span>
)}
{kind === 'number' && (
<span class="bs-cell-number">{numberText}</span>
)}
{kind === 'tag-list' && (
tags.length > 0
? <ul class="bs-cell-tags">
{tags.map(t => <li class="bs-tag">{t}</li>)}
</ul>
: <span class="bs-cell-empty">—</span>
)}