# Collections and globals (/docs/admin/collections)

---
title: Collections and globals
description: 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.
kind: guide
package: "@questpie/admin"
---

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.

| Method                         | What it decides                              | On a global |
| ------------------------------ | -------------------------------------------- | ----------- |
| `.admin(config)`               | The name, icon, sidebar group and sort order | Yes         |
| `.list(({ v, f, a, c }) => …)` | The list renderer and everything on it       | No          |
| `.form(({ v, f }) => …)`       | The create and the edit form                 | Yes         |
| `.preview(config)`             | A live preview pane beside the form          | No          |
| `.actions(({ a, c, f }) => …)` | Buttons of your own, run on the server       | No          |

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

```ts title="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] },
			],
		}),
	);
```

```bash
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.

| Key           | Effect                                                                 |
| ------------- | ---------------------------------------------------------------------- |
| `label`       | The sidebar name. Unset, the panel title-cases the key.                |
| `description` | One line under the heading on the list screen.                         |
| `icon`        | A `c.icon(…)` reference.                                               |
| `group`       | Puts the resource in its own sidebar section.                          |
| `order`       | Sorts within that section. Missing counts as `0`.                      |
| `hidden`      | Drops it from the sidebar. The URL still answers.                      |
| `audit`       | `false` keeps it out of the audit log. See [Audit](/docs/admin/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](/docs/admin/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.

```ts title="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],
				},
			],
		}),
	);
```

<Callout type="warn" title="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()`.
</Callout>

## Where each topic lives

| Topic                                           | Page                                          |
| ----------------------------------------------- | --------------------------------------------- |
| Columns, sorting, filters, grouping, reordering | [The list](/docs/admin/collections/list)      |
| Sections, tabs, sidebars, fields that react     | [The form](/docs/admin/collections/form)      |
| Your own buttons and the handlers behind them   | [Actions](/docs/admin/collections/actions)    |
| A live page beside the form                     | [Preview](/docs/admin/collections/preview)    |
| Writing a renderer of your own                  | [Custom views](/docs/admin/custom-views)      |
| Sidebar, dashboard, branding, mounting          | [Configuration](/docs/admin/configuration)    |
| Who may sign in at all                          | [Authentication](/docs/admin/auth)            |
| Rules that allow, deny or filter                | [Access control](/docs/schema/access-control) |
| The builder these methods extend                | [Collections](/docs/schema/collections)       |

## Next

**[The list](/docs/admin/collections/list)** is the screen most people spend
their day on, and `.list()` is the method with the most in it.
