# Pages

URL: https://zerodev.ca/docs/kora/developer-documentation/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`.

```js title="modules/example/resources/dashboard/pages/items/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](https://lucide.dev/icons) 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](#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](/docs/kora/developer-documentation/routes) 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

```js
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:

```js
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:

```js
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:

```tsx title="modules/example/resources/dashboard/pages/items/view.tsx"
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`](/docs/kora/developer-documentation/languages#dashboard-text) 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. |
