Skip to main content
zerotal

Documentation


Documentation / @zerotal/admin / index / Resource

Abstract Class: Resource

Defined in: admin/src/Resource.ts:142

@zerotal/admin — a declarative, server-driven admin panel built on @zerotal/flow (reactivity) and @zerotal/flow-ui (components).

Quick start:

// bootstrap/providers.ts import { FlowProvider } from "@zerotal/flow"; import { AdminProvider } from "@zerotal/admin"; export default [FlowProvider, AdminProvider];

// app/admin.ts import { Panel, Resource, text } from "@zerotal/admin"; import { User } from "./models/User.ts";

Panel.configure({ brand: "Acme", path: "/admin" });

class UserResource extends Resource { static model = User; static navigationIcon = "users"; static navigationGroup = "Access"; static columns() { return [ text("id").sortable(), text("name").searchable().sortable(), text("email").searchable(), text("role").badge((v) => (v === "admin" ? "primary" : "muted")), ]; } }

Panel.register(UserResource);

The default UI ships with light + dark mode out of the box (Tailwind via CDN for now; swap to a real build later by editing theme.ts only).

Constructors

Constructor

new Resource(): Resource

Returns

Resource

Properties

model

static model: AdminModel

Defined in: admin/src/Resource.ts:144

The ORM model class this resource manages.


slug?

static optional slug?: string

Defined in: admin/src/Resource.ts:147

URL slug; defaults to a kebab-cased plural of the model name.


label?

static optional label?: string

Defined in: admin/src/Resource.ts:149

Singular label; defaults to the model name.


pluralLabel?

static optional pluralLabel?: string

Defined in: admin/src/Resource.ts:151

Plural label; defaults to the pluralized label.


cluster?

static optional cluster?: typeof Cluster

Defined in: admin/src/Resource.ts:157

The Cluster this resource belongs to. Members share the cluster's URL segment and sit under one sidebar entry.


parent?

static optional parent?: object

Defined in: admin/src/Resource.ts:171

Nest this resource under a parent record, so its pages live at /admin/posts/7/comments rather than /admin/comments. Every list query is scoped to the parent by foreignKey, and new records inherit it.

static parent = { resource: () => PostResource, foreignKey: "post_id" };

The parent is named by a function because the two resources almost always reference each other — the parent lists the child as a relation, the child names the parent here — and a direct reference would resolve to undefined on whichever side the module cycle happened to evaluate first.

resource

resource: () => typeof Resource

Returns

typeof Resource

foreignKey

foreignKey: string


singular

static singular: boolean = false

Defined in: admin/src/Resource.ts:184

Back a single row rather than a collection — site settings, a company profile. The resource mounts one route (/admin/settings) that opens the edit form directly; there is no list, no create page and no record id. The row is resolved by singularRecord, which creates it on first use.


static navigationIcon: string = "collection"

Defined in: admin/src/Resource.ts:187

Navigation icon key (see ui/icons.ts).


static optional navigationGroup?: string

Defined in: admin/src/Resource.ts:189

Optional sidebar group heading.


static optional navigationParentItem?: string

Defined in: admin/src/Resource.ts:191

Nest this item under another resource's nav label.


static navigationSort: number = 0

Defined in: admin/src/Resource.ts:193

Sort order within the sidebar (lower = higher).


static navigationBadgeColor: "success" | "primary" | "destructive" | "muted" = "primary"

Defined in: admin/src/Resource.ts:196

Tone for the sidebar navigation badge.


perPage

static perPage: number = 15

Defined in: admin/src/Resource.ts:210

Default page size for the list table.


primaryKey

static primaryKey: string = "id"

Defined in: admin/src/Resource.ts:213

Primary-key column, used to build View links and to find/delete records.


defaultSort?

static optional defaultSort?: object

Defined in: admin/src/Resource.ts:216

Initial sort applied when the URL doesn't specify one.

column

column: string

direction?

optional direction?: "asc" | "desc"


eager

static eager: string[] = []

Defined in: admin/src/Resource.ts:219

Relations to eager-load for list + view (so columns/entries can read them).


defaultGroup?

static optional defaultGroup?: string

Defined in: admin/src/Resource.ts:237

Grouping applied by default (a column key from groups).


reorderable?

static optional reorderable?: string

Defined in: admin/src/Resource.ts:244

Enable drag-style row reordering by persisting a position column. Set to the integer column that stores order (e.g. "sort"); the list page then shows up/down reorder controls and orders by this column.


recordTitleAttribute?

static optional recordTitleAttribute?: string

Defined in: admin/src/Resource.ts:247

Column used to title a record in global search, breadcrumbs, etc.


history

static history: boolean = false

Defined in: admin/src/Resource.ts:320

Show a history of changes on the record page, read from @zerotal/audit.

