Skip to content

Instantly share code, notes, and snippets.

@wsydney76
Last active April 12, 2026 13:21
Show Gist options
  • Select an option

  • Save wsydney76/f73a78d8518958e6d4a46bda52cea30c to your computer and use it in GitHub Desktop.

Select an option

Save wsydney76/f73a78d8518958e6d4a46bda52cea30c to your computer and use it in GitHub Desktop.
Inspect element database records (for education purpose only. Mostly AI generated, so there may be errors)
{#
displayAll — Entry point for recursive element inspection.
Renders the element itself, then walks its field layout to discover
child elements (Matrix/relation fields, ContentBlock fields, rich-text
fields) and recurses into each one, avoiding infinite loops via a
global "handled" set.
@param element Any Craft element (Entry, Asset, Product, Order, …)
@param config Optional overrides: type, heading, level, include
#}
{% macro displayAll(element, config = {}) %}
{# Resolve display options, falling back to sensible defaults derived from the element class name #}
{% set type = config.type ?? className(element)|split('\\')|last|lower %}
{% set heading = config.heading ?? type|capitalize %}
{% set level = config.level ?? 1 %}
{% set include = config.include ?? '' %}
{# Initialise the global de-duplication registry on first call #}
{% set handledIds =_globals.get('handledIds') %}
{% if handledIds == null %}
{% do _globals.set('handledIds', []) %}
{% endif %}
{# Skip if this element has already been rendered (prevents cycles and duplicates) #}
{% if element.id not in _globals.get('handledIds') %}
{# Register the element *before* recursing so that siblings discovered
in the same pass are also deduplicated against each other #}
{% do _globals.set('handledIds', _globals.get('handledIds')|push(element.id)) %}
{# Step 1 — Render the element's own DB rows #}
{{ _self.display(element, {
type: type,
heading: heading,
level: level,
include: include
}) }}
{# Step 2 — Recurse into children only for top-level elements or
Matrix-nested entries (entries with no section are nested entries) #}
{% if (type == 'entry' and not element.section) or level == 1 %}
{% set fieldLayout = element.getFieldLayout() %}
{% if fieldLayout %}
{# --- First pass: collect direct children ---
We queue every child before recursing so that the full sibling
list is marked "handled" before any of them is expanded. This
prevents the same element appearing twice when it is referenced
by more than one field of the same owner. #}
{% set toProcess = [] %}
{# Commerce products carry variants outside the field layout;
add them to the queue manually #}
{% if type == 'product' %}
{% for variant in element.variants %}
{% set toProcess = toProcess|push({element: variant, heading: 'Variant', level: level + 1}) %}
{% endfor %}
{% endif %}
{% if type == 'user' %}
{% for address in element.addresses %}
{% set toProcess = toProcess|push({element: address, heading: 'Address', level: level + 1}) %}
{% endfor %}
{% endif %}
{% for field in fieldLayout.getCustomFields() %}
{% set fieldClass = className(field) %}
{# Determine how to handle this field type #}
{% set isMatrix = 'Matrix' in fieldClass or 'Address' in fieldClass %}
{# Matrix / Address fields — queue each child element for full display.
Relation fields are intentionally excluded: their targets are
linked via targetId in the relations table instead. #}
{% if isMatrix %}
{% set fieldValue = element.getFieldValue(field.handle) %}
{% if fieldValue is not null %}
{% set fieldElements = fieldValue.all() %}
{% for child in fieldElements %}
{# Only queue elements not yet handled or already queued #}
{% if child.id not in _globals.get('handledIds') %}
{% set childType = className(child)|split('\\')|last|capitalize %}
{% set toProcess = toProcess|push({element: child, heading: childType ~ ' Field: ' ~ field.name, level: level + 1}) %}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
{# ContentBlock field — queue the block element directly #}
{% if 'ContentBlock' in fieldClass %}
{% set block = element.getFieldValue(field.handle) %}
{% if block is not null %}
{% set toProcess = toProcess|push({element: block, heading: 'ContentBlock Field: ' ~ field.name, level: level + 1}) %}
{% endif %}
{% endif %}
{# Rich-text fields — render inline (chunks contain nested entries) #}
{% if 'ckeditor' in fieldClass %}
{{ _self.richText(element, field.handle, 'CKEditor', level + 1) }}
{% endif %}
{% if 'VizyField' in fieldClass %}
{{ _self.vizy(element, field.handle, 'Vizy', level + 1) }}
{% endif %}
{% if 'Doxter' in fieldClass %}
{{ _self.doxter(element, field.handle, 'Doxter', level + 1) }}
{% endif %}
{% endfor %}
{# --- Second pass: recurse ---
All direct siblings are now registered; recursing here means
no child can accidentally re-render a sibling it also references. #}
{% for child in toProcess %}
{% if child.element %}
{{ _self.displayAll(child.element, {heading: child.heading, level: child.level, include: include}) }}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
{% endif %}
{% endmacro %}
{#
display — Renders a single element's raw DB rows inside a collapsible panel.
Shows the core `elements` record plus type-specific tables (entries, assets,
variants, orders …), the `elements_sites` content blob, and the relations table.
@param element The Craft element to inspect
@param config type, heading, level, include (comma-separated extras: "changed,searchindex")
#}
{% macro display(element, config = {}) %}
{# Resolve display options #}
{% set type = config.type ?? className(element)|split('\\')|last|lower %}
{% set heading = config.heading ?? type|capitalize %}
{% set level = config.level ?? 1 %}
{% set include = config.include ?? '' %}
{# Convert the comma-separated include string to an array for `in` checks #}
{% set include = include|split(',') %}
{# Instantiate AR record classes used for DB lookups throughout this macro #}
{% set elementRecord = create('\\craft\\records\\Element') %}
{% set entryRecord = create('\\craft\\records\\Entry') %}
{% set userRecord = create('\\craft\\records\\User') %}
{% set assetRecord = create('\\craft\\records\\Asset') %}
{% set addressRecord = create('\\craft\\records\\Address') %}
{% set elementsSitesRecord = create('\\craft\\records\\Element_SiteSettings') %}
{% set contentBlocksRecord = create('\\craft\\records\\ContentBlock') %}
{# Only instantiate Commerce records when the plugin is active #}
{% if craft.app.plugins.pluginEnabled('commerce') %}
{% set productRecord = create('\\craft\\commerce\\records\\Product') %}
{% set variantRecord = create('\\craft\\commerce\\records\\Variant') %}
{% set purchasableRecord = create('\\craft\\commerce\\records\\Purchasable') %}
{% set purchasableStoreRecord = create('\\craft\\commerce\\records\\PurchasableStore') %}
{% set orderRecord = create('\\craft\\commerce\\records\\Order') %}
{% set lineItemRecord = create('\\craft\\commerce\\records\\LineItem') %}
{% set transactionRecord = create('\\craft\\commerce\\records\\Transaction') %}
{% set orderHistoryRecord = create('\\craft\\commerce\\records\\OrderHistory') %}
{% endif %}
<hr>
{# Collapsible panel — open by default so all data is visible on load #}
<details open>
<summary style="cursor: pointer; list-style: none; display: flex; align-items: center; gap: 6px;">
<span class="details-arrow" style="font-size: 0.8em; transition: transform 0.2s;">&#9654;</span>
{# Build a short type-specific suffix for the panel heading #}
{% set extraText = '' %}
{% if type == 'entry' %}
{% set extraText = "/#{element.type.name}" %}
{% elseif type == 'asset' %}
{# {% set extraText = "/#{element.volume.name}" %} #}
{% elseif type == 'order' %}
{% set extraText = " ##{element.reference ?? element.number|slice(0, 8)}" %}
{% endif %}
<h2 style="display: inline; margin: 0; font-family: sans-serif; font-weight:normal;">Level: {{ level }}
ID: {{ element.id }}
{{ heading }} {{ element.section.name ?? '' }}{{ extraText }} </h2>
</summary>
{# If element is not editable, the current page url is returned instead of an edit link #}
{% set cpEditUrl = element.cpEditUrl %}
{% if craft.app.config.general.cpTrigger in cpEditUrl and craft.app.elements.canSave(element) %}
<a href="{{ cpEditUrl }}">Edit</a>
{% endif %}
<div style="display:flex; flex-wrap: wrap; gap: 24px;">
{# --- elements table --- always shown for every element type #}
<div>
<h3>elements</h3>
{% set record = elementRecord.findOne(element.id) %}
{{ _self.renderArray(record.attributes) }}
{# If this is a draft, also show the drafts row #}
{% if record.draftId %}
<h4>drafts</h4>
{% set draft = craft.app.db.createCommand(
'SELECT * FROM {{%drafts}} WHERE [[id]] = :draftId',
{':draftId': element.draftId}
).queryOne() %}
{{ _self.renderArray(draft) }}
{% endif %}
{# If this is a revision, also show the revisions row #}
{% if record.revisionId %}
<h4>revisions</h4>
{% set revision = craft.app.db.createCommand(
'SELECT * FROM {{%revisions}} WHERE [[id]] = :revisionId',
{':revisionId': element.revisionId}
).queryOne() %}
{{ _self.renderArray(revision) }}
{% endif %}
{# Optional: show changedattributes / changedfields for drafts
(only when "changed" is present in the include list) #}
{% if 'changed' in include and element.isDraft ?? false %}
{% set changedAttributes = craft.app.db.createCommand(
'SELECT * FROM {{%changedattributes}} WHERE [[elementId]] = :elementId AND [[siteId]] = :siteId',
{':elementId': element.id, ':siteId': element.siteId}
).queryAll() %}
{% if changedAttributes %}
<h4>changedattributes</h4>
{{ _self.renderArray(changedAttributes) }}
{% endif %}
{% set changedFields = craft.app.db.createCommand(
'SELECT * FROM {{%changedfields}} WHERE [[elementId]] = :elementId AND [[siteId]] = :siteId',
{':elementId': element.id, ':siteId': element.siteId}
).queryAll() %}
{% if changedFields %}
<h4>changedfields</h4>
{{ _self.renderArray(changedFields) }}
{% endif %}
{% endif %}
</div>
{# --- Type-specific tables --- #}
{% switch type %}
{% case "entry" %}
{# entries table + optional authors join table #}
<div>
<h3>entries</h3>
{{ _self.renderArray(entryRecord.findOne(element.id).attributes) }}
{% set authors = craft.app.db.createCommand(
'SELECT * FROM {{%entries_authors}} WHERE [[entryId]] = :entryId',
{':entryId': element.id}
).queryAll() %}
{% if authors %}
<h4>users</h4>
{{ _self.renderArray(authors) }}
{% endif %}
</div>
{% case "user" %}
<div>
<h3>users</h3>
{{ _self.renderArray(userRecord.findOne(element.id).attributes) }}
</div>
{% case "asset" %}
{# assets table + per-site alt-text row from assets_sites #}
<div>
<h3>assets</h3>
{{ _self.renderArray(assetRecord.findOne(element.id).attributes) }}
<h4>assets_sites</h4>
{% set altRecord = craft.app.db.createCommand(
'SELECT * FROM {{%assets_sites}} WHERE [[assetId]] = :elementId AND [[siteId]] = :siteId',
{':elementId': element.id, ':siteId': element.siteId}
).queryOne() %}
{{ _self.renderArray(altRecord) }}
</div>
{% case "address" %}
{% set address = addressRecord.findOne(element.id) %}
{% if address %}
<div>
<h3>addresses</h3>
{{ _self.renderArray(address.attributes) }}
</div>
{% endif %}
{% case "contentblock" %}
{% set block = contentBlocksRecord.findOne(element.id) %}
{% if block %}
<div>
<h3>contentblocks</h3>
{{ _self.renderArray(block.attributes) }}
</div>
{% endif %}
{% case "product" %}
<div>
<h3>products</h3>
{{ _self.renderArray(productRecord.findOne(element.id).attributes, 'product') }}
</div>
{% case "variant" %}
{# variants + purchasables (one row) + purchasables_stores (one row per store) #}
<div>
<h3>variants</h3>
{{ _self.renderArray(variantRecord.findOne(element.id).attributes, 'variant') }}
<h3>purchasables</h3>
{{ _self.renderArray(purchasableRecord.findOne(element.id).attributes, 'purchasable') }}
{% set purchasableStores = purchasableStoreRecord.findAll({purchasableId: element.id}) %}
{% for purchasableStore in purchasableStores %}
<h3>purchasables_store #{{ loop.index }}</h3>
{{ _self.renderArray(purchasableStore.attributes, 'purchasable') }}
{% endfor %}
</div>
{% case "order" %}
{# orders + line items + payment transactions + status history #}
<div>
<h3>orders</h3>
{{ _self.renderArray(orderRecord.findOne(element.id).attributes, 'order') }}
</div>
<div>
<h3>lineitems</h3>
{% set lineItems = lineItemRecord.findAll({orderId: element.id}) %}
{% if lineItems %}
{% for lineItem in lineItems %}
<h4>Line Item #{{ loop.index }}{% if lineItem.description %}: {{ lineItem.description }}{% endif %}</h4>
{{ _self.renderArray(lineItem.attributes, 'lineitem') }}
{% endfor %}
{% else %}
<em style="color: #aaa;">(no line items)</em>
{% endif %}
</div>
<div>
<h3>transactions</h3>
{% set transactions = transactionRecord.findAll({orderId: element.id}) %}
{% if transactions %}
{% for transaction in transactions %}
<h4>Transaction #{{ loop.index }}: {{ transaction.type }} / {{ transaction.status }}{% if transaction.amount %} {{ transaction.amount }} {{ transaction.currency }}{% endif %}</h4>
{{ _self.renderArray(transaction.attributes, 'transaction') }}
{% endfor %}
{% else %}
<em style="color: #aaa;">(no transactions)</em>
{% endif %}
</div>
<div>
<h3>orderhistories</h3>
{% set histories = orderHistoryRecord.findAll({orderId: element.id}) %}
{% if histories %}
{% for history in histories %}
<h4>History #{{ loop.index }}</h4>
{{ _self.renderArray(history.attributes, 'orderhistory') }}
{% endfor %}
{% else %}
<em style="color: #aaa;">(no order history)</em>
{% endif %}
</div>
{% endswitch %}
{# --- elements_sites --- per-site settings and the JSON content blob #}
<div>
<h3>elements_sites</h3>
{% set record = elementsSitesRecord.findOne({elementId: element.id, siteId: element.siteId}) %}
{% set attrs = record.attributes %}
{# If a `content` column exists, decode it and render it through
renderContentArray which resolves UUID field keys to human names #}
{% if attrs.content is defined %}
{{ _self.renderArray(attrs|merge({content: '--- see below ---'})) }}
<h4>content</h4>
{% set contentData = attrs.content is iterable ? attrs.content : (attrs.content ? attrs.content|json_decode : null) %}
{{ _self.renderContentArray(contentData) }}
{% else %}
{{ _self.renderArray(attrs) }}
{% endif %}
</div>
{# --- elements_owners — only shown when records exist #}
{% set elementOwners = craft.app.db.createCommand(
'SELECT * FROM {{%elements_owners}} WHERE [[elementId]] = :elementId',
{':elementId': element.id}
).queryAll() %}
{% if elementOwners %}
<div>
<h3>elements_owners</h3>
{{ _self.renderArray(elementOwners) }}
</div>
{% endif %}
{# --- structureelements — only shown when the element is part of a structure #}
{% set structureElement = craft.app.db.createCommand(
'SELECT * FROM {{%structureelements}} WHERE [[elementId]] = :elementId',
{':elementId': element.id}
).queryOne() %}
{% if structureElement %}
<div>
<h3>structureelements</h3>
{{ _self.renderArray(structureElement) }}
<h4>Structure</h4>
{{ _self.renderArray({
parent: element.parent ? element.parent.title : null,
prevSibling: element.prevSibling ? element.prevSibling.title : null,
nextSibling: element.nextSibling ? element.nextSibling.title : null,
children: element.children ? element.children|map(e => e.title)
}) }}
</div>
{% endif %}
{# --- relations — outgoing relation rows for this element/site #}
<div>
<h3>relations</h3>
{% set relations = craft.app.db.createCommand(
'SELECT * FROM {{%relations}} WHERE [[sourceId]] = :sourceId AND ([[sourceSiteId]] IS NULL OR [[sourceSiteId]] = :siteId)',
{':sourceId': element.id, ':siteId': element.siteId}
).queryAll() %}
{{ _self.renderArray(relations) }}
</div>
{# --- searchindex — optional; included only when "searchindex" is in the include list #}
{% if 'searchindex' in include %}
<div>
<h3>searchindex</h3>
{% set records = craft.app.db.createCommand(
'SELECT * FROM {{%searchindex}} WHERE [[elementId]] = :elementId AND [[siteId]] = :siteId',
{':elementId': element.id, ':siteId': element.siteId}
).queryAll() %}
{{ _self.renderArray(records) }}
</div>
{% endif %}
</div>
</details>
{% endmacro %}
{#
elementLink — Renders a link to the storage page for a given element ID.
Retrieves the element via craft.app.elements.getElementById to populate
the title attribute with the element's title or name.
@param id The element ID to link to
@param style Optional additional inline CSS for the anchor tag
#}
{% macro elementLink(id, style = '') %}
{% if id %}
{% set _linkedEl = craft.app.elements.getElementById(id) %}
{% set _elTitle = _linkedEl ? (_linkedEl.title ?? '') : '' %}
{% if not _elTitle and _linkedEl is instance of('craft\\elements\\User') %}
{% set _elTitle = _linkedEl.name %}
{% endif %}
<a href="{{ siteUrl('@extras/storage', {id: id}) }}"
style="font-family: monospace;{{ style ? ' ' ~ style : '' }}">{{ id }}</a>
{% if _elTitle %}
<span style="color: #aaa">({{ _elTitle }})</span>
{% endif %}
{% endif %}
{% endmacro %}
{#
renderArray — Generic key/value table renderer.
Handles nested iterables recursively. For well-known FK columns
(sectionId, typeId, siteId, …) it looks up the human-readable name
and shows it as a greyed-out annotation.
@param data Any array/object, or a scalar/null
@param type Optional element type hint used to disambiguate typeId lookups
(e.g. 'product' vs entry)
#}
{% macro renderArray(data, type = '') %}
{# TODO: Find a generic way to find relevant keys, some keys ending with Id do point to elements, some not... #}
{# Keys, whose values shall be linked to the elements storage page #}
{% set elementIdKeys = ['targetId', 'ownerId', 'authorId', 'creatorId', 'customerId', 'orderId', 'purchasableId', 'primaryOwnerId'] %}
{# Keys, whose values should be annotated with human readable names #}
{% set lookupIdKeys = ['sectionId', 'typeId', 'siteId', 'sourceSiteId', 'fieldId', 'volumeId', 'folderId',
'storeId', 'orderStatusId', 'taxCategoryId', 'shippingCategoryId', 'gatewayId', 'newStatusId', 'prevStatusId'] %}
{% if data is iterable %}
{% if data|length > 0 %}
<table style="border-collapse: collapse; font-size: 12px; font-family: monospace; width: 100%;">
{% for key, value in data %}
<tr style="border-bottom: 1px solid #e0e0e0;">
<th style="text-align: left; padding: 2px 8px 2px 2px; color: #555; white-space: nowrap; vertical-align: top; font-weight: bold;"
title="{{ key }}">
{# Truncate UUID keys to keep the table readable #}
{%- if key matches '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i' -%}
{{ key|slice(0, 8) }}&hellip;:
{%- else -%}
{{ key }}:
{%- endif -%}
</th>
<td style="padding: 2px; vertical-align: top; width: 100%;">
{% if value is iterable %}
{# Nested array — recurse #}
{{ _self.renderArray(value) }}
{% elseif value is null %}
<em style="color: #aaa;">null</em>
{% elseif value is same as(true) %}
<span style="color: green;">true</span>
{% elseif value is same as(false) %}
<span style="color: #c00;">false</span>
{% elseif (key in elementIdKeys or key ends with 'AddressId') and value %}
{# Link relation targets, element owners, authors and creators etc. to their storage page #}
{{ _self.elementLink(value) }}
{% elseif value matches '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i' %}
{# Truncate UUID values too; full UUID in title tooltip #}
<span title="{{ value }}">{{ value|slice(0, 8) }}&hellip;</span>
{% else %}
{# Truncate long scalar values and put the full text in a tooltip #}
{% if value|length > 40 %}
<span title="{{ value }}">{{ value|slice(0, 40) }}&hellip;</span>
{% else %}
{{ value }}
{% endif %}
{# For known FK columns, look up and display the human name as a hint #}
{% if key in lookupIdKeys and value %}
{% set _lookupName = null %}
{% if key == 'siteId' or key == 'sourceSiteId' %}
{% set _obj = craft.app.sites.getSiteById(value) %}
{% if _obj %}{% set _lookupName = _obj.name %}{% endif %}
{% elseif key == 'sectionId' %}
{% set _obj = craft.app.entries.getSectionById(value) %}
{% if _obj %}{% set _lookupName = _obj.name %}{% endif %}
{% elseif key == 'typeId' %}
{# Commerce products use a different type registry than entries #}
{% if type == 'product' %}
{% set _obj = craft.app.plugins.plugin('commerce').productTypes.getProductTypeById(value) %}
{% if _obj %}{% set _lookupName = _obj.name %}{% endif %}
{% else %}
{% set _obj = craft.app.entries.getEntryTypeById(value) %}
{% if _obj %}{% set _lookupName = _obj.name %}{% endif %}
{% endif %}
{% elseif key == 'fieldId' %}
{% set _obj = craft.app.fields.getFieldById(value) %}
{% if _obj %}{% set _lookupName = _obj.name %}{% endif %}
{% elseif key == 'volumeId' %}
{% set _obj = craft.app.volumes.getVolumeById(value) %}
{% if _obj %}{% set _lookupName = _obj.name %}{% endif %}
{% elseif key == 'folderId' %}
{% set _obj = craft.app.assets.getFolderById(value) %}
{% if _obj %}{% set _lookupName = _obj.path %}{% endif %}
{% elseif key == 'storeId' %}
{% set _obj = craft.app.plugins.plugin('commerce').stores.getStoreById(value) %}
{% if _obj %}{% set _lookupName = _obj.name %}{% endif %}
{% elseif key == 'orderStatusId' or key == 'newStatusId' or key == 'prevStatusId' %}
{% set _obj = craft.app.plugins.plugin('commerce').orderStatuses.getOrderStatusById(value) %}
{% if _obj %}{% set _lookupName = _obj.name %}{% endif %}
{% elseif key == 'taxCategoryId' %}
{% set _obj = craft.app.plugins.plugin('commerce').taxCategories.getTaxCategoryById(value) %}
{% if _obj %}{% set _lookupName = _obj.name %}{% endif %}
{% elseif key == 'shippingCategoryId' %}
{% set _obj = craft.app.plugins.plugin('commerce').shippingCategories.getShippingCategoryById(value) %}
{% if _obj %}{% set _lookupName = _obj.name %}{% endif %}
{% elseif key == 'gatewayId' %}
{% set _obj = craft.app.plugins.plugin('commerce').gateways.getGatewayById(value) %}
{% if _obj %}{% set _lookupName = _obj.name %}{% endif %}
{% endif %}
{% if _lookupName %}
<span style="color: #aaa;">({{ _lookupName }})</span>
{% endif %}
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<em style="color: #aaa;">(empty)</em>
{% endif %}
{% elseif data is null %}
<em style="color: #aaa;">null</em>
{% else %}
{{ data }}
{% endif %}
{% endmacro %}
{#
renderContentArray — Variant of renderArray for the `elements_sites.content` blob.
The content column stores field values keyed by the field-layout-element UUID
rather than the field handle. This macro resolves each UUID to a human name by
querying the fieldlayouts config JSON, and delegates Table field values to
renderTableField for a proper column-headed table.
@param data Decoded content array (UUID → value)
#}
{% macro renderContentArray(data) %}
{% if data is iterable %}
{% if data|length > 0 %}
{% set hasGeneratedFields = false %}
<table style="border-collapse: collapse; font-size: 12px; font-family: monospace; width: 100%;">
{% for key, value in data %}
{% set _currentField = null %}
<tr style="border-bottom: 1px solid #e0e0e0;">
<th style="text-align: left; padding: 2px 8px 2px 2px; color: #555; white-space: nowrap; vertical-align: top; font-weight: bold;"
title="{{ key }}">
{%- if key matches '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i' -%}
{{ key|slice(0, 8) }}&hellip;
{# Look up the field layout that contains this UUID so we can show the field name #}
{% set _layoutRow = craft.app.db.createCommand(
"SELECT [[config]] FROM {{%fieldlayouts}} WHERE [[config]] LIKE :pattern",
{':pattern': '%' ~ key ~ '%'}
).queryOne() %}
{% if _layoutRow %}
{# Decode the layout config JSON if it arrived as a string #}
{% set _layoutConfig = _layoutRow.config is iterable ? _layoutRow.config : _layoutRow.config|json_decode %}
{% set _fieldName = null %}
{# Walk tabs → elements to find the matching layout element by UID #}
{% for _tab in (_layoutConfig.tabs ?? []) %}
{% for _el in (_tab.elements ?? []) %}
{% if _el.uid is defined and _el.uid == key %}
{# Prefer an explicit label on the layout element #}
{% if _el.label is defined and _el.label %}
{% set _fieldName = _el.label %}
{% endif %}
{# Fall back to the field's own name and store the field object
so we can use its type info when rendering the value #}
{% if _el.fieldUid is defined %}
{% set _f = craft.app.fields.getFieldByUid(_el.fieldUid) %}
{% if _f %}
{% if not _fieldName %}{% set _fieldName = _f.name %}{% endif %}
{% set _currentField = _f %}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
{# Also check generatedFields (virtual fields not stored in the fields table) #}
{% if not _fieldName %}
{% for _gf in (_layoutConfig.generatedFields ?? []) %}
{% if _gf.uid is defined and _gf.uid == key and _gf.name is defined %}
{% set _fieldName = _gf.name ~ ' *' %}
{% set hasGeneratedFields = true %}
{% endif %}
{% endfor %}
{% endif %}
{% if _fieldName %}
<span style="color: #888; font-weight: normal;">({{ _fieldName }})</span>
{% endif %}
{% endif %}
:
{%- else -%}
{{ key }}:
{%- endif -%}
</th>
<td style="padding: 2px; vertical-align: top; width: 100%;">
{% if value is iterable %}
{# Determine field type for specialised rendering #}
{% set _isRelationField = _currentField and (
'Entries' in className(_currentField) or
'Assets' in className(_currentField) or
'Users' in className(_currentField) or
'Categories' in className(_currentField) or
'Tags' in className(_currentField) or
'Products' in className(_currentField) or
'Variants' in className(_currentField)
) %}
{# Table fields get a special columnar renderer #}
{% if _currentField and 'Table' in className(_currentField) %}
{{ _self.renderTableField(value, _currentField) }}
{# Relation fields: render each ID as a link instead of recursing #}
{% elseif _isRelationField %}
{% for _relId in value %}
{{ _self.elementLink(_relId, 'display: inline-block; margin-right: 6px;') }}<br>
{% endfor %}
{% else %}
{{ _self.renderArray(value) }}
{% endif %}
{% elseif value is null %}
<em style="color: #aaa;">null</em>
{% elseif value is same as(true) %}
<span style="color: green;">true</span>
{% elseif value is same as(false) %}
<span style="color: #c00;">false</span>
{% elseif value matches '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i' %}
<span title="{{ value }}">{{ value|slice(0, 8) }}&hellip;</span>
{% else %}
{% if value|length > 40 %}
<span title="{{ value }}">{{ value|slice(0, 40) }}&hellip;</span>
{% else %}
{{ value }}
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{# Footnote explaining the * marker used for generated fields #}
{% if hasGeneratedFields %}
<div style="margin-top: 12px;">
* = Generated field
</div>
{% endif %}
{% else %}
<em style="color: #aaa;">(empty)</em>
{% endif %}
{% elseif data is null %}
<em style="color: #aaa;">null</em>
{% else %}
{{ data }}
{% endif %}
{% endmacro %}
{#
renderTableField — Renders a Craft Table field value as a proper HTML table
with column headings taken from the field definition.
Supports both dynamic rows (numbered) and static rows (col1 used as row label).
@param data Row array stored in the content blob
@param field The Craft Table field object (provides columns, staticRows, defaults)
#}
{% macro renderTableField(data, field) %}
{# Extract column definitions and static-rows settings from the field config #}
{% set columns = field.columns %}
{% set isStatic = field.staticRows ?? false %}
{# Default row labels (used in static mode to fill in col1 from the field definition) #}
{% set defaults = field.defaults ?? [] %}
{% if data is iterable and data|length > 0 %}
<table style="border-collapse: collapse; font-size: 12px; font-family: monospace; width: 100%;">
<thead>
<tr style="border-bottom: 2px solid #aaa;">
{# First column header: row label (static) or row number (dynamic) #}
<th style="text-align: left; padding: 2px 8px 2px 2px; color: #888; white-space: nowrap;">
{%- if isStatic -%}
{{ columns.col1.heading ?? 'Row' }}
<span style="color: #aaa; font-weight: normal;">(col1)</span>
{%- else -%}
#
{%- endif -%}
</th>
{% for colHandle, colDef in columns %}
{# col1 is used as the row label in static-rows mode — skip it as a data column #}
{% if not (isStatic and colHandle == 'col1') %}
<th style="text-align: left; padding: 2px 8px 2px 2px; color: #555; white-space: nowrap;">
{{ colDef.heading ?? colHandle }}
<span style="color: #aaa; font-weight: normal;">({{ colHandle }})</span>
</th>
{% endif %}
{% endfor %}
</tr>
</thead>
<tbody>
{% for rowKey, row in data %}
<tr style="border-bottom: 1px solid #e0e0e0;">
{# Row identifier cell #}
<td style="padding: 2px 8px 2px 2px; color: #888; vertical-align: top; white-space: nowrap;
{% if not isStatic %}text-align: right;{% endif %}">
{%- if isStatic -%}
{# In static mode, use the field's default col1 label if available #}
{%- set defaultRow = defaults[loop.index0] ?? null -%}
{{- (defaultRow.col1 ?? row.col1 ?? loop.index) -}}
{%- else -%}
{{- loop.index -}}
{%- endif -%}
</td>
{% for colHandle, colDef in columns %}
{% if not (isStatic and colHandle == 'col1') %}
<td style="padding: 2px; vertical-align: top;">
{% set cellValue = row[colHandle] ?? null %}
{# Render each cell with type-aware formatting #}
{% if cellValue is null %}
<em style="color: #aaa;">null</em>
{% elseif cellValue is same as(true) %}
<span style="color: green;">true</span>
{% elseif cellValue is same as(false) %}
<span style="color: #c00;">false</span>
{% elseif cellValue|length > 40 %}
<span title="{{ cellValue }}">{{ cellValue|slice(0, 40) }}&hellip;</span>
{% else %}
{{ cellValue }}
{% endif %}
</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<em style="color: #aaa;">(empty)</em>
{% endif %}
{% endmacro %}
{#
richText — Renders a CKEditor field value for inspection.
Shows the raw HTML stored in the DB, then iterates over each "chunk"
(markup segments and nested entries) and renders them individually.
@param element Owner element
@param field Field handle
@param heading Label for the panel heading
@param level Current nesting level (used when recursing into nested entries)
#}
{% macro richText(element, field, heading = '', level = 1) %}
{# @var data \craft\ckeditor\data\FieldData #}
<hr>
<details open>
<summary style="cursor: pointer; list-style: none; display: flex; align-items: center; gap: 6px;">
<span class="details-arrow" style="font-size: 0.8em; transition: transform 0.2s;">&#9654;</span>
<h2 style="display: inline; margin: 0;">{{ heading }} Field: {{ field }}</h2>
</summary>
{% set data = element.getFieldValue(field) %}
{# The raw CKEditor HTML as stored in the DB, before chunk splitting #}
<h4>Raw content</h4>
<div style="margin:12px; padding:8px; border: 1px solid #333; background-color: #eeeeee">
{{ data.rawContent }}
</div>
{# Chunks: CKEditor splits the content at nested-entry boundaries.
Each chunk is either a `markup` segment or an embedded `entry`. #}
<h4>Chunks</h4>
<div style="margin-left: 12px;">
{% for chunk in data.chunks %}
{% if chunk.type == 'markup' %}
<h5>Markup</h5>
<div style="margin:12px; padding:8px; border: 1px solid #333; background-color: #eeeeee">
{{ chunk }}
</div>
{% else %}
{# Nested entry embedded inside the CKEditor field — render its DB rows #}
<h5>Entry</h5>
{{ _self.display(chunk.entry, {heading: 'Nested entry in CKEditor', level: level}) }}
{% endif %}
{% endfor %}
</div>
</details>
{% endmacro %}
{#
vizy — Renders a Vizy field value as formatted JSON for inspection.
@param element Owner element
@param field Field handle
@param heading Label for the panel heading
@param level Current nesting level (unused here, kept for API consistency)
#}
{% macro vizy(element, field, heading = '', level = 1) %}
{# @var data \verbb\vizy\models\NodeCollection #}
<hr>
<details open>
<summary style="cursor: pointer; list-style: none; display: flex; align-items: center; gap: 6px;">
<span class="details-arrow" style="font-size: 0.8em; transition: transform 0.2s;">&#9654;</span>
<h2 style="display: inline; margin: 0;">{{ heading }} Field: {{ field }}</h2>
</summary>
{% set data = element.getFieldValue(field) %}
{# Dump the raw Vizy node tree as pretty-printed JSON #}
<pre>
{{ data.getRawNodes()|json_encode(constant('JSON_PRETTY_PRINT')) }}
</pre>
</details>
{% endmacro %}
{#
doxter — Renders a Doxter (Markdown) field value for inspection.
@param element Owner element
@param field Field handle
@param heading Label for the panel heading
@param level Current nesting level (unused here, kept for API consistency)
#}
{% macro doxter(element, field, heading = '', level = 1) %}
{# @var data \verbb\doxter\fields\Doxter #}
<hr>
<details open>
<summary style="cursor: pointer; list-style: none; display: flex; align-items: center; gap: 6px;">
<span class="details-arrow" style="font-size: 0.8em; transition: transform 0.2s;">&#9654;</span>
<h2 style="display: inline; margin: 0;">{{ heading }} Field: {{ field }}</h2>
</summary>
{% set data = element.getFieldValue(field) %}
{# Show the raw Markdown source with line breaks preserved #}
<div style="margin-top: 12px;">
{{ data.raw|nl2br }}
</div>
</details>
{% endmacro %}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment