QUESTPIE

Custom Views

Create custom list and form view types for the admin panel.

Create custom view types beyond the built-in table and form views -- kanban boards, calendars, galleries, or domain-specific layouts.

Overview

A custom view requires two parts:

  1. View definition -- register the view using the view() factory with a kind discriminant
  2. React component -- the actual rendering logic

Views have two kinds:

  • "list" -- displayed on the collection list page (e.g., table, kanban, gallery)
  • "form" -- displayed on the collection edit/create page (e.g., form, builder)

Defining a Custom View

File Convention

Create a view definition file using the view() factory:

src/admin/views/kanban.ts
import { view } from "@questpie/admin/client";

export default view("kanban", {
	kind: "list",
	component: () => import("./kanban-view.js"),
});

After codegen, this view is registered in the admin client and available for use.

React Component

src/admin/views/kanban-view.tsx
import type { CollectionListViewProps } from "@questpie/admin/client";

type KanbanItem = {
	id: string;
	_title?: string;
} & Record<string, unknown>;

export function KanbanView({ config, onRowClick }: CollectionListViewProps) {
	const data = (config?.data ?? []) as KanbanItem[];
	const statusColumn = config?.groupBy || "status";
	const groups = groupByField(data, statusColumn);

	return (
		<div style={{ display: "flex", gap: 16, overflowX: "auto" }}>
			{Object.entries(groups).map(([status, items]) => (
				<div key={status} style={{ minWidth: 280, flex: "0 0 280px" }}>
					<h3 style={{ fontWeight: 600, marginBottom: 8 }}>
						{status} ({items.length})
					</h3>
					<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
						{items.map((item) => (
							<div
								key={item.id}
								onClick={() => onRowClick?.(item)}
								style={{
									padding: 12,
									borderRadius: 8,
									border: "1px solid #e5e7eb",
									cursor: "pointer",
								}}
							>
								<div style={{ fontWeight: 500 }}>{item._title}</div>
							</div>
						))}
					</div>
				</div>
			))}
		</div>
	);
}

function groupByField(data: KanbanItem[], field: string) {
	const groups: Record<string, KanbanItem[]> = {};
	for (const item of data) {
		const value = item[field];
		const key =
			typeof value === "string" || typeof value === "number"
				? String(value)
				: "unknown";
		(groups[key] ??= []).push(item);
	}
	return groups;
}

Using Custom Views

Once registered, reference the view in your collection's .list() configuration:

collection("tasks")
	.fields(({ f }) => ({
		title: f.text().required(),
		status: f.select([
			{ value: "todo", label: "Todo" },
			{ value: "in-progress", label: "In Progress" },
			{ value: "done", label: "Done" },
		]),
		assignee: f.relation("users"),
	}))
	.list(({ v }) =>
		v.kanban({
			groupBy: "status",
		}),
	);

The view configuration object is passed to your component as config in the view context.


Custom Form Views

Form views work the same way but use kind: "form":

src/admin/views/page-builder.ts
import { view } from "@questpie/admin/client";

export default view("page-builder", {
	kind: "form",
	component: () => import("./page-builder-view.js"),
});
src/admin/views/page-builder-view.tsx
import type { CollectionFormViewProps } from "@questpie/admin/client";

export function PageBuilderView(props: CollectionFormViewProps) {
	return <div>{/* Custom drag-and-drop page builder UI */}</div>;
}

Use in a collection:

.form(({ v }) => v.pageBuilder({
  // Custom config
}))

Module Registration

To distribute custom views as part of a module:

import { module } from "questpie/app";
export const kanbanModule = module({
	name: "kanban-views",
	// Views are discovered by file convention through the admin plugin
	// The module can include the view files in its package
});

The admin plugin's codegen automatically discovers view files in the configured directories.


On this page