Requires the model to compose Auditable; without it there is nothing recorded to show. An update can be put back from there.


impersonatable

static impersonatable: boolean = false

Defined in: admin/src/Resource.ts:327

Allow operators to act as one of these records — a user resource, in practice. Off by default: can() defaults to allowing, which is the wrong default for becoming somebody else, so this carries the refusal.


optimisticLock?

static optional optimisticLock?: string

Defined in: admin/src/Resource.ts:336

Guard against two people saving the same record over each other.

Names the column holding the row's version — updated_at in almost every schema. The edit form carries the value it loaded, and a save whose value no longer matches is refused rather than silently overwriting the other change.


treeParentColumn?

static optional treeParentColumn?: string

Defined in: admin/src/Resource.ts:344

Render this resource as a tree, nesting each record under its parent.

Names the self-referencing column. The list orders and indents by depth, so categories or an org chart read as the shape they are.


translatable

static translatable: string[] = []

Defined in: admin/src/Resource.ts:352

Fields stored per locale, as { en: "…", fr: "…" } JSON columns.

The form and table show one locale at a time, switched from the list; the stored shape is unchanged, so nothing outside the panel has to know.


locales

static locales: string[]

Defined in: admin/src/Resource.ts:355

Locales offered when a resource is translatable.


tableLayout

static tableLayout: "table" | "calendar" | "grid" | "kanban" = "table"

Defined in: admin/src/Resource.ts:366

How the list renders its records.

"table" is right for data you scan and compare. "grid" suits records you recognise by sight — products, media, people — where a thumbnail and a name beat a row of columns.


kanbanColumn?

static optional kanbanColumn?: string

Defined in: admin/src/Resource.ts:369

Column a kanban board groups its lanes by — a status, usually.


kanbanLanes

static kanbanLanes: Record<string, string> = {}

Defined in: admin/src/Resource.ts:372

Lane order and labels for a kanban board, keyed by the column's values.


calendarColumn?

static optional calendarColumn?: string

Defined in: admin/src/Resource.ts:375

Date column a calendar lays records out on.


striped

static striped: boolean = false

Defined in: admin/src/Resource.ts:378

Shade alternating rows. Helps the eye track across a wide table.


static stickyHeader: boolean = false

Defined in: admin/src/Resource.ts:381

Keep the header visible while the body scrolls.


density

static density: "comfortable" | "compact" = "comfortable"

Defined in: admin/src/Resource.ts:384

Row height. "compact" fits noticeably more on screen.


filterLayout

static filterLayout: "inline" | "panel" | "drawer" = "inline"

Defined in: admin/src/Resource.ts:393

Where the filters sit.

"inline" keeps them above the table, which is fine for two or three. "panel" collapses them behind a Filters button, and "drawer" slides them in from the side — both worth it once filters outnumber the space for them.

Methods

parentResource()

static parentResource(): typeof Resource | undefined

Defined in: admin/src/Resource.ts:174

The parent resource, resolved.

Returns

typeof Resource | undefined


static navigationBadge(): string | number | Promise<string | number | null> | null

Defined in: admin/src/Resource.ts:205

A count or label shown beside this item in the sidebar. Return null/undefined for no badge. Resolved on each render, so cache an expensive count rather than paying for it on every page.

static async navigationBadge() { return this.count(); }

Returns

string | number | Promise<string | number | null> | null


tabs()

static tabs(): Tab[]

Defined in: admin/src/Resource.ts:222

List-page filter tabs. Override to add them.

Returns

Tab[]


filters()

static filters(): Filter[]

Defined in: admin/src/Resource.ts:227

List-page filters. Override to add them.

Returns

Filter[]


groups()

static groups(): Group[]

Defined in: admin/src/Resource.ts:232

Row groupings offered on the list table.

Returns

Group[]


recordTitle()

static recordTitle(record): string

Defined in: admin/src/Resource.ts:250

Resolve a human-readable title for a record.

Parameters

record

Record<string, unknown>

Returns

string


globallySearchable()

static globallySearchable(): boolean

Defined in: admin/src/Resource.ts:257

Whether this resource participates in global search (has searchable columns).

Returns

boolean


relations()

static relations(): RelationManager[]

Defined in: admin/src/Resource.ts:262

Relation managers shown on the View page.

Returns

RelationManager[]


columns()

static columns(): Column[]

Defined in: admin/src/Resource.ts:267

Table columns. Override in the subclass.

Returns

Column[]


infolist()

static infolist(): InfolistComponent[]

Defined in: admin/src/Resource.ts:276

View-page schema — an infolist: an ordered list of Sections and/or entries. Override to customize the detail page; when left empty it falls back to a single section derived from columns().

Returns

InfolistComponent[]


form()

static form(): FormComponent[]

