ubiquitous-invention/apps/web/components/forms/form-responses.tsx
Randall Stillwell 663bc77afe feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:

App
- New routes: ai, forms, planner, settings (templates/types), teams,
  doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
  builder/renderer/responses, types manager, objects creation dialog,
  card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
  health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in

Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
  (uses CT 102 shared services), removes host port mappings, adds
  Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
  metadata title flipped to ECHODO

Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
  credentials.*, *.key, *.crt, *.pem, ssh keys

Made-with: Cursor
2026-04-26 14:34:34 -05:00

137 lines
4.3 KiB
TypeScript

"use client";
import * as React from "react";
import { ExternalLink, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { api } from "@/lib/trpc";
import { usePanelStore } from "@/lib/stores/panel-store";
import { cn } from "@/lib/utils";
import type { FormField } from "./form-renderer";
function formatCellValue(value: unknown): string {
if (value === undefined || value === null) return "—";
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return String(value);
}
if (Array.isArray(value)) {
return value.map((x) => (typeof x === "string" ? x : JSON.stringify(x))).join(", ");
}
try {
return JSON.stringify(value);
} catch {
return "—";
}
}
function tableColumns(fields: FormField[]): FormField[] {
return fields.filter((f) => f.type !== "section_header" && f.type !== "divider");
}
export interface FormResponsesProps {
formId: string;
fields: FormField[];
className?: string;
}
export function FormResponses({ formId, fields, className }: FormResponsesProps) {
const open = usePanelStore((s) => s.open);
const cols = React.useMemo(() => tableColumns(fields), [fields]);
const listQuery = api.forms.listResponses.useQuery(
{ formId },
{ enabled: Boolean(formId) },
);
if (listQuery.isPending) {
return (
<div className={cn("flex justify-center py-12", className)}>
<Loader2 className="size-6 animate-spin text-muted-foreground" aria-hidden />
<span className="sr-only">Loading responses</span>
</div>
);
}
if (listQuery.isError) {
return (
<p className={cn("text-sm text-muted-foreground", className)}>
Could not load responses.
</p>
);
}
const rows = listQuery.data?.responses ?? [];
if (rows.length === 0) {
return (
<p className={cn("text-sm text-muted-foreground", className)}>No responses yet.</p>
);
}
return (
<div className={cn("w-full overflow-x-auto rounded-md border border-border", className)}>
<table className="w-full min-w-[640px] border-collapse text-left text-sm">
<thead>
<tr className="border-b border-border bg-muted/40">
{cols.map((f) => (
<th
key={f.id}
className="whitespace-nowrap px-3 py-2 font-medium text-muted-foreground"
>
{f.label}
</th>
))}
<th className="whitespace-nowrap px-3 py-2 font-medium text-muted-foreground">
Submitted
</th>
<th className="whitespace-nowrap px-3 py-2 font-medium text-muted-foreground">
Task
</th>
</tr>
</thead>
<tbody>
{rows.map((row) => {
const data =
row.data && typeof row.data === "object" && row.data !== null
? (row.data as Record<string, unknown>)
: {};
const submitted =
row.submittedAt instanceof Date
? row.submittedAt.toLocaleString()
: String(row.submittedAt ?? "—");
return (
<tr key={row.id} className="border-b border-border last:border-0">
{cols.map((f) => (
<td key={f.id} className="max-w-[240px] truncate px-3 py-2 align-top">
{formatCellValue(data[f.id])}
</td>
))}
<td className="whitespace-nowrap px-3 py-2 align-top text-muted-foreground">
{submitted}
</td>
<td className="px-3 py-2 align-top">
{row.createdObjectId ? (
<Button
type="button"
variant="link"
className="inline-flex h-auto items-center gap-1 p-0 text-primary"
onClick={() => open("object-detail", row.createdObjectId)}
>
Open task
<ExternalLink className="size-3.5 opacity-70" aria-hidden />
</Button>
) : (
<span className="text-muted-foreground"></span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}