List module like TYPO3's: one list per page type and element type, sidecar-defined columns, search, sort, New per list #150

Closed
opened 2026-10-06 00:08:54 +02:00 by dominik.polakovics · 1 comment

Summary

Make the List module a generic record list modelled on TYPO3's List module. For the selected page it shows one list per child page type and one list per element type used on the page. Each list has its own columns (declared by the type's sidecar), search, sort, a New button, and a view showing only that type. Nothing in it is news-specific: news items get a good list because they are a page type whose sidecar declares sensible columns.

Current behavior

  • The List module (web/src/components/ListModule.tsx, from #79) shows one table of the selected page's direct child pages:
    • fixed columns Title, Type, Date, Status and Actions (Enable/Disable, Delete…), in the server's order;
    • one create button ("New " or "New page…").
  • Rows come from the polled /api/tree payload, which carries only title, type, date, draft and weight per page, so no other front-matter field can be shown.
  • There is no search, no sort and no grouping by type, and content elements are not listed at all.
  • Clicking a row title opens the page module.

Desired behavior

What the List shows for the selected page

  • One list per page type among the page's child pages. There is also a list for every allowed, non-singleton child type that has no pages yet, so its New button is one click away; an empty News section still offers "New News item".
  • One list per element type among the page's elements. Inline-group elements and hidden: true item children are excluded, as in the page module. Whether nested elements get rows is an open question.
  • Page-type lists come first, then element-type lists.
  • Each list has a header with the type's icon and label, the row count, and a "New " button when that type can be created here. The header also links to the single-type view.
  • The single-type view shows only that one list for the page, with a way back to all lists. Its URL names the type, e.g. ?type=page:news-item or ?type=element:text; the prefix keeps the page-type and element namespaces apart. Reload and Back keep it.
  • A docheader "New…" menu offers every type that can be created here, so the first record of a type without a list can still be created. That means allowed child page types, plus element types insertable on this page under placement rules and the page type's elements allowlist.
  • The List of any page shows that page's own lists, and the docheader gains an "Up one level" button to the parent's List. This replaces the interim rule from #149 that a hidden page's List shows its parent's list.

Rows

  • Columns come from the type's sidecar (see Decisions). The first column is the row's title link.
  • Page rows:
    • The title opens the page's properties view (?props=1), with return to the list (from #149).
    • Row actions are the shared page actions from #149 (Rename, Enable/Disable, Delete…) plus "Edit content", which opens the page module with return to the list.
    • The status column stays: the disabled marker and the git badge.
  • Element rows:
    • The title opens the element edit page, with return to the list.
    • The row action is Delete…, with what-links-here as in the page module.

Search and sort

  • One search box filters every list on the page view, and the single-type view. It matches case-insensitively against the values of the listed columns.
  • Clicking a column header sorts that list by the column; clicking again reverses. This is a view sort: it never changes stored order (weights, dates, content positions).
  • With no sort chosen, lists sort by date created, newest first, so a just-created record shows at the top of its list.
  • Rows, columns, search and sort come from a new server endpoint: page path + lang → the page's lists. Each list carries its kind and type, label, icon, columns, whether it can be created here, and rows (id, title, column values, draft, badge). Search and sort are endpoint parameters, so the next phase can add pagination without moving them.

Creating

  • "New " opens the existing create dialog: title, required fields and fields marked create: true. Properties then open when fields remain, per the current documented rule, and Save & close returns to the list.
  • "New " inserts an element of that type; the server enforces placement and the allowlist. Its edit page then opens with return to the list. Where the element goes in the page content is an open question.

Decisions

  • Generic, not a news manager. The maintainer's direction: behave like TYPO3's List module, not like a specialised news UI.
  • Pages and elements both get lists. The maintainer's choice. TYPO3's List shows every table stored on a page; deckle's equivalents are page types (child pages) and element types (content).
  • A page's title opens its properties form. The maintainer's choice; in TYPO3 a record's title opens its edit form.
  • One list per type, a New button per list, and a single-type view. The maintainer's choice, after TYPO3's per-table lists and table view.
  • Default sort is date created, newest first. The maintainer's choice, so new records appear on top.
  • Search and sort run on the server. The next phase paginates; sorting or searching only the visible page would be wrong.
  • Each type declares its own columns, in its sidecar. The maintainer asked that each element (and page type) defines its list columns. The proposed key is listColumns: a list of declared field/param names, plus implicit ones such as title, date and status.
    • It is a sidecar schema change, so scaffold/site/AGENTS.md and the README's sidecar reference are updated in the same change (CLAUDE.md rule).
    • The news bundle's news-item.yaml declares its columns (e.g. date, categories, author), so the news list keeps a date column.

Key interfaces

  • ListModule (web/src/components/ListModule.tsx): rebuilt around lists per type.
  • The new endpoint goes beside the page-operation handlers (internal/server). It reads pages through the page-tree and settings machinery (internal/pagetree, the settings read in internal/pageops) and elements through the content parser behind /api/page/elements (internal/content).
  • Sidecar schemas: internal/pagetype (Schema, LoadDir, the package doc) and internal/element (Schema, its loader, the package doc). The new optional key is validated against the declared fields/params and the allowed implicit names.
  • Element insertion and placement checks: the element insert endpoint (internal/server) and internal/elemops.
  • The return helper and the shared page actions from #149.

Acceptance criteria

  • On a fresh site with news, home's List shows a list per child page type (e.g. Page, Section, News) and a list per element type used on the home page, each header with its label and count.
  • The News section's List shows a "News item" list with the columns news-item.yaml declares and a "New News item" button; an empty News section still shows that list and its button.
  • A list header's link opens the single-type view; reload keeps it; the way back shows all lists.
  • A page row's title opens its properties, and Save & close returns to the list.
  • An element row's title opens its edit page, and Save & Close returns to the list.
  • Declaring columns in a sidecar changes that type's list. An unknown name in the key is a load error naming the file. A sidecar without the key gets the documented default columns.
  • The search box filters every list on the page, and the single-type view, by the listed columns' values.
  • Clicking a column header sorts by it, and again reverses. Stored order (weights, content positions, dates) is unchanged.
  • With no sort chosen, lists are newest-created first; a just-created page or element shows at the top of its list.
  • "New " inserts an element of that type and opens its edit page. Types that cannot be inserted here (insertable: false, placement, allowlist) have no New button and are not in the "New…" menu.
  • The List of a page without child pages shows its element lists (or an empty state) and an "Up one level" button to the parent's List.
  • Row actions behave exactly as in the tree.
  • AGENTS.md and the README document the new sidecar key, its defaults and the List's behavior; the drift test still passes.

How to verify

  • go test ./...: endpoint tests (lists, columns, search, sort, creatability) and sidecar load tests for the new key.
  • cd web && npm test: List module tests.
  • A browser check against harness serve on a site with harness add news.

Assumptions to check

  • Deckle stores no creation time for pages or elements today; see Open questions. Do not invent one without the answer.
  • The /api/tree payload must stay as it is: route repair and the link pickers read hidden children from it.

Out of scope

  • Pagination (next phase).
  • A shared category system (separate issue).
  • Bulk selection and bulk actions.
  • Changes to what the page module shows.

Open questions

  • Where "date created" comes from. Deckle stores no creation time. Pages have only an optional front-matter date (for news, the publish date); elements have nothing but a random uid. Options:

    • stamp a tool-managed creation timestamp at create/insert time (a reserved front-matter key for pages, a reserved param for elements; reserved names are documented in AGENTS.md);
    • derive it from git history;
    • something else for content that already exists.

    Also: for a date-ordered section such as news, should the default sort be date created or the publish date?

  • Where a new element lands in the page content. The list shows it on top (newest first), but it still needs a position in the content: the top or the end of which slot, and what happens when several slots accept the type?

  • Key name and defaults for list columns. The proposed key is listColumns. Proposed defaults for a type that declares none:

    • pages: Title and Status;
    • elements: their first text-like param, falling back to an excerpt of the body, then to the element's label.
  • Nested elements. Do elements inside other elements get rows in their type's list, or only top-level ones?

Blocked by

## Summary Make the List module a generic record list modelled on TYPO3's List module. For the selected page it shows one list per child page type and one list per element type used on the page. Each list has its own columns (declared by the type's sidecar), search, sort, a New button, and a view showing only that type. Nothing in it is news-specific: news items get a good list because they are a page type whose sidecar declares sensible columns. ## Current behavior - The List module (`web/src/components/ListModule.tsx`, from #79) shows one table of the selected page's direct child pages: - fixed columns Title, Type, Date, Status and Actions (Enable/Disable, Delete…), in the server's order; - one create button ("New <type>" or "New page…"). - Rows come from the polled `/api/tree` payload, which carries only title, type, date, draft and weight per page, so no other front-matter field can be shown. - There is no search, no sort and no grouping by type, and content elements are not listed at all. - Clicking a row title opens the page module. ## Desired behavior ### What the List shows for the selected page - One list per page type among the page's child pages. There is also a list for every allowed, non-singleton child type that has no pages yet, so its New button is one click away; an empty News section still offers "New News item". - One list per element type among the page's elements. Inline-group elements and `hidden: true` item children are excluded, as in the page module. Whether nested elements get rows is an open question. - Page-type lists come first, then element-type lists. - Each list has a header with the type's icon and label, the row count, and a "New <type>" button when that type can be created here. The header also links to the single-type view. - The single-type view shows only that one list for the page, with a way back to all lists. Its URL names the type, e.g. `?type=page:news-item` or `?type=element:text`; the prefix keeps the page-type and element namespaces apart. Reload and Back keep it. - A docheader "New…" menu offers every type that can be created here, so the first record of a type without a list can still be created. That means allowed child page types, plus element types insertable on this page under placement rules and the page type's `elements` allowlist. - The List of any page shows that page's own lists, and the docheader gains an "Up one level" button to the parent's List. This replaces the interim rule from #149 that a hidden page's List shows its parent's list. ### Rows - Columns come from the type's sidecar (see Decisions). The first column is the row's title link. - Page rows: - The title opens the page's properties view (`?props=1`), with `return` to the list (from #149). - Row actions are the shared page actions from #149 (Rename, Enable/Disable, Delete…) plus "Edit content", which opens the page module with `return` to the list. - The status column stays: the disabled marker and the git badge. - Element rows: - The title opens the element edit page, with `return` to the list. - The row action is Delete…, with what-links-here as in the page module. ### Search and sort - One search box filters every list on the page view, and the single-type view. It matches case-insensitively against the values of the listed columns. - Clicking a column header sorts that list by the column; clicking again reverses. This is a view sort: it never changes stored order (weights, dates, content positions). - With no sort chosen, lists sort by date created, newest first, so a just-created record shows at the top of its list. - Rows, columns, search and sort come from a new server endpoint: page path + lang → the page's lists. Each list carries its kind and type, label, icon, columns, whether it can be created here, and rows (id, title, column values, draft, badge). Search and sort are endpoint parameters, so the next phase can add pagination without moving them. ### Creating - "New <page type>" opens the existing create dialog: title, required fields and fields marked `create: true`. Properties then open when fields remain, per the current documented rule, and Save & close returns to the list. - "New <element type>" inserts an element of that type; the server enforces placement and the allowlist. Its edit page then opens with `return` to the list. Where the element goes in the page content is an open question. ## Decisions - **Generic, not a news manager.** The maintainer's direction: behave like TYPO3's List module, not like a specialised news UI. - **Pages and elements both get lists.** The maintainer's choice. TYPO3's List shows every table stored on a page; deckle's equivalents are page types (child pages) and element types (content). - **A page's title opens its properties form.** The maintainer's choice; in TYPO3 a record's title opens its edit form. - **One list per type, a New button per list, and a single-type view.** The maintainer's choice, after TYPO3's per-table lists and table view. - **Default sort is date created, newest first.** The maintainer's choice, so new records appear on top. - **Search and sort run on the server.** The next phase paginates; sorting or searching only the visible page would be wrong. - **Each type declares its own columns, in its sidecar.** The maintainer asked that each element (and page type) defines its list columns. The proposed key is `listColumns`: a list of declared field/param names, plus implicit ones such as title, date and status. - It is a sidecar schema change, so `scaffold/site/AGENTS.md` and the README's sidecar reference are updated in the same change (CLAUDE.md rule). - The news bundle's `news-item.yaml` declares its columns (e.g. date, categories, author), so the news list keeps a date column. ## Key interfaces - `ListModule` (`web/src/components/ListModule.tsx`): rebuilt around lists per type. - The new endpoint goes beside the page-operation handlers (`internal/server`). It reads pages through the page-tree and settings machinery (`internal/pagetree`, the settings read in `internal/pageops`) and elements through the content parser behind `/api/page/elements` (`internal/content`). - Sidecar schemas: `internal/pagetype` (`Schema`, `LoadDir`, the package doc) and `internal/element` (`Schema`, its loader, the package doc). The new optional key is validated against the declared fields/params and the allowed implicit names. - Element insertion and placement checks: the element insert endpoint (`internal/server`) and `internal/elemops`. - The `return` helper and the shared page actions from #149. ## Acceptance criteria - [ ] On a fresh site with news, home's List shows a list per child page type (e.g. Page, Section, News) and a list per element type used on the home page, each header with its label and count. - [ ] The News section's List shows a "News item" list with the columns `news-item.yaml` declares and a "New News item" button; an empty News section still shows that list and its button. - [ ] A list header's link opens the single-type view; reload keeps it; the way back shows all lists. - [ ] A page row's title opens its properties, and Save & close returns to the list. - [ ] An element row's title opens its edit page, and Save & Close returns to the list. - [ ] Declaring columns in a sidecar changes that type's list. An unknown name in the key is a load error naming the file. A sidecar without the key gets the documented default columns. - [ ] The search box filters every list on the page, and the single-type view, by the listed columns' values. - [ ] Clicking a column header sorts by it, and again reverses. Stored order (weights, content positions, dates) is unchanged. - [ ] With no sort chosen, lists are newest-created first; a just-created page or element shows at the top of its list. - [ ] "New <element type>" inserts an element of that type and opens its edit page. Types that cannot be inserted here (`insertable: false`, placement, allowlist) have no New button and are not in the "New…" menu. - [ ] The List of a page without child pages shows its element lists (or an empty state) and an "Up one level" button to the parent's List. - [ ] Row actions behave exactly as in the tree. - [ ] AGENTS.md and the README document the new sidecar key, its defaults and the List's behavior; the drift test still passes. ## How to verify - `go test ./...`: endpoint tests (lists, columns, search, sort, creatability) and sidecar load tests for the new key. - `cd web && npm test`: List module tests. - A browser check against `harness serve` on a site with `harness add news`. ## Assumptions to check - Deckle stores no creation time for pages or elements today; see Open questions. Do not invent one without the answer. - The `/api/tree` payload must stay as it is: route repair and the link pickers read hidden children from it. ## Out of scope - Pagination (next phase). - A shared category system (separate issue). - Bulk selection and bulk actions. - Changes to what the page module shows. ## Open questions - **Where "date created" comes from.** Deckle stores no creation time. Pages have only an optional front-matter `date` (for news, the publish date); elements have nothing but a random `uid`. Options: - stamp a tool-managed creation timestamp at create/insert time (a reserved front-matter key for pages, a reserved param for elements; reserved names are documented in AGENTS.md); - derive it from git history; - something else for content that already exists. Also: for a date-ordered section such as news, should the default sort be date created or the publish `date`? - **Where a new element lands in the page content.** The list shows it on top (newest first), but it still needs a position in the content: the top or the end of which slot, and what happens when several slots accept the type? - **Key name and defaults for list columns.** The proposed key is `listColumns`. Proposed defaults for a type that declares none: - pages: Title and Status; - elements: their first text-like param, falling back to an excerpt of the body, then to the element's label. - **Nested elements.** Do elements inside other elements get rows in their type's list, or only top-level ones? ## Blocked by - #149
Author
Owner

Superseded by #153. On reflection, a TYPO3-style List doesn't fit Hugo. TYPO3's List shows records from uniform tables. In Hugo a page is a file, and an element is a shortcode at a position inside a page body, with no identity, metadata or creation date of its own. That is why this issue kept collecting open questions (where a creation date comes from, where a new element goes, whether nested elements get rows). The part that does map onto Hugo, a collection of pages of one type held in sections, moves into editor modules that feature bundles declare in YAML. The first is a News module, and the List module is removed (#153).

Superseded by #153. On reflection, a TYPO3-style List doesn't fit Hugo. TYPO3's List shows records from uniform tables. In Hugo a page is a file, and an element is a shortcode at a position inside a page body, with no identity, metadata or creation date of its own. That is why this issue kept collecting open questions (where a creation date comes from, where a new element goes, whether nested elements get rows). The part that does map onto Hugo, a collection of pages of one type held in sections, moves into editor modules that feature bundles declare in YAML. The first is a News module, and the List module is removed (#153).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#150
No description provided.