Defined in: admin/src/Resource.ts:285

Form schema for the Create/Edit pages: an ordered list of Fields. Use .visibleOn("create") / .hiddenOn("edit") to vary a field by page. An empty list disables Create/Edit for the resource.

Returns

FormComponent[]


isEditable()

static isEditable(): boolean

Defined in: admin/src/Resource.ts:290

Whether this resource exposes Create/Edit pages (any fields defined).

Returns

boolean


recordActions()

static recordActions(): ActionItem[]

Defined in: admin/src/Resource.ts:300

Per-row actions. Defaults to View + Edit + Delete; override to customize.

Returns

ActionItem[]


headerActions()

static headerActions(): ActionItem[]

Defined in: admin/src/Resource.ts:305

Actions shown above the table (defaults to a Create button).

Returns

ActionItem[]


bulkActions()

static bulkActions(): ActionItem[]

Defined in: admin/src/Resource.ts:310

Actions applied to the selected rows (defaults to bulk Delete).

Returns

ActionItem[]


widgets()

static widgets(): DashboardWidget[]

Defined in: admin/src/Resource.ts:407

Widgets shown above this resource's table, and on its record pages.

The dashboard answers "how is the business doing"; these answer "what is going on in this list" — a pending count above the orders table, a revenue chart above products. Same widget builders as the dashboard, so .poll() works here too.

static widgets() { return [statsWidget(async () => [stat("Pending", await Order.pending())])]; }

Returns

DashboardWidget[]


emptyState()

static emptyState(): EmptyState

Defined in: admin/src/Resource.ts:429

What a user sees instead of a table when there is nothing to show.

A blank table teaches nobody anything. Override this to say why the list is empty and what to do about it — the difference between "No records" and "No orders yet. They'll appear here once a customer checks out."

static emptyState() { return { heading: "No orders yet", description: "Orders appear here as soon as a customer checks out.", icon: "inbox", }; }

A search or filter that matches nothing gets a different, automatic message — this is for a genuinely empty resource.

Returns

EmptyState


can()

static can(_ability, _record?): boolean

Defined in: admin/src/Resource.ts:447

Authorization gate (policies). Returns true by default; override to enforce permissions, e.g. delegate to @zerotal/auth's Gate:

static can(ability: string, record?: AdminRecord) { return Gate.allows(ability, record ?? this.model); }

Abilities used by the built-in actions: create, update, delete, restore, forceDelete.

Parameters

_ability

string

_record?

AdminRecord

Returns

boolean


mutateFormDataBeforeFill()

static mutateFormDataBeforeFill(data): AdminRecord

Defined in: admin/src/Resource.ts:454

Transform a record into form state before the Edit form is filled.

Parameters

data

AdminRecord

Returns

AdminRecord


mutateBeforeSave()

static mutateBeforeSave(data, _mode): AdminRecord

Defined in: admin/src/Resource.ts:459

Transform validated form data just before create/update.

Parameters

data

AdminRecord

_mode

"create" | "edit"

Returns

AdminRecord


afterSave()

static afterSave(_record, _mode): Promise<void>

Defined in: admin/src/Resource.ts:464

Hook fired after a successful create/update (e.g. sync relations).

Parameters

_record

AdminRecord

_mode

"create" | "edit"

Returns

Promise<void>


getModelName()

static getModelName(): string

Defined in: admin/src/Resource.ts:468

Returns

string


getLabel()

static getLabel(): string

Defined in: admin/src/Resource.ts:472

Returns

string


getPluralLabel()

static getPluralLabel(): string

Defined in: admin/src/Resource.ts:476

Returns

string


getSlug()

static getSlug(): string

Defined in: admin/src/Resource.ts:480

Returns

string


arrangeTree()

static arrangeTree(rows): object[]

Defined in: admin/src/Resource.ts:493

Order rows so each sits under its parent, and report how deep each one is.

Done in memory over the page's rows rather than in SQL: a recursive CTE is the right answer for a deep tree, but it is not portable across the drivers the panel supports, and a tree small enough to browse is small enough to arrange here. Orphans — rows whose parent is not in the set — are kept at the top level rather than dropped, so a filtered tree never hides records.

Parameters

rows

Record<string, unknown>[]

Returns

object[]


translated()

static translated(value, locale): unknown

Defined in: admin/src/Resource.ts:536

Read a translatable field for one locale.

A translatable column stores { en: "…", fr: "…" }; a value that was never translated is returned as-is, so turning translation on for an existing column does not blank it.

Parameters

value

unknown

locale

string

Returns

unknown


routePath()

static routePath(): string

Defined in: admin/src/Resource.ts:551

The route pattern for this resource's index, relative to the panel base and with any parent id still a :param placeholder.

Returns

string


parentParam()

