feat(db): roadmap_items.metadata_text + admin field
Migration 0007 (spec said 0006 but 0006 was already roadmap_considering) adds a single nullable metadata_text column to roadmap_items — a short admin-set narrative cue shown on hover in the route cards. ~60 chars suggested in admin helper text. Hidden in the UI when NULL. db.ts: RoadmapItem type gains the field. createRoadmapItem + updateRoadmapItem accept an optional metadata_text parameter. moveRoadmapItem passes it through when swapping display_order between siblings so the helper preserves it. Admin: /admin?tab=roadmap edit form gets a new 'Hover note' input under the description, with the helper text and a 120-char hard cap. Empty string saves as NULL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f659b70814
commit
d17d9b93a7
4 changed files with 40 additions and 13 deletions
6
migrations/0007_roadmap_metadata.sql
Normal file
6
migrations/0007_roadmap_metadata.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-- Roadmap items gain an optional metadata_text field — a short admin-set
|
||||
-- narrative cue shown in the route card's hover expansion. Free-form,
|
||||
-- ~60 chars suggested in admin helper text. NULL when not set; UI hides
|
||||
-- the line in that case.
|
||||
|
||||
ALTER TABLE roadmap_items ADD COLUMN metadata_text TEXT;
|
||||
|
|
@ -66,6 +66,20 @@ const grouped: Record<Status, RoadmapItemWithAttribution[]> = {
|
|||
<textarea id="description" name="description" class="input body-md" rows="3">{editing?.description ?? ''}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="metadata_text" class="label-sm field-label">Hover note (~60 chars)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="metadata_text"
|
||||
name="metadata_text"
|
||||
class="input body-md"
|
||||
value={editing?.metadata_text ?? ''}
|
||||
placeholder="e.g. Open question on key custody · Council input wanted"
|
||||
maxlength="120"
|
||||
/>
|
||||
<span class="body-sm muted">A short narrative cue shown on hover in /roadmap. Optional.</span>
|
||||
</div>
|
||||
|
||||
<fieldset class="attribution-grid">
|
||||
<legend class="label-sm field-label">Attributed members (who shaped this)</legend>
|
||||
{cabUsers.map(u => (
|
||||
|
|
@ -183,4 +197,6 @@ const grouped: Record<Status, RoadmapItemWithAttribution[]> = {
|
|||
padding: 0;
|
||||
}
|
||||
.action-link:hover { color: var(--on-surface-variant); border-bottom: none; }
|
||||
|
||||
.muted { color: var(--on-surface-muted); }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -675,6 +675,7 @@ export interface RoadmapItem {
|
|||
target: string | null;
|
||||
display_order: number;
|
||||
shipped_at: string | null;
|
||||
metadata_text: string | null; // short narrative cue shown on hover in /roadmap
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
|
@ -689,11 +690,12 @@ export function createRoadmapItem(data: {
|
|||
status: RoadmapStatus;
|
||||
target?: string | null;
|
||||
display_order?: number;
|
||||
metadata_text?: string | null;
|
||||
}): number {
|
||||
const shipped_at = data.status === 'shipping' ? new Date().toISOString().slice(0, 19).replace('T', ' ') : null;
|
||||
const r = db.prepare(`
|
||||
INSERT INTO roadmap_items (title, description, status, target, display_order, shipped_at)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
INSERT INTO roadmap_items (title, description, status, target, display_order, shipped_at, metadata_text)
|
||||
VALUES (?,?,?,?,?,?,?)
|
||||
`).run(
|
||||
data.title,
|
||||
data.description,
|
||||
|
|
@ -701,6 +703,7 @@ export function createRoadmapItem(data: {
|
|||
data.target ?? null,
|
||||
data.display_order ?? 0,
|
||||
shipped_at,
|
||||
data.metadata_text ?? null,
|
||||
);
|
||||
return Number(r.lastInsertRowid);
|
||||
}
|
||||
|
|
@ -715,6 +718,7 @@ export function updateRoadmapItem(id: number, data: {
|
|||
status: RoadmapStatus;
|
||||
target: string | null;
|
||||
display_order: number;
|
||||
metadata_text?: string | null;
|
||||
}): { shippedNow: boolean } {
|
||||
const current = db.prepare('SELECT status, shipped_at FROM roadmap_items WHERE id = ?')
|
||||
.get(id) as { status: RoadmapStatus; shipped_at: string | null } | undefined;
|
||||
|
|
@ -728,9 +732,9 @@ export function updateRoadmapItem(id: number, data: {
|
|||
db.prepare(`
|
||||
UPDATE roadmap_items
|
||||
SET title = ?, description = ?, status = ?, target = ?, display_order = ?,
|
||||
shipped_at = ?, updated_at = datetime('now')
|
||||
shipped_at = ?, metadata_text = ?, updated_at = datetime('now')
|
||||
WHERE id = ?
|
||||
`).run(data.title, data.description, data.status, data.target, data.display_order, shipped_at, id);
|
||||
`).run(data.title, data.description, data.status, data.target, data.display_order, shipped_at, data.metadata_text ?? null, id);
|
||||
|
||||
return { shippedNow };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,12 +200,13 @@ if (Astro.request.method === 'POST') {
|
|||
const status = String(data.get('status') ?? '') as RoadmapStatus;
|
||||
const target = String(data.get('target') ?? '').trim() || null;
|
||||
const displayOrder = Number(data.get('display_order') ?? 0);
|
||||
const metadataText = String(data.get('metadata_text') ?? '').trim() || null;
|
||||
const attributedIds = data.getAll('attributed_user_ids').map(v => Number(v)).filter(Boolean);
|
||||
|
||||
if (!title || !['shipping','in_beta','exploring','considering'].includes(status)) {
|
||||
formError = 'Title and status are required.';
|
||||
} else if (action === 'create_roadmap') {
|
||||
const id = createRoadmapItem({ title, description, status, target, display_order: displayOrder });
|
||||
const id = createRoadmapItem({ title, description, status, target, display_order: displayOrder, metadata_text: metadataText });
|
||||
setRoadmapAttributions(id, attributedIds);
|
||||
if (status === 'shipping') recordActivity(user.id, 'roadmap_shipped', 'roadmap', id);
|
||||
return Astro.redirect('/admin?tab=roadmap&msg=roadmap_created');
|
||||
|
|
@ -213,7 +214,7 @@ if (Astro.request.method === 'POST') {
|
|||
const id = Number(data.get('roadmap_id'));
|
||||
if (id) {
|
||||
const { shippedNow } = updateRoadmapItem(id, {
|
||||
title, description, status, target, display_order: displayOrder,
|
||||
title, description, status, target, display_order: displayOrder, metadata_text: metadataText,
|
||||
});
|
||||
setRoadmapAttributions(id, attributedIds);
|
||||
if (shippedNow) recordActivity(user.id, 'roadmap_shipped', 'roadmap', id);
|
||||
|
|
@ -284,11 +285,11 @@ function moveRoadmapItem(id: number, dir: 'up' | 'down'): void {
|
|||
const other = sameStatus[swapIdx];
|
||||
updateRoadmapItem(item.id, {
|
||||
title: item.title, description: item.description, status: item.status,
|
||||
target: item.target, display_order: other.display_order,
|
||||
target: item.target, display_order: other.display_order, metadata_text: item.metadata_text,
|
||||
});
|
||||
updateRoadmapItem(other.id, {
|
||||
title: other.title, description: other.description, status: other.status,
|
||||
target: other.target, display_order: item.display_order,
|
||||
target: other.target, display_order: item.display_order, metadata_text: other.metadata_text,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue