"use client";
/**
* BoardView — kanban renderer for BOARD views (dnd-kit).
*
* Columns come from view.group (select/status/multi_select/person/date —
* enforced by the server, see GroupBuilder). Cards show the title + up to 2
* secondary properties as chips (view.config.cardProperties).
*
* Interactions:
* - drag a card between columns → PATCH the record's group property
* (optimistic via useUpdateRecord, rollback on 409/error);
* - drag within a column → PATCH position (fractional, via the
* two-neighbor rule in docs/api/databases.md §7.2);
* - "+" on a column header → create a record in that group;
* - card → open the record panel;
* - column header quick filter → ad-hoc filter override (contains).
*/
import { useMemo, useState } from "react";
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
PointerSensor,
useSensor,
useSensors,
useDraggable,
useDroppable,
type CollisionDetection
} from "@dnd-kit/core";
import { ChevronDown, Filter, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DatabaseProperty,
DatabaseView,
RecordDto,
RecordGroup,
RecordValue,
UseDatabaseRecordsResult,
useCreateRecord,
useUpdateRecord,
valueToText
} from "@/hooks/use-database";
import { cellToText } from "@/components/database/cell-renderer";
export interface BoardProps {
databaseId: string;
view: DatabaseView;
properties: DatabaseProperty[];
records: UseDatabaseRecordsResult;
onOpenRecord: (record: RecordDto) => void;
readOnly?: boolean;
}
type ColumnKey = string; // JSON of the group value
const NO_GROUP_KEY = "__no_group__";
function columnKey(g: RecordGroup): ColumnKey {
return JSON.stringify(g.key.value);
}
/** Value stored on the group property for a column (null = "No group"). */
function groupValueForColumn(key: ColumnKey, groupProperty: DatabaseProperty): RecordValue {
const v = JSON.parse(key) as RecordValue | string | number;
// Date groups: the key may arrive as a bare ISO string.
if (groupProperty.type === "date" && typeof v === "string") {
return { start: v, end: null };
}
const value = (v ?? {}) as RecordValue;
if (groupProperty.type === "multi_select") {
return { optionIds: [value.optionId].filter(Boolean) as string[] };
}
return value;
}
// ---------------------------------------------------------------------------
// Card
// ---------------------------------------------------------------------------
function CardContent({ record, titleProp, props, compact }: { record: RecordDto; titleProp?: DatabaseProperty; props: DatabaseProperty[]; compact?: boolean }) {
const chips = (viewConfigCardProperties(props, titleProp)).slice(0, 2);
return (