Skip to content

Process Mapper Developer Guide

Ed Mozley edited this page Oct 25, 2026 · 1 revision

Process Mapper - Developer Guide

What it does for the analyst: Process Mapper Β· on a phone: Mobile-Friendly: Process Mapper

In plain words

A process is a flowchart: steps (boxes of a configurable type), connectors (arrows), and three things that are only there to organise the picture - lanes (horizontal bands), groups (rectangles behind steps) and notes (sticky notes). The editor is one hand-written JavaScript module (PM) on a single page: a sidebar list of processes on the left, the canvas in the middle, a detail panel on the right. It keeps the open process in memory and saves by sending all of it; the server deletes every row and inserts them again, so every save gives everything a new id. There is no versioning and no REST API.

The user page already describes every feature, including the right-click menu and the clipboard, in detail. This page is about how the code is put together.

Files

File What it is
process-mapper/index.php The whole editor page. Reads process_step_types and renders the toolbar buttons and the right-click Create new / Change to items from the active types; exports window.SHAPE_SIZES, window.STEP_TYPES (active and inactive) and window.STEP_TYPES_BY_SLUG
process-mapper/includes/shapes.php The twelve shapes and the default size of a new step of each - the one list both pages use
process-mapper/settings/index.php Settings: the Step types tab and the Left panel tab
process-mapper/settings/manifest.php Which capability guards each settings tab
process-mapper/help.php The in-app guide
assets/js/process-mapper.js The editor. const PM = (() => {...})(), started on DOMContentLoaded
assets/css/process-mapper.css All the shape geometry, as [data-shape="..."] rules
api/process-mapper/*.php Eight endpoints (below)

The tables

Seven tables, no tenant_id anywhere and no foreign keys - only indexes on process_id, lane_id and group_id (database/freeitsm.sql, includes/db_verify_indexes.php).

Table Columns
processes id, title, description, created_by, created_datetime, updated_datetime, is_demo
process_steps id, process_id, type (the step type's slug, free text), label, description, url, x, y, width, height, color, color2, lane_id, group_id, is_demo
process_connectors id, process_id, from_step_id, to_step_id, label, is_demo
process_lanes id, process_id, label, color, color2, display_order, height
process_groups id, process_id, label, color, color2, x, y, width, height
process_annotations id, process_id, text, x, y, width, height, color (default #fff59d), color2
process_step_types id, name, slug (unique), shape, color, display_order, is_active, is_builtin

color2 everywhere means "gradient second stop"; NULL is a flat fill (fillStyle() builds the linear-gradient(135deg, ...)).

A lane has no y. Its top is worked out from the lanes above it: recomputeLaneBandTops() sorts by display_order and adds up heights into a cached _bandTop. A group has no lane_id - which lane it is in is worked out from its vertical centre whenever it matters.

The data model in the browser

get.php?id=N returns data = the processes row plus five arrays, each SELECT * from its table:

{
  "success": true,
  "data": {
    "id": 4, "title": "Incident triage", "description": null, "created_by": 3,
    "created_datetime": "...", "updated_datetime": "...", "is_demo": 0,
    "steps":       [{ "id": 51, "process_id": 4, "type": "decision", "label": "P1?", "description": "",
                      "url": null, "x": 300, "y": 40, "width": 150, "height": 150,
                      "color": "#f59e0b", "color2": null, "lane_id": 7, "group_id": null, "is_demo": 0 }],
    "connectors":  [{ "id": 90, "process_id": 4, "from_step_id": 50, "to_step_id": 51, "label": "", "is_demo": 0 }],
    "groups":      [],
    "lanes":       [{ "id": 7, "process_id": 4, "label": "Service desk", "color": "#f5f7fa",
                      "color2": null, "display_order": 0, "height": 180 }],
    "annotations": []
  }
}

openProcess() maps these into the module arrays steps, connectors, groups, lanes and annotations. Two renames to know: a connector becomes {id, fromId, toId, label} in the browser and goes back as from_step_id / to_step_id; every object also carries its element in el.

Unsaved things get a negative tempId from nextTempId--, and anything that points at them (a connector end, a step's lane_id or group_id) holds that negative number until the save. The lookups all accept either: getStep(id), getGroup(), getLane(), getAnnotation(), and the groupRef() / laneRef() / annotationRef() helpers return id if there is one, else tempId. Selection is the same values: selectedStepIds (a Set), selectedConnectorId, selectedGroupId, selectedLaneId, selectedAnnotationId.

Rendering

HTML for shapes, SVG for lines. Not a canvas element.

  • #pmCanvas (.pm-canvas, tabindex="0") is the scroll container and holds everything. #connectorsSvg inside it holds the connectors; addArrowheadDef() adds the marker.
  • renderAll() removes and rebuilds in back-to-front order: lanes (renderLanes()), groups (createGroupEl()), steps (createStepEl()), notes (createAnnotationEl()), then renderConnectors().
  • A step is a <div class="pm-step" data-type="<slug>" data-shape="<shape>">. shapeForSlug() looks the slug up in STEP_TYPES_BY_SLUG; the CSS draws the shape. The cloud shape also needs the <clipPath id="pmShapeCloud"> SVG block in index.php. The step gets four .pm-edge-handle dots and, if it has a url, an <a class="pm-step-url-badge">.
  • renderConnectors() redraws every connector as a straight line between the facing sides of the two step boxes (calcConnectorPoints(), the larger of the two gaps wins), with a wide hit path and an optional label. Connectors store no geometry, so they follow whatever moved.
  • There is no zoom. Coordinates are canvas pixels: mouse position minus the canvas's screen position plus its scroll.

Step types

The palette is data, not code:

  • process-mapper/index.php queries process_step_types at page load. A type added in Settings appears in the editor only after a reload, because the toolbar and context-menu markup is rendered by PHP.
  • stepDims(slug) gives a new step its size from SHAPE_SIZES[shape] and its colour from the type. addStep() and createConnectedStep() both use it.
  • changeStepType() swaps the slug, takes the new type's colour and clears the gradient.
  • A step whose slug no longer matches a type falls back to FALLBACK_SHAPE ('rounded') and FALLBACK_COLOR. (The comment in delete_step_type.php says "plain rectangle" - the code says rounded.)
Endpoint Guard Does
get_step_types.php module access Every type, ordered
save_step_type.php Cap::PROCESS_MAPPER_STEP_TYPES Create or update. Shape must be one of the twelve, colour #rrggbb. On create the slug is made from the name and made unique with -2, -3...; the slug never changes after that
delete_step_type.php Cap::PROCESS_MAPPER_STEP_TYPES Refuses is_builtin = 1 (the four seeded: process, decision, start, document)
reorder_step_types.php Cap::PROCESS_MAPPER_STEP_TYPES {order: [ids]} rewrites display_order in steps of 10

The capability and its umbrella Cap::PROCESS_MAPPER_MANAGE are declared in includes/capabilities.php and mapped to the tab in settings/manifest.php. Drawing processes needs plain module access only. See Roles - Developer Guide.

Interaction at the code level

One set of document listeners. bindCanvasEvents() registers a single mousemove and mouseup on the document. onDocMouseMove() and onDocMouseUp() look at which drag state is set and hand over: annotationDragging, then laneDragging, then groupDragging, then dragging (steps), then connectDrag, then rubberBand. Adding a new draggable thing means adding a branch to both.

Gesture Code
Select / move steps onStepMouseDown() - Ctrl / Cmd toggles membership of selectedStepIds; otherwise it selects and starts dragging with the start position of every selected step, so the group moves together by the snapped delta. The detail panel opens in onDocMouseUp() only when dragging.moved is false
After a step move reassignStepLanes() and reassignStepGroups() re-derive lane_id / group_id from each step's centre
Rubber band onCanvasMouseDown() starts it only on the bare canvas, the empty-state element or a lane body. On mouseup, steps whose centre is inside are selected; a box under 5px is a click and clears the selection
Draw a connector startConnectDrag() from an edge handle; onDocMouseUp() finds the target with findStepAt() and calls addConnector()
Click to connect startClickConnect() sets clickConnectFromId; the next step mousedown runs completeClickConnect(), a canvas mousedown or Escape runs cancelClickConnect()
Rename onStepDblClick() lays an input.pm-rename-input over the step; connector labels use editConnectorLabel()
Groups groupAtPoint() returns the smallest group containing a point. onGroupMouseDown() / onGroupDocMouseMove() / onGroupDocMouseUp() move or resize, carrying the steps whose group_id matches
Lanes onLaneHeaderMouseDown() reorders, onLaneDividerMouseDown() resizes. reorderLaneTo() snapshots each step's (and group's) offset inside its lane before the move and re-applies it after; resizeLaneTo() (minimum 80px) shifts everything whose centre is below the lane. autoAssignAllStepLanes() tags every step first so steps drawn before the lanes existed come along too
Notes addAnnotation(), onAnnotationMouseDown() and its two document handlers. Notes take no part in lanes, groups or connectors
Right-click menu onStepContextMenu() stores ctxTargetStep and ctxRightClickPos; showContextMenu() enables / hides items; bindContextMenu() wires them. Clipboard: cutStep(), copyStepForPaste(), pasteStep(), duplicateStep(), all through cloneStepData() and placeClonedStep(); formatting: copyFormat() / applyFormat()
Delete deleteSelected() - whichever of note, lane, group, connector or steps is selected, in that order. Deleting a group or lane leaves its steps in place with the id cleared; deleting steps drops their connectors

Keyboard. onCanvasKeyDown() is on the canvas element, not the document, so these only work while the canvas has focus - onStepMouseDown() and onCanvasMouseDown() focus it for that reason: Delete / Backspace, arrow keys (nudge by GRID, 20px), Ctrl+S, Ctrl+A. The sidebar shortcuts (/, Alt+Shift+O, arrows, Enter, Delete on a row) are document-level, in bindProcessListKeys().

There is no undo. Nothing records history.

addConnector() refuses a second connector between the same two steps in either direction. (Network Mapper only refuses the same direction.)

Saving

markDirty()  ->  setStatus('unsaved')  ->  scheduleAutosave()   (2s debounce, only if autosave is on)
save()       ->  POST save.php (everything)  ->  openProcess(id, preserveDetail = true)  ->  re-select
  • save() sends {id, title, steps, connectors, groups, lanes, annotations}. Each item carries id (or null) and tempId.
  • api/process-mapper/save.php runs in one transaction: update or insert the processes row, delete every connector, step, group, lane and note of the process, then insert lanes first (building $laneIdMap), groups ($groupIdMap), steps (translating lane_id / group_id through those maps, building $idMap), connectors (through $idMap) and notes. Each map is keyed by id ?? tempId.
  • A connector whose end is not in $idMap is dropped without an error, and so is a lane_id or group_id that maps to nothing.
  • Because every id is new, save() first remembers what was selected by position - a step by x, y; a group or note by x, y, width, height; a lane by display_order - and finds it again after the reload. Two steps at exactly the same position would re-select the first.
  • scheduleAutosave() waits while any drag is in progress (dragging, groupDragging, laneDragging, connectDrag, rubberBand), because the reload destroys the dragged elements.
  • Autosave on / off is the user preference process_mapper_autosave; the sidebar mode is process_mapper_sidebar_mode.
  • A new process is created by submitNewProcess() posting save.php with no id and empty arrays.
  • get.php and save.php wrap the groups, lanes and notes tables in try / catch, so an install that has not run DB Verify since those shipped still loads and saves the rest.

The open process's id is in the address bar: syncUrl() uses history.pushState only when the URL actually changes, openInitialFromUrl() opens ?id= once the list has loaded, and onPopState() follows back / forward.

The other endpoints

Endpoint Does
list.php Every process, newest first, with created_by_name, step_count and lane_count for the sidebar hover card
get.php?id= One process (above)
save.php Create or full replace (above)
delete.php Deletes the process, its connectors and its steps

All check the session and requireModuleAccessJson('process-mapper'). The write endpoints also call csrfRequireWriteMethod(), on top of the central check in CSRF protection.

Elsewhere in the app

  • Documents on a process: toggleProcessDocs() mounts the shared documents panel with parentType: 'process' - Attached documents - Developer Guide.
  • Demo data: autoLayoutProcessMapper() in api/system/import_demo_data.php lays out demo steps left to right, because the demo JSON has no coordinates - Demo data - Developer Guide.
  • Export: captureCanvas() (html2canvas, vendored), exportToPdf() (jsPDF), exportToMermaid(). The user page covers what each produces.

Traps and known gaps

  • πŸ”΄ TRAP (in the code): e.code, not e.key, for Alt+Shift+O - on a Mac the key types Ø. In bindProcessListKeys().
  • πŸ”΄ TRAP (in the code): look for .fitsm-confirm-overlay.active - confirm.js leaves its overlay in the page after the first confirm, so its presence means nothing. In dialogOpen().
  • πŸ”΄ TRAP (in the code): esc() must escape quotes. textContent then innerHTML escapes < > & but not " or '; the two extra replaces make it safe inside an attribute (a connector label in value="", the sidebar's aria-label). See Code traps.
  • πŸ”΄ Every id changes on every save. Never hold a step, lane or group id across a save.
  • ⚠️ delete.php leaves rows behind. It deletes connectors, steps and the process, but not the process's process_lanes, process_groups or process_annotations - and with no foreign keys nothing cascades.
  • ⚠️ processes.description is never written by the editor. save.php only sets title; the sidebar hover card shows a description if one is there.
  • ⚠️ The Unsaved changes confirm in openProcess() is hard-coded English, not a translation key.
  • Processes have no company; anyone with module access sees every process.

Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally