Grouped List Developer Guide - edmozley/freeitsm GitHub Wiki
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?".
| 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. |
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: '' },
});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.-
sortorders 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 likenone. -
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.
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: nullmeans the reader has never opened or closed a group, so the first group opens.[]means they closed everything, and that is kept. - While
filteredis true, every matching group is open and toggles are not saved. A search should not overwrite the reader's tidy tree. A newfilterSigstarts with everything open again. - Page numbers are kept for the visit only.
| 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. |
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.