static parentParam(): string

Defined in: admin/src/Resource.ts:562

Route-parameter name carrying the parent record's id, for a nested resource.

Returns

string


indexUrl()

static indexUrl(base, parentId?): string

Defined in: admin/src/Resource.ts:570

This resource's index URL under a panel base. Nested resources need the parent record's id; passing none leaves the placeholder in place.

Parameters

base

string

parentId?

unknown

Returns

string


recordUrl()

static recordUrl(base, id, parentId?): string

Defined in: admin/src/Resource.ts:579

Parameters

base

string

id

unknown

parentId?

unknown

Returns

string


createUrl()

static createUrl(base, parentId?): string

Defined in: admin/src/Resource.ts:583

Parameters

base

string

parentId?

unknown

Returns

string


editUrl()

static editUrl(base, id, parentId?): string

Defined in: admin/src/Resource.ts:587

Parameters

base

string

id

unknown

parentId?

unknown

Returns

string


singularRecord()

static singularRecord(): Promise<Record<string, unknown> | null>

Defined in: admin/src/Resource.ts:597

Resolve the single row a singular resource edits, creating it from the form's defaults when it doesn't exist yet.

Returns

Promise<Record<string, unknown> | null>


searchableColumns()

static searchableColumns(): string[]

Defined in: admin/src/Resource.ts:609

Database columns flagged .searchable() (honours .column() overrides).

Returns

string[]


data()

static data(): Record<string, unknown>[] | Promise<Record<string, unknown>[] | null> | null

Defined in: admin/src/Resource.ts:633

Rows for a resource that isn't backed by an ORM model — an external API, a config file, a computed report.

Return the full set; the panel filters, sorts and paginates it in memory, so search, tabs, summaries and the query builder keep working. Returning null (the default) means "use model", which is the normal case.

static async data() { return (await fetch("https://api.example.com/regions").then((r) => r.json())); }

Writes are a separate question: a read-only source needs no form(), and a writable one overrides create, update and destroy to push changes back wherever they belong.

Returns

Record<string, unknown>[] | Promise<Record<string, unknown>[] | null> | null


records()

static records(options?): Promise<RecordPage>

Defined in: admin/src/Resource.ts:641

Load a page of records honoring search, sort, and pagination. Defensive about the ORM surface so resources keep working under partial mocks/tests.

Parameters

options?

ListOptions = {}

Returns

Promise<RecordPage>


listAll()

static listAll(options?): Promise<Record<string, unknown>[]>

Defined in: admin/src/Resource.ts:716

Load all rows matching the current scope (search + tab/filters + trashed), with no pagination — used for column summaries and reorder swaps. Mirrors the scoping of records but skips limit/offset.

Parameters

options?

ListOptions = {}

Returns

Promise<Record<string, unknown>[]>


count()

static count(modifyQuery?): Promise<number>

Defined in: admin/src/Resource.ts:769

Count records, optionally scoped by a query modifier (used for tab badges).

Parameters

modifyQuery?

QueryModifier

Returns

Promise<number>


find()

static find(id): Promise<Record<string, unknown> | null>

Defined in: admin/src/Resource.ts:785

Load a single record by primary key, as a plain row (or null). Uses the query builder so the row shape matches records; falls back to the in-memory model surface used by tests/mocks.

Parameters

id

unknown

Returns

Promise<Record<string, unknown> | null>


usesSoftDeletes()

static usesSoftDeletes(): boolean

Defined in: admin/src/Resource.ts:813

Whether this resource's model uses soft deletes.

Returns

boolean


restore()

static restore(id): Promise<boolean>

Defined in: admin/src/Resource.ts:832

Restore a soft-deleted record. Returns true when a record was restored.

Parameters

id

unknown

Returns

Promise<boolean>


forceDelete()

static forceDelete(id): Promise<boolean>

Defined in: admin/src/Resource.ts:840

Permanently delete a (possibly soft-deleted) record.

Parameters

id

unknown

Returns

Promise<boolean>


destroy()

static destroy(id): Promise<boolean>

Defined in: admin/src/Resource.ts:854

Permanently delete a record by primary key. Prefers loading the model instance and calling its delete() (so soft-deletes / model hooks run); returns true when a matching record was found and removed.

Parameters

id

unknown

Returns

Promise<boolean>


create()

static create(data): Promise<AdminRecord | null>

Defined in: admin/src/Resource.ts:866

Create a record from form data. Returns the new record (with its id).

Parameters

data

Record<string, unknown>

Returns

Promise<AdminRecord | null>


update()

static update(id, data): Promise<boolean>

Defined in: admin/src/Resource.ts:875

Update a record by primary key from form data. Returns true on success.

Parameters

id

unknown

data

Record<string, unknown>

Returns

Promise<boolean>