Skip to content

Grouped List Developer Guide

Ed Mozley edited this page Oct 25, 2026 · 2 revisions

Grouped list - Developer Guide

Added in 4.0.0 Β· #208 Β· first used by System β†’ Analysts Β· user page: Finding people in the Analysts list

assets/js/grouped-list.js (+ assets/css/grouped-list.css) lays out a list of rows four ways - flat, collapsible tree, tabs, or one page with a jump list - with up to two levels of grouping and paging per group. It knows nothing about analysts, so the next page that needs grouping (Teams, Requesters) should use it rather than grow its own.

It is not the full-screen spreadsheet table, assets/js/data-table.js (Saved table views). That one is column-driven, with show/hide, drag-reorder and CSV. This one is for settings-style lists where the question is "who is in which group?".


Who does what

Part Owner
The controls (search, Group by, filters, sort) The page. It filters and sorts, then hands over the rows.
What a group is The page, through groupers
The table for one group's rows The page, through renderTable
Layout, open/closed groups, the active tab, paging, the jump list The component
Remembering the view The page. The component reports changes through onStateChange.

Using it

const list = createGroupedList({
    mount: document.getElementById('analysts-list'),
    scrollEl: document.querySelector('.settings-scroll'),   // what scrolls - the jump list follows it
    groupers: {
        team: row => row.teams.length
            ? row.teams.map(t => ({ key: 't' + t.id, label: t.name }))
            : [{ key: 'none', label: 'No team', sort: 9 }],
    },
    renderTable: (rows, ctx) => '<table>…</table>',    // ctx.keys = this group's keys, level by level
    emptyHtml: '<p>No matches.</p>',
    labels: { expand, collapse, prev, next, range: '{from}-{to} of {total}', of: '{shown} of {total}', jumpTo },
    onStateChange: ({ open, tab }) => { /* save them */ },
});

list.render(filteredSortedRows, {
    allRows,                 // before filtering - for the "3 of 12" counts
    filtered: true,          // a filter or search is on
    filterSig: '…',          // changes when the filters change, so matching groups reopen
    view: { group1: 'company', group2: 'team', layout: 'tree', page_size: 25, open: [...] | null, tab: '' },
});

Groupers

A grouper takes a row and returns an array of { key, label, sort? }.

  • ⚠️ A row is shown under every group it returns. An analyst in two teams is in both team groups, and both counts include them. That is deliberate - putting them in only one means choosing which, and the other group's count would then be wrong. Do not "fix" the totals by de-duplicating.
  • sort orders the groups (0 first, 1 normal, 2 or more last), then the label (case-insensitive, numbers in number order). Use it for All companies first and No team / Not set last.
  • Keys must be unique within one grouper. Prefix them (c12, t3) so a key can never collide with a fixed one like none.
  • Free text: make the key case-insensitive ('x' + s.toLowerCase()) and return the text as typed. When rows spell it differently, the group takes the spelling most rows use.

Paths and state

A group's path is its keys joined by \u001f (c12\u001ft3). Paths are what open holds, what tab holds, and what the page pages by.

  • open: null means the reader has never opened or closed a group, so the first group opens. [] means they closed everything, and that is kept.
  • While filtered is true, every matching group is open and toggles are not saved. A search should not overwrite the reader's tidy tree. A new filterSig starts with everything open again.
  • Page numbers are kept for the visit only.

The Analysts page

Rows api/tickets/get_analysts.php?signin=1&teams=1. teams=1 adds teams, job_title and department in one query - the page used to fetch teams one analyst at a time.
Company grouper Uses effective access: tenant_ids βˆͺ team_tenant_ids, or All companies when can_access_all_tenants or team_all_access. The row shows a via team chip in a company group it reaches only through a team.
Saved view api/system/analysts_list_view.php - get / save_mine / reset_mine / save_default, administrators only. Each analyst's own is user_preferences.system_analysts_list_view. The install default is system_settings.analysts_list_default_view and never holds open or tab. Every value is checked against a fixed list before it is stored.
Not saved The search and every filter except Status, so nobody comes back to a list that looks empty for no reason.
Columns One <table> per group, table-layout: fixed, with the same <colgroup>. The short columns have fixed widths and the four text columns share the rest, so the columns line up from group to group.

Phones

mobile.css LAYER 46. The tree and the tabs need nothing: the tab bar is the shared settings .tabs, which already scrolls sideways, and each table scrolls like every settings table. The jump list's side column is hidden, and the <select class="gl-jump-select"> the script always renders is shown instead, sticky at the top.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally