Skip to content

Commit d7fa3dd

Browse files
committed
"Used by" columns for Fields & Entry Types index pages
Resolves #14984
1 parent 1967431 commit d7fa3dd

File tree

12 files changed

+229
-85
lines changed

12 files changed

+229
-85
lines changed

CHANGELOG-WIP.md

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
- Custom field selectors within field layouts now display a pencil icon if their name, instructions, or handle have been overridden. ([#15597](https://github.com/craftcms/cms/discussions/15597))
1717
- Custom field settings within field layouts now display a chip for the associated global field. ([#15619](https://github.com/craftcms/cms/pull/15619), [#15597](https://github.com/craftcms/cms/discussions/15597))
1818
- Field layouts can now define tips and warnings that should be displayed for fields. ([#15632](https://github.com/craftcms/cms/discussions/15632))
19+
- The Fields index page now has a “Used by” column that shows how many field layouts each field is used by. ([#14984](https://github.com/craftcms/cms/discussions/14984))
20+
- The Entry Types index page now has a “Used by” column that lists the sections and custom fields that each entry type is used by. ([#14984](https://github.com/craftcms/cms/discussions/14984))
1921
- Single sections can now have multiple entry types. ([#15630](https://github.com/craftcms/cms/discussions/15630))
2022
- Increased the text size for handle buttons within admin tables.
2123

@@ -36,8 +38,10 @@
3638
- Added `craft\gql\types\input\criteria\EntryRelation`.
3739
- Added `craft\gql\types\input\criteria\TagRelation`.
3840
- Added `craft\gql\types\input\criteria\UserRelation`.
41+
- Added `craft\helpers\Cp::componentPreviewHtml()`.
3942
- Added `craft\helpers\Inflector`.
4043
- Added `craft\services\Sites::getEditableSitesByGroupId()`.
44+
- `craft\helpers\Cp::chipHtml()` now supports a `hyperlink` option.
4145
- `craft\services\Elements::saveContent()`’ now saves dirty fields’ content even if `$saveContent` is `false`. ([#15393](https://github.com/craftcms/cms/pull/15393))
4246
- Deprecated `craft\db\mysql\Schema::quoteDatabaseName()`.
4347
- Deprecated `craft\db\pgqsl\Schema::quoteDatabaseName()`.

src/controllers/EntryTypesController.php

+4-12
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99

1010
use Craft;
1111
use craft\base\ElementContainerFieldInterface;
12+
use craft\base\FieldInterface;
1213
use craft\elements\Entry;
1314
use craft\enums\Color;
1415
use craft\helpers\Cp;
1516
use craft\helpers\Html;
16-
use craft\helpers\UrlHelper;
1717
use craft\models\EntryType;
1818
use craft\models\Section;
1919
use craft\web\Controller;
@@ -105,16 +105,8 @@ public function actionEdit(?int $entryTypeId = null, ?EntryType $entryType = nul
105105

106106
$labels = [];
107107
$items = array_map(function(Section|ElementContainerFieldInterface $usage) use (&$labels) {
108-
if ($usage instanceof Section) {
109-
$label = Craft::t('site', $usage->name);
110-
$url = $usage->getCpEditUrl();
111-
$icon = 'newspaper';
112-
} else {
113-
$label = Craft::t('site', $usage->name);
114-
$url = UrlHelper::cpUrl("settings/fields/edit/$usage->id");
115-
$icon = $usage::icon();
116-
}
117-
$labels[] = $label;
108+
$icon = $usage instanceof FieldInterface ? $usage::icon() : $usage->getIcon();
109+
$label = $labels[] = $usage->getUiLabel();
118110
$labelHtml = Html::beginTag('span', [
119111
'class' => ['flex', 'flex-nowrap', 'gap-s'],
120112
]) .
@@ -123,7 +115,7 @@ public function actionEdit(?int $entryTypeId = null, ?EntryType $entryType = nul
123115
]) .
124116
Html::tag('span', Html::encode($label)) .
125117
Html::endTag('span');
126-
return Html::a($labelHtml, $url);
118+
return Html::a($labelHtml, $usage->getCpEditUrl());
127119
}, $entryType->findUsages());
128120

129121
// sort by label

src/helpers/Cp.php

+44-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use craft\base\Actionable;
1313
use craft\base\Chippable;
1414
use craft\base\Colorable;
15+
use craft\base\CpEditable;
1516
use craft\base\Element;
1617
use craft\base\ElementInterface;
1718
use craft\base\FieldLayoutElement;
@@ -327,6 +328,7 @@ public static function chipHtml(Chippable $component, array $config = []): strin
327328
'autoReload' => true,
328329
'id' => sprintf('chip-%s', mt_rand()),
329330
'class' => null,
331+
'hyperlink' => false,
330332
'inputName' => null,
331333
'inputValue' => null,
332334
'labelHtml' => null,
@@ -345,7 +347,6 @@ public static function chipHtml(Chippable $component, array $config = []): strin
345347
$config['showStatus'] = $config['showStatus'] && $component instanceof Statusable;
346348
$config['showThumb'] = $config['showThumb'] && ($component instanceof Thumbable || $component instanceof Iconic);
347349

348-
$labelHtml = $component->getUiLabel();
349350
$color = $component instanceof Colorable ? $component->getColor() : null;
350351

351352
$attributes = ArrayHelper::merge([
@@ -407,7 +408,10 @@ public static function chipHtml(Chippable $component, array $config = []): strin
407408
if (isset($config['labelHtml'])) {
408409
$html .= $config['labelHtml'];
409410
} elseif ($config['showLabel']) {
410-
$labelHtml = Html::encode($labelHtml);
411+
$labelHtml = Html::encode($component->getUiLabel());
412+
if ($config['hyperlink'] && $component instanceof CpEditable) {
413+
$labelHtml = Html::a($labelHtml, $component->getCpEditUrl());
414+
}
411415
if ($config['showHandle']) {
412416
/** @var Chippable&Grippable $component */
413417
$handle = $component->getHandle();
@@ -1081,6 +1085,44 @@ public static function elementPreviewHtml(
10811085
return $html;
10821086
}
10831087

1088+
/**
1089+
* Returns component preview HTML, for a list of elements.
1090+
*
1091+
* @param Chippable[] $components The components
1092+
* @param array $chipConfig
1093+
* @return string
1094+
* @since 5.4.0
1095+
*/
1096+
public static function componentPreviewHtml(array $components, array $chipConfig = []): string
1097+
{
1098+
if (empty($components)) {
1099+
return '';
1100+
}
1101+
1102+
$first = array_shift($components);
1103+
$html = Html::beginTag('div', ['class' => 'inline-chips']) .
1104+
static::chipHtml($first, $chipConfig);
1105+
1106+
if (!empty($components)) {
1107+
$otherHtml = '';
1108+
foreach ($components as $other) {
1109+
$otherHtml .= static::chipHtml($other, $chipConfig);
1110+
}
1111+
$html .= Html::tag('span', '+' . Craft::$app->getFormatter()->asInteger(count($components)), [
1112+
'title' => implode(', ', array_map(fn(Chippable $component) => $component->getId(), $components)),
1113+
'class' => 'btn small',
1114+
'role' => 'button',
1115+
'onclick' => sprintf(
1116+
'const r=jQuery(%s);jQuery(this).replaceWith(r);',
1117+
Json::encode($otherHtml),
1118+
),
1119+
]);
1120+
}
1121+
1122+
$html .= Html::endTag('div'); // .inline-chips
1123+
return $html;
1124+
}
1125+
10841126
/**
10851127
* Returns the HTML for an element index.
10861128
*

src/models/Section.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Craft;
1111
use craft\base\Chippable;
1212
use craft\base\CpEditable;
13+
use craft\base\Iconic;
1314
use craft\base\Model;
1415
use craft\db\Query;
1516
use craft\db\Table;
@@ -33,7 +34,7 @@
3334
* @property EntryType[] $entryTypes Entry types
3435
* @property bool $hasMultiSiteEntries Whether entries in this section support multiple sites
3536
*/
36-
class Section extends Model implements Chippable, CpEditable
37+
class Section extends Model implements Chippable, CpEditable, Iconic
3738
{
3839
public const TYPE_SINGLE = 'single';
3940
public const TYPE_CHANNEL = 'channel';
@@ -392,6 +393,14 @@ public function getCpEditUrl(): ?string
392393
return $this->id ? UrlHelper::cpUrl("settings/sections/$this->id") : null;
393394
}
394395

396+
/**
397+
* @inheritdoc
398+
*/
399+
public function getIcon(): ?string
400+
{
401+
return 'newspaper';
402+
}
403+
395404
/**
396405
* Returns the section’s config.
397406
*

src/services/Entries.php

+37
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
use Craft;
1111
use craft\base\Element;
12+
use craft\base\ElementContainerFieldInterface;
1213
use craft\base\Field;
1314
use craft\base\MemoizableArray;
1415
use craft\db\Query;
@@ -1700,6 +1701,8 @@ public function getTableData(
17001701
));
17011702

17021703
$tableData = [];
1704+
$usages = $this->allEntryTypeUsages();
1705+
17031706
foreach ($entryTypes as $entryType) {
17041707
$label = $entryType->getUiLabel();
17051708
$tableData[] = [
@@ -1711,6 +1714,9 @@ public function getTableData(
17111714
]),
17121715
]),
17131716
'handle' => $entryType->handle,
1717+
'usages' => Cp::componentPreviewHtml($usages[$entryType->id] ?? [], [
1718+
'hyperlink' => true,
1719+
]),
17141720
];
17151721
}
17161722

@@ -1719,6 +1725,37 @@ public function getTableData(
17191725
return [$pagination, $tableData];
17201726
}
17211727

1728+
/**
1729+
* @return array<int,array<Section|ElementContainerFieldInterface>>
1730+
*/
1731+
private function allEntryTypeUsages(): array
1732+
{
1733+
$usages = [];
1734+
1735+
// Sections
1736+
foreach (Craft::$app->getEntries()->getAllSections() as $section) {
1737+
foreach ($section->getEntryTypes() as $entryType) {
1738+
$usages[$entryType->id][] = $section;
1739+
}
1740+
}
1741+
1742+
// Fields
1743+
$fieldsService = Craft::$app->getFields();
1744+
foreach ($fieldsService->getNestedEntryFieldTypes() as $type) {
1745+
/** @var ElementContainerFieldInterface[] $fields */
1746+
$fields = $fieldsService->getFieldsByType($type);
1747+
foreach ($fields as $field) {
1748+
foreach ($field->getFieldLayoutProviders() as $provider) {
1749+
if ($provider instanceof EntryType) {
1750+
$usages[$provider->id][] = $field;
1751+
}
1752+
}
1753+
}
1754+
}
1755+
1756+
return $usages;
1757+
}
1758+
17221759
/**
17231760
* Returns the sql expression to be used in the 'where' param for the query.
17241761
*

src/services/Fields.php

+27
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,26 @@ public function findFieldUsages(FieldInterface $field): array
844844
return $layouts;
845845
}
846846

847+
/**
848+
* @return array<int,FieldLayout[]>
849+
*/
850+
private function allFieldUsages(): array
851+
{
852+
$usages = [];
853+
854+
foreach ($this->getAllLayouts() as $layout) {
855+
$uniqueFieldIds = [];
856+
foreach ($layout->getCustomFields() as $field) {
857+
$uniqueFieldIds[$field->id] = true;
858+
}
859+
foreach (array_keys($uniqueFieldIds) as $fieldId) {
860+
$usages[$fieldId][] = $layout;
861+
}
862+
}
863+
864+
return $usages;
865+
}
866+
847867
// Layouts
848868
// -------------------------------------------------------------------------
849869

@@ -1459,6 +1479,8 @@ public function getTableData(
14591479
$result = $query->all();
14601480

14611481
$tableData = [];
1482+
$usages = $this->allFieldUsages();
1483+
14621484
foreach ($result as $item) {
14631485
$field = $this->createField($item);
14641486

@@ -1474,6 +1496,11 @@ public function getTableData(
14741496
'label' => $field instanceof MissingField ? $field->expectedType : $field->displayName(),
14751497
'icon' => Cp::iconSvg($field::icon()),
14761498
],
1499+
'usages' => isset($usages[$field->id])
1500+
? mb_ucfirst(Craft::t('app', '{count, number} {count, plural, =1{layout} other{layouts}}', [
1501+
'count' => count($usages[$field->id]),
1502+
]))
1503+
: null,
14771504
];
14781505
}
14791506

src/templates/settings/entry-types/index.twig

+22-14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
'Handle',
1010
'No entry types exist yet.',
1111
'No results.',
12+
'No usages',
1213
'Used by',
1314
]) %}
1415

@@ -30,18 +31,25 @@
3031
{% endblock %}
3132

3233
{% js %}
33-
var columns = [
34-
{ name: 'chip', title: Craft.t('app', 'Entry Type'), sortField: true },
35-
{ name: '__slot:handle', title: Craft.t('app', 'Handle'), sortField: true },
36-
];
37-
38-
new Craft.VueAdminTable({
39-
columns: columns,
40-
container: '#entrytypes-vue-admin-table',
41-
deleteAction: 'entry-types/delete',
42-
deleteConfirmationMessage: Craft.t('app', 'Are you sure you want to delete “{name}” and all entries of that type?'),
43-
emptyMessage: Craft.t('app', 'No entry types exist yet.'),
44-
tableDataEndpoint: 'entry-types/table-data',
45-
search: true,
46-
});
34+
(() => {
35+
const columns = [
36+
{ name: 'chip', title: Craft.t('app', 'Entry Type'), sortField: true },
37+
{ name: '__slot:handle', title: Craft.t('app', 'Handle'), sortField: true },
38+
{
39+
name: 'usages',
40+
title: Craft.t('app', 'Used by'),
41+
callback: (value) => value || `<i class="light">${Craft.t('app', 'No usages')}</i>`,
42+
},
43+
];
44+
45+
new Craft.VueAdminTable({
46+
columns,
47+
container: '#entrytypes-vue-admin-table',
48+
deleteAction: 'entry-types/delete',
49+
deleteConfirmationMessage: Craft.t('app', 'Are you sure you want to delete “{name}” and all entries of that type?'),
50+
emptyMessage: Craft.t('app', 'No entry types exist yet.'),
51+
tableDataEndpoint: 'entry-types/table-data',
52+
search: true,
53+
});
54+
})();
4755
{% endjs %}

0 commit comments

Comments
 (0)