diff --git a/apps/web/app/icon.png b/apps/web/app/icon.png new file mode 100644 index 0000000..157728b Binary files /dev/null and b/apps/web/app/icon.png differ diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index e152ee9..95fbb49 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -4,8 +4,9 @@ import { Sidebar, type SidebarTable } from "@/components/Sidebar"; import { getMeta } from "@/lib/server/gridApi"; export const metadata: Metadata = { - title: "grid", - description: "Database-backed workspace (Airtable replacement)", + title: "gridbase", + description: "Database-backed workspace (open-source Airtable replacement)", + icons: { icon: "/icon.png" }, }; /** First non-hidden view for a table, by position. */ diff --git a/apps/web/app/t/[tableId]/[viewId]/page.tsx b/apps/web/app/t/[tableId]/[viewId]/page.tsx index b2e71f7..6241391 100644 --- a/apps/web/app/t/[tableId]/[viewId]/page.tsx +++ b/apps/web/app/t/[tableId]/[viewId]/page.tsx @@ -56,7 +56,7 @@ export default async function ViewPage({
- {view.type !== "form" ? : null} + {view.type !== "form" ? : null}
diff --git a/apps/web/components/KanbanView.tsx b/apps/web/components/KanbanView.tsx index 039fca1..deed6aa 100644 --- a/apps/web/components/KanbanView.tsx +++ b/apps/web/components/KanbanView.tsx @@ -35,7 +35,13 @@ export function KanbanView({ if (!stackField || !stackId) return

This Kanban view has no stack field configured.

; - const previewFields = fields.filter((f) => f.fieldId !== primaryId && f.fieldId !== stackId && f.type !== "link").slice(0, 3); + // Card fields follow the view's FieldEditor (show/hide/reorder) — the single + // source of truth — minus the stack (it's the column) and the primary (the + // card title). A soft cap (default 8) keeps cards readable. + const maxPreview = view.config.kanban?.maxPreviewFields ?? 8; + const previewFields = fields + .filter((f) => f.fieldId !== primaryId && f.fieldId !== stackId) + .slice(0, maxPreview); const columns = [ ...(stackField.options?.choices?.map((c) => ({ id: c.name, label: c.name })) ?? []), { id: UNSET, label: "Uncategorized" }, @@ -75,6 +81,7 @@ export function KanbanView({ href={`/t/${view.tableId}/${view.viewId}/${rec.id}`} primaryId={primaryId} previewFields={previewFields} + labels={labels} /> ))} @@ -98,7 +105,19 @@ function Column({ id, label, count, children }: { id: string; label: string; cou ); } -function Card({ rec, href, primaryId, previewFields }: { rec: RecordEnvelope; href: string; primaryId?: string; previewFields: FieldMeta[] }) { +/** Render a card-preview cell value: link/lookup arrays become comma-joined + * labels (via the id→label map); scalars stringify. Returns "" when empty. */ +function previewValue(field: FieldMeta, value: unknown, labels: Map): string { + if (value == null || value === "") return ""; + if (field.type === "link") { + const ids = Array.isArray(value) ? (value as string[]) : [String(value)]; + return ids.map((id) => labels.get(id) ?? id).join(", "); + } + if (Array.isArray(value)) return value.filter((v) => v != null && v !== "").map((v) => String(v)).join(", "); + return String(value); +} + +function Card({ rec, href, primaryId, previewFields, labels }: { rec: RecordEnvelope; href: string; primaryId?: string; previewFields: FieldMeta[]; labels: Map }) { const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: rec.id }); const style = transform ? { transform: `translate(${transform.x}px, ${transform.y}px)`, zIndex: 50 } : undefined; return ( @@ -113,11 +132,11 @@ function Card({ rec, href, primaryId, previewFields }: { rec: RecordEnvelope; hr {primaryId && rec.fields[primaryId] ? String(rec.fields[primaryId]) : "(untitled)"} {previewFields.map((f) => { - const v = rec.fields[f.fieldId]; - if (v == null || v === "") return null; + const text = previewValue(f, rec.fields[f.fieldId], labels); + if (!text) return null; return (
- {f.name}: {String(v)} + {f.name}: {text}
); })} diff --git a/apps/web/components/Sidebar.tsx b/apps/web/components/Sidebar.tsx index daff954..38e5c59 100644 --- a/apps/web/components/Sidebar.tsx +++ b/apps/web/components/Sidebar.tsx @@ -1,4 +1,5 @@ "use client"; +import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; @@ -29,6 +30,9 @@ export function Sidebar({ tables }: { tables: SidebarTable[] }) { if (collapsed) { return (