Process Mapper Developer Guide - edmozley/freeitsm GitHub Wiki
What it does for the analyst: Process Mapper · on a phone: Mobile-Friendly: Process Mapper
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.
| 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) |
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.
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.
HTML for shapes, SVG for lines. Not a canvas element.
-
#pmCanvas(.pm-canvas,tabindex="0") is the scroll container and holds everything.#connectorsSvginside 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()), thenrenderConnectors(). - A step is a
<div class="pm-step" data-type="<slug>" data-shape="<shape>">.shapeForSlug()looks the slug up inSTEP_TYPES_BY_SLUG; the CSS draws the shape. Thecloudshape also needs the<clipPath id="pmShapeCloud">SVG block inindex.php. The step gets four.pm-edge-handledots and, if it has aurl, 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.
The palette is data, not code:
-
process-mapper/index.phpqueriesprocess_step_typesat 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 fromSHAPE_SIZES[shape]and its colour from the type.addStep()andcreateConnectedStep()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') andFALLBACK_COLOR. (The comment indelete_step_type.phpsays "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.
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.)
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 carriesid(or null) andtempId. -
api/process-mapper/save.phpruns in one transaction: update or insert theprocessesrow, delete every connector, step, group, lane and note of the process, then insert lanes first (building$laneIdMap), groups ($groupIdMap), steps (translatinglane_id/group_idthrough those maps, building$idMap), connectors (through$idMap) and notes. Each map is keyed byid ?? tempId. -
A connector whose end is not in
$idMapis dropped without an error, and so is alane_idorgroup_idthat maps to nothing. - Because every id is new,
save()first remembers what was selected by position - a step byx, y; a group or note byx, y, width, height; a lane bydisplay_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 isprocess_mapper_sidebar_mode. -
A new process is created by
submitNewProcess()postingsave.phpwith noidand empty arrays. -
get.phpandsave.phpwrap the groups, lanes and notes tables intry / 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.
| 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.
-
Documents on a process:
toggleProcessDocs()mounts the shared documents panel withparentType: 'process'- Attached documents - Developer Guide. -
Demo data:
autoLayoutProcessMapper()inapi/system/import_demo_data.phplays out demo steps left to right, because the demo JSON has no coordinates - Demo data - Developer Guide. -
Export:
captureCanvas()(html2canvas, vendored),exportToPdf()(the picture placed in a PDF by the shared writer,window.FitsmPdf- see PDF writer - Developer Guide),exportToMermaid(). The user page covers what each produces.
- 🔴 TRAP (in the code):
e.code, note.key, for Alt+Shift+O - on a Mac the key types Ø. InbindProcessListKeys(). - 🔴 TRAP (in the code): look for
.fitsm-confirm-overlay.active-confirm.jsleaves its overlay in the page after the first confirm, so its presence means nothing. IndialogOpen(). - 🔴 TRAP (in the code):
esc()must escape quotes.textContenttheninnerHTMLescapes< > &but not"or'; the two extra replaces make it safe inside an attribute (a connector label invalue="", the sidebar'saria-label). See Code traps. - 🔴 Every id changes on every save. Never hold a step, lane or group id across a save.
⚠️ delete.phpleaves rows behind. It deletes connectors, steps and the process, but not the process'sprocess_lanes,process_groupsorprocess_annotations- and with no foreign keys nothing cascades.⚠️ processes.descriptionis never written by the editor.save.phponly setstitle; the sidebar hover card shows a description if one is there.⚠️ The Unsaved changes confirm inopenProcess()is hard-coded English, not a translation key.- Processes have no company; anyone with module access sees every process.
- Process Mapper · Mobile-Friendly: Process Mapper
- Network Mapper - Developer Guide - the sister canvas, which copied this one's connector geometry and arrowhead pattern
- Roles - Developer Guide · Database verification - Developer Guide