Skip to content

Display Order Developer Guide

Ed Mozley edited this page Oct 17, 2026 · 1 revision

Display Order - Developer Guide

How FreeITSM puts a saved order on top of a list that keeps growing - the personal module order and the shared Watchtower card order - and the reorder control both use. Introduced for issue #187.

User-facing: Watchtower Settings β†’ Cards and Preferences β†’ Display β†’ Module order (see System).

The short version

You want to Use
Apply a saved order to today's list displayOrderMerge($saved, $defaults) - includes/display_order.php
Read a stored order displayOrderParse($raw) - a JSON array of keys, anything else = no order
The modules this analyst may open, in their order analystVisibleModules($modules)
The Watchtower cards in the install's order wtCardOrder($conn) - includes/watchtower_settings.php
A "put these in order" control ReorderList.mount(el, {items, onChange, label}) - assets/js/reorder-list.js

The one rule

displayOrderMerge():

  1. the saved keys that still exist, in the saved order;
  2. then every key the saved order does not mention, in its default order.

What that gives you, without any migration ever:

  • A new module or card (added in a later release) appears for everybody who has saved an order, in its default place relative to the other keys they have not ordered.
  • A removed one drops out of the saved order silently.
  • Junk - unknown keys, duplicates, non-strings, a hand-edited row - degrades to the default order instead of losing anything.

It is the PHP twin of applyColumnConfig() in assets/js/data-table.js, which has kept saved table views working across new columns.

Order first, filter second

Ordering always happens over every key, before anything is hidden. Two consequences:

  • Order never grants anything. Every launcher filters exactly as it did before #187.
  • A hidden entry keeps its place. A module the analyst cannot open today, or a card switched off or unavailable, sits in its saved slot and reappears there when it comes back.

The Preferences screen relies on this: the list shows only the modules the analyst can open, and on save the new visible order is written back into the slots the visible modules already held (withHidden() in system/preferences/index.php), so the others never move.

Personal module order

Stored user_preferences.module_order - a JSON array of module keys. '' or no row = the default order
Default order the order of $modules in includes/waffle-menu.php
Read analystModuleOrderSaved(), memoised per request
Written Preferences β†’ Display β†’ Module order, via api/system/set_user_preference.php on every move; Reset writes ''

analystVisibleModules($modules) returns the modules this analyst may open, ordered, with the existing filter applied once:

  • System only for administrators;
  • everything else only if it is in $_SESSION['allowed_modules'] (null = all);
  • a Forms approver without Forms gets its approvals inbox ('approvals' => true on that entry).

Four launchers draw from it, so they cannot disagree:

Launcher Where
The waffle menu renderWaffleMenuPanel()
Ctrl+K "Go to" renderWaffleMenuJS() β†’ window.CP_MODULES - so Ctrl+K then Enter opens the analyst's first module
The home page index.php - one loop since #187; it was 27 hand-written cards
The Files start menu files/index.php

Adding a module

The home page no longer needs editing: add the module to $modules (in its default place) and it appears in all four launchers, and in every saved order, on its own. The other steps are in Module Access - Developer Guide.

Shared Watchtower card order

Stored system_settings.watchtower_card_order - a JSON array of card keys. Only stored when it differs from the default: an order put back by hand, or Reset, deletes the row, so a card added later takes its intended place
Default order the order of wtCardRegistry()
Read wtCardOrder($conn)
Written api/watchtower/save_settings.php, key card_order (an array, or null to reset), with the Cards tab's permission Cap::WATCHTOWER_CARDS

wtCardRegistry() is now the one card list - key => [element, module]:

  • wtCardKeys() is derived from it;
  • the dashboard's WT_CARD_ELEMENTS is printed from it;
  • the settings screen's CARD_MODULE (each card's colour) is printed from it - the hand-kept copy had already missed Projects;
  • tests/display-order.php checks every entry has its element on the dashboard and a module colour, and that every card on the dashboard is in the list.

The dashboard moves the cards, it does not restyle them: a small script right after the grid appends each card element to the grid in the saved order, before the page first paints. CSS order would have put the keyboard and a screen reader out of step with what is on screen. Hidden and unavailable cards are moved too, so they keep their place. It runs once - the five-minute refresh fills cards in and never redraws the grid.

Refusing, not ignoring. save_settings.php used to skip silently any section the analyst had no permission for and still answer "saved". It now answers 403 when a section the analyst may not change is sent (cards with entries or card_order without the Cards permission; items or paused_hours without the Counts permission). The settings page only sends the sections whose tabs it draws, so an honest page never trips it.

The reorder control

assets/js/reorder-list.js - one control for every "choose the order" setting.

const rl = ReorderList.mount(containerEl, {
    items: [{ key: 'tickets', label: 'Tickets', icon: '<svg…>', iconClass: 'waffle-module-icon tickets' }, …],
    label: 'Module order',          // the list's accessible name
    onChange: keys => save(keys),   // after every completed move
});
rl.getOrder(); rl.setOrder(keys);   // setOrder does not call onChange

An item can carry html - its own row markup, escaped by the caller - in place of the label (Watchtower puts its show/hide tick box there). label stays its accessible name.

Three ways to move, because each fails somewhere the others work:

Notes
Drag by the grip Native HTML5 drag-and-drop. Only the grip is draggable, with touch-action:none, so on a phone a long press on the grip starts it and a swipe anywhere else still scrolls (Mobile-Friendly Techniques Β§22). Cannot be checked in a headless browser
↑ / ↓ buttons Work everywhere, including where drag does not. tabindex=-1 so they do not double the tab stops
Alt+↑ / Alt+↓ On a focused row. Plain ↑/↓ walks between rows. Focus stays on the item that moved, and the new position is announced through an aria-live region

The list is real DOM order, so reading order and tab order match the screen. Styles are injected once per page from theme tokens, so dark mode follows. Labels come from common.reorder.*, with English fallbacks.

Saving is the caller's job. Preferences saves on every move, one request at a time, and puts the list back to the last saved order if a save fails (savePref() shows the error). Watchtower settings saves with its Save button, like everything else on that screen.

Tests

tests/display-order.php - no database:

  • the merge rule (new keys, removed keys, duplicates, junk) and the parser;
  • the Watchtower card list against the dashboard markup, the module colours and the labels, with a positive control;
  • that the home page, the waffle (drawer and Ctrl+K) and the Files start menu all draw from analystVisibleModules().
php tests/display-order.php

The screens were verified in a real browser: module order saved, followed by all four launchers, kept the hidden modules' slots for an analyst with three modules, and Reset; card order saved, shown on the dashboard, Reset, and refused (403) for an analyst without the permission.

See also

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally