Skip to content

Low-level TGrid sessions

View as Markdown

Use a low-level TGrid session when registered Sapporta tables still own the rows but the application must own the entire Grid composition. Prefer TableGridView when standard page chrome, URL state, lookups, loading, and session disposal fit the screen.

import { useMemo } from "react";
import { ROW_PRIMARY_MASTER_DETAIL } from "@sapporta/grid";
import {
TGrid,
defineTGrid,
useTGridLifecycle,
useTGridSession,
} from "@sapporta/frontend";
import type { TableSchema } from "@sapporta/shared/contracts";
type RowsByLevel = {
projects: { id: number; name: string };
"projects.tasks": {
id: number;
project_id: number;
title: string;
status: "open" | "in_progress" | "completed";
};
};
export function ProjectTaskGrid(props: {
projects: TableSchema;
tasks: TableSchema;
}) {
const definition = useMemo(
() =>
defineTGrid<RowsByLevel>({
rootLevel: "projects",
interaction: ROW_PRIMARY_MASTER_DETAIL,
levels: {
projects: {
table: props.projects,
childLevels: ["projects.tasks"],
rowHeaderColumn: "none",
query: { owner: "host", pageSize: 50 },
columns: (columns) => [columns.table("name", { edit: "none" })],
},
"projects.tasks": {
table: props.tasks,
parent: {
level: "projects",
foreignKey: "project_id",
defaultSort: "title",
},
childLevels: [],
rowHeaderColumn: "none",
query: { owner: "source", pageSize: 25 },
columns: (columns) => [
columns.table("title", { edit: "none" }),
columns.table("status", { edit: "none" }),
],
},
},
}),
[props.projects, props.tasks],
);
const session = useTGridSession(definition);
useTGridLifecycle({ session });
return session ? <TGrid session={session} /> : null;
}

owner: "host" gives the screen control of the root query. owner: "source" lets the parent relationship provide the child predicate. useTGridLifecycle() loads lookup labels and cleans up those side effects. useTGridSession() owns and disposes the session.

Call session.reloadRows() from controls that already hold the session. Supply registerAs to useTGridLifecycle() only when an external flow must address this mounted root through reloadTGridRows(). The registry is a narrow command bridge; it does not own the session.

For a full route with URL query state, use TableGridView or compose useTableGridUrlState() and pass its seeds and change handler to useTGridSession(). Setting urlSync only declares that the query may participate; it does not read or write the URL itself.

Table column builders retain schema codecs, select options, lookups, formatting, copy behavior, and the save client. A custom writer returns authoritative reconciliation without moving authorization into the cell:

columns.table("status", {
edit: "default",
saveCellValue: async (context) => {
const patch = await context.appServices.setStatus({
id: context.row.id,
status: context.value,
});
return { kind: "patch", patch };
},
});

The server operation still owns ability, row scope, conflict handling, and the transaction.