diff --git a/docs/book/v7/how-to/using-simple-tables.md b/docs/book/v7/how-to/using-simple-tables.md
new file mode 100644
index 0000000..2a87ba2
--- /dev/null
+++ b/docs/book/v7/how-to/using-simple-tables.md
@@ -0,0 +1,205 @@
+# Use simple tables
+
+## Summary
+
+This page explains how to build a list page as a simple table, with sortable columns, row selection and column visibility that is remembered for each admin.
+
+## Details
+
+Simple tables are used by the list pages of Dotkernel Admin, such as the lists of admins, admin logins and users.
+Each table is made of these parts:
+
+- a Twig template that renders the table, using the `sortableColumn` macro from `@partial/macros.html.twig`
+- the `table_settings.js` script, which shows or hides columns and enables the edit and delete buttons
+- the `Setting` entity, which stores the selected columns for each admin and table
+- two routes that read and write the setting: `setting::view-setting` (`GET /{identifier}`) and `setting::store-setting` (`POST /{identifier}`)
+
+When the page loads, `table_settings.js` requests the saved column selection, applies it, then displays the table.
+Whenever the admin toggles a column, the script saves the new selection.
+If no selection was saved yet, all columns are displayed.
+
+The examples below build a list page for books.
+For a complete module, see [Creating a book module using DotMaker](../tutorials/create-book-module-via-dot-maker.md).
+
+## Create a simple table
+
+Creating a simple table requires four steps:
+
+- add a setting identifier
+- send the identifier to the template
+- render the table
+- load the scripts
+
+### Add a setting identifier
+
+Each table stores its selected columns under its own identifier.
+Open `src/Core/src/Setting/src/Enum/SettingIdentifierEnum.php` and add a new case:
+
+```php
+case IdentifierTableBookListSelectedColumns = 'table_book_list_selected_columns';
+```
+
+The `identifier` column of the `settings` table is a database `ENUM`, so the new case requires a migration.
+Generate it, check that it alters the `settings.identifier` column, then run it:
+
+```shell
+php ./vendor/bin/doctrine-migrations diff
+php ./vendor/bin/doctrine-migrations migrate
+```
+
+See [Create Database Migrations](creating-migrations.md) for more details.
+
+### Send the identifier to the template
+
+In the handler that renders the list, send the identifier to the template:
+
+```php
+return new HtmlResponse(
+ $this->template->render('book::list-book', [
+ 'pagination' => $this->bookService->getBooks($request->getQueryParams()),
+ 'identifier' => SettingIdentifierEnum::IdentifierTableBookListSelectedColumns->value,
+ ])
+);
+```
+
+### Render the table
+
+In the template, import the macro and render the column selector and the table.
+The example below shows only the basic listing.
+The full template, with the add, edit and delete buttons and modals, pagination and the remaining columns, is in [Creating a book module using DotMaker](../tutorials/create-book-module-via-dot-maker.md).
+
+```html
+{% from '@partial/macros.html.twig' import sortableColumn %}
+
+{% extends '@layout/default.html.twig' %}
+
+{% block title %}Manage books{% endblock %}
+
+{% block content %}
+
+{% endblock %}
+```
+
+Things to note:
+
+- the table is hidden with `style="display: none;"`, the script displays it after applying the saved selection
+- the `#column-selector` list is empty, the script fills it with one checkbox for each sortable column
+- the first column holds the row checkbox and has no `sortableColumn`, so it cannot be hidden
+
+### Load the scripts
+
+At the end of the template, define the three variables that `table_settings.js` requires, then load the script:
+
+```html
+{% block javascript %}
+{{ parent() }}
+
+
+{% endblock %}
+```
+
+If any of the three variables is missing, the script logs an error in the browser console and does nothing.
+Any page specific script, such as `book.js`, is loaded the same way, using `type="module"`.
+It must be registered in the `entries` object of `vite.config.js` and built, see [Use NPM Commands](npm_commands.md).
+
+## Name the columns
+
+The `sortableColumn` macro receives the sort key as its fourth argument.
+It adds the `table-column` class to the header link and sets its `data-column` attribute to the sort key, with dots replaced by dashes.
+The script hides and shows a column by the class `column-`, so every `
` and `
` of that column must use the same class:
+
+| Sort key | `data-column` | Class |
+|--------------------|--------------------|---------------------------|
+| `user.identity` | `user-identity` | `column-user-identity` |
+| `detail.firstName` | `detail-firstName` | `column-detail-firstName` |
+| `book.releaseDate` | `book-releaseDate` | `column-book-releaseDate` |
+
+## Edit and delete buttons
+
+`table_settings.js` also handles row selection:
+
+- clicking a row (`.table-row`) toggles its checkbox
+- the buttons with the ids `btn-edit-resource` and `btn-delete-resource` are enabled only while exactly one `.ui-checkbox` is checked
+
+The URLs of the selected row are read from the `data-edit-url` and `data-delete-url` attributes of the checked box.
+Opening the modals that use them is done by the page specific script, see the `_book.js` file in [Creating a book module using DotMaker](../tutorials/create-book-module-via-dot-maker.md).
+
+## FAQ
+
+**Q: Why is a column always displayed, even if I untick it in the column selector?**
+
+A: The `
` or `
` class does not match the column's `data-column` value.
+Check that every cell of that column uses `column-` followed by the sort key with dots replaced by dashes.
+
+**Q: Why is the column selector empty?**
+
+A: The selector is built from the elements with the `table-column` class.
+Make sure the headers use the `sortableColumn` macro and that the template contains an empty `
`.
+
+**Q: Why is the table not displayed at all?**
+
+A: The table stays hidden until `table_settings.js` runs.
+Check the browser console for errors about `tableId`, `storeSettingsUrl` or `getSettingsUrl`, and make sure the script is loaded with `type="module"`.
+
+**Q: Why are my column selections not saved?**
+
+A: The identifier is probably not a case of `SettingIdentifierEnum`, or the migration that adds it to the `settings.identifier` column was not run.
+In both cases, the setting routes respond with an error, which you can see in the browser's network tab.
+
+**Q: Why are all columns displayed again after a reload?**
+
+A: When the saved selection is empty, all columns are displayed.
+This is also the state of an admin that never changed the selection for that table.
diff --git a/docs/book/v7/tutorials/create-book-module-via-dot-maker.md b/docs/book/v7/tutorials/create-book-module-via-dot-maker.md
index 81f3125..e3f202f 100644
--- a/docs/book/v7/tutorials/create-book-module-via-dot-maker.md
+++ b/docs/book/v7/tutorials/create-book-module-via-dot-maker.md
@@ -133,15 +133,27 @@ The next step is filling in the required logic for the proposed flow of this mod
While `dot-maker` does also include common logic in the relevant files, the tutorial adds custom functionality.
As such, the following section will go over the files that require changes.
+* `src/Core/src/Setting/src/Enum/SettingIdentifierEnum.php`
+
+Each table stores its selected columns under its own setting identifier.
+Add a new case for the books table, so it does not share its column selection with another table:
+
+```php
+case IdentifierTableBookListSelectedColumns = 'table_book_list_selected_columns';
+```
+
+The `identifier` column of the `settings` table is a database `ENUM`, so adding a case also requires a migration, which is generated in the [Migrations](#migrations) section.
+For more details about how the column selection works, see [Use simple tables](../how-to/using-simple-tables.md).
+
* `src/Book/src/Handler/GetListBookHandler.php`
-The overall class structure is fully generated, but for this tutorial you will need to send the `indentifier` key to the template, as shown below:
+The overall class structure is fully generated, but for this tutorial you will need to send the `identifier` key to the template, as shown below:
```php
return new HtmlResponse(
- $this->template->render('book::book-list', [
+ $this->template->render('book::list-book', [
'pagination' => $this->bookService->getBooks($request->getQueryParams()),
- 'identifier' => SettingIdentifierEnum::IdentifierTableUserListSelectedColumns->value,
+ 'identifier' => SettingIdentifierEnum::IdentifierTableBookListSelectedColumns->value,
])
);
```
@@ -170,6 +182,7 @@ namespace Core\Book\Entity;
use Core\App\Entity\AbstractEntity;
use Core\App\Entity\TimestampsTrait;
+use Core\App\Entity\UuidIdentifierTrait;
use Core\Book\Repository\BookRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
@@ -180,6 +193,7 @@ use Doctrine\ORM\Mapping as ORM;
class Book extends AbstractEntity
{
use TimestampsTrait;
+ use UuidIdentifierTrait;
#[ORM\Column(name: "name", type: "string", length: 100)]
protected string $name;
@@ -238,7 +252,7 @@ class Book extends AbstractEntity
public function getArrayCopy(): array
{
return [
- 'uuid' => $this->getUuid()->toString(),
+ 'id' => $this->getId()->toString(),
'name' => $this->getName(),
'author' => $this->getAuthor(),
'releaseDate' => $this->getReleaseDate(),
@@ -355,9 +369,9 @@ class BookService implements BookServiceInterface
* @throws NotFoundException
*/
public function findBook(
- string $uuid,
+ string $id,
): Book {
- $book = $this->bookRepository->find($uuid);
+ $book = $this->bookRepository->find($id);
if (! $book instanceof Book) {
throw new NotFoundException(Message::resourceNotFound('Book'));
}
@@ -529,11 +543,13 @@ $this->add($releaseDateInput);
* `src/App/assets/js/components/_book.js`
-As the listing pages make use of JavaScript, you will need to manually create your module specific `_book.js` file and register it in `webpack.config.js` for building.
+As the listing pages make use of JavaScript, you will need to manually create your module specific `_book.js` file and register it in `vite.config.js` for building.
You may copy this sample `_book.js` file to the `src/App/assets/js/components/` directory:
```js
+import $ from 'jquery';
+
$(document).ready(() => {
const request = async(url, options = {}) => {
try {
@@ -651,12 +667,10 @@ $(document).ready(() => {
});
```
-Next you have to register the file in the `entries` array of `webpack.config.js` by adding the following key:
+Next you have to register the file in the `entries` object of `vite.config.js` by adding the following key:
```js
-book: [
- './App/assets/js/components/_book.js'
-]
+book: `${assetsPath}/js/components/_book.js`,
```
To make use of the newly added scripts, make sure to build your assets by running the command:
@@ -682,6 +696,7 @@ For this tutorial you may copy the following default page layout in the `list-bo
{{ sortableColumn('book::list-book', {}, pagination.queryParams, 'book.created', 'Created') }}
@@ -737,19 +754,19 @@ For this tutorial you may copy the following default page layout in the `list-bo
{% for book in pagination.items %}
-
+
{{ book.name }}
{{ book.author }}
-
{{ book.releaseDate|date('Y-m-d') }}
+
{{ book.releaseDate|date('Y-m-d') }}
{{ book.getCreated()|date('Y-m-d H:i:s') }}
{{ book.getUpdated() is not null ? book.getUpdated()|date('Y-m-d H:i:s') : '' }}
@@ -764,13 +781,17 @@ For this tutorial you may copy the following default page layout in the `list-bo
{% endif %}
+ {# Main content: end #}
+ {# Pagination: begin #}
@@ -806,6 +827,7 @@ For this tutorial you may copy the following default page layout in the `list-bo
+ {# Modals: end #}
{% endblock %}
@@ -816,8 +838,8 @@ For this tutorial you may copy the following default page layout in the `list-bo
const storeSettingsUrl = '{{ path('setting::store-setting', {identifier: identifier}) }}';
const getSettingsUrl = '{{ path('setting::view-setting', {identifier: identifier}) }}';
-
-
+
+
{% endblock %}
```
@@ -945,6 +967,8 @@ php ./vendor/bin/doctrine-migrations diff
```
This will check for differences between your entities and database structure and create migration files if necessary, in `src/Core/src/App/src/Migration`.
+Besides creating the `book` table, the generated migration should also alter the `identifier` column of the `settings` table, to include the new `table_book_list_selected_columns` value.
+Check the generated file before running it.
To execute the migrations, run:
diff --git a/mkdocs.yml b/mkdocs.yml
index 6f6c70b..a4b80c7 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -34,6 +34,7 @@ nav:
- "Use NPM Commands": v7/how-to/npm_commands.md
- "Inject Dependencies": v7/how-to/dependency-injection.md
- "Set Up CSRF": v7/how-to/csrf.md
+ - "Use Simple Tables": v7/how-to/using-simple-tables.md
- Security:
- "Basic Security": v7/security/basic-security.md
- "Two-Factor Authentication with Time-based One-Time Password": v7/security/2fa-with-totp.md