-
Notifications
You must be signed in to change notification settings - Fork 15
Mobile Friendly Tickets
The tickets inbox is the first module made mobileβfriendly and the reference implementation for the patterns described in MobileβFriendly. This page records what's been done, how it works, the challenges that came up, and how each was solved.
π§° For the codeβlevel catalogue of every CSS/JS trick used here β the ShadowβDOM perβmessage horizontal scroll, the iOS reflow trap, section sheets, the sticky dayβheading, and how it's all verified headlessly β see Mobile: Techniques & Tricks.
Scope: the inbox (
/tickets/) and, since #1005, the settings page β see The settings page below. The remaining tickets pages (dashboard, calendar, users, help) are reachable via the views drawer but their bodies aren't reflowed yet.
Everything below is gated to phoneβwidth (β€768px); desktop is unchanged.
- Waffle module drawer (topβleft) β a fullβheight left slideβin to switch modules, with a β close button. (This lives in the shared waffle component, so it works on every page.)
- Views hamburger (topβright) β a β° button opens a right slideβin drawer with the tickets views (Inbox / Dashboard / Users / Calendar / Rota / CSAT / Settings / Help), with a dim backdrop that closes it.
-
Masterβdetail stack. The desktop 3βpane split (folders / list / conversation) becomes a oneβpaneβatβaβtime slide stack, defaulting to the ticket list.
- Tap a ticket β the conversation slides in fullβscreen.
- A βΉ Back control (and the device back button) returns to the list.
- A β° Folders control opens the folder list; picking a folder returns to the list.
-
Ticket reference in the subβbar β the ticket number (e.g.
LUNβ779β88063) sits at the topβright of the subβbar, on the same row as βΉ Back and in the same colour/size, so it's out of the subject's way. - Subjectβonly heading β the heading drops the "Ticket <ref> β" prefix and shows just the subject, truncated to one line.
- Attachment badge β the fullβwidth yellow "β¦has N attachments" bar is squished into a compact π badge with a count, rightβaligned on the subject row (taps through to the attachment list).
- Emailβdetails chevron β the βΎ that reveals From/To/Date sits right after the subject (moved away from the attachment badge, which was easy to misβtap). The whole subject line toggles the details; the badge is a separate tap target.
- Collapsible header β the From/To/Date/Cc block starts collapsed behind the subject + βΎ chevron (Gmailβapp style); tap to expand.
- Singleβrow bottom action bar β the action buttons are iconβonly and pinned to a single row at the bottom; the first five show and the rest live behind a "β―" overflow popover (with their word labels).
-
Section panels β crowded sections are relocated out of the ticket into their own fullβscreen sheets, each opened by an icon button:
- π Links β problem / change linking
- β Properties β status / priority / owner β¦
- β± Time entries
- π₯ Affected objects (CMDB)
- Company (tenant) switcher β on multiβcompany installs, moved out of the cramped top bar into the module/waffle drawer, restyled for the light panel.
Every readingβpane action opens as a proper fullβscreen panel with a real working area:
- Add note β fullβscreen sheet; the note box flexes to fill the whole panel.
- Reply / forward β fullβscreen sheet; the To/Cc row stacks and the richβtext editor grows to fill between the fields and the Send bar.
- Schedule β fullβscreen sheet; native date/time inputs tamed so they don't spill off the right.
-
Ask AI β the 420px side panel becomes fullβwidth
100dvh, message list scrolling, input pinned at the bottom. -
Audit history β on a phone this is not the desktop modal at all. The desktop 5βcolumn table (Date / Analyst / Field / Old / New) is wider than the screen, and on iOS a tooβwide element makes Safari reflow the page to a desktop width β which switches the mobile rules off and drops the modal back to a small centred box (the same "spills wide β reflows to desktop" trap as the reply modal). So mobile routes audit through the same fullβscreen
.mobile-sheetslideβover used for Links / Properties / Time / Objects, filled with a dayβgrouped feed of one card per change: field + time on top, old β new beneath (old struck through), who did it under that. The date is a sticky heading said once per day; a firstβtime value shows just the new value (not "β β Open"); long values wrap rather than clip. Audit history isn't in the reading pane to relocate, so the sheet fetches it on open (the endpointinbox.jsalready uses). Desktop keeps its table, untouched.
mobile.js loads after inbox.js and wraps the globals inbox.js already exposes β it never edits the large inbox.js (the one exception being a single TinyMCE content_style string; see below).
The active pane is a single attribute, so CSS ancestor selectors drive the whole layout:
body[data-mobile-pane="reading"] .email-list-container { transform: translateX(-100%); }
body[data-mobile-pane="reading"] .reading-pane { transform: translateX(0); }List rows already call selectEmail(id); folders already call selectFolder(type,id). mobile.js wraps them:
var _selectEmail = window.selectEmail;
window.selectEmail = function () {
var r = _selectEmail.apply(this, arguments);
if (mq.matches && currentPane() !== 'reading') pushPane('reading'); // slide to conversation
if (r && typeof r.then === 'function') r.then(afterTicketRender); // relocate sections + refinements
return r;
};pushPane calls history.pushState, and a popstate listener restores the pane β so the device back button pops conversation β list. It also reads the page's own state directly (currentEmail, ticketAttachments are topβlevel lets, shared across classic scripts) to build the reference badge, subjectβonly heading and attachment badge, and wraps renderAttachmentInfoBar to refresh the badge when attachments load async.
Each crowded section is one entry in a list; a sheet is built for each, and after every ticket render the nodes are moved in and a toolbar button added:
var SECTIONS = [
{ cls:'links', title:'Links', icon:'π', label:'Links', sel:'.problem-strip', all:true },
{ cls:'props', title:'Properties', icon:'β', label:'Properties', sel:'#ticketPropertiesContainer', all:false },
{ cls:'time', title:'Time entries', icon:'β±', label:'Time', sel:'#timeEntriesContainer', all:false },
{ cls:'cmdb', title:'Affected objects', icon:'π₯', label:'Objects', sel:'#cmdbObjectsContainer', all:false }
];Adding another panel later is one line in this array.
The stylesheet is one @media (max-width: 768px) block, organised into numbered layers:
| Layer | Purpose |
|---|---|
| 1 | App shell β top bar, views hamburger + right drawer, .user-menu clamp |
| 2 | Inbox masterβdetail pane stack + the Back/Folders subβbar |
| 3 | Modals β fullβscreen sheets; perβmodal fill (note box, reply/forward TinyMCE); 16px antiβzoom fields; native date/time fix |
| 4 | Readingβpane density (tighter side padding, smaller subject) |
| 5 | Action toolbar β iconβonly bottom bar (single row + "β―" overflow) |
| 6 | Collapsible ticket header |
| 7 | Section sheets (Links / Properties / Time / Objects) |
| 8 |
Openedβticket refinements β ref in subβbar, subjectβonly heading, π badge, chevron reorder, tenant switcher in drawer, body.ticket-popout backstop |
| 9 |
AskβAI chat panel β fullβwidth 100dvh
|
| 10 |
Audit history β its own fullβscreen .mobile-sheet with a dayβgrouped card feed (not the desktop modal) |
(The waffle drawer itself lives in includes/waffle-menu.php, not mobile.css, so it works on every page.)
| Challenge | Solution |
|---|---|
| A 3βpane split is a nonβstarter on a phone. | Masterβdetail stack: absolutelyβpositioned panes slid with transform, one visible at a time, state in body[data-mobile-pane]. |
The reading pane's height: calc(100vh - 48px) assumed a fixed 48px header β but the mobile header wraps and is taller, pushing the conversation offβscreen. |
On mobile the body becomes a flex column; the header takes its natural height and .main-container uses flex: 1; min-height: 0. 100dvh instead of 100vh. |
| The device back button would leave the app instead of closing the conversation. | Each inβpane navigation history.pushStates; a popstate listener restores the pane. |
| Back didn't return to the list (a real bug, #762). | Root cause: a saved desktop tickets_popout pref made inbox.js add body.ticket-popout on every open β including on the phone β and its .email-list-container { display:none } hid the list. Fixed by neutralising popβout on mobile at the source: wrap syncPopoutToTicketState to strip the class when mq.matches, with a CSS backstop; Back also forces the list pane directly. (General lesson: desktop localStorage modes leak onto mobile β watch for other body classes doing this.)
|
| Too much crowds the open ticket. | Relocate each section into its own fullβscreen sheet; collapse the header; singleβrow icon bottom bar + overflow. |
| The reading pane reβrenders on every open, so anything moved gets recreated inline. |
afterTicketRender runs after each render (hooked onto the promise selectEmail returns) and reβmoves the nodes + (idempotently) rebuilds the toolbar. Moving a node keeps its id, so async loaders still find it. |
| Properties is normally an absolute collapsible dropdown; in a sheet it would be invisible. | In the sheet its panel is forced position: static; max-height: none; opacity: 1. |
| The addβnote / reply sheets had a small field with dead space below (built as centred desktop boxes). | Make the modal a flex column and let the field grow to fill (scoped by #id); the TinyMCE editor is found via .form-group:has(#emailBody) and set to flex: 1; height: auto !important to beat its inline height. |
| iOS zoomed and the sheet "went to desktop mode" when you tapped a field. | iOS autoβzooms fields under 16px, which spills the sheet wide β Safari reflows to desktop width β the max-width:768px rules stop matching. Fix: 16px on every modal field; for the reply editor (iframe, CSS can't reach), set the TinyMCE content_style to 16px on @media (pointer: coarse) only, so desktop mouse users stay 14px. |
The schedule panel's native date/time inputs spilled off the right. |
iOS gives them an intrinsic width that ignores width:100%. Fix: -webkit-appearance: none + max-width: 100% + reset ::-webkit-date-and-time-value margin; the native picker still opens on tap. |
Injected mobile chrome must not appear on desktop, but mobile.css can't set a desktop display:none. |
Sheets created with inline display:none; subβbar and hamburger toggled by syncBar() on load and on matchMedia change. |
| No way to selfβverify visually β the inbox needs a login and there's no headless browser. | Build to spec, prove desktop safety statically, iterate in a tight loop on a real device (fullβclear Safari each time β it caches the HTML page, so ?v= bumps don't fully bust it). Most refinements came directly from that testing. |
tickets/settings/ is the largest settings screen in the product β 18 tabs, 17 tables, 5,776 lines β and it came along on LAYER 15e plus three additions it forced.
β οΈ It is the first page that had to opt OUT of a LAYER 2 rule. Its own stylesheet documents, in as many words, that<body>must not be flex: "a flex<body>turned extensionβinjected nodes (e.g. LastPass) into flex items and wrecked the layout". So it builds the same structure one level down in a.settings-shellwrapper. LAYER 2 makes<body>a100dvhflex column on every optedβin page β opting this one in blind would have reβcreated a bug somebody had already hit, diagnosed and left a comment about.<body data-mobile-page="settings" data-mobile-shell="own">body[data-mobile-shell="own"] { display: block; height: auto; } .settings-shell { height: 100dvh; } /* 100vh is wrong once mobile chrome shows */π Read a page's own CSS comments before opting it in. The warning was right there, and nothing in the harness would have caught it β an extension node is not something a headless run has.
Three things this page forced into the shared settings layer, all of which now benefit every module:
| Found here | Now the shared rule |
|---|---|
17 tables, some inside a .settings-group, one inside an inlineβstyled scroll wrapper β a third structural variant after .settings-section-body and .tab-content >
|
Every table on a settings page scrolls. Enumerating containers was losing to the codebase; all 17 carry a <thead>, so the selector is simply "a table on a settings page" |
A datetime-local at the browser default of 13.3px, and two fields with an inline font-size: 14px
|
The antiβzoom rule now reads input:not([type=checkbox]):not([type=radio])β¦ β excluding what must keep its native size rather than listing what's covered β and carries !important, because an inline style beats any ordinary rule |
Nine inline display:grid; grid-template-columns:1fr 1fr forms |
Inline multiβcolumn grids stack on a settings page |
β οΈ The grid one had a second half that looked exactly like the first half failing. Stacking the template to1frchanged nothing visible, because 35 children carry an inlinegrid-column: span 2β and a child spanning two columns of a oneβcolumn grid makes the browser create an implicit second column. The tell was that the two tracks came out unequal (measured 93px + 111px);1fr 1frwould have been even. Both the template and the spans have to be neutralised.
π The bug the eye caught that the measurements didn't. The SLA tab's twoβcolumn radio form passed every overflow check β it didn't overflow, it just squeezed "When a ticket's priority changes midβflight" into a 150px column five lines deep. That is the Calendar settings lesson for the third time, so this round stopped relying on eyes for it: the harness now asserts "nothing inside the container is still laid out in more than one column at phone width" β a question measurement can answer.
Verification walked all 18 tabs, activating each and measuring it. The page renders one tab at a time, so measuring it as loaded verifies 1/18 of it β the singleβtab pass reported clean, and the walk is what found the fields and the grids.
-
Bottomβanchored inputs vs the keyboard β the note/reply/AskβAI compose inputs can sit behind the iOS keyboard. A
visualViewportfix was tried and made it worse, so it was reverted. This is the top remaining rough edge and needs a safer approach. -
Accessibility β iconβonly action buttons rely on the emoji being recognisable; an
aria-labelsweep across the injected chrome is still owed. - The collapsed header shows the subject only β not a Gmailβstyle oneβline sender+date summary (that would need editing the shared ticket renderer).
- The other tickets pages (dashboard, calendar, users, help) have the shell but their bodies aren't reflowed yet. Settings is done β see above.
- General density/spacing polish is ongoing.
-
assets/css/mobile.css(LAYERS 1β10, plus 15e for the settings page),assets/js/mobile.js. - Waffle drawer:
includes/waffle-menu.php(@mediablock in its own<style>). - Optβin wiring lives in
tickets/index.phpandtickets/settings/index.php(<head>link + script tag, versioned β currentlymobile.css?v=39,mobile.js?v=21,inbox.js?v=53).β οΈ mobile.cssandmobile.jsare now shared across five modules β bump the?v=on every one of the 19 pages that link them, not just this one, or a page will run the stale file. - Changelog: settings page #1005; mobile inbox shipped across entries #756β#768 (plus #855 auditβhistory feed) (masterβdetail + shell β modal sheets β readingβpane polish β views/bottomβbar/sectionβpanels β reference/subject/badge/overflow refinements β Back fix β tenant switcher + chevron β fullβheight action panels for note/reply/forward/schedule/AskβAI).
- Parent: MobileβFriendly Β· Siblings: Mobile: Assets, Mobile: Calendar, Mobile: Knowledge, Mobile: Service Status.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- 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
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« 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: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ 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)