Skip to content

Mobile Friendly Workflow

Ed Mozley edited this page Sep 4, 2026 · 2 revisions

Mobile‑Friendly: Workflow

The eighteenth module, and the third canvas β€” but the first one that is fully usable on a phone, which is a fact about the module rather than about how much harder anyone tried. Seven pages: the rules list, the editor, the execution log, settings and three guides. Shipped in #1469‑#1471, mobile.css v131 / mobile.js v54, LAYER 32 β€” plus LAYER 33 for system/webhooks/, the page this round missed because it sits in another directory and the module nav links to it anyway.

Read Mobile‑Friendly first for the strategy and the one hard rule, and Techniques & Tricks for the catalogue this round draws on.


The pre‑flight, in five greps

Grep Result
a pre‑existing @media πŸ”΄ one, at 760px β€” benign, but it leaves a band. Below
localStorage none
:hover revealing a control (Β§26) none
its own modal class πŸ”΄ two β€” .wf-ai-modal, .wf-var-modal, both invisible to LAYER 3
the wf- prefix in other modules (Β§15) πŸ”΄ shared β€” wf-diagnosis, wf-count, wf-err, wf-item, wf-name also appear in system/webhooks/ and watchtower/

That last one is the Process Mapper pm- collision again, and this time the prefix really is shared. None of those five classes is styled by workflow.css or touched here, but that is luck rather than design β€” so every rule in the layer is scoped on [data-mobile-module="workflow"], which makes the question moot.


⭐ The scope decision, and why it is different from the other two canvases

Β§22 gives the same answer as it did for both mappers:

workflow-editor.js : 6 Γ— mousedown / mousemove / mouseup
                     0 Γ— touchstart / touchmove / pointerdown

What is different is what the drag is for:

const COL_X = 220;   // x at which auto-layout stacks nodes
addConditionNode(COL_X, lastY + ROW_GAP, …)

Nodes are added by toolbar buttons and auto‑stacked in a single column. They are configured in the detail panel. Deleting one is a real button (WFE.deleteSelected()), not a keypress. So the whole of authoring already works by tap, and dragging a box is decoration β€” rearranging a layout the module generated for you.

πŸ”‘ Β§22 tells you whether a drag fires. It does not tell you what breaks if it never does. Ask what the drag is for before scoping a canvas. Network Mapper needed three new gestures because placing and deleting a node were behind a mouse and a Delete key. This module needed none, because neither is.

Three canvases, three different answers, all from the same grep.


πŸ”΄ Β§30 again, and the narrowest pane in the rollout

Every page sets overflow: hidden on <body> or .wf-layout, so docScrollW === innerWidth cannot fail. Measured by box at 360Γ—740, before anything:

editor      .wf-detail        w=340    ← of a 360px screen
            .wf-canvas        w=20     ← the workflow diagram
            .wf-toolbar-left  w=381, right=393
