/** * @factory/database-engine/views — view config defaults + the 9 view-type * render-data resolvers (TASK-0069/0070–0075, pure half). * * `defaultViewConfig(type, database)` — per-type defaults shared by the API * and the UI preview (recorded: board groups by the first STATUS/SELECT, * calendar by the first DATE/DATE_RANGE; a db with neither → the "choose a * property" state = `group: []`). The `viewData` resolvers are PURE data * transforms (records → what each view renders): the React renderers live in * apps/web (AGENTS.md rule 23: domain logic independent from UI — the * package produces render-ready data, not DOM). */ import type { ViewType } from "@factory/shared/dto"; import { groupRecords, type GroupMeta } from "../query/group.js"; import { propertiesMap } from "../filter/index.js"; import type { RecordRow, PropertyInfo } from "../values/types.js"; import * as buttons from "./buttons.js"; export * from "./buttons.js"; export interface ViewDefaultsInput { properties: PropertyInfo[]; } /** Per-type config defaults (TASK-0069 R5 `defaultViewConfig`). */ export function defaultViewConfig( type: ViewType, input: ViewDefaultsInput, ): Record { const props = input.properties; const visiblePropertyIds = props.map((p) => p.id); const firstOf = (...types: string[]) => props.find((p) => types.includes(p.type))?.id; const base: Record = { visiblePropertyIds, color: "default", density: "default", pageSize: 50, }; switch (type) { case "table": return { ...base, density: "default", pageSize: 100 }; case "list": return { ...base, cardLayout: { properties: [] } }; case "board": { const group = firstOf("status", "select"); return { ...base, group: group ? [{ propertyId: group, direction: "asc" }] : [], cardLayout: { showCover: true, properties: [], showAvatar: true }, }; } case "gallery": return { ...base, cardLayout: { cover: true, size: "M", properties: [] } }; case "calendar": { const group = firstOf("date", "date_range"); return { ...base, group: group ? [{ propertyId: group, direction: "asc" }] : [] }; } case "timeline": { const group = firstOf("date", "date_range"); return { ...base, group: group ? [{ propertyId: group, direction: "asc" }] : [] }; } case "chart": return { ...base, chartId: null }; case "dashboard": return { ...base, dashboardId: null }; case "form": // Phase 10 — the picker disables this row; creation 409s (recorded). return { ...base }; } } /** * Render data for the 9 view types (pure). `records` are already * filter+sorted by the query pipeline; `groups` (when present) come from * groupRecords. This is the single source the apps/web renderers consume — * a new view type adds one case here + one component in apps/web. */ export interface ViewData { type: ViewType; table?: { rows: RecordRow[] }; list?: { rows: RecordRow[] }; board?: { groups: GroupMeta[] }; gallery?: { items: RecordRow[] }; calendar?: { events: Array<{ record: RecordRow; date: string }> }; timeline?: { events: Array<{ record: RecordRow; date: string }> }; chart?: { chartId: string | null }; dashboard?: { dashboardId: string | null }; form?: { disabled: boolean; reason: string }; } export function viewData( type: ViewType, records: RecordRow[], config: Record, properties: PropertyInfo[], ): ViewData { const props = propertiesMap(properties); const groupSpecs: Array<{ propertyId: string; direction: "asc" | "desc" }> = Array.isArray( config["group"], ) ? (config["group"] as Array>).flatMap((g) => typeof g?.["propertyId"] === "string" ? [ { propertyId: g["propertyId"] as string, direction: g["direction"] === "desc" ? "desc" : "asc", }, ] : [], ) : []; const dateOf = (r: RecordRow): string | null => { for (const g of groupSpecs) { const p = props.get(g.propertyId); if (!p) continue; const v = r.values[g.propertyId]; if (p.type === "date" && typeof v === "string") return v; if ( p.type === "date_range" && v && typeof v === "object" && typeof (v as { start?: unknown }).start === "string" ) { return (v as { start: string }).start; } } return null; }; switch (type) { case "table": return { type, table: { rows: records } }; case "list": return { type, list: { rows: records } }; case "board": return { type, board: { groups: groupRecords(records, groupSpecs, props) } }; case "gallery": return { type, gallery: { items: records } }; case "calendar": return { type, calendar: { events: records .map((r) => ({ record: r, date: dateOf(r) ?? "" })) .filter((e) => e.date !== ""), }, }; case "timeline": return { type, timeline: { events: records .map((r) => ({ record: r, date: dateOf(r) ?? "" })) .filter((e) => e.date !== ""), }, }; case "chart": return { type, chart: { chartId: typeof config["chartId"] === "string" ? config["chartId"] : null }, }; case "dashboard": return { type, dashboard: { dashboardId: typeof config["dashboardId"] === "string" ? config["dashboardId"] : null, }, }; case "form": return { type, form: { disabled: true, reason: "form_view_phase_10" } }; } } /** The 9 view types (the prisma ViewType enum). */ export const VIEW_TYPES: readonly ViewType[] = [ "table", "list", "board", "gallery", "calendar", "timeline", "chart", "dashboard", "form", ]; export const buttonsModule = buttons;