import type { SnapshotSummary } from '../core/store/SnapshotStore'; import { Button, HStack, Label, ListView, Popup, ScrollContainer, TextField, Theme } from '../ui'; import { dialogButtons } from './FormDialog'; import { snapshotTime } from './snapshots'; const LIST_WIDTH = 360; const LIST_HEIGHT = 220; /** * "Snapshots…" (#231): the snapshots of the open project, newest first, a field to name the next * one, and Restore and Delete on the one picked. * * Like the project picker, Restore and Delete close the dialog and hand the choice on: the caller * asks for confirmation, and a question over a list it is no longer about would only confuse. * Create keeps it open – the caller opens it again with the new snapshot in the list. */ export class SnapshotsDialog extends Popup { private readonly list = new ListView(); private readonly scroll = new ScrollContainer(); private readonly nameField: TextField; private readonly restoreButton: Button; private readonly deleteButton: Button; private picked: string | null; private readonly rows: { name: string; time: string }[]; constructor( snapshots: readonly SnapshotSummary[], actions: { onCreate(name: string): void; onRestore(snapshot: SnapshotSummary): void; onDelete(snapshot: SnapshotSummary): void; }, ) { super({ modal: true, padding: Theme.padding * 2, gap: Theme.gap }); this.picked = snapshots[0]?.id ?? null; this.rows = snapshots.map((s) => ({ name: s.name, time: snapshotTime(new Date(s.createdAt)) })); this.content.add(new Label('Snapshots', { style: 'bodyBold' })); const create = (): void => actions.onCreate(this.nameField.value.trim()); this.nameField = new TextField('', { placeholder: 'Name (optional)', width: 220, onSubmit: create }); const createButton = new Button('Create snapshot', { variant: 'primary', onClick: create }); const createRow = new HStack({ gap: Theme.gap, align: 'center' }); createRow.add(this.nameField, 1); createRow.add(createButton); this.content.add(createRow); this.list.minWidth = LIST_WIDTH; this.list.setItems(snapshots.map((s, i) => ({ id: s.id, label: s.name, hint: this.rows[i]?.time }))); this.list.select(this.picked, true); this.list.onSelect = (id) => { this.picked = id; this.refresh(); }; this.scroll.minWidth = LIST_WIDTH; this.scroll.minHeight = LIST_HEIGHT; this.scroll.setContent(this.list); this.content.add(this.scroll); if (snapshots.length === 0) this.content.add(new Label('No snapshots yet', { style: 'muted' })); const withPicked = (action: (snapshot: SnapshotSummary) => void) => () => { const snapshot = snapshots.find((s) => s.id === this.picked); if (snapshot) this.closeThen(() => action(snapshot))(); }; this.deleteButton = new Button('Delete', { variant: 'danger', onClick: withPicked(actions.onDelete) }); this.restoreButton = new Button('Restore', { onClick: withPicked(actions.onRestore) }); this.content.add(dialogButtons(this.closeThen(), this.restoreButton, this.deleteButton)); this.refresh(); } /** What the list shows and which row is picked (test hook). */ info(): { rows: { name: string; time: string }[]; picked: string | null } { return { rows: this.rows.map((r) => ({ ...r })), picked: this.picked }; } /** The name field, for the check scripts to type into. */ get nameControl(): TextField { return this.nameField; } private refresh(): void { this.restoreButton.setEnabled(this.picked !== null); this.deleteButton.setEnabled(this.picked !== null); } }