list        6-col table 637px in a 312px parent; three cells crushed to
            78Γ—194, 82Γ—194, 78Γ—194  (Β§11's signature)
executions  6-col table 615px, two crushed; a 538px-tall filter stack;
            the tally squeezed to 252px

20px of canvas. CMDB browse and the Forms approvals sidebar both left 100px; Network Mapper 119px. And unlike either mapper's panel this one is not a drawer β€” .wf-detail is width: 340px; min-width: 340px, present and open at all times, because it is an inspector.


πŸ”΄ The list pages got WORSE when opted in, before they got better

before opt-in   .container w=360, clipping a 637px table with its own overflow
after opt-in    .container w=705   ← the table's width, not the screen's

LAYER 2 makes <body> a flex column, so .container became a flex item β€” and a flex item's automatic minimum size is its content width (Β§14). The 637px table set the floor, the container grew to meet it, and the page went with it.

⭐ The fix already existed and could not reach it. LAYER 16h pins exactly this β€” width/max-width/min-width plus flex: 1 1 auto β€” keyed on body[data-mobile-page="settings"], with a comment describing an identical 776px measurement. A list page is not a settings page, so it got none of it.

πŸ”‘ The standing note is "a rule that already covers this is worth measuring". This is its other half: a rule that covers the same fault on a different page type. Grep finds neither, because the selector you would grep for is the one that is missing.


32c β€” the inspector to the bottom, and the dvh lesson applied properly

Both mappers put their panel across the bottom, and this module wants the same shape for a stronger reason: the panel is where the work happens, so it should be the larger half β€” and you still need to see which node you are editing (Ed's point when he asked for the Process Mapper sheet).

⭐ But not in dvh, and that is last round's lesson used rather than repeated. LAYER 31f copied a 58dvh sheet out of Process Mapper and left 71px of map, because the two modules have different amounts of chrome above them. Here the toolbar wraps, so the chrome is not even a fixed number.

.wf-main is already the box holding exactly these two panes and nothing else:

[data-mobile-module="workflow"] .wf-main       { flex-direction: column; min-height: 0; }
[data-mobile-module="workflow"] .wf-canvas-wrap{ flex: 0 0 34%; min-height: 0; }
[data-mobile-module="workflow"] .wf-detail     { flex: 1 1 auto; width: auto; min-width: 0; }

πŸ”‘ Split the box that holds the two panes, not the viewport. A percentage of .wf-main needs no arithmetic, cannot drift when the bar above wraps, and is right in every locale and at every height. Reach for dvh only when the two halves really are the whole screen.

width: auto and min-width: 0, because the 340px is declared twice.

The one thing CSS could not do

The map pane is ~200px and a condition node is 160px tall, so a node further down a chain lands below the fold. The canvas is its own scroller and the module never scrolls it on selection.

#wfDetailTitle is the single funnel: every path into the panel β€” tapping a node, tapping bare canvas, + Condition, + Action, applying an AI proposal β€” ends in showBody(title, id), which sets that element's text. One MutationObserver covers all five and cannot get out of step with call sites this file does not own. It reveals only when a node is selected, so a bare‑canvas tap (which also changes the title, to the workflow's own settings) does not yank the map about.

⚠️ On the next tick, not synchronously β€” observer callbacks are microtasks, and LAYER 30f spent a round on exactly that.

Not needing JS is not a virtue in itself. The rest of this module genuinely needed none; this one thing the page cannot express in CSS, so it got wrapped.


The rest of the layer

  • 32b β€” the toolbar, 681px of controls. Wraps to two rows rather than becoming one long strip, and the split is already meaningful: the left group builds the workflow, the right group runs and saves it. A single strip would put Save behind a swipe. ⚠️ Β§16: the row sets both justify-content and align-items, and both change meaning once it wraps.
  • 32d β€” .wf-ai-modal and .wf-var-modal promoted to full‑screen sheets. Sixth module to roll its own modal class, and as always nothing in the measurements complains. ⚠️ The templates dialogue is not one of them β€” it uses the shared .modal-content, so LAYER 3 already has it, and needs its !important to beat the inline style="max-width: 900px" the page writes.
  • 32e / 32f β€” two card feeds. The list settles itself under Β§11: the name cell carries the workflow's description, and if any column holds a sentence, it is a card feed. For the log, Β§11's follow‑up question β€” can you compare down a column at 360px? β€” answers no at two and a half of six columns, and the comparison is already served better on the same page by the tally and the status filter.
  • Β§21 labels on the trigger slug, the dates and the duration; not on "48 runs", which says what it is, nor on the status pill. Verified stamped, and zero on empty cells β€” the "never run" workflow has a blank Last run, so the harvester's blank guard is doing real work here.
  • 32f β€” the filter stack at 538px tall, five dropdowns, two dates and a search each on their own row, so the log began below the fold. Two up: 287px. The tally became an aligned three‑column grid (Β§10) rather than a ragged wrap.
  • 32g β€” Β§20, the sixth time. Two input[type="date"] filters. The reset is not optional and cannot be cleared headlessly: iOS spells the month out where Chrome on Windows draws digits, so a desktop measurement understates it.
  • 32h β€” the module's own @media (max-width: 760px), restated inside the 768px block. ⚠️ Restated, not edited: widening the module's own breakpoint from 760 to 768 would change what a 764px screen renders β€” a tablet band, and therefore a desktop change made as a side effect of mobile work (Β§25). This module's file is untouched.

⚠️ Three probe false alarms, and no real fault found by looking

The last four rounds were each saved by a screenshot. This one was the reverse: every red the probes raised was the probe.

  1. "The guides' tables and code blocks run 200px past the edge." They sit inside .help-table and <pre>, both of which declare overflow-x: auto deliberately. Β§18's distinction exactly: a wrapper that scrolls by design versus one that scrolls because only overflow-y was declared. I nearly wrote a rule for a working scroller β€” the Software round's mistake.
  2. "The settings Message‑formats table has crushed cells β€” 70Γ—68, 79Γ—68, 78Γ—68." The table is a 1307px scroller inside a 296px box, working exactly as the shared settings rule intends; its cells are 607px and 473px wide, and the three flagged ones are narrow because their content is short ("slack", "Built‑in", "Copy"). All 68px tall because a real table row shares one height β€” Β§19, and my heuristic had no shared‑height test in it.
  3. "The condition node's text overlaps itself." The <em> is a child of .wf-node-title, not a sibling; my overlap detector compared all descendants pairwise without excluding ancestor/descendant pairs. The desktop control at 1100px reported the identical "overlap".

πŸ”‘ A crushed‑cell probe needs Β§19's shared‑height test, and an overlap probe needs a containment test. Both of mine reported a fault on code that was correct, and (2) was on a rule this rollout wrote deliberately. Β§30's addendum keeps being right: a probe earns trust by agreeing with a known‑good control first.


How it was verified

  • All seven pages contained at 360Γ—740, measured by box (Β§30), with the Β§18 scroller filter off as well as on.
  • Every page has a scroller that reaches its end (Β§28): list 368/368, editor canvas 413/413 and inspector 1310/1310, log 4539/4539, and the three guides 12133, 10441 and 7951 β€” all of them to the last pixel.
  • The editor driven: tapping a node moved the panel from "Workflow" to "Condition" with the node selected and fully inside the map pane; + Action took 3 nodes to 4 and pointed the panel at the new one. Nothing was written β€” this editor has no autosave, markDirty() only sets a flag.
  • Both settings tabs driven β€” "a page that renders one tab at a time is 1/N verified". Tab 2 is where probe alarm (2) came from.
  • Β§21 labels checked on the page: stamped on the right columns, zero on empty cells, and the feed signature confirmed by cell heights (60, 16, 16, 16, 24, 38 β€” a table row would share one).
  • Desktop control at 1100Γ—900, byte‑identical to the pre‑change baseline: editor canvas 760 / inspector 340, toolbar unwrapped at 44px, both tables real tables with every cell sharing one row height (89 and 66).
  • The Β§25 audit: the module's diff is seven pages Γ— three lines of opt‑in, every other page changed only its ?v=, and workflow.css, workflow-editor.js and workflow-executions.js are untouched.

⬜ Known and not fixed

  • Dragging a node to rearrange it still needs a mouse. Deliberate, and explained at the top β€” it rearranges a generated layout.
  • πŸ”΄ Four strings in the editor are hardcoded English and show that way in all 24 languages: the detail panel's own title (showBody('Workflow', …)) and the three save states (Unsaved changes, Saving…, Saved). The module calls window.t() 28 times elsewhere, so this is four that were missed rather than a module without i18n β€” but the keys do not exist, so fixing it needs new locale entries across 24 files. Found here, not fixed here: it is a bug at every width, not a mobile one, and it belongs in its own round.
  • contracts/help.php scrolls sideways by 13px with a 15px scrollbar (Β§12's tell). Noticed while checking whether the guide finding generalised; pre‑existing, unrelated to this module, recorded rather than chased.

πŸ”΄ The page the round missed: system/webhooks/ (LAYER 33, #1471)

Ed, straight after the round shipped:

"on the webhooks screen there's a panel at the bottom with content 'when', 'Workflow', 'format' etc spilling off to the right so we might need to rearrange this table"

⭐ A gap in this round, not a separate discovery. Workflow's own module nav links straight to it β€”

// workflow/includes/header.php
<a href="<?php echo BASE_URL; ?>system/webhooks/" …>  <?php echo t('workflow.nav.webhooks'); ?>

β€” so opting seven Workflow pages in and stopping at the directory boundary left a phone user one tap from a screen that had never been touched. The page lives in system/ and renders the System header, so no grep of workflow/ would ever have found it.

πŸ”‘ Scope a round by the module's own navigation, not by its directory. Walk the nav bar the module actually renders and check every destination, including the ones that belong to somebody else.

What it was

The delivery log is nine columns β€” When Β· Workflow Β· Format Β· URL Β· Status Β· Attempts Β· Last code Β· Next retry Β· Actions β€” measured at 556px wide, right edge at 600 on a 360px screen.

⭐ And Ed named the headings, not data, which is the tell: the log is empty on his install, so what was spilling is the nine-column <thead> on its own. A table with no rows can still be 556px wide.

πŸ”΄ Why nothing had ever reported it

.wh-container { height: calc(100vh - 48px); overflow-y: auto; }

Β§12: declaring one axis computes the other to auto, so the container was a horizontal scroller nobody asked for. It absorbed the table, and the containment sweep β€” which deliberately excludes anything with a scrolling ancestor β€” reported the page clean.

Β§18 says a suppressed finding is either working as intended or the bug is hidden, and the probe cannot tell you which. This was the second, in its worst form: the whole page dragged sideways instead of the table scrolling in its own box, and nothing on screen says that has happened. overflow-x: hidden on the container is the load‑bearing half of the fix β€” without it, the next oversized table hides the same way.

The layer

  • 33a the shell β†’ LAYER 2, plus that overflow-x: hidden, plus card padding (22px 24px was 48px of a 360px screen, on five cards).
  • 33b the log as a card feed, with Β§21 labels on six of nine β€” When, Format, URL, Attempts, Last code and Next retry are every one a bare value. Consistency decided it as much as Β§11 did: this is the delivery log, one tap from the execution log (32f), which is already a feed.
  • 33c the payload dialogue's <pre> blocks contained β€” .modal-content means LAYER 3 already makes it a sheet, but not what is inside it.

⚠️ Keyed on table.wh, and the first draft keyed on #deliveries β€” an id this page does not have. It would have been a whole layer with precisely zero measured effect, which Β§9 teaches you to blame on load order and which looks identical to a cache that did not bust. Read the markup for the selector. It also matters that .wh excludes the two table.mini summaries, which stay tables.

How it was verified

  • 556px β†’ 304px at 360, and the container now clips rather than absorbing.
  • The feed driven with fabricated rows through the page's own renderRows() β€” no database writes β€” giving 12 labels across two cards, all six per row correct, zero on empty cells, and the feed signature in the cell heights (16, 20, 16, 16, 19, 16, 16, 16, 38).
  • Desktop at 1100: display: table, every cell 49px β€” one shared row height β€” and [data-mobile-label] finds NOTHING, which is Β§21's own strongest control.
  • The two .mini summaries measured with real data, because they are server‑rendered and simply absent when the log is empty: two throwaway rows inserted (the table had 0 rows, so nothing of Ed's was at risk), measured at 304px with every cell 29px, then deleted by the exact ids captured on insert. They stay tables and they fit.
  • The Β§25 audit: three lines of opt‑in, every other page only its ?v=.

Related

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally