Mobile Friendly Workflow - edmozley/freeitsm GitHub Wiki
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.
| 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.
§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.
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.
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.
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-mainneeds no arithmetic, cannot drift when the bar above wraps, and is right in every locale and at every height. Reach fordvhonly when the two halves really are the whole screen.
width: auto and min-width: 0, because the 340px is declared twice.
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.
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.
-
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 bothjustify-contentandalign-items, and both change meaning once it wraps. -
32d —
.wf-ai-modaland.wf-var-modalpromoted 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!importantto beat the inlinestyle="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.
The last four rounds were each saved by a screenshot. This one was the reverse: every red the probes raised was the probe.
-
"The guides' tables and code blocks run 200px past the edge." They sit inside
.help-tableand<pre>, both of which declareoverflow-x: autodeliberately. §18's distinction exactly: a wrapper that scrolls by design versus one that scrolls because onlyoverflow-ywas declared. I nearly wrote a rule for a working scroller — the Software round's mistake. - "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.
-
"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.
- 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;
+ Actiontook 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=, andworkflow.css,workflow-editor.jsandworkflow-executions.jsare untouched.
- 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 callswindow.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.phpscrolls 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.
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.
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.
.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.
-
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-contentmeans LAYER 3 already makes it a sheet, but not what is inside it.
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.
- 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
.minisummaries 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=.
- Mobile‑Friendly · Techniques & Tricks
- Mobile: Network Mapper · Mobile: Process Mapper — the other two canvases, and the other two scope answers
- Workflows — what the module does