Repository navigation
Asset Type Layouts Developer Guide
How per-type layouts (#200, part A) are built. For what they do and how to use them, see Asset type layouts; for why they are shaped this way, the design.
In one paragraph: a type's layout is a handful of rows in asset_type_layout saying which built-in Key info sections and which tabs are shown, and in what order the tabs come. AssetTypeLayout reads and writes them; get_asset_types.php hands each type's layout to every consumer; the type dialog in settings edits it and sends it with the type; and the asset page tags its built-in blocks with data-layout-section, then hides and reorders after it renders. Nothing outside the asset page reads a layout, because a layout is presentation only.
| File | What it does |
|---|---|
includes/services/asset_type_layout.php |
AssetTypeLayout: SECTIONS, TABS, PRESETS, defaults(), forTypes(), normalise(), save(), noticeMode()
|
api/assets/get_asset_types.php |
Adds layout to every type it returns |
api/assets/save_asset_type.php |
Saves layout when the request carries one |
asset-management/settings/index.php |
The Layout section of the type dialog (atlSetup, atlApplyPreset, atlCollect) and the Details a type hides setting (loadLayoutNotice, saveLayoutNotice) |
asset-management/settings/manifest.php |
asset_layout_hidden_notice is written from the Asset types tab |
asset-management/index.php |
data-layout-section on the built-in blocks; applyAssetLayout(), atlRenderNotice(), atlSaw(), the badge observer |
lang/en/asset-management.php |
asset-management.layout.*, and help.layout_* for the help page |
asset-management/help.php |
A page that fits the kind of asset, in the Asset detail section |
tests/asset-type-layout.php |
The test - section 8 |
A layout decides what one asset's page shows. Never what is stored, never what is required, never who may write.
That rule is why this is a few hundred lines and not a second field system. Hold it when extending:
- No reader but the asset page. Search, the table view, exports, handover columns, the dashboard, Watchtower and the REST API ignore layouts. They are estate-wide or machine-facing.
- No write path checks a layout. The agent, Intune, the spreadsheet import, REST and the page itself write exactly as before. A layout that hid a field and also blocked writing it would make inventory depend on presentation.
- No "not applicable". A hidden section is hidden by this profile. Part B's freshness states are worked out from what each source did, never from the layout (design, Where we landed).
-
Identity is not a section. Name, asset tag, serial number (
service_tag), type, status, location and company are not inSECTIONS, so no request can hide them -normalise()drops any key it does not know.
asset_type_layout
asset_type_id -> asset_types.id, ON DELETE CASCADE
item_kind 'section' | 'tab'
item_key a key from AssetTypeLayout::SECTIONS or ::TABS
is_shown TINYINT(1)
sort_order INT
UNIQUE (asset_type_id, item_kind, item_key)
In database/freeitsm.sql, includes/db_verify_schema.php, the FK list in api/system/db_verify.php, and the generated includes/db_verify_indexes.php.
No row means shown, in the default place. Everything depends on it:
- Existing installs need no migration. No rows, so every type is "everything shown" - the page as it was.
-
save()stores nothing for the default layout. It deletes the type's rows first, and writes none when the layout equalsdefaults(). "Never configured" and "configured back to the default" are therefore the same state, which is what you want for the next point. -
A tab added in a later release appears everywhere.
fromRows()puts stored tabs in their saved order and any tab with no row after them, in default order, shown. A type configured before the tab existed still gets it. -
A key removed in a later release is ignored.
fromRows()only accepts keys inSECTIONS/TABS; stale rows do no harm and are cleared the next time the type is saved. - If the table does not exist yet (DB Verify not run since upgrading),
forTypes()catches the error and returns defaults - the page simply looks as it did before.
Reading. get_asset_types.php calls AssetTypeLayout::forTypes($conn, ids) once for every row it returns and adds layout:
"layout": {
"sections": { "make_model": true, "hardware": false, "os": false, "reporting": true, "storage": true },
"tabs": [ { "key": "documents", "shown": true }, { "key": "financial", "shown": true },
{ "key": "devices", "shown": false }, ... ]
}sections is keyed in the fixed order of SECTIONS; tabs is in display order and always holds every key in TABS.
Saving. The type dialog sends layout in the same shape with the rest of the type. save_asset_type.php saves it only when the key is present (array_key_exists('layout', $data)), so a caller that knows nothing of layouts - an older page, a script - cannot reset one by renaming the type. It goes through normalise() first, which:
- ignores unknown section and tab keys (including
identityandkeyinfo); - counts a tab sent twice once, first wins;
- appends any tab not sent, shown;
- turns anything that is not an array into the default layout rather than an error, so a bad request can never leave a type half-saved.
The layout is saved after the type's own row, under the same ownership checks - a client company cannot edit a shared type's layout, exactly as it cannot rename it. The wf_emit event is created or updated as before.
The Key info template marks each built-in block with the section it belongs to:
<div class="info-item" data-layout-section="hardware"> ... CPU ... </div>
<div class="disks-section" data-layout-section="storage"> ... </div>Identity items carry no attribute and are never touched. A new built-in field goes into one of the existing sections by giving it the attribute - and then is covered by that section's switch on every type with no other change.
selectAsset() builds the pane, then calls atlStartAsset() and applyAssetLayout(). Layouts come from assetTypeLayouts, filled from get_asset_types.php for every type, including inactive ones - an asset can still have a type that is no longer offered in the dropdown. If the types arrive after an asset is already open (a deep link), loadAssetTypesForDropdown() re-applies.
applyAssetLayout():
- toggles
.atl-hiddenon each[data-layout-section]block; - moves each tab button after the previous one, starting from Key info, in the layout's order, toggling
.atl-hiddenon hidden ones. Buttons the layout does not know (the Intune tab, appended later byrenderIntuneTab()) keep their place after; - switches to Key info if the active tab has just been hidden;
- calls
atlRenderNotice().
updateAssetField() re-applies when the type changes (and when a financial field changes, because the hidden-data line counts those).
π΄ TRAP - a class, not
[hidden]..detail-taband.info-itemset their owndisplay, which beats thehiddenattribute's UA style. Hiding is.atl-hidden { display: none !important }.
Everything still loads - the page does not skip a request because its tab is hidden - so the page knows what a hidden part holds:
| Part | How the page knows it has data |
|---|---|
| Make and model, Hardware, Operating system, Reporting | The asset's own fields (atlSectionFields) |
| Financial |
atlFinancialFields on the asset |
| Storage |
loadDisks() calls atlSaw('storage', volumes + physical drives)
|
| Devices, Software, Tickets, Contracts | A MutationObserver on the tab strip reads the leading number of each count badge |
| Documents | The documents panel's own fd:count event |
| Projects | Not known - no count exists, so it is never named in the line |
The observer is deliberate: four loaders fill those badges in several formats, and reading the badge keeps this feature out of each of them.
atlRenderNotice() then shows one of three things, driven by window.ASSET_LAYOUT_NOTICE (from AssetTypeLayout::noticeMode(), setting asset_layout_hidden_notice):
| State | Shows |
|---|---|
| Nothing hidden | Nothing |
Revealed (atlReveal) |
Showing everything ... with Hide them again |
Mode notice and a hidden part holds data |
Hidden by this type's layout: Hardware, Software (214). with Show them |
| Otherwise | A Show hidden button at the end of the tab strip |
Revealed parts get .atl-revealed (a dashed outline) and the title Hidden by this profile. Revealing is per asset and per page view - opening another asset resets it - and is never saved.
The type dialog is shared with statuses and lease outcomes, so atlSetup(type, layout) shows the Layout row only for asset-type, exactly as icSetup() does for the icon picker. It fills the section ticks from the type's layout (or window.ASSET_LAYOUT.defaults for a new type) and mounts a ReorderList (Display order guide) whose items carry a tick box in their html. atlCollect() reads the ticks and getOrder() into the payload on submit.
Presets are AssetTypeLayout::PRESETS, printed into the page as window.ASSET_LAYOUT.presets. atlApplyPreset() only sets ticks - it never moves a tab and nothing records which preset was used. Adding a preset is one entry in PRESETS and one layout.preset_<key> string; the test checks every preset names real keys.
π΄ TRAP - the modal's label rule.
.modal .form-group label:not(...)makes every labeldisplay: block. The layout's option labels are styled under#itemLayoutRowso the ID wins; without it each tick box sits on a line of its own above its name.
The Details a type hides select sits under the types table and saves on change through save_system_settings.php; the key is listed in the Asset types tab's setting_keys in the manifest, so the Manage asset types capability is what may write it.
A new tab on the asset page. Add its key to AssetTypeLayout::TABS (in its default position), give its button data-dtab="<key>", and add layout.tab_<key>. Every type shows it until somebody hides it (section 3). If it has a count, add its badge to atlBadges or call atlSaw().
A new built-in field. Put it in an existing section with data-layout-section, and add its column to atlSectionFields so the hidden-data line sees it.
A new section. Add to SECTIONS, tag its blocks, add atlSectionFields (or an atlSaw() call), and layout.section_<key> + _desc. Think twice: sections are coarse on purpose (design, section 3).
Anything that wants to act on a layout - skip a write, require a field, filter a report - is outside this feature by design. It belongs with validation (decision 2) or with Part B's freshness, not here.
php tests/asset-type-layout.php - database, inside one transaction that is rolled back. It proves the storage rules that keep old installs unchanged:
- a type nobody configured shows everything, in the default order;
- a saved layout comes back - hidden sections, hidden tabs, tab order - and tabs not sent are kept, shown, after;
- saving the default stores no rows;
- a tab with no row (one added in a later release) still appears, shown; an unknown stored key is ignored;
-
normalise()will not hide identity or move Key info, de-duplicates tabs, and turns garbage into the default; - every preset names real keys, and everything hides nothing;
- deleting the type takes its layout with it.
The page and dialog were checked live in headless Chrome against a temporary ZZ type and asset (deleted afterwards): sections and tabs hidden and ordered; the line naming hidden data with counts; Show them / Hide them again; the link mode; the dialog reflecting a saved layout, a preset only ticking, and Save going through save_asset_type.php; a type with no layout identical to before; and at 390px wide (in an iframe - headless ignores a resized window) with no sideways scroll.
- Projects has no count, so it never appears in the hidden-data line (it does come back with Show them).
- The asset page only. Mobile and desktop share it; nothing else reads layouts, by design.
- Per type, not per company for a shared type - a company wanting a different layout makes its own type.
- No required built-in fields - deferred (decision 2).
- Asset type layouts
- Asset type profiles - design (#200)
- Custom asset fields - Developer Guide - the per-type custom half
-
Display order - Developer Guide -
ReorderList
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
-
Architecture
- β³ π§Ή Root folder tidy (URL routing)
- β³ π Database Integrity (FKs & constraints)
- β³ π οΈ Database Verification β Developer Guide
- β³ π§© Service Layer (UI + API share code)
- β³ πͺ€ Code traps (the TRAP: comments)
- β³ π Commenting for the Engineering wiki
- β³ π οΈ Engineering wiki scanner - Developer Guide
- β³ π Service-Layer progress
- π€ Contributing (read before a pull request)
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- π± Currencies
- π§ The sidebar (optional module rail)
- π§© Preference templates
- π Display order (modules, Watchtower cards) β Developer Guide
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ ποΈ Mobile: Files
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π Proposal: custom fields, inventory import and change evidence (#194)
- β³ π οΈ Custom fields, evidence + inventory - review (#195-#197)
- β³ π οΈ Long forms in a task's Work view - review (#198)
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ π§βπΌ REST API: Analysts
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π§Ύ Custom ticket fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ π Attachment size limits and large emails
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Case reports - a ticket and its work as one PDF
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Leases
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π§© Asset type layouts
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- ποΈ Files
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ π Escaped text could leave its attribute
- β³ π Task descriptions were not cleaned
- β³ π Task documents ignored company scope
- β³ π Repeating tasks ignored company scope
- β³ π¬ #183 Mailbox checks overlapped
- β³ π #188 Transfer lock shown off when unknown
- β³ β #184 Subtask names and Overdue
- β³ π§© #186 Teams: the buttons did nothing
- β³ π Text in a change could run script
- β³ π AI settings changed without the AI permission
- β³ πͺ #178 Deleting an analyst failed
- β³ π« A team with no departments showed 0 tickets
- β³ π Projects: adding a phase did nothing on Save
- β³ π Leavers could still sign in
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96