Repository navigation
Task Work View Developer Guide
How a task's Work view folds an empty Checklists, Forms or Follow-ups section to a heading such as Forms (0), how an opened section and half-typed text survive the task panel being redrawn, and why a section that fails to load now says so instead of vanishing. Built for #218 part B (changelog #2719, 4.9.0).
For what it does from a user's point of view, see The Work view. The agreed spec is Simpler ticket and task forms - design. How the Work view itself is assembled (the head, progress, Complete, the Work tab) is in Forms on tasks and the Work view - Developer Guide, and the original design is Task work view - design.
No new tables, no new endpoints. It is one personal preference and a block of screen code:
| File | Job |
|---|---|
system/preferences/index.php |
The preference tasks_collapse_empty_work - its default, the Empty work sections on a task dropdown, and the save |
tasks/index.php |
Reads the preference into window.TASK_COLLAPSE_EMPTY_WORK
|
assets/js/tasks.js |
The whole feature. workSectionHead(), toggleWorkFold(), workSectionFailed(), the drafts (workDrafts, restoreWorkDrafts(), dropWorkDrafts()), and the three render functions that use them |
assets/css/tasks.css |
.work-fold (the heading button and its chevron) and .work-load-error
|
lang/*/system.php, lang/*/tasks.php
|
system.preferences.work_fold_*, tasks.work.section_count, tasks.work.load_failed
|
Everything goes through workSectionHead(). The three render functions - renderTaskChecklists(), renderTaskForms(), renderTaskFollowUps() - each ask it for their heading and whether to draw a body at all. If you add a fourth foldable section, call it too; do not write a second fold.
A personal preference, off by default, with no install-wide default (agreed in the spec). It is stored in user_preferences like every other one. The default list in system/preferences/index.php:
// GH #218 part B: 'on' folds an empty Checklists / Forms / Follow-ups section to
// a heading in a task's Work view. '' shows them in full, as before.
'tasks_collapse_empty_work' => '',The dropdown saves through the page's shared savePref(), which posts to api/system/set_user_preference.php:
// ===== Empty Work sections on a task (tasks_collapse_empty_work, GH #218) =====
const workFoldSelect = document.getElementById('workFoldSelect');
const workFoldHint = document.getElementById('workFoldSavingHint');
if (workFoldSelect) {
workFoldSelect.addEventListener('change', async function() {
workFoldHint.classList.add('show');
await savePref('tasks_collapse_empty_work', workFoldSelect.value);
setTimeout(() => workFoldHint.classList.remove('show'), 1200);
});
}tasks/index.php reads it in the same single query as the other task preferences, and only the exact value 'on' turns it on:
if ($__k === 'tasks_collapse_empty_work' && $__v === 'on') { $taskCollapseEmptyWork = true; } window.TASK_COLLAPSE_EMPTY_WORK = <?php echo json_encode($taskCollapseEmptyWork); ?>;Two consequences worth knowing:
-
It is read once, at page load. Changing the preference on System -> Preferences does not reach an already open Tasks tab until it is reloaded. Unlike Open here (
setWorkOpenPref()), there is no in-panel control that flips the global. -
A task opened inside a ticket follows it too.
tasks/embedded.phpends inrequire __DIR__ . '/index.php', so the embedded task editor is the same page with the same globals. The other task pages (table, timeline, calendar) do not set the global, and do not need to: they do not draw the Work view.
A section is drawn in one of three states, and only one of them ever shows (0):
| State | What the user sees | Where it is decided |
|---|---|---|
| Loading | Nothing - the section stays display:none
|
The style="display:none" on each host in renderDetailPanel()
|
| Could not load | Its plain heading, open, Could not load Checklists. and Retry | workSectionFailed() |
| Loaded | Records: in full, as before. None: the fold button | workSectionHead() |
There is no "loading" spinner on purpose. A section only learns whether its feature is switched on (data.enabled) when its fetch returns, so a heading drawn before that would flash up for a section that then disappears.
"Empty" means no records at all. The count passed in is the length of the section's list - checklistState.checklists.length, formState.forms.length, followState.items.length - so a completed checklist, a submitted form or a closed follow-up still counts and keeps its section open.
let workFold = { taskId: null, open: {} };
let workDrafts = { taskId: null, values: {} };
/** The fold state for the open task, started afresh when another task opens. */
function workFoldState() {
if (workFold.taskId !== selectedTaskId) workFold = { taskId: selectedTaskId, open: {} };
return workFold;
}function workSectionHead(part, label, count) {
const f = workFoldState();
if (count > 0) f.open[part] = true;
if (!window.TASK_COLLAPSE_EMPTY_WORK || count > 0) return { head: `<label>${esc(label)}</label>`, folded: false };
const open = f.open[part] === true;
return {
folded: !open,
head: `<button type="button" class="work-fold${open ? ' is-open' : ''}" data-work-fold="${part}" aria-expanded="${open}" onclick="toggleWorkFold('${part}')">
<svg class="work-fold-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 6 15 12 9 18"></polyline></svg>
<span>${esc(window.t('tasks.work.section_count', { name: label, count }))}</span>
</button>`,
};
}Reading it line by line:
-
if (count > 0) f.open[part] = true;runs before anything else, and runs whether or not the preference is on. A section that has ever been drawn with records for this task counts as opened. So removing the last checklist leaves Checklists open with its picker under the pointer, instead of snapping shut and moving everything below it up. The same line is what makes adding the first item keeps it open work: once the count is 1 the section is open, and it stays open after that item is removed again. -
The preference off, or any records - the old plain
<label>,folded: false. With the preference off the Work view is byte-for-byte what it was. -
Otherwise it is a real
<button>, so click, Enter and Space all work with no key handler, andaria-expandedtells a screen reader which way it is. The heading text is a translation key,'{name} ({count})', rather than string concatenation, so a locale can put the count where its grammar wants it.
Each render function uses the pair the same way. Checklists and Forms draw only the head when folded:
const { head, folded } = workSectionHead('checklists', T('checklist_heading'), checklistState.checklists.length);
host.innerHTML = folded ? head : `
${head}
${lists ? `<div class="chk-lists" data-sort="lists" ondragover="chkDragOver(event)">${lists}</div>`
: `<div class="chk-empty">${esc(T('checklist_none'))}</div>`}
${attach}`;
restoreWorkDrafts(host);Follow-ups returns early instead, because the rest of that function builds the create and link row:
const { head, folded } = workSectionHead('followups', T('heading'), followState.items.length);
if (folded) { host.innerHTML = head; return; }When the section is opened, the body is exactly what it always was - the attach picker, Add a form, the create and link row - with the same permission checks, because it is the same code path.
/** Opens or folds an empty Work section, keeping keyboard focus on its heading. */
function toggleWorkFold(part) {
const f = workFoldState();
f.open[part] = !f.open[part];
workRenderPart(part);
document.querySelector(`#taskWorkSection [data-work-fold="${part}"]`)?.focus();
}
/** Opens a Work section without the user pressing its heading - an action that sends them there. */
function workOpenPart(part) { workFoldState().open[part] = true; }
/** Redraws one Work section from the state it already holds. */
function workRenderPart(part) {
if (part === 'checklists') renderTaskChecklists();
else if (part === 'forms') renderTaskForms();
else if (part === 'followups') renderTaskFollowUps();
}-
A toggle never fetches.
workRenderPart()redraws fromchecklistState,formStateorfollowState, which the loader already filled. Opening a folded section is instant and cannot fail. -
The re-focus is needed because
innerHTMLreplaced the button the user just pressed. Without it, a keyboard user pressing Enter would find focus dropped back to the document body. -
workOpenPart()is for actions that send you somewhere. Follow up on a failed step or a submitted form callsfollowUpFrom(), which now opens Follow-ups before drawing it, so the prefilled title box it focuses actually exists:
function followUpFrom(kind, id, name, note) {
followState.source = { kind, id, name, title: window.t('tasks.followups.default_title', { name }) , note: note || '' };
workOpenPart('followups');
dropWorkDrafts('fuTitle'); // the prefilled title, not whatever was typed before
renderTaskFollowUps();
const box = document.getElementById('fuTitle');
if (box) { box.focus(); box.scrollIntoView({ block: 'center' }); }The dropWorkDrafts('fuTitle') matters: without it, restoreWorkDrafts() inside renderTaskFollowUps() would put back whatever was half-typed earlier over the title that Follow up just prefilled.
This is the part to understand before changing anything here. From the comment above the block:
// TRAP: the fold state and the typed drafts live OUTSIDE the panel's markup, keyed by
// task. openDetailPanel() rebuilds the whole panel after many actions (Complete, a
// refused status change...), so anything kept in the DOM would be lost on every
// redraw - an opened section would snap shut and half-typed text would vanish.openDetailPanel(taskId) is not only "open a task". It is also the screen's general "refresh this task" call: setTaskContractor(), saving a project, a refused status change and many others end with it. It calls renderDetailPanel(), which writes the whole panel's innerHTML - including the empty #taskChecklistSection, #taskFormsSection and #taskFollowUpSection hosts - and then starts all the loaders again:
renderDetailPanel(data.task);
workState = { taskId, task: data.task, loaded: {}, autoOpened: false };
loadTaskChecklists(taskId);
loadTaskForms(taskId);
loadTaskProcedures(taskId);
loadTaskFollowUps(taskId);So a data-open attribute, an .is-open class or the text in an <input> all die on every one of those calls. workFold and workDrafts are plain module-level objects that the redraw never touches.
What keys them is the task id, and nothing else. Both are checked against selectedTaskId on every use and quietly reset when it changes. That gives the behaviour the spec asked for:
- Redraw the same task as often as you like: the state survives.
- Open another task: it starts folded, with no drafts.
- Go back to the first task: it starts afresh too. Only the most recent task's state is kept, which is deliberate - this is "keep my place while I work on this task", not a store.
It is memory only. A browser reload, or closing the tab, loses both. "Survives a refresh" in the user docs means a refresh of the task window, not of the page.
// Typed but not yet sent: the follow-up boxes, a step's value and its Fail note.
// Kept as they are typed, put back after any redraw, dropped once sent.
const WORK_DRAFT_ID = /^(fuTitle|fuRef|chkValue\d+|chkNote\d+)$/;
document.addEventListener('input', e => {
const el = e.target;
if (!el || !el.id || !WORK_DRAFT_ID.test(el.id) || !el.closest('#taskWorkSection')) return;
if (workDrafts.taskId !== selectedTaskId) workDrafts = { taskId: selectedTaskId, values: {} };
workDrafts.values[el.id] = el.value;
});-
One delegated listener on
document, not one per box. The boxes are recreated on every redraw, so a listener attached to a box would be thrown away with it; the document is never replaced. -
Keyed by element id, inside a task. The ids already identified the thing being typed for:
fuTitleandfuRefare the follow-up create and link boxes,chkValue<itemId>is a checklist step's value, andchkNote<itemId>is a step's Fail note. Because the step's id is in the element id, two steps' drafts can never collide. -
The allow-list regex is the boundary. Only those four shapes are captured, and only inside
#taskWorkSection. A new draftable box needs its id added here, or it will not survive a redraw.
Putting them back:
/** Puts typed drafts back into a freshly drawn section; a Fail note that was being typed reopens its box. */
function restoreWorkDrafts(host) {
if (workDrafts.taskId !== selectedTaskId) return;
Object.entries(workDrafts.values).forEach(([id, v]) => {
const el = host.querySelector('#' + id);
if (!el || el.disabled || v === '') return;
el.value = v;
if (id.startsWith('chkNote')) {
const box = document.getElementById('chkException' + id.slice(7));
if (box) box.style.display = '';
}
});
}-
Scoped to the host just drawn.
renderTaskChecklists()andrenderTaskFollowUps()each pass their own host, so a checklist redraw never writes into the follow-up boxes and the other way round.renderTaskForms()does not call it, because no form box is in the allow-list. -
el.disabledis skipped. A completed step's value box is drawndisabledwith the savedresponse_valuein it; a stale draft must not overwrite the recorded answer. -
v === ''is skipped, so a box someone emptied falls back to whatever the redraw drew (forfuTitle, the prefilled title). -
A Fail note reopens its box. The note's
<textarea>lives in#chkException<itemId>, drawndisplay:noneand only shown after Fail is pressed. Restoring the text into a hidden box would keep the words and hide them, so the box is shown again.id.slice(7)strips the seven characters ofchkNoteto get the item id.
Forgetting them once they are sent:
/** Forgets drafts once what they were typed for has been sent. */
function dropWorkDrafts(...ids) { ids.forEach(id => delete workDrafts.values[id]); }It is called in three places: after a successful checklist post for a step (dropWorkDrafts('chkValue' + payload.item_id, 'chkNote' + payload.item_id) in postTaskChecklist()), after a successful follow-up create or link (if (payload.action !== 'unlink') dropWorkDrafts('fuTitle', 'fuRef'); in postTaskFollowUp() - an unlink sends neither box), and in followUpFrom() above. A failed post keeps the draft, which is the point.
Before #218 each loader treated "the request failed" the same as "the feature is off": it emptied the host and hid it. A broken Forms endpoint looked exactly like Forms on tasks being switched off. Now the two are told apart:
const data = await fetch(API_BASE + 'forms.php?answers=1&task_id=' + taskId).then(r => r.json());
if (!data.success) throw new Error(data.error || 'forms');
if (!data.enabled) { host.innerHTML = ''; host.style.display = 'none'; workLoaded(taskId, 'forms', false, 0); return; } } catch (e) {
console.error(e);
workSectionFailed(host, taskId, 'forms', window.t('tasks.forms.heading'), 'loadTaskForms');
}data.enabled === false is still "switched off, hide it". success: false, a network error and a JSON parse error all land in the catch:
function workSectionFailed(host, taskId, part, label, retry) {
if (taskId !== selectedTaskId) return;
host.style.display = '';
host.innerHTML = `<label>${esc(label)}</label>
<div class="work-load-error" role="alert">${esc(window.t('tasks.work.load_failed', { name: label }))}
<button type="button" class="btn btn-secondary btn-sm" onclick="${retry}(selectedTaskId)">${esc(window.t('common.retry'))}</button>
</div>`;
if (part !== 'followups') workLoaded(taskId, part, true, 0);
}-
taskId !== selectedTaskId: a slow failure for a task the user has already left must not paint an error into the next task's panel. - Always the plain heading, never the fold button, so a failure can never read as (0).
-
Retry calls the loader by name (
loadTaskChecklists,loadTaskForms,loadTaskFollowUps) with the current task. The name comes from the caller as a fixed string, never from data. -
workLoaded(taskId, part, true, 0)reports the section as switched on with nothing in it.renderWorkHead()shows the whole Work block only when one of checklists, forms or procedures saysenabled, so without this a failure in the only switched-on section would hide the block - and the error with it. Follow-ups are not one of the partsrenderWorkHead()counts, so it does not report.
#taskWorkSection .work-fold {
display: inline-flex; align-items: center; gap: 4px;
margin: 0 0 5px -4px; padding: 2px 6px 2px 4px;
border: none; border-radius: 5px; background: none; cursor: pointer;
font-family: inherit; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
color: var(--text-dim, #999);
}
#taskWorkSection .work-fold:hover { background: var(--surface-2, #f1f1f4); color: var(--text, #333); }
#taskWorkSection .work-fold:focus-visible { outline: 2px solid var(--tsk-accent, #7c3aed); outline-offset: 1px; }
#taskWorkSection .work-fold-chevron { flex-shrink: 0; transition: transform 0.15s ease; }
#taskWorkSection .work-fold.is-open .work-fold-chevron { transform: rotate(90deg); }-
It is dressed as the section label it replaces - the same 11px, weight 600, uppercase,
0.5pxletter-spaced--text-dimheading as.detail-field label- so a folded section reads as a quiet heading, not as a button competing with Complete. -
margin-left: -4pxwithpadding-left: 4pxkeeps the text exactly where the plain label's text was, while giving the hover background some room on the left. Without it the heading would jump 4px right when the preference is turned on. -
:focus-visible, not:focus, so a mouse click does not leave an outline, but a keyboard user gets one. - The chevron is the one used on the board card's subtask disclosure, rotated 90 degrees when open. The rotation only shows on an empty section that has been opened; a section with records gets the plain label and no chevron at all.
#taskWorkSection .work-load-error { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 13px; color: var(--danger-text, #b91c1c); margin-bottom: 8px; }flex-wrap keeps Retry on the screen in the narrow side panel and on a phone, where the message and the button do not fit on one line.
Editing
assets/css/tasks.cssmeans bumping its?v=NNon every page that loads it:tasks/index.php,tasks/settings/,tasks/table/,tasks/timeline/,tasks/calendar/,tasks/evidence/index.phpandtasks/evidence/report.php.assets/js/tasks.jsis loaded only bytasks/index.php(and so bytasks/embedded.php). Part B moved them fromtasks.css?v=56/tasks.js?v=71to 57 / 72.
margin-bottom: 5px on the button matches the 5px under .detail-field label, so nothing below the heading moves either.
-
Do not keep Work view state in the DOM. Anything a user does to a section that should outlast a redraw goes in
workFoldorworkDrafts(or a state object like them, keyed by task), never on an element. See Why the state lives outside the markup. -
A new box with half-typed text needs its id in
WORK_DRAFT_ID, arestoreWorkDrafts(host)call at the end of its render function, and adropWorkDrafts()once its text has been sent. Miss the last one and the old text comes back into the empty box after a successful send. -
Do not hide a section in the
catch. That was the old behaviour and it made a broken endpoint indistinguishable from a switched-off feature. UseworkSectionFailed(). - Never show a count you did not load. The heading's count is the length of the list the loader returned. A section that failed or is still loading has no count, which is why neither state can draw the fold button.
There is no automated test for this: nothing under tests/ covers workSectionHead(), the drafts or workSectionFailed(). It was checked by hand in a browser on a test install through the real Tasks page. To check it again:
- Switch on Checklists on tasks and Forms on tasks (Tasks -> Settings -> Checklists). Set System -> Preferences -> Empty work sections on a task to Fold empty ones to a heading and reload Tasks.
- Open a task with no checklist, no form and no follow-up. Expect Checklists (0), Forms (0) and Follow-ups (0), each a heading with a chevron.
- Tab to one and press Enter, then Space: it opens and folds, and focus stays on the heading.
- Open Follow-ups, type a title, then change the task's project (or anything else that calls
openDetailPanel()). Follow-ups is still open and the title is still there. - Attach a checklist: it shows in full. Remove it: the section stays open with its picker.
- On a pass/fail step press Fail, type a note, and trigger a redraw: the note box is open with the text in it.
- Fold Follow-ups, then press Follow up on a failed step: Follow-ups opens with the prefilled title, focused.
- Open another task: everything starts folded again, with no drafts.
- Make a loader fail (for example block
api/tasks/forms.phpin the browser's dev tools). The section shows its heading, Could not load Forms. and Retry, never (0); unblock it and Retry recovers. - Do steps 2 and 4 on a task opened inside a ticket.
- Set the preference back to Show them in full and reload: the Work view looks exactly as it did before 4.9.0.
- The Work view - the user page
- Forms on tasks and the Work view - Developer Guide - how the Work view is assembled
- Checklists on tasks - Developer Guide - the checklist steps, values and Fail notes whose drafts are kept
- Simpler ticket and task forms - design - #218, parts A to D
- Task work view - design - #190
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
-
Architecture
- β³ π§Ή Root folder tidy (URL routing)
- β³ π Database Integrity (FKs & constraints)
- β³ π οΈ Database Verification β Developer Guide
- β³ π§© Service Layer (UI + API share code)
- β³ πͺ€ Code traps (the TRAP: comments)
- β³ π Commenting for the Engineering wiki
- β³ π οΈ Engineering wiki scanner - Developer Guide
- β³ π Service-Layer progress
- π€ Contributing (read before a pull request)
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- π± Currencies
- π§ The sidebar (optional module rail)
- π§© Preference templates
- π Display order (modules, Watchtower cards) β Developer Guide
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ ποΈ Mobile: Files
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- β³ π Switching modules off
- β³ π οΈ Module availability β Developer Guide
- β³ π Module availability β design (#217)
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
-
ποΈ LDAP & Active Directory
- β³ π₯ Importing people from a directory
- β³ π₯ Importing people from Microsoft Entra ID
- β³ π³ OpenLDAP & Samba AD (Docker) setup
- β³ π οΈ LDAP β Developer Guide
- β³ π οΈ Directory sync β Developer Guide
- β³ π§© Extending directory sync
- β³ π οΈ Entra directory sync β Developer Guide
- β³ π₯ Directory sync β design
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π Proposal: custom fields, inventory import and change evidence (#194)
- β³ π οΈ Custom fields, evidence + inventory - review (#195-#197)
- β³ π οΈ Long forms in a task's Work view - review (#198)
- β³ π οΈ Microsoft Entra ID import - review (#235)
- β³ π οΈ Assets from their Intune primary user - review (#236)
- β³ π οΈ Organisation chart - review (#242)
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ π§βπΌ REST API: Analysts
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π οΈ Inbox.js map β Developer Guide
- β³ π οΈ Floating windows & the file viewer β Developer Guide
- β³ π οΈ Tickets settings β Developer Guide
- β³ π Simpler ticket and task forms - design (#218)
- β³ ποΈ The ticket workspace
- β³ π The ticket table
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π§Ύ Custom ticket fields
- β³ π·οΈ Ticket categories
- β³ π§Ύ A type for every new ticket
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ π Attachment size limits and large emails
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Case reports - a ticket and its work as one PDF
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Leases
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π§© Asset type layouts
- β³ π Inventory freshness
- β³ π Linked configuration item
- β³ π Connected assets
- β³ π Asset review
-
β³ π·οΈ QR asset labels
- β³ π οΈ QR asset labels β Developer Guide
- β³ π Asset label designer - design (#229)
- β³ π· Asset scanning β Developer Guide
- β³ π οΈ Asset tag numbering β Developer Guide
- β³ π οΈ Asset reconciliation β Developer Guide
- β³ π οΈ Reconciliation, tags + labels - review (#164)
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ πͺ Hyper-V and Failover Clusters
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Starters & leavers
- Domains
- ποΈ Files
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- π‘ Zabbix (monitoring)
- π¬ Microsoft Teams alerts
- π 3CX (phone system)
-
System
- β³ π οΈ Preferences β Developer Guide
- β³ π Finding people in the Analysts list
- β³ πͺ When an analyst leaves
- β³ π οΈ System β Branding β Developer Guide
- β³ π©Ί Page health (D019) β Developer Guide
- β³ β Feature Bingo
- β³ π·οΈ Cost centres
- β³ π· Photo Album
- β³ Demo Data
- β³ π οΈ Demo Data β Developer Guide
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ π A directory account could be matched to someone else
- β³ π« #244 Reply went to the assigned analyst
- β³ π A lookup answer could name any record
- β³ π« #227 A mailbox could not be deleted
- β³ π« #226 A picture on a note could not be closed
- β³ π» #222 The Assets page never loaded in 4.3.0
- β³ βοΈ #214 A "contains" condition did not match capitals
- β³ βοΈ #216 A workflow email from a chosen mailbox always failed
- β³ π³ The Docker image was missing PHP extensions
- β³ π A failed DNS lookup read as a missing record
- β³ π A link could change data
- β³ π Access outlived its removal
- β³ π Knowledge articles could run scripts
- β³ π Knowledge writes ignored permissions
- β³ π Company boundaries (Oct 2026)
- β³ π Smaller gaps (Oct 2026)
- β³ π Escaped text could leave its attribute
- β³ π Task descriptions were not cleaned
- β³ π Task documents ignored company scope
- β³ π Repeating tasks ignored company scope
- β³ π¬ #183 Mailbox checks overlapped
- β³ π #188 Transfer lock shown off when unknown
- β³ β #184 Subtask names and Overdue
- β³ π§© #186 Teams: the buttons did nothing
- β³ π Text in a change could run script
- β³ π AI settings changed without the AI permission
- β³ πͺ #178 Deleting an analyst failed
- β³ π« A team with no departments showed 0 tickets
- β³ π Projects: adding a phase did nothing on Save
- β³ π Leavers could still sign in
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96