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 theStoreFactoryContext(set up in step 1) for a grid store against this path. NouseGridstore(...)call needed here.gridRefexposesrefresh()and other imperative hooks (select, clear filters). After a form saves, callgridRef.current?.refresh()to reload the current page.getPluginOptionscontrols the toolbar — action buttons, export visibility, back/breadcrumb behavior.DataGridControlsis your app-wide toolbar component (row actions — edit, delete, row-level buttons). The sample factors this once intoClinicDataGridControlsand 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.