3. Grid pages#

Pages are thin. The clinic sample uses SummaryGrid from @palmyralabs/template-tribble — it bundles server-side pagination, sort, search, row selection, and toolbar actions, driven by a column-definition array from @palmyralabs/rt-forms.

Column definitions#

Columns are data, not JSX. Keep them near the page that uses them.

// src/pages/masterdata/manufacturer/columns.ts
import type { ColumnDefinition } from '@palmyralabs/rt-forms';

export const manufacturerColumns: ColumnDefinition[] = [
  { attribute: 'name',          name: 'name',          label: 'Name',    searchable: true, sortable: true },
  { attribute: 'contactMobile', name: 'contactMobile', label: 'Mobile' },
  { attribute: 'contactEmail',  name: 'contactEmail',  label: 'Email' },
  { attribute: 'rating',        name: 'rating',        label: 'Rating', type: 'number', sortable: true },
  { attribute: 'address',       name: 'address',       label: 'Address' },
];

Declarative fields like searchable / sortable compile into the query parameters the Palmyra backend expects — no custom wiring per column.

The page#

// src/pages/masterdata/manufacturer/ManufacturerGridPage.tsx
import { useRef } from 'react';
import { useDisclosure } from '@mantine/hooks';
import { SummaryGrid } from '@palmyralabs/template-tribble';
import type { IPageQueryable } from '@palmyralabs/rt-forms';

import { ServiceEndpoint } from '@/config/ServiceEndpoints';
import { manufacturerColumns } from './columns';
import ManufacturerNewForm from './ManufacturerNewForm';
import ClinicDataGridControls from '@/components/ClinicDataGridControls';

export default function ManufacturerGridPage() {
  const [opened, { open, close }] = useDisclosure(false);
  const gridRef = useRef<IPageQueryable>(null);

  const endPoint = ServiceEndpoint.masterData.manufacturer.restApi;

  return (
    <>
      <SummaryGrid
        title="Manufacturer"
        pageName=""
        columns={manufacturerColumns}
        pageSize={[15, 30, 45]}
        gridRef={gridRef}
        DataGridControls={ClinicDataGridControls}
        getPluginOptions={() => ({
          addText:     'Add Manufacturer',
          onNewClick:  open,
          export:      { visible: false },
          backOption:  false,
        })}
        options={{ endPoint }}
      />
      <ManufacturerNewForm
        open={opened}
        onClose={close}
        onRefresh={() => gridRef.current?.refresh()}
        pageName="Manufacturer"
        title="New Manufacturer"
      />
    </>
  );
}

Notes on the API surface:

  • options={{ endPoint }} — the grid asks the StoreFactoryContext (set up in step 1) for a grid store against this path. No useGridstore(...) call needed here.
  • gridRef exposes refresh() and other imperative hooks (select, clear filters). After a form saves, call gridRef.current?.refresh() to reload the current page.
  • getPluginOptions controls the toolbar — action buttons, export visibility, back/breadcrumb behavior.
  • DataGridControls is your app-wide toolbar component (row actions — edit, delete, row-level buttons). The sample factors this once into ClinicDataGridControls and reuses it across every grid page.

Custom cell rendering#

When a cell needs more than a plain value — stars for a rating, a status badge, a thumbnail — attach cellRenderer to the column:

import { Star } from 'lucide-react';

const ratingColumn: ColumnDefinition = {
  attribute: 'rating',
  name: 'rating',
  label: 'Rating',
  cellRenderer: ({ value }) => (
    <div className="flex gap-1">
      {Array.from({ length: 5 }, (_, i) => (
        <Star key={i} size={14}
              className={i < (value ?? 0) ? 'fill-yellow-400 stroke-yellow-400' : 'stroke-gray-400'} />
      ))}
    </div>
  ),
};

The renderer receives the parsed row value plus the whole row, so you can render derived UI without reshaping the backend response.