Instantly share code, notes, and snippets.
Created
August 6, 2025 13:46
-
Star
2
(2)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save ryanleichty/92dcc02bb76d308fee6035957c64cd68 to your computer and use it in GitHub Desktop.
Related Entries
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% set relatedEntries = craft.entries | |
.relatedTo({ targetElement: element.rootOwner }) | |
.provisionalDrafts(false) | |
.all() %} | |
{# Filter out entries that have provisional draft owners #} | |
{% set withoutProvisionalDrafts = [] %} | |
{% for entry in relatedEntries %} | |
{% set hasProvisionalOwner = false %} | |
{% set currentEntry = entry.owner %} | |
{# Check up to 10 levels of ownership for provisional drafts #} | |
{% for i in 1..10 %} | |
{% if currentEntry %} | |
{% if currentEntry.isProvisionalDraft %} | |
{% set hasProvisionalOwner = true %} | |
{% set currentEntry = null %} | |
{% else %} | |
{% set currentEntry = currentEntry.owner %} | |
{% endif %} | |
{% endif %} | |
{% endfor %} | |
{% if not hasProvisionalOwner %} | |
{% set withoutProvisionalDrafts = withoutProvisionalDrafts|merge([entry]) %} | |
{% endif %} | |
{% endfor %} | |
{# Group nested entries by root owner #} | |
{% set groupedByRootOwner = withoutProvisionalDrafts|group(entry => | |
entry.ownerId is empty ? 'self' : entry.rootOwner.id | |
) %} | |
{# Entries without nested entries #} | |
{% set withoutNested = groupedByRootOwner['self'] ?? [] %} | |
{% set computedWithoutNested = withoutNested|map(entry => { | |
entry: entry, | |
nestedEntries: [] | |
}) %} | |
{# Entries with nested entries #} | |
{% set withNested = groupedByRootOwner|filter((group, id) => id != 'self') %} | |
{% set computedWithNested = withNested|map((group) => { | |
entry: group[0].rootOwner, | |
nestedEntries: group | |
}) %} | |
{# Group by section #} | |
{% set computedEntries = computedWithoutNested|merge(computedWithNested) %} | |
{% set groupedBySection = computedEntries|group(entryObj => | |
entryObj.entry.section.name ?? 'Assets' | |
) %} | |
{% set sortedSectionNames = groupedBySection|keys|sort %} | |
{# Template #} | |
<div class="RelatedEntries"> | |
{% if groupedBySection|length %} | |
{% for sectionName in sortedSectionNames %} | |
{% set entryObjArray = groupedBySection[sectionName] %} | |
<div class="RelatedEntries__section"> | |
<div class="RelatedEntries__sectionHeader"> | |
<span class="RelatedEntries__sectionName">{{ sectionName }}</span> | |
</div> | |
<ul> | |
{% for entryObj in entryObjArray|sort((a, b) => a.entry.title <=> b.entry.title) %} | |
{% set rootEntry = entryObj.entry %} | |
<li class="RelatedEntries__listItem"> | |
{{ _self.item(rootEntry) }} | |
{% if entryObj.nestedEntries|length %} | |
<ul class="RelatedEntries__nestedList"> | |
{% for nestedEntry in entryObj.nestedEntries|sort((a, b) => a.title <=> b.title) %} | |
<li class="RelatedEntries__nestedListItem"> | |
{{ _self.item(nestedEntry, currentUser.admin) }} | |
</li> | |
{% endfor %} | |
</ul> | |
{% endif %} | |
</li> | |
{% endfor %} | |
</ul> | |
</div> | |
{% endfor %} | |
{% else %} | |
<p>No usages</p> | |
{% endif %} | |
</div> | |
{# Macros #} | |
{% macro chip(label, color) %} | |
<span | |
class="RelatedEntries__chip" | |
style="--chip-color: var(--{{ color }}-900); --chip-bg: var(--{{ color }}-500);" | |
> | |
{{ label }} | |
</span> | |
{% endmacro %} | |
{% macro icon(entry) %} | |
{% set elementClass = className(entry) %} | |
{% set key = elementClass|split('\\')|last %} | |
{% set icon = entry.type.icon ?? null %} | |
{% set color = entry.type.color.value ?? 'gray' %} | |
<div class="cp-icon small {{ color }}"> | |
{% if icon %} | |
{{ iconSvg(icon) }} | |
{% else %} | |
{% switch key %} | |
{% case 'Entry' %} | |
{{ iconSvg('newspaper') }} | |
{% case 'Category' %} | |
{{ iconSvg('sitemap') }} | |
{% case 'Asset' %} | |
{{ iconSvg('asset') }} | |
{% case 'Tag' %} | |
{{ iconSvg('tags') }} | |
{% endswitch %} | |
{% endif %} | |
</div> | |
{% endmacro %} | |
{% macro item(entry, isAdmin = true) %} | |
{% set color = entry.type.color.value ?? 'gray' %} | |
{% set tag = isAdmin ? 'a' : 'div' %} | |
{# Check entry and all owners for draft/disabled status #} | |
{% set isDraft = false %} | |
{% set isDisabled = false %} | |
{% set currentEntry = entry %} | |
{% for i in 1..10 %} | |
{% if currentEntry %} | |
{% set isDraft = currentEntry.draftId ? true : isDraft %} | |
{% set isDisabled = currentEntry.status == 'disabled' ? true : isDisabled %} | |
{% set currentEntry = currentEntry.owner %} | |
{% endif %} | |
{% endfor %} | |
{# Entry is considered draft or disabled if it or an owner is a draft or disabled #} | |
{% set status = isDraft ? 'gray' : isDisabled ? 'disabled' : entry.status %} | |
<div class="RelatedEntries__item"> | |
{% if isDraft %} | |
<div class="RelatedEntries__status" data-icon="draft"></div> | |
{% else %} | |
<div class="RelatedEntries__status status {{ status }}"></div> | |
{% endif %} | |
<{{ tag }} class="RelatedEntries__itemLink"{% if isAdmin %} href="{{ entry.cpEditUrl }}"{% endif %}> | |
<span class="RelatedEntries__itemContent"> | |
{{ _self.icon(entry) }} | |
{{ entry.title ?? 'Untitled' }} | |
</span> | |
</{{ tag }}> | |
{{ _self.chip(entry.type.handle ?? 'asset', color) }} | |
</div> | |
{% endmacro %} | |
{# Styles #} | |
<style> | |
.RelatedEntries { | |
display: flex; | |
flex-direction: column; | |
align-items: stretch; | |
gap: var(--xl); | |
margin-top: var(--m); | |
} | |
.RelatedEntries__section { | |
border: 1px solid var(--border-hairline-medium); | |
border-radius: var(--large-border-radius); | |
} | |
.RelatedEntries__sectionHeader { | |
display: flex; | |
gap: var(--s); | |
align-items: center; | |
padding: var(--s) var(--m); | |
border-top-left-radius: inherit; | |
border-top-right-radius: inherit; | |
background: var(--gray-100); | |
} | |
.RelatedEntries__sectionName { | |
font-weight: 600; | |
} | |
.RelatedEntries__listItem { | |
padding: var(--m); | |
border-top: 1px solid var(--border-hairline); | |
} | |
.RelatedEntries__nestedList { | |
padding-inline: var(--m); | |
} | |
.RelatedEntries__nestedListItem { | |
margin-top: var(--m); | |
padding-top: var(--m); | |
border-top: 1px solid var(--border-hairline); | |
} | |
.RelatedEntries__item { | |
display: flex; | |
align-items: center; | |
gap: var(--s); | |
} | |
.RelatedEntries__status { | |
display: flex; | |
margin: 0; | |
} | |
.RelatedEntries__itemLink { | |
display: inline-flex; | |
flex-wrap: nowrap; | |
gap: var(--s); | |
} | |
.RelatedEntries__itemContent { | |
display: inline-flex; | |
flex-wrap: nowrap; | |
align-items: center; | |
gap: var(--s); | |
} | |
.RelatedEntries__chip { | |
display: inline-flex; | |
align-items: center; | |
height: 1.25rem; | |
padding-inline: var(--s); | |
border-radius: var(--radius-full); | |
font-size: 0.5625rem; | |
font-weight: 500; | |
letter-spacing: 0.05em; | |
text-transform: uppercase; | |
color: var(--chip-color, var(--gray-900)); | |
background: rgb(from var(--chip-bg, var(--gray-500)) r g b/15%); | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment