Build a UI page

Add a custom page to the dashboard with form submission via a UI action.

Pages are declared in the manifest with a render script path; form submissions are handled by a separate UI action declared under ui.actions. This guide walks through the pattern using basic-tax-calcs’s nexus address form as the reference.

1. Declare in the manifest

pages: [
  {
    id: "nexus-addresses",
    title: "Nexus Addresses",
    path: "nexus-addresses",
    render: "src/pages/nexus-addresses.tsx",
    kind: "list",
    nav: { label: "Nexus Addresses", section: "Tax", order: 10 },
  },
  {
    id: "nexus-address-form",
    title: "Nexus Address",
    path: "nexus-address-form",
    render: "src/pages/nexus-address-form.tsx",
    kind: "page",
  },
],
ui: {
  actions: [
    {
      id: "save-nexus-address",
      label: "Save",
      page: "nexus-address-form",
      run: "src/pages/actions/save-nexus-address.ts",
    },
  ],
},

2. The list page

// src/pages/nexus-addresses.tsx
/** @jsxImportSource @backfill-io/sdk */
import { Page, Section, Toolbar, Link, Table, EmptyState } from "@backfill-io/sdk/ui";

declare const NexusAddress: any;

export default function NexusAddresses() {
  const rows = NexusAddress.query({ where: { active: true } });

  return (
    <Page title="Nexus Addresses" toolbar={<Toolbar><Link href="/nexus-address-form">Add</Link></Toolbar>}>
      

      {rows.length === 0 ? (
        <EmptyState title="No addresses yet" />
      ) : (
        <Table
          rows={rows}
          columns={[
            { key: "nexusName", label: "Name" },
            { key: "state", label: "State" },
            { key: "city", label: "City" },
            { key: "postalCode", label: "Postal code" },
          ]}
        />
      )}
    </Page>
  );
}

3. The form page

// src/pages/nexus-address-form.tsx
/** @jsxImportSource @backfill-io/sdk */
import { Page, Form, Field, TextInput, SubmitButton } from "@backfill-io/sdk/ui";

export default function NexusAddressForm() {
  return (
    <Page title="New Nexus Address">
      <Form action="save-nexus-address">
        <Field name="nexusName" label="Name" required>
          <TextInput name="nexusName" />
        </Field>
        <Field name="street" label="Street" required>
          <TextInput name="street" />
        </Field>
        <Field name="city" label="City" required>
          <TextInput name="city" />
        </Field>
        <Field name="state" label="State" required>
          <TextInput name="state" />
        </Field>
        <Field name="postalCode" label="Postal code" required>
          <TextInput name="postalCode" />
        </Field>
        <SubmitButton>Save</SubmitButton>
      </Form>
    </Page>
  );
}

Form action="save-nexus-address" references the action by its id.

4. The action handler

// src/pages/actions/save-nexus-address.ts
import type { ExtensionPageActionContext } from "@backfill-io/sdk/ui";
import { redirect, validationError } from "@backfill-io/sdk/ui";

declare const NexusAddress: any;

export default async function (ctx: ExtensionPageActionContext) {
  const v = ctx.values;
  const errs: { field: string; message: string }[] = [];
  if (!v.nexusName) errs.push({ field: "nexusName", message: "Name is required" });
  if (!v.street)   errs.push({ field: "street",   message: "Street is required" });
  if (errs.length) return validationError("Please fix the errors below", errs);

  NexusAddress.create({
    nexusName: v.nexusName,
    street: v.street,
    city: v.city,
    state: v.state,
    postalCode: v.postalCode,
  });

  return redirect("/nexus-addresses", "Saved", "success");
}

Helpers (redirect, validationError, ok, message) are in @backfill-io/sdk/ui.

How submission flows

  1. Admin clicks Submit. The dashboard collects form values.
  2. The dashboard invokes the page’s bound action handler server-side, passing values in ctx.values.
  3. The handler returns one of: validationError(...) (re-render the form with errors), redirect(...) (navigate elsewhere with an optional toast), ok(...) / message(...) (toast in place), or null/undefined (no-op).

Available components

Full prop reference: Components. The render script must use only these primitives — passing unknown props or mismatched types throws at runtime.