feat(10-03): search, sort, filter, page and bulk-delete any list

- List state (search, sort, dir, page, per_page, filter[<name>]) lives in
  the URL query through parseListQuery/toListQuery; every change is a
  router replace and clears the selection; search is debounced 300 ms and
  resets the page
- DataTable renders the schema columns with a tri-state page checkbox,
  asc/desc/none sorting with aria-sort, selected rows, a sticky header,
  eight skeleton rows while loading and an empty slot
- CellValue renders text (muted dash when empty, arrays comma-joined),
  datetime as YYYY-MM-DD HH:mm and switch as the Tak/Nie pills
- The heading shows the plural recordCount and the create button;
  delete sits in the toolbar, disabled without a selection, and confirms
  with the plural deleteConfirm in a Reka alert dialog before POSTing
  bulk-delete; a 409 shows a danger toast
- FilterBar renders switch (JSON of the option value), daterange
  (from..to) and scope filters (choices from filters/{scope}/options)
- Pagination shows the range, the per-page select over perPageOptions
  (hidden with one choice) and a pager with ellipsis
- The tracer smoke test skips the new checkbox column; new backend::lang
  list keys; boardwalk/dist rebuilt
This commit is contained in:
Jakub Zych
2026-09-27 16:51:00 +02:00
parent 126ca5b8ed
commit 8b5f85603f
22 changed files with 1421 additions and 118 deletions

View File

@@ -117,12 +117,27 @@
"backend::lang.list.column_updated": {
"other": "Zaktualizowano"
},
"backend::lang.list.delete_failed": {
"other": "Nie udało się usunąć zaznaczonych rekordów."
},
"backend::lang.list.delete_selected": {
"other": "Usuń zaznaczone"
},
"backend::lang.list.empty_value": {
"other": "—"
},
"backend::lang.list.filter_all": {
"other": "Wszystkie"
},
"backend::lang.list.filter_from": {
"other": "Od"
},
"backend::lang.list.filter_to": {
"other": "Do"
},
"backend::lang.list.filters": {
"other": "Filtry"
},
"backend::lang.list.load_failed": {
"other": "Nie udało się wczytać listy."
},
@@ -138,6 +153,9 @@
"backend::lang.list.no_results": {
"other": "0 wyników"
},
"backend::lang.list.pagination": {
"other": "Stronicowanie"
},
"backend::lang.list.pagination_range": {
"other": ":from–:to z :total"
},
@@ -153,6 +171,9 @@
"many": ":count wyników",
"other": ":count wyniku"
},
"backend::lang.list.search": {
"other": "Szukaj"
},
"backend::lang.list.search_prompt": {
"other": "Szukaj…"
},

View File

@@ -8,38 +8,35 @@
"showCheckboxes": true,
"showSorting": true,
"searchTerm": "",
"toolbarButtons": [
"create"
],
"recordUrl": "acme/demo/widgets/update/:id",
"toolbarButtons": ["create", "delete"],
"columns": [
{
"key": "name",
"label": "Name",
"searchable": true,
"sortable": true
},
{
"key": "code",
"label": "Code",
"searchable": true,
"sortable": true
},
{
"key": "tags",
"label": "Tags",
"searchable": false,
"sortable": false,
"type": "relation",
"relation": "tags",
"select": "name"
}
{ "key": "name", "label": "Name", "searchable": true, "sortable": true },
{ "key": "code", "label": "Code", "searchable": true, "sortable": true },
{ "key": "tags", "label": "Tags", "searchable": false, "sortable": false, "type": "relation", "relation": "tags", "select": "name" },
{ "key": "created_at", "label": "Created", "searchable": false, "sortable": true, "type": "datetime" },
{ "key": "active", "label": "Active", "searchable": false, "sortable": false, "type": "switch" }
],
"filters": [
{ "name": "active", "label": "Active", "type": "switch", "column": "active", "trueValue": true, "falseValue": false },
{ "name": "created", "label": "Created", "type": "daterange", "column": "created_at" },
{ "name": "maker", "label": "Maker", "type": "scope", "scope": "byMaker", "modelClass": "Acme\\Demo\\Models\\Maker", "nameFrom": "name" }
],
"filters": [],
"rowActions": [],
"bulkActions": [],
"recordUrl": "acme/demo/widgets/update/:id"
"messages": {
"create": { "other": "New widget" },
"deleteConfirm": { "one": "Delete :count widget?", "other": "Delete :count widgets?" },
"deleteSelected": { "other": "Delete selected" },
"deleted": { "one": "Deleted :count widget", "other": "Deleted :count widgets" },
"empty": { "other": "No widgets yet." },
"emptySearch": { "other": "Nothing found" },
"emptySearchHint": { "other": "No widget matches “:term”." },
"recordCount": { "one": ":count widget", "other": ":count widgets" },
"searchPrompt": { "other": "Search widgets…" },
"selected": { "other": "Selected :count" }
},
"meta": { "locale": "en" }
},
"meta": {
"locale": "en"
}
"meta": { "locale": "en" }
}

View File

@@ -1,7 +1,7 @@
{
"data": [
{ "id": 1, "name": "Blue widget", "code": "W-01", "tags": ["small", "round"] },
{ "id": 2, "name": "Green widget", "code": "W-02", "tags": [] }
{ "id": 1, "name": "Blue widget", "code": "W-01", "tags": ["small", "round"], "created_at": "2026-09-21T14:32:00", "active": true },
{ "id": 2, "name": "Green widget", "code": "W-02", "tags": [], "created_at": null, "active": false }
],
"meta": { "page": 1, "per_page": 20, "total": 2, "last_page": 1 }
}

View File

@@ -0,0 +1,9 @@
{
"maker": {
"data": [
{ "value": "1", "label": "Acme Works" },
{ "value": "2", "label": "Bolt & Co" }
],
"meta": { "locale": "en" }
}
}