Recipes
Step-by-step changes to the web app. Run commands from the repo root of grandstack-web with the stack running (docker compose up in the grandstack repo) so codegen can read the live schema. Each recipe ends with how to check it.
Where to run this. Commands on this page that start with
npm,nodeordocker buildrun inside a clone of this repository:git clone https://github.com/grand-stack/grandstack-web, thencd grandstack-web(Node 22 or later; runnpm cionce). Commands that need the running stack use thegrandstackrepository’s compose files: see the quickstart.
Show a new field from the schema
Example: show country on the movie page after adding it to the server schema.
- Server first: add the field to
schema/schema.graphqland restart (server recipe “Add a field”). - In
src/pages/MovieDetail.tsx, addcountryto the fields in thegraphql()query. - Run
npm run codegen. It fails if the field does not exist on the server. - Use it in the JSX. On
MovieDetail.tsxthe facts are rows of a definition list, so follow that: add{movie.country && (<><dt>Country</dt><dd>{movie.country}</dd></>)}inside the<dl className="facts">. - Check:
npm run typecheck, then open a movie page (for example/movies/Inceptionon the standard stack at http://localhost:3000, or your own web port) and look at it. Add a unit test if you added logic. To make the field editable, also follow “Add a field to the forms” below.
If your server runs somewhere other than http://localhost:4000, run codegen with GRAPHQL_URL=http://host:port/graphql npm run codegen.
Add a filter facet
Example: filter movies by minimum year.
- In
src/lib/filters.ts: add the field toFilters, read it inparseFilters, write it inserializeFilters, count it inactiveCount, and add its clause inbuildWhere(use theomitparameter if the facet needs counts that ignore itself). - In
src/search/FilterSidebar.tsx: add the control and callonChange({ minYear: ... }). - In
src/pages/Results.tsx: show a chip for it with a remove handler, and include it in “Clear all” (add it toemptyFilterswith an explicitundefined, or spreading will not clear it). - Add cases to
src/lib/filters.test.ts(URL round trip, where clause, clearing). - Check:
npm test, then open/movies?...with the new parameter and confirm the count and chips; reload to confirm it persists.
Add a field to the forms
- Add it to
MovieFormStateandemptyMovieForminsrc/forms/model.ts; validate it invalidateMovie. - Include it in
buildCreateInputand inbuildUpdateInput(a changed scalar is{ set: value }, blank is{ set: null }). - Add the input in
src/forms/MovieForm.tsxusing thefield(...)andfieldError(...)helpers. - Load it into the edit form in
src/pages/MovieEdit.tsx(query it and map it to the form state). - Add cases to
src/forms/model.test.ts. Check:npm testand a manual create and edit.
Add a page
- Create
src/pages/Thing.tsxwith its query written usinggraphql(). - Add a
<Route>insrc/App.tsx(and a nav link if needed). - Run
npm run codegenandnpm run typecheck. - Operation names must be unique across the whole app (codegen fails on duplicates), so name queries after what they do (
PeopleBrowse, notPeople, ifPeopleexists).
Add another entity type (for example Studio)
- Server: add the type and relationships (see
docs/schema-guide.mdin thegrandstack-serverrepo). - Copy the People pattern:
peopleFilters.ts->studioFilters.ts,People.tsx->Studios.tsx,PersonCard.tsx->StudioCard.tsx, and the detail and form pages. Keep the URL-state and diff-update patterns. - Add routes, run codegen, add tests next to each pure module.
Before you commit
npm run typecheck && npm test && npm run docs:check
# with the stack running and freshly seeded:
npm run e2e
Source: grandstack-web/docs/recipes.md (this page is a copy; edit it in the repository)