Skip to content

Mobile Friendly Tickets

Ed Mozley edited this page Jul 15, 2026 · 11 revisions

Mobile: 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/). The other tickets pages (dashboard, calendar, users, settings, help) are reachable via the views drawer but their bodies aren't reflowed yet.


What's been done

Everything below is gated to phone‑width (≀768px); desktop is unchanged.

App shell / navigation

  • 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.

The inbox itself

  • 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.

The open ticket (reading pane)

  • 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.

The action panels (all full‑screen on a phone)

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-sheet slide‑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 endpoint inbox.js already uses). Desktop keeps its table, untouched.

How it works

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).

Pane state on <body>

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); }

Wrapping the existing handlers

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.

Section sheets are config‑driven

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.

mobile.css layers

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.)


Challenges & solutions

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.

Known rough edges / future polish

  • Bottom‑anchored inputs vs the keyboard β€” the note/reply/Ask‑AI compose inputs can sit behind the iOS keyboard. A visualViewport fix 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-label sweep 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, settings, help) have the shell but their bodies aren't reflowed yet.
  • General density/spacing polish is ongoing.

Reference

  • assets/css/mobile.css (LAYERS 1–10), assets/js/mobile.js.
  • Waffle drawer: includes/waffle-menu.php (@media block in its own <style>).
  • Opt‑in wiring lives in tickets/index.php (<head> link + script tag, versioned β€” currently mobile.css?v=22, mobile.js?v=11, inbox.js?v=53).
  • Changelog: 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.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally