Pages
Add pages to the staff or client dashboard, from a ready-made table to your own React view.
A page class adds a page to the dashboard sidebar. Most pages describe a table and a form and let Kora build them. For anything else, add a React view next to the page.
Put each page in its own folder under resources/dashboard/pages/, with the class in page.js.
import { Pages } from "#kora/modules";
export class ItemsPage extends Pages {
constructor(kora) {
super(kora, {
name: "items",
label: "Items",
description: "Everything the example module syncs.",
icon: "box",
group: "licensing",
order: 7,
component: "resource",
permission: "items.view",
config: {
endpoint: "/items",
title: "name",
columns: [
{ key: "name" },
{ key: "sku", label: "SKU", format: "code" },
{ key: "price", format: "money" },
{ key: "status", format: "badge", tones: { active: "success", paused: "warning" } },
{ key: "updated_at", label: "Updated", format: "relative" }
],
fields: [
{ key: "name", required: true },
{ key: "sku", label: "SKU", required: true, edit: false },
{ key: "price", type: "number", default: 0 },
{ key: "status", type: "select", options: ["active", "paused"] }
],
remove: false
}
});
}
}| Option | Default | What it does |
|---|---|---|
name | Required | The page ID, used in its URL. |
label | name | The sidebar label and page title. |
description | "" | A line under the title. |
icon | "puzzle" | A Lucide icon name. |
group | "Modules" | The sidebar group: general, licensing, sales, security, automation, team or system. |
order | 0 | Sort order inside the group. Lower comes first. |
component | "records" | resource, stats, records, or the name of your own view. |
audience | "staff" | staff or customer. See Client Pages. |
permission | <module>.view | The permission needed to see the page. |
config | {} | Options for the component. |
Pages are only listed when their module is turned on and the member has the permission.
Resource Pages
component: "resource" builds a searchable, paged table with create, edit, detail and delete, all backed by your routes:
| Request | When |
|---|---|
GET <endpoint>?limit=&offset=&search= | Listing. Return { rows, total }. Filters are added as more query values. |
POST <endpoint> | Create. |
PATCH <endpoint>/<key> | Save an edit. |
DELETE <endpoint>/<key> | Delete. |
Requests go to your module's routes as the signed-in member, so your route permissions still apply.
| Config | Default | What it does |
|---|---|---|
endpoint | Required | The route the table uses. |
module | This module | Use another module's routes. |
key | "id" | The column that identifies a row. |
title | key | The column used as the detail view's title. |
columns | Required | The table columns. |
fields | [] | The create and edit form. |
filters | [] | Dropdown filters, as { key, label, options }. |
actions | [] | Extra buttons. See below. |
sections | [] | Related tables in the detail view. |
create, edit, detail, remove | true | Set to false to turn that part off. |
search | true | Show the search box. |
export | true | Show a CSV export button. |
pageSize | 25 | Rows per page. |
poll | Off | Refresh every this many seconds. |
empty | { title, hint } for an empty table. |
Columns
| Option | What it does |
|---|---|
key | The value to show. Dots read nested values, such as product.name. |
label | The heading. Defaults to the key in title case. |
format | How to show it. See below. |
tones | Badge colours by value: default, success, warning, danger, info or muted. |
align | left, right or center. |
hidden | Only show it in the detail view. |
Formats: text, code, date, datetime, relative, badge, boolean, number, tags, json, link, bytes, money and duration.
Fields
| Option | What it does |
|---|---|
key, label | The value and its label. |
type | string, text, number, boolean, select, multiselect, tags, date, datetime, json, password, reference, references, file or hidden. |
options | Choices for select, as strings or { label, value, image }. image is a URL or data URI drawn at 16px before the label with no frame, such as a store logo. |
source | Choices for reference and references, loaded from a route: { endpoint, label, value, hint }. Use :field in the endpoint to filter by another field. |
required, default, placeholder | As you would expect. |
create, edit | Set to false to hide the field on that form. |
generate | A route that returns a new value, such as a random key, shown as a button. |
empty | Text for a source that returns nothing. |
visibleWhen | { field, value }, { field, in } or { field, not }. Checked against the form values and the record being edited, so a field can depend on one that cannot be edited. |
states | Labels for a boolean's two states. |
accept | Allowed file types for file. A form with a file is sent as multipart. |
Actions
actions: [
{ name: "resync", label: "Resync", endpoint: "/items/:id/resync", method: "POST" },
{ name: "archive", label: "Archive", endpoint: "/items/archive", method: "POST", scope: "bulk", tone: "danger", confirm: "Archive the selected items?" },
{ name: "import", label: "Import", endpoint: "/items/import", scope: "page", fields: [{ key: "file", type: "file", accept: ".csv", required: true }] }
]| Option | What it does |
|---|---|
scope | row on each row and in the detail view, bulk for selected rows (sent as ids), page at the top. Defaults to row. |
endpoint, method | The request. :field is replaced from the row. method defaults to POST. |
fields | Ask for values first. |
body | Fixed values to send. |
confirm | Ask before running. |
tone | danger for a red button. |
when | Only show it when { field, in: [...] } matches the row. |
The route's response can drive what happens next:
| Return | Result |
|---|---|
{ message } | A toast. |
{ secret } or { reveal: [{ label, value }] } | A one-time dialog with copy buttons, for keys and tokens. |
{ download: { filename, content, type } } | A file download. |
{ url } | Opens the link in a new tab. |
Sections
Sections are tables inside the detail view, for related records:
sections: [
{ name: "history", label: "History", endpoint: "/items/:id/history", columns: [{ key: "created_at", format: "datetime" }, { key: "event", format: "badge" }], empty: "Nothing yet" }
]Sections take their own columns, actions and empty.
Stats and Records
component: "stats" shows a grid of cards from { stats: [{ label, value, hint }] }. records shows a plain table from { rows }, with optional columns. Both get their data from the page's load().
Loading Data
load() runs on the server when the page opens. By default it calls GET on config.endpoint in your module. Override it to return anything:
async load({ client, session, params }) {
const stats = await client.send("GET", "/api/modules/example/stats");
return { available: true, data: { stats: [{ label: "Synced", value: stats.count }] } };
}| Context | What it is |
|---|---|
client.send(method, path, body) | Calls Kora's API as the signed-in member, so their permissions apply. |
session | The signed-in member. |
params | URL segments after the page name, such as ["42"] for /m/example/items/42. |
Return { available: false } to show an error state, or { notFound: true } for a 404.
Your Own View
Add a view.tsx next to page.js. Kora compiles it when the module loads and uses it in place of the built-in component:
import { Card, CardContent } from "@/components/ui/card";
import { useT } from "@/components/i18nProvider";
import type { ViewProps } from "@/components/pages/types";
export default function ItemsView({ data, params, config, module }: ViewProps) {
const { t } = useT();
const rows = data && typeof data === "object" && "rows" in data ? (data as { rows: unknown[] }).rows : [];
return (
<Card>
<CardContent className="p-4 text-sm">{t("page.example.items.count", { count: rows.length }, `${rows.length} items`)}</CardContent>
</Card>
);
}| Prop | What it is |
|---|---|
data | What load() returned. |
params | The URL segments after the page name. |
config | The page's config. |
module | Your module ID. |
Views can import react, react-router, anything from the dashboard under @/, such as @/components/ui/*, @/lib/api and @/components/pages/resourceForm, and packages installed in the dashboard such as lucide-react. Tailwind classes work. Call your routes with request(module, path, init) from @/lib/api.
Client Pages
Set audience: "customer" to add a page to the client dashboard that buyers sign in to. Its permission defaults to customer, and its routes should only return the signed-in customer's own records. Client pages are hidden when Enable Client Dashboard is turned off in Settings.
Translating Labels
Page text can be translated in your module's common.json with keys under page.<module>.<name>:
| Key | Translates |
|---|---|
page.example.items.column.sku | A column heading. |
page.example.items.field.sku | A form label. |
page.example.items.filter.status | A filter. |
page.example.items.value.status.active | A filter option. |
page.example.items.action.resync | An action button. |
page.example.items.action.resync.confirm | Its confirm text. |
page.example.items.action.resync.done | Its success toast. |
page.example.items.section.history | A section heading. |