"use client"; import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; import { FileStack, Search, X } from "lucide-react"; import { api } from "@/lib/trpc"; import { cn } from "@/lib/utils"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; export type TemplateSchemaJson = { properties?: { name: string; fieldType: string; defaultValue?: unknown; }[]; defaultContent?: string; }; export type PickerTemplate = { id: string; workspaceId: string; name: string; targetType: string; schema: TemplateSchemaJson | null; isBuiltin?: boolean; }; /** Preset templates for UI / offline use; merge with API `templates.list` when available. */ export const BUILTIN_TEMPLATES: PickerTemplate[] = [ { id: "00000000-0000-4000-8000-000000000001", workspaceId: "", name: "Bug Report", targetType: "task", isBuiltin: true, schema: { properties: [ { name: "Priority", fieldType: "select", defaultValue: "medium" }, { name: "Severity", fieldType: "select", defaultValue: "major" }, { name: "Steps to Reproduce", fieldType: "textarea", defaultValue: "", }, { name: "Expected Behavior", fieldType: "textarea", defaultValue: "", }, ], defaultContent: "## Summary\n\n## Steps to reproduce\n\n1. \n\n## Expected behavior\n\n", }, }, { id: "00000000-0000-4000-8000-000000000002", workspaceId: "", name: "Meeting Notes", targetType: "document", isBuiltin: true, schema: { properties: [ { name: "Date", fieldType: "date", defaultValue: "" }, { name: "Attendees", fieldType: "text", defaultValue: "" }, { name: "Agenda", fieldType: "textarea", defaultValue: "" }, { name: "Action Items", fieldType: "textarea", defaultValue: "" }, ], defaultContent: "# Meeting\n\n## Agenda\n\n## Notes\n\n## Action items\n\n", }, }, { id: "00000000-0000-4000-8000-000000000003", workspaceId: "", name: "Sprint", targetType: "project", isBuiltin: true, schema: { properties: [ { name: "Sprint Goal", fieldType: "text", defaultValue: "" }, { name: "Start Date", fieldType: "date", defaultValue: "" }, { name: "End Date", fieldType: "date", defaultValue: "" }, { name: "Velocity", fieldType: "number", defaultValue: null }, ], defaultContent: "## Sprint goal\n\n## Commitments\n\n", }, }, ]; const CREATE_SENTINEL = "__create__"; function schemaPreview(schema: TemplateSchemaJson | null | undefined): string { const props = schema?.properties ?? []; if (props.length === 0) { return "No custom properties"; } const names = props.slice(0, 6).map((p) => p.name); const extra = props.length > 6 ? ` +${props.length - 6} more` : ""; return `${names.join(" · ")}${extra}`; } function propertyCount(schema: TemplateSchemaJson | null | undefined): number { return schema?.properties?.length ?? 0; } export type TemplatePickerProps = { objectId: string; objectType: string; onSelect: (templateId: string) => void; open: boolean; onOpenChange: (open: boolean) => void; }; export function TemplatePicker({ objectId, objectType, onSelect, open, onOpenChange, }: TemplatePickerProps) { const [search, setSearch] = React.useState(""); const [showAllTypes, setShowAllTypes] = React.useState(false); const objectQuery = api.objects.getById.useQuery( { id: objectId }, { enabled: open && Boolean(objectId) }, ); const objWorkspace = (objectQuery.data as unknown as { workspaceId?: string | null } | undefined) ?.workspaceId; const workspaceId = typeof objWorkspace === "string" && objWorkspace.length > 0 ? objWorkspace : undefined; const listQuery = api.templates.list.useQuery( { workspaceId: workspaceId!, targetType: showAllTypes ? undefined : objectType }, { enabled: open && Boolean(workspaceId) }, ); const merged = React.useMemo(() => { const fromApi = listQuery.data?.templates ?? []; const builtinFiltered = BUILTIN_TEMPLATES.filter( (b) => showAllTypes || b.targetType === objectType, ); const seen = new Set(); const out: PickerTemplate[] = []; for (const t of [...builtinFiltered, ...fromApi]) { if (seen.has(t.id)) continue; seen.add(t.id); out.push({ id: t.id, workspaceId: t.workspaceId, name: t.name, targetType: t.targetType, schema: (t.schema as TemplateSchemaJson | null) ?? null, isBuiltin: "isBuiltin" in t ? t.isBuiltin : false, }); } return out; }, [listQuery.data?.templates, objectType, showAllTypes]); const filtered = React.useMemo(() => { const q = search.trim().toLowerCase(); if (!q) return merged; return merged.filter((t) => t.name.toLowerCase().includes(q)); }, [merged, search]); const grouped = React.useMemo(() => { if (!showAllTypes) { return new Map([[objectType, filtered]]); } const m = new Map(); const order = ["project", "task", "document", "whiteboard", "group", "workspace"]; for (const t of filtered) { const k = t.targetType; if (!m.has(k)) m.set(k, []); m.get(k)!.push(t); } const keys = [...m.keys()].sort( (a, b) => order.indexOf(a) - order.indexOf(b) || a.localeCompare(b), ); return new Map(keys.map((k) => [k, m.get(k)!])); }, [filtered, objectType, showAllTypes]); React.useEffect(() => { if (!open) { setSearch(""); setShowAllTypes(false); } }, [open]); return (
Use a template Apply structure and default fields to this {objectType}.
setSearch(e.target.value)} />
{listQuery.isPending && workspaceId ? (

Loading templates…

) : null} {!workspaceId && objectQuery.isPending ? (

Loading object…

) : null} {!workspaceId && objectQuery.isError ? (

Could not load workspace.

) : null} {Array.from(grouped.entries()).map(([typeKey, items], gi) => ( {gi > 0 && showAllTypes ? : null}
{showAllTypes ? (
{typeKey}
) : null} {items.length === 0 ? (

No templates match this filter.

) : ( items.map((t) => (
{t.name} {t.targetType} {t.isBuiltin ? ( Built-in ) : null}

{propertyCount(t.schema)} properties ·{" "} {schemaPreview(t.schema)}

)) )}
))}
); } export { CREATE_SENTINEL as TEMPLATE_PICKER_CREATE_SENTINEL };