QUESTPIE
Admin

Collections and globals

The admin module adds five methods to a collection and two to a global. They decide what the panel calls a resource, which columns its list shows, how its form is laid out, and what a person can do to a row.

View markdown

Your collection already has a list and a form. You never asked for them. So every method on this page is a correction, not a requirement. Reach for one when the sidebar reads blogPosts, or when the form is one long stack of inputs.

The five methods

@questpie/admin adds them to the generated collection() and global() factories. They exist only while the module is registered. Each one stores a single key on the builder, so a second call replaces the first.

MethodWhat it decidesOn a global
.admin(config)The name, icon, sidebar group and sort orderYes
.list(({ v, f, a, c }) => …)The list renderer and everything on itNo
.form(({ v, f }) => …)The create and the edit formYes
.preview(config)A live preview pane beside the formNo
.actions(({ a, c, f }) => …)Buttons of your own, run on the serverNo

None of them is access control. Admin config arranges the UI. .access() decides who may read and write, and it still runs underneath every screen here.

One collection, configured

src/questpie/server/collections/posts.ts
import { collection } from "#questpie/factories";

export const posts = collection("posts")
	.fields(({ f }) => ({
		title: f.text(255).label("Title").required(),
		slug: f.text(120).label("Slug").required(),
		status: f
			.select([
				{ value: "draft", label: "Draft" },
				{ value: "published", label: "Published" },
			])
			.label("Status")
			.default("draft"),
		body: f.richText().label("Body"),
		cover: f.upload({ to: "assets" }).label("Cover image"),
	}))
	.title(({ f }) => f.title)
	.admin(({ c }) => ({
		label: "Posts",
		description: "Everything the blog publishes",
		icon: c.icon("ph:article"),
		group: "content",
		order: 10,
	}))
	.list(({ v, f }) =>
		v.collectionTable({
			columns: [f.status, "updatedAt"],
			defaultSort: { field: "updatedAt", direction: "desc" },
		}),
	)
	.form(({ v, f }) =>
		v.collectionForm({
			sidebar: { fields: [f.status, f.slug, f.cover] },
			fields: [
				{ type: "section", label: "Content", fields: [f.title, f.body] },
			],
		}),
	);
questpie generate

/admin/collections/posts now sits under a Content heading, titled Posts, with an article icon. Its list shows the title, the status and when the row last changed, newest first. Its form has one Content section and a right sidebar holding status, slug and cover.

The four proxies

The callbacks hand you proxies, not strings you type by hand.

f is the field names. f.title evaluates to "title", and TypeScript narrows it to the keys you declared in .fields(). Rename a field and the compiler points at every place that used it.

v picks the renderer. .list() offers v.collectionTable() and v.listView(). .form() offers v.collectionForm(), and a global offers v.globalForm(). Your own views join the same proxy. Skip v and return a plain object, and you get the default renderer for that method.

c builds a serializable component reference. c.icon("ph:article") becomes { type: "icon", props: { name: "ph:article" } }. The client resolves that from its registry. c.badge({ text, color }) is the other built-in. A name the registry does not have is a type error, not a runtime one.

a is actions. A built-in reference is a function that also serializes to its own name, so .list() takes either a.delete or a.delete(). The builtin list in .actions() compares strings, so call it there: a.delete().

Every callback runs once, when the file loads. No request is in scope.

Columns add, form fields replace

These two look symmetrical and are not.

.list({ columns }) is additive. The title column is always first, and your columns follow it. Everything else you declared stays in the column picker, so a person can still switch it on for themselves.

.form({ fields }) is exhaustive. A field you leave out of the layout is not on the form, and nobody can put it back. Sidebar fields are pulled out of the main column automatically, so listing one in both places renders it once.

Naming and placement

.admin() is metadata. It never touches the database.

KeyEffect
labelThe sidebar name. Unset, the panel title-cases the key.
descriptionOne line under the heading on the list screen.
iconA c.icon(…) reference.
groupPuts the resource in its own sidebar section.
orderSorts within that section. Missing counts as 0.
hiddenDrops it from the sidebar. The URL still answers.
auditfalse keeps it out of the audit log. See Audit.

label and description take a string, a { en, sk } locale map, or a { key, fallback } translation reference.

How a group becomes a section

Collections with no group land in one Content section. Each distinct group value becomes its own section below that, titled from the value with the first letter capitalised. Sections sort alphabetically by group name, items inside sort by order. Globals always get their own Globals section last.

That is the sidebar you get for free. Writing an explicit one in config/admin.ts replaces the whole arrangement, and hidden still applies. See Configuration.

Globals

A global holds one row, so it has one screen. It takes .admin() and .form(), with the same keys and the same layout vocabulary.

src/questpie/server/globals/site-settings.ts
import { global } from "#questpie/factories";

export const siteSettings = global("siteSettings")
	.fields(({ f }) => ({
		siteName: f.text(120).label("Site name").required(),
		socialImage: f.upload({ to: "assets" }).label("Default social image"),
	}))
	.admin(({ c }) => ({ label: "Site settings", icon: c.icon("ph:gear-six") }))
	.form(({ v, f }) =>
		v.globalForm({
			fields: [
				{
					type: "section",
					label: "General",
					fields: [f.siteName, f.socialImage],
				},
			],
		}),
	);

Import from the generated factory

collection() and global() come from #questpie/factories, not from questpie. Only the generated factory knows which modules are on. Only it carries these five methods, and field types such as f.richText().

Where each topic lives

TopicPage
Columns, sorting, filters, grouping, reorderingThe list
Sections, tabs, sidebars, fields that reactThe form
Your own buttons and the handlers behind themActions
A live page beside the formPreview
Writing a renderer of your ownCustom views
Sidebar, dashboard, branding, mountingConfiguration
Who may sign in at allAuthentication
Rules that allow, deny or filterAccess control
The builder these methods extendCollections

Next

The list is the screen most people spend their day on, and .list() is the method with the most in it.

On this page