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>
112 lines
4 KiB
Text
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>
|
|
)}
|