Skip to content

Asset Type Layouts Developer Guide

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

πŸ› οΈ 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.


1. πŸ“ The files

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

2. πŸ”‘ The rule that keeps it small

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 in SECTIONS, so no request can hide them - normalise() drops any key it does not know.

3. πŸ—„οΈ Storage - no row means shown

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 equals defaults(). "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 in SECTIONS/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.

4. πŸ”„ Reading and saving

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 identity and keyinfo);
  • 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.


5. πŸ–₯️ The asset page

Tagging

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.

Applying

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():

  1. toggles .atl-hidden on each [data-layout-section] block;
  2. moves each tab button after the previous one, starting from Key info, in the layout's order, toggling .atl-hidden on hidden ones. Buttons the layout does not know (the Intune tab, appended later by renderIntuneTab()) keep their place after;
  3. switches to Key info if the active tab has just been hidden;
  4. 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-tab and .info-item set their own display, which beats the hidden attribute's UA style. Hiding is .atl-hidden { display: none !important }.

Hidden data, and the way back

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.


6. βš™οΈ The settings dialog

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 label display: block. The layout's option labels are styled under #itemLayoutRow so 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.


7. βž• Extending 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.


8. πŸ§ͺ Testing it

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.


9. πŸ“ Known limits

  • 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).

See also

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally