Saved Table Views Developer Guide - edmozley/freeitsm GitHub Wiki
How saved views are put together, and the decisions worth not re-litigating.
The user page is Saved table views. Related: Assets ยท Roles and Permissions
Built for discussion #96 (dschipfel), expanded by Ed.
| File | What it does |
|---|---|
includes/table_views.php |
The visibility rules. Every read and write goes through it |
api/table-views/list.php |
Views on one table, plus the reader's teams and their default |
api/table-views/save.php |
Create or update |
api/table-views/delete.php |
Delete |
api/table-views/use.php |
Stamp last-used, and set or clear the personal default |
assets/js/data-table.js |
Capture, apply, the library and the editor |
includes/data-table-skeleton.php |
The Views and Save view toolbar buttons |
assets/css/data-table.css |
The library, the editor, the buttons |
tests/table-views.php |
31 assertions, touches the database |
asset-management/table.php is a thin page over assets/js/data-table.js, and so are tasks/table, calendar/table and change-management/table. Views are built into the engine, so all four have them.
A module opts in with one line in its createDataTable() config:
viewsKey: 'assets', // 'assets' | 'tasks' | 'calendar' | 'changes'A table with no viewsKey hides both buttons rather than showing ones that do nothing.
โ ๏ธ viewsApiis derived fromprefApi, not set again. The four host pages sit at different depths (../api/โฆvs../../api/โฆ), and a second copy of that path is a second chance to get it wrong โ which would present as a Views button that silently does nothing.
table_views
id, table_key, name, description
owner_id -- FK analysts, ON DELETE SET NULL
visibility -- 'private' | 'team' | 'public'
team_id -- FK teams, ON DELETE SET NULL
config -- MEDIUMTEXT, the engine's own state as JSON
created_datetime, updated_datetime, last_used_datetimeconfig is opaque to the database on purpose: the engine owns that shape, and a column per setting would need a migration every time it gained one. It is validated as JSON on the way in โ a malformed one would break the table for everybody it is shared with, and the moment to find that out is at save.
owner_id is SET NULL rather than CASCADE, so a team or public view survives the person who wrote it leaving. A private view with no owner then matches nobody: unreachable rather than exposed.
No tenant_id, and none is needed. A view is a way of looking at rows; its filters are applied to rows the reader was already allowed to load.
Everything reads through:
function tableViewVisibleClause(PDO $conn, int $analystId): arrayThree answers โ mine, my team's, everyone's โ and a fourth that must never happen: somebody else's private view. Four endpoints each writing their own clause is four chances to get that wrong.
โ ๏ธ It returns a BRACKETED group. Callers append it to aWHEREthat already carriestable_key, and an unbracketed set of alternatives would bind that condition to the last branch only โ a tasks view would appear on the asset table. There is an assertion for exactly this, because it is a one-character mistake.
Analysts belong to many teams. analyst_teams is a join table; there is no analysts.team_id. So "their team" was ambiguous and Ed chose: sharing names a specific team, and tableViewSave() refuses a team the saver does not belong to โ otherwise anybody could share into any team by posting its id.
An analyst in no teams is a real case: the clause is built from their teams, so with none it has to fall back to owner-or-public rather than producing broken SQL. Asserted.
Write access is owner-only. can_edit is computed server-side and returned with each row, so the browser shows the same answer the write path enforces rather than working it out from owner_id itself.
-
The default is a row in
user_preferences(dtview_default_<table_key>). It is a fact about the reader: two people can have different defaults pointing at the same shared view, and one changing theirs must not change the other's. - Last used is one timestamp on the view, not one per reader. It answers "is anybody still using this?", which is what decides whether it can be deleted. "When did I last use it?" would need a row per person per view, and nobody has asked for that.
captureViewConfig() reads columnState, sort and filters. Filters are held as a Set per column, so they are unpacked to arrays going out and repacked coming back.
โ ๏ธ searchTermis deliberately excluded. A filter is how you like to look at things; a search is a question you asked once.
applyColumnConfig() is one function shared by preferences and views โ both restore the same thing, and two copies would drift the first time a column was added. Its merge is what makes a view survive a changing table: unknown keys dropped, columns the config never mentioned appended at their defaultOrder.
That merge is why a view naming six columns can render eight, which is correct and worth knowing before somebody "fixes" it.
applyDefaultView() runs after loadPreferences(), so a chosen view beats the loose column state โ picking a view is the more deliberate act. A default pointing at a deleted or now-unreachable view silently falls back to the table's own defaults rather than erroring every morning.
-
.dt-btnis scoped.dt-toolbar .dt-btn. The modal footer buttons shipped with no padding, border or radius, rendering as bare text on a coloured block..dt-modal .dt-btnnow defines them. Third time in one day a class was used outside the scope it is defined in. -
The English fallback must interpolate.
vt()fell back to raw strings without substituting, so before the strings existed the library renderedCreated {d}andby {name}literally. A safety net that produces visible nonsense is not a safety net. -
Save viewfetches the list before opening the editor. The teams an analyst can share into arrive with the view list, so opening from a cold toolbar would show "A team" disabled the first time and working the second.
php tests/table-views.php # 31 assertions
Everything it makes is named ZZTV and removed in a finally. It picks real analysts with differing team membership rather than creating them, because the whole feature turns on analyst_teams being what it says it is โ and skips with a message if analyst 1 is not in two teams, rather than passing vacuously.
Every visibility answer is checked from the side that must be refused as well as the side that must work, with positive controls: somebody who can see a public view cannot update or delete it, and can_edit says so before they try.
Beyond the unit tests, the library was driven in a real browser on the asset table: applying a saved view took it from 10 columns and 597 rows to 4 columns and 14 rows, with the sort applied and the button marked active.