Skip to content

Instantly share code, notes, and snippets.

@Dan6erbond
Last active March 13, 2026 16:45
Show Gist options
  • Select an option

  • Save Dan6erbond/e0dd89744c21aaa8c25925717d589eeb to your computer and use it in GitHub Desktop.

Select an option

Save Dan6erbond/e0dd89744c21aaa8c25925717d589eeb to your computer and use it in GitHub Desktop.
PayloadCMS Kanban list view utilizing DnD-Kit and Payload orderable with custom status field.

PayloadCMS Kanban Board

A drag-and-drop Kanban board implemented as a custom list view for a PayloadCMS collection. Built with DnD-Kit, designed to look native inside the Payload admin UI, and wired directly into Payload's orderable plugin via fractional indexing.

Goals

  • Replace the default collection list view with a Kanban board without losing access to the standard table view
  • Keep the UI consistent with Payload's admin design system — no jarring third-party component styles
  • Persist drag-and-drop order durably using fractional indexing so records sort correctly across sessions and concurrent users
  • Stay lightweight: no extra state management libraries, no custom backend endpoints — just Payload's existing REST API

How it works

Custom list view

Payload lets you replace the default list view for any collection by pointing admin.components.views.list at your own component:

admin: {
  components: {
    views: {
      list: {
        Component: "@/components/admin/lead/list",
      },
    },
  },
},
orderable: true,

The entry point (list-view.tsx) is a server component. It fetches the first page of leads for each status column in parallel using Payload's local API (available as payload on ListViewServerProps), then passes the results down to the client-side <Kanban /> component as initial data. This means the board renders fully on first load without a client-side waterfall.

The list view also retains access to the standard Payload table view. Rather than fully replacing the UI, you can wrap both views in a tab switcher — the Kanban tab renders <Kanban /> while the default tab renders Payload's built-in <DefaultListView />. This way users get the best of both: a visual pipeline view and the familiar filterable/sortable table when they need it.

Payload's design system

The board uses Payload's CSS custom properties throughout rather than hardcoded colours, so it respects both light and dark mode automatically and stays visually consistent with the rest of the admin panel:

// Elevation scale for text hierarchy
"text-(--theme-elevation-1000)"  // primary text
"text-(--theme-elevation-800)"   // column headers
"text-(--theme-elevation-400)"   // secondary/muted text

// Structural colours
"border-(--theme-border-color)"  // matches Payload's panel borders
"bg-(--theme-elevation-50)"      // matches Payload's card/panel background

// Intent colours for the drop-zone highlight
"bg-(--theme-success-50) border-(--theme-success-200)"

Card shells use Payload's .card CSS class directly, which provides the correct background, border, border-radius, and padding that matches the rest of the admin UI. Buttons use <Button /> from @payloadcms/ui. The assignee picker uses <SelectInput /> from the same package.

Drag and drop

DnD-Kit handles all drag interactions. A few key decisions:

No optimistic cross-column moves during drag. The card stays in its original column in React state for the entire drag. onDragOver only updates which column is highlighted. The actual move — both in local state and via API — happens once in onDragEnd. This is what allows dragging across multiple non-adjacent columns without the card getting "stuck" in an intermediate column.

pointerWithin + closestCorners collision detection. pointerWithin checks where the actual cursor is rather than the dragged card's bounding rect, which means empty columns register as valid drop targets naturally. closestCorners is used as a fallback for precision within a populated column.

Column droppables via useDroppable. Each column registers itself as a droppable zone with its status string as the ID (e.g. "new", "contact"). This is separate from the card-level sortable IDs, and is what makes dropping into an empty column possible.

orderable: true and fractional indexing

Enabling orderable: true on the collection tells Payload to maintain a _order field on each document. Rather than storing integer positions (which require renumbering every record on each move), the board uses fractional indexing to generate a sort key that slots between its neighbours without touching any other records.

Payload ships the generateKeyBetween helper from the payload/shared entry point:

import { generateKeyBetween } from "payload/shared";

function orderKeyForIndex(docs: Lead[], index: number): string {
  const prev = (docs[index - 1]?._order ?? null) as string | null;
  const next = (docs[index + 1]?._order ?? null) as string | null;
  return generateKeyBetween(prev, next);
}

Both ends accept null: passing null as prev generates a key before the first item; passing null as next generates one after the last. On drop, the card's new _order is calculated from its neighbours in the updated array and written to Payload in the same API call that updates its status:

updateLead(leadId, { status: targetColumn, _order: newOrder })

Adapting it to your own collection

1. Enable ordering on your collection

export const Tasks: CollectionConfig = {
  slug: "tasks",
  orderable: true,      // adds and manages the _order field
  fields: [
    { name: "status", type: "select", options: ["todo", "in-progress", "done"] },
    { name: "title",  type: "text" },
    // ...
  ],
  admin: {
    components: {
      views: {
        list: { Component: "@/components/admin/task/list" },
      },
    },
  },
};

2. Update the column definitions

Replace the COLUMNS array with your own statuses:

const COLUMNS = [
  { id: "todo",        label: "To Do",       pillBg: "bg-blue-500/10",   pillText: "text-blue-400"   },
  { id: "in-progress", label: "In Progress",  pillBg: "bg-amber-500/10",  pillText: "text-amber-400"  },
  { id: "done",        label: "Done",         pillBg: "bg-emerald-500/10",pillText: "text-emerald-400"},
] satisfies ColumnDef[];

3. Update the card

The <LeadCard /> component is the only place that knows about lead-specific fields (firstName, lastName, address, etc.). Replace it with a card that renders your own document shape. Everything else — drag handling, column layout, pagination, ordering — is generic and requires no changes.

4. Wire up the list view

The server component just needs to know your collection slug and which field holds the status:

const STATUSES = ["todo", "in-progress", "done"] as const;

// inside ListView:
payload.find({ collection: "tasks", where: { status: { equals: status } }, ... })

Dependencies

"use client";
import {
DndContext,
DragEndEvent,
DragOverEvent,
DragOverlay,
DragStartEvent,
PointerSensor,
closestCorners,
pointerWithin,
useDroppable,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { Lead, User } from "@payload-types";
import React, { useCallback, useRef, useState } from "react";
import {
SortableContext,
arrayMove,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import Link from "next/link";
import type { PaginatedDocs } from "payload";
import { SelectInput } from "@payloadcms/ui";
import { UserAvatar } from "@/components/admin/avatar";
import { cn } from "@heroui/react";
import { generateKeyBetween } from "payload/shared";
import { sdk } from "@/lib/payload/sdk";
// ─── Types ────────────────────────────────────────────────────────────────────
type LeadStatus = NonNullable<Lead["status"]>;
type ColumnDef = {
id: LeadStatus;
label: string;
pillBg: string;
pillText: string;
};
// Type alias — no need to redefine what PaginatedDocs already provides
type ColumnState = PaginatedDocs<Lead>;
// ─── Column definitions ───────────────────────────────────────────────────────
const COLUMNS: ColumnDef[] = [
{
id: "new",
label: "Neu",
pillBg: "bg-blue-500/10",
pillText: "text-blue-400",
},
{
id: "contact",
label: "Kontakt",
pillBg: "bg-violet-500/10",
pillText: "text-violet-400",
},
{
id: "wait",
label: "Abwarten",
pillBg: "bg-amber-500/10",
pillText: "text-amber-400",
},
{
id: "converted",
label: "Konvertiert",
pillBg: "bg-emerald-500/10",
pillText: "text-emerald-400",
},
{
id: "declined",
label: "Abgelehnt",
pillBg: "bg-red-500/10",
pillText: "text-red-400",
},
];
// ─── API helpers ──────────────────────────────────────────────────────────────
async function updateLead(
leadId: number,
data: { status?: LeadStatus; _order?: string },
): Promise<void> {
await sdk.update({ collection: "leads", id: leadId, data });
}
async function fetchMoreLeads(
status: LeadStatus,
page: number,
): Promise<ColumnState> {
return sdk.find({
collection: "leads",
where: { status: { equals: status } },
limit: 10,
page,
}) as Promise<ColumnState>;
}
// ─── Fractional indexing ──────────────────────────────────────────────────────
function orderKeyForIndex(docs: Lead[], index: number): string {
const prev = (docs[index - 1]?._order ?? null) as string | null;
const next = (docs[index + 1]?._order ?? null) as string | null;
return generateKeyBetween(prev, next);
}
// ─── Misc helpers ─────────────────────────────────────────────────────────────
function isColumnId(id: string | number): id is LeadStatus {
return typeof id === "string" && COLUMNS.some((c) => c.id === id);
}
// Given an over.id (either a column string or a lead number), return the column it belongs to
function resolveColumn(
overId: string | number,
columns: Record<LeadStatus, ColumnState>,
): LeadStatus | null {
if (isColumnId(overId)) return overId;
for (const col of COLUMNS) {
if (columns[col.id].docs.some((l) => l.id === overId)) return col.id;
}
return null;
}
// ─── Assignee select ──────────────────────────────────────────────────────────
function AssigneeSelect({
leadId,
value,
users,
onChange,
}: {
leadId: number;
value: number | null;
users: User[];
onChange: (userId: number | null) => void;
}) {
const options = [
{ label: "Nicht zugewiesen", value: "" },
...users.map((u) => ({
label: [u.firstName, u.lastName].filter(Boolean).join(" ") || u.email,
value: String(u.id),
})),
];
const path = `assignee-${leadId}`;
return (
// Stop pointer events from bubbling to the drag listeners on the card
<div
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
<SelectInput
path={path}
name={path}
options={options}
value={value ? String(value) : ""}
onChange={(opt) => {
if (Array.isArray(opt)) return;
const next = opt?.value ? Number(opt.value) : null;
onChange(next);
updateLead(leadId, { assignee: next ?? undefined } as any).catch(
console.error,
);
}}
/>
</div>
);
}
// ─── Lead card ────────────────────────────────────────────────────────────────
function LeadCard({
lead,
users,
isDragging = false,
listeners,
attributes,
setNodeRef,
style,
onAssigneeChange,
}: {
lead: Lead;
users: User[];
isDragging?: boolean;
listeners?: ReturnType<typeof useSortable>["listeners"];
attributes?: ReturnType<typeof useSortable>["attributes"];
setNodeRef?: (node: HTMLElement | null) => void;
style?: React.CSSProperties;
onAssigneeChange?: (userId: number | null) => void;
}) {
const fullName =
[lead.firstName, lead.lastName].filter(Boolean).join(" ") || "—";
const location = [lead.address?.zip, lead.address?.area]
.filter(Boolean)
.join(" ");
const assigneeId =
typeof lead.assignee === "object" && lead.assignee !== null
? (lead.assignee as User).id
: ((lead.assignee as number | null) ?? null);
const assigneeUser = assigneeId
? (users.find((u) => u.id === assigneeId) ?? null)
: null;
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className={cn(
"card flex-col gap-4",
"cursor-grab active:cursor-grabbing select-none",
"transition-opacity duration-150",
isDragging ? "opacity-30" : "opacity-100",
)}
>
{/* Name + gender */}
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
<p
className={cn(
"text-sm font-semibold leading-snug",
"text-(--theme-elevation-1000)",
)}
>
{fullName}
</p>
{lead.sex && (
<span className={cn("text-xs", "text-(--theme-elevation-400)")}>
{lead.sex === "male" ? "Herr" : "Frau"}
</span>
)}
</div>
{(location || lead.birthdate) && (
<div className="text-xs text-(--theme-elevation-400) space-y-1">
{location && <p>{location}</p>}
{lead.birthdate && (
<p>
{new Date(lead.birthdate).toLocaleDateString("de-DE", {
day: "numeric",
month: "short",
year: "numeric",
})}
</p>
)}
</div>
)}
{/* Assignee row */}
<div className="flex items-center gap-2">
{assigneeUser && <UserAvatar user={assigneeUser} size={20} />}
<div className="flex-1 min-w-0">
<AssigneeSelect
leadId={lead.id}
value={assigneeId}
users={users}
onChange={onAssigneeChange ?? (() => {})}
/>
</div>
</div>
<Link
href={`/admin/collections/leads/${lead.id}`}
onClick={(e) => e.stopPropagation()}
className="text-xs inline-block"
>
Öffnen →
</Link>
</div>
);
}
// ─── Sortable wrapper ─────────────────────────────────────────────────────────
function SortableLeadCard({
lead,
users,
onAssigneeChange,
}: {
lead: Lead;
users: User[];
onAssigneeChange: (userId: number | null) => void;
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: lead.id, data: { type: "lead", lead } });
return (
<LeadCard
lead={lead}
users={users}
isDragging={isDragging}
setNodeRef={setNodeRef}
style={{ transform: CSS.Transform.toString(transform), transition }}
listeners={listeners}
attributes={attributes}
onAssigneeChange={onAssigneeChange}
/>
);
}
// ─── Column ───────────────────────────────────────────────────────────────────
function KanbanColumn({
column,
state,
users,
isOver,
onLoadMore,
onAssigneeChange,
}: {
column: ColumnDef;
state: ColumnState;
users: User[];
isOver: boolean;
onLoadMore: () => void;
onAssigneeChange: (leadId: number, userId: number | null) => void;
}) {
const ids = state.docs.map((l) => l.id);
const { setNodeRef } = useDroppable({ id: column.id });
return (
<div className="flex flex-col w-80 shrink-0 h-full">
<div className="flex items-center justify-between mb-2 px-1">
<span
className={cn(
"text-sm font-semibold",
"text-(--theme-elevation-800)",
)}
>
{column.label}
</span>
<span
className={cn(
"text-xs font-mono px-2 py-0.5 rounded",
column.pillBg,
column.pillText,
)}
>
{state.totalDocs}
</span>
</div>
<SortableContext items={ids} strategy={verticalListSortingStrategy}>
<div
ref={setNodeRef}
className={cn(
"flex flex-1 flex-col gap-2 min-h-20 p-2 rounded border transition-colors duration-150",
"border-(--theme-border-color) bg-(--theme-elevation-50)",
isOver && "bg-(--theme-success-50) border-(--theme-success-200)",
)}
>
{ids.length === 0 ? (
<div
className={cn(
"flex items-center justify-center h-15 text-xs italic",
"text-(--theme-elevation-350)",
isOver && "text-(--theme-success-500)",
)}
>
{isOver ? "Hier ablegen" : "Keine Leads"}
</div>
) : (
state.docs.map((lead) => (
<SortableLeadCard
key={lead.id}
lead={lead}
users={users}
onAssigneeChange={(userId) => onAssigneeChange(lead.id, userId)}
/>
))
)}
</div>
</SortableContext>
{state.hasNextPage && (
<button
onClick={onLoadMore}
className={cn(
"mt-2 w-full text-xs py-1.5 rounded border transition-colors duration-150",
"border-(--theme-border-color) text-(--theme-elevation-500)",
"hover:bg-(--theme-elevation-100) hover:text-(--theme-elevation-800)",
)}
>
Mehr laden ({state.docs.length} / {state.totalDocs})
</button>
)}
</div>
);
}
// ─── Main board ───────────────────────────────────────────────────────────────
export type KanbanProps = {
initialColumns: Record<LeadStatus, PaginatedDocs<Lead>>;
users: User[];
className?: string;
};
export function Kanban({ initialColumns, users, className }: KanbanProps) {
const [columns, setColumns] = useState<Record<LeadStatus, ColumnState>>(
() => ({ ...initialColumns }),
);
const [activeLead, setActiveLead] = useState<Lead | null>(null);
// overColumn: stored in a ref for synchronous reads + state for re-renders.
// We write the ref first so the isDragging guard in onDragOver can see the
// cleared value before React has flushed anything.
const overColumnRef = useRef<LeadStatus | null>(null);
const [overColumn, setOverColumnState] = useState<LeadStatus | null>(null);
const setOverColumn = useCallback((col: LeadStatus | null) => {
overColumnRef.current = col;
setOverColumnState(col);
}, []);
// isDragging ref: set false at the very top of handleDragEnd so that the
// spurious final onDragOver DnD-Kit emits after drop is ignored completely.
const isDragging = useRef(false);
// sourceCol: locked at dragStart, never mutated during the drag.
// We do NOT do cross-column optimistic moves in onDragOver — only in onDragEnd.
// This is what allows dragging across multiple columns: the card always lives
// in its original column in state until the drag ends.
const sourceCol = useRef<LeadStatus | null>(null);
const activeId = useRef<number | null>(null);
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
);
// ── Assignee change ───────────────────────────────────────────────────────
const handleAssigneeChange = useCallback(
(leadId: number, userId: number | null) => {
setColumns((prev) => {
const entries = Object.entries(prev) as [LeadStatus, ColumnState][];
for (const [colId, state] of entries) {
const idx = state.docs.findIndex((l) => l.id === leadId);
if (idx < 0) continue;
const updatedDocs = [...state.docs];
updatedDocs[idx] = { ...updatedDocs[idx], assignee: userId as any };
return { ...prev, [colId]: { ...state, docs: updatedDocs } };
}
return prev;
});
},
[],
);
// ── Load more ─────────────────────────────────────────────────────────────
const handleLoadMore = useCallback(
async (status: LeadStatus) => {
const nextPage = (columns[status].page ?? 1) + 1;
const result = await fetchMoreLeads(status, nextPage);
setColumns((prev) => ({
...prev,
[status]: {
...result,
docs: [...prev[status].docs, ...result.docs],
},
}));
},
[columns],
);
// ── DnD handlers ─────────────────────────────────────────────────────────
const handleDragStart = useCallback(({ active }: DragStartEvent) => {
isDragging.current = true;
activeId.current = active.id as number;
// Snapshot source column and active lead
setColumns((prev) => {
for (const col of COLUMNS) {
const lead = prev[col.id].docs.find((l) => l.id === active.id);
if (lead) {
sourceCol.current = col.id;
setActiveLead(lead);
break;
}
}
return prev;
});
}, []);
const handleDragOver = useCallback(
({ over }: DragOverEvent) => {
// If drag has ended, ignore — DnD-Kit fires one last dragOver after dragEnd
if (!isDragging.current) return;
if (!over) {
setOverColumn(null);
return;
}
// Only update the highlight — no state moves here.
// Don't highlight the source column itself, only foreign ones.
setColumns((prev) => {
const col = resolveColumn(over.id as string | number, prev);
setOverColumn(col !== sourceCol.current ? col : null);
return prev;
});
},
[setOverColumn],
);
const handleDragEnd = useCallback(
({ active, over }: DragEndEvent) => {
// Must be first — kills the spurious final onDragOver
isDragging.current = false;
setActiveLead(null);
setOverColumn(null);
const draggedId = active.id as number;
const from = sourceCol.current;
sourceCol.current = null;
activeId.current = null;
if (!over || !from) return;
const overId = over.id as string | number;
setColumns((prev) => {
const to = resolveColumn(overId, prev);
if (!to) return prev;
if (from === to) {
// ── Within-column reorder ──────────────────────────────────────────
// over.id is a card id (number), not a column id
if (isColumnId(overId)) return prev;
const docs = prev[from].docs;
const oldIdx = docs.findIndex((l) => l.id === draggedId);
const newIdx = docs.findIndex((l) => l.id === overId);
if (oldIdx < 0 || newIdx < 0 || oldIdx === newIdx) return prev;
const reordered = arrayMove(docs, oldIdx, newIdx);
const newOrder = orderKeyForIndex(reordered, newIdx);
const withOrder = reordered.map((l, i) =>
i === newIdx ? { ...l, _order: newOrder } : l,
);
updateLead(draggedId, { _order: newOrder }).catch(console.error);
return { ...prev, [from]: { ...prev[from], docs: withOrder } };
} else {
// ── Cross-column move ──────────────────────────────────────────────
const lead = prev[from].docs.find((l) => l.id === draggedId);
if (!lead) return prev;
// Remove from source
const newFromDocs = prev[from].docs.filter((l) => l.id !== draggedId);
// Insert into target: if over a card place before it, otherwise append
const toDocs = [...prev[to].docs];
const overIdx = isColumnId(overId)
? toDocs.length // dropped on empty column
: toDocs.findIndex((l) => l.id === overId); // dropped on a card
const insertIdx = overIdx >= 0 ? overIdx : toDocs.length;
toDocs.splice(insertIdx, 0, { ...lead, status: to });
const newOrder = orderKeyForIndex(toDocs, insertIdx);
toDocs[insertIdx] = { ...toDocs[insertIdx], _order: newOrder };
updateLead(draggedId, { status: to, _order: newOrder }).catch(
(err) => {
console.error("Failed to update lead:", err);
// TODO: roll back on failure
},
);
return {
...prev,
[from]: {
...prev[from],
docs: newFromDocs,
totalDocs: prev[from].totalDocs - 1,
},
[to]: {
...prev[to],
docs: toDocs,
totalDocs: prev[to].totalDocs + 1,
},
};
}
});
},
[setOverColumn],
);
// ─────────────────────────────────────────────────────────────────────────
return (
<div className={cn("w-full overflow-x-auto flex items-stretch", className)}>
<DndContext
sensors={sensors}
collisionDetection={(args) => {
const pointerHits = pointerWithin(args);
return pointerHits.length > 0 ? pointerHits : closestCorners(args);
}}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div className="flex gap-4 min-w-max items-stretch">
{COLUMNS.map((col) => (
<KanbanColumn
key={col.id}
column={col}
state={columns[col.id]}
isOver={activeLead !== null && overColumn === col.id}
users={users}
onLoadMore={() => handleLoadMore(col.id)}
onAssigneeChange={handleAssigneeChange}
/>
))}
</div>
<DragOverlay dropAnimation={{ duration: 120, easing: "ease" }}>
{activeLead && (
<div className="rotate-1 shadow-xl">
<LeadCard lead={activeLead} users={users} />
</div>
)}
</DragOverlay>
</DndContext>
</div>
);
}
"use client";
import {
Button,
DefaultListView,
Gutter,
Link,
ListHeader,
} from "@payloadcms/ui";
import { ComponentProps, useState } from "react";
import { Kanban } from "./kanban";
import { ListViewClientProps } from "payload";
import { cn } from "@heroui/react";
export default function ListViewClient({
initialColumns,
users,
...props
}: ListViewClientProps & ComponentProps<typeof Kanban>) {
const [mode, setMode] = useState<"kanban" | "table">("kanban");
return (
<div className="min-h-screen flex flex-col overflow-y-auto">
<div className="flex self-end mr-18 mt-4">
<Button
size="small"
buttonStyle="tab"
className={cn(
"my-0",
mode === "kanban" && "bg-zinc-200 dark:bg-zinc-800",
)}
onClick={() => setMode("kanban")}
>
Kanban
</Button>
<Button
size="small"
buttonStyle="tab"
className={cn(
"my-0",
mode === "table" && "bg-zinc-200 dark:bg-zinc-800",
)}
onClick={() => setMode("table")}
>
Tabelle
</Button>
</div>
{mode === "kanban" ? (
<Gutter className="flex-1 flex flex-col">
<header className="list-header mb-4">
<div className="list-header__content">
<div className="list-header__title-and-actions">
<h1 className="list-header__title">Leads</h1>
<div className="list-header__title-actions">
<Button
el="link"
buttonStyle="pill"
aria-label="Create new Lead"
size="small"
to={props.newDocumentURL}
>
Create New
</Button>
</div>
</div>
<div className="list-header__actions"></div>
</div>
<div className="list-header__after-header-content">
<div className="collection-list__sub-header"></div>
</div>
</header>
<Kanban
initialColumns={initialColumns}
className="flex-1 min-h-0"
users={users}
/>
</Gutter>
) : (
<DefaultListView {...props} />
)}
</div>
);
}
import ListViewClient from "@/components/admin/lead/list.client";
import { Lead } from "@payload-types";
import type { PaginatedDocs } from "payload";
import { ListViewServerProps } from "payload";
type LeadStatus = NonNullable<Lead["status"]>;
const STATUSES = ["new", "contact", "wait", "converted", "declined"] as const;
export default async function ListView({
payload,
Table,
collectionSlug,
columnState,
hasCreatePermission,
newDocumentURL,
viewType,
beforeActions,
disableBulkDelete,
disableBulkEdit,
disableQueryPresets,
hasDeletePermission,
enableRowSelections,
listMenuItems,
queryPreset,
queryPresetPermissions,
renderedFilters,
resolvedFilterOptions,
}: ListViewServerProps) {
const results = await Promise.all(
STATUSES.map((status) =>
payload
.find({
collection: "leads",
where: { status: { equals: status } },
limit: 10,
page: 1,
})
.then((res) => ({ status, result: res as PaginatedDocs<Lead> }))
.catch(() => ({
status,
result: {
docs: [],
hasNextPage: false,
hasPrevPage: false,
limit: 10,
page: 1,
pagingCounter: 1,
totalDocs: 0,
totalPages: 1,
} satisfies PaginatedDocs<Lead>,
})),
),
);
const initialColumns = Object.fromEntries(
results.map(({ status, result }) => [status, result]),
) as Record<LeadStatus, PaginatedDocs<Lead>>;
const users = await payload
.find({
collection: "users",
where: { roles: { in: ["admin", "consultant"] } },
pagination: false,
})
.then(({ docs }) => docs);
return (
<ListViewClient
initialColumns={initialColumns}
users={users}
Table={Table}
collectionSlug={collectionSlug}
columnState={columnState}
hasCreatePermission={hasCreatePermission}
newDocumentURL={newDocumentURL}
viewType={viewType}
beforeActions={beforeActions}
disableBulkDelete={disableBulkDelete}
disableBulkEdit={disableBulkEdit}
disableQueryPresets={disableQueryPresets}
hasDeletePermission={hasDeletePermission}
enableRowSelections={enableRowSelections}
listMenuItems={listMenuItems}
queryPreset={queryPreset}
queryPresetPermissions={queryPresetPermissions}
renderedFilters={renderedFilters}
resolvedFilterOptions={resolvedFilterOptions}
/>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment