diff --git a/CHANGELOG.md b/CHANGELOG.md index d498acdf249..dd3ec86e0f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Release Notes for Craft CMS 4 +## 4.2.2 - 2022-08-23 + +### Added +- Added the `utils/fix-field-layout-uids` command. ([#11746](https://github.com/craftcms/cms/issues/11746)) + +### Changed +- Improved the styling of Categories fields. +- The first field group is now automatically selected by default when creating a new custom field. +- Improved console output for the `gc` command. +- The `gc` command now runs garbage collection for data caches. +- Exception JSON responses now include `name` and `code` keys. ([#11799](https://github.com/craftcms/cms/discussions/11799)) +- `elements/*` actions no longer include custom field values in the failure response data, improving performance. ([#11807](https://github.com/craftcms/cms/discussions/11807)) + +### Fixed +- Fixed a bug where keyboard focus wasn’t being maintained when changing the element type within a “Related To” condition rule. +- Fixed a bug where keyboard focus wasn’t being maintained when changing the country within an address’s “Administrative Area” condition rule. +- Fixed a bug where Date fields’ Timezone menus could be clipped. ([#11780](https://github.com/craftcms/cms/pull/11780)) +- Fixed an error that could occur when saving an unpublished draft, if any custom validation errors were added to it after its draft status had been removed. ([#11407](https://github.com/craftcms/cms/issues/11407)) +- Fixed a bug where custom validation errors would be shown twice for unpublished drafts, if they were added to it after its draft status had been removed. ([#11407](https://github.com/craftcms/cms/issues/11407)) +- Fixed PHP warnings that would occur when passing `0` into `craft\helpers\DateTimeHelper::humanDuration()`. ([#11787](https://github.com/craftcms/cms/issues/11787)) +- Fixed a bug where selected assets weren’t getting automatically replaced when an image was edited and “Save as a new asset” was chosen. ([#11805](https://github.com/craftcms/cms/issues/11805)) +- Fixed a JavaScript error that occurred when editing a user via a slideout, if the user had any addresses. ([#11810](https://github.com/craftcms/cms/issues/11810)) +- Fixed a beg where some invalid slideout submissions weren’t being handled properly. ([#11812](https://github.com/craftcms/cms/issues/11812)) +- Fixed a bug where `craft\helpers\DateTimeHelper::toDateInterval()` was returning negative interval durations when integers were passed in. ([#11814](https://github.com/craftcms/cms/pull/11814)) +- Fixed a bug where `iframeResizer.contentWindow.js` was getting loaded for all preview requests, not just Live Preview, and even when `useIframeResizer` was disabled. ([#11778](https://github.com/craftcms/cms/issues/11778)) +- Fixed a bug where deleted relations and Matrix blocks could persist if the edit form was submitted before they had been fully animated away. ([#11789](https://github.com/craftcms/cms/issues/11789)) +- Fixed a PHP error that could occur if `craft\services\Assets::getUserTemporaryUploadFolder()` was called when there was no logged-in user account. ([#11751](https://github.com/craftcms/cms/issues/11751)) + ## 4.2.1.1 - 2022-08-10 ### Fixed @@ -38,7 +66,6 @@ - Fixed a bug where it wasn’t possible to preview or edit image assets if their filesystem and transform filesystem didn’t have public URLs. ([#11686](https://github.com/craftcms/cms/issues/11686), [#11687](https://github.com/craftcms/cms/issues/11687)) - Fixed a bug where not all project config changes would be applied if a site was deleted. ([#9567](https://github.com/craftcms/cms/issues/9567)) - Fixed a bug where `$` characters in database connection passwords weren’t being escaped property when backing up/restoring the database. ([#11750](https://github.com/craftcms/cms/issues/11750)) -- Fixed a PHP error that could occur if `craft\services\Assets::getUserTemporaryUploadFolder()` was called when there was no logged-in user account. ([#11751](https://github.com/craftcms/cms/issues/11751)) ### Security - Fixed XSS vulnerabilities. @@ -1282,6 +1309,7 @@ - Removed `craft\models\EntryDraft`. - Removed `craft\models\EntryVersion`. - Removed `craft\models\FieldLayout::setFields()`. +- Removed `craft\models\FieldLayoutTab::getFields()`. - Removed `craft\models\Site::$originalBaseUrl`. - Removed `craft\models\Site::$originalName`. - Removed `craft\models\Site::overrideBaseUrl()`. diff --git a/src/base/Element.php b/src/base/Element.php index 21b26b0c7a5..ad76cf6371d 100644 --- a/src/base/Element.php +++ b/src/base/Element.php @@ -468,7 +468,7 @@ abstract class Element extends Component implements ElementInterface * ); * ``` * - * @see canDelete() + * @see canDeleteForSite() * @since 4.0.0 */ public const EVENT_AUTHORIZE_DELETE_FOR_SITE = 'authorizeDeleteForSite'; diff --git a/src/config/app.php b/src/config/app.php index acca2dfc587..3f6aebc5d09 100644 --- a/src/config/app.php +++ b/src/config/app.php @@ -3,7 +3,7 @@ return [ 'id' => 'CraftCMS', 'name' => 'Craft CMS', - 'version' => '4.2.1.1', + 'version' => '4.2.2', 'schemaVersion' => '4.0.0.9', 'minVersionRequired' => '3.7.11', 'basePath' => dirname(__DIR__), // Defines the @app alias diff --git a/src/console/controllers/GcController.php b/src/console/controllers/GcController.php index 5dea4902f62..d01bc653b3d 100644 --- a/src/console/controllers/GcController.php +++ b/src/console/controllers/GcController.php @@ -52,9 +52,9 @@ public function actionRun(): int $gc = Craft::$app->getGc(); $deleteAllTrashed = $gc->deleteAllTrashed; $gc->deleteAllTrashed = $this->deleteAllTrashed || ($this->interactive && $this->confirm('Delete all trashed items?')); - $this->stdout('Running garbage collection ... '); + $this->stdout("Running garbage collection ...\n"); $gc->run(true); - $this->stdout('done' . PHP_EOL, Console::FG_GREEN); + $this->stdout('Finished running garbage collection.' . PHP_EOL, Console::FG_GREEN); $gc->deleteAllTrashed = $deleteAllTrashed; return ExitCode::OK; } diff --git a/src/console/controllers/utils/FixFieldLayoutUidsController.php b/src/console/controllers/utils/FixFieldLayoutUidsController.php new file mode 100644 index 00000000000..9265dd055f5 --- /dev/null +++ b/src/console/controllers/utils/FixFieldLayoutUidsController.php @@ -0,0 +1,101 @@ + + * @since 4.2.3 + */ +class FixFieldLayoutUidsController extends Controller +{ + /** + * Fixes any duplicate UUIDs found within field layout components in the project config. + * + * @return int + */ + public function actionIndex(): int + { + $this->stdout("Looking for duplicate UUIDs ...\n"); + $count = 0; + $this->_fixUids(Craft::$app->getProjectConfig()->get(), $count); + + if ($count) { + $summary = sprintf('Fixed %s duplicate %s.', $count, $count === 1 ? 'UUID' : 'UUIDs'); + } else { + $summary = 'No duplicate UUIDs were found.'; + } + + $this->stdout('Done. ', Console::FG_GREEN); + $this->stdout("$summary\n"); + + + return ExitCode::OK; + } + + private function _fixUids(array $config, int &$count, string $path = '', array &$uids = []): void + { + if (isset($config['fieldLayouts']) && is_array($config['fieldLayouts'])) { + $modified = false; + + foreach ($config['fieldLayouts'] as $fieldLayoutUid => &$fieldLayoutConfig) { + if (isset($fieldLayoutConfig['tabs']) && is_array($fieldLayoutConfig['tabs'])) { + foreach ($fieldLayoutConfig['tabs'] as $tabIndex => &$tabConfig) { + $tabPath = ($path ? "$path." : '') . "fieldLayouts.$fieldLayoutUid.tabs.$tabIndex"; + $this->_checkUid($tabConfig, $count, $uids, $modified, $tabPath); + + if (isset($tabConfig['elements']) && is_array($tabConfig['elements'])) { + foreach ($tabConfig['elements'] as $elementIndex => &$elementConfig) { + $elementPath = "$tabPath.elements.$elementIndex"; + $this->_checkUid($elementConfig, $count, $uids, $modified, $elementPath); + } + } + } + } + } + + if ($modified) { + Craft::$app->getProjectConfig()->set($path, $config); + } + + return; + } + + foreach ($config as $key => $value) { + if (is_array($value)) { + $this->_fixUids($value, $count, ($path ? "$path." : '') . $key, $uids); + } + } + } + + private function _checkUid(array &$config, int &$count, array &$uids, bool &$modified, string $path): void + { + if (isset($config['uid'])) { + if (isset($uids[$config['uid']])) { + $config['uid'] = StringHelper::UUID(); + $count++; + $modified = true; + + $this->stdout(' > Duplicate found at '); + $this->stdout($path, Console::FG_CYAN); + $this->stdout(".\n Changing to "); + $this->stdout($config['uid'], Console::FG_CYAN); + $this->stdout(".\n"); + } else { + $uids[$config['uid']] = true; + } + } + } +} diff --git a/src/controllers/ElementsController.php b/src/controllers/ElementsController.php index 3384a04f39b..13236116fd4 100644 --- a/src/controllers/ElementsController.php +++ b/src/controllers/ElementsController.php @@ -1735,7 +1735,12 @@ private function _asSuccess(string $message, ElementInterface $element, array $d private function _asFailure(ElementInterface $element, string $message): ?Response { - /** @var Element $element */ - return $this->asModelFailure($element, $message, 'element'); + $data = [ + 'modelName' => 'element', + 'element' => $element->toArray($element->attributes()), + 'errors' => $element->getErrors(), + ]; + + return $this->asFailure($message, $data, ['element' => $element]); } } diff --git a/src/controllers/FieldsController.php b/src/controllers/FieldsController.php index 784bd379d5e..d7cae016027 100644 --- a/src/controllers/FieldsController.php +++ b/src/controllers/FieldsController.php @@ -198,17 +198,18 @@ public function actionEditField(?int $fieldId = null, ?FieldInterface $field = n if ($groupId) { $fieldGroup = $fieldsService->getGroupById($groupId); - if ($fieldGroup === null) { throw new NotFoundHttpException('Field group not found'); } + } elseif (!$field->id && !$field->hasErrors()) { + $fieldGroup = reset($allGroups); } else { $fieldGroup = null; } $groupOptions = []; - if (!$groupId) { + if (!$fieldGroup) { $groupOptions[] = ['value' => '', 'label' => '']; } diff --git a/src/elements/actions/Delete.php b/src/elements/actions/Delete.php index 5c944cb4d90..ab354f52558 100644 --- a/src/elements/actions/Delete.php +++ b/src/elements/actions/Delete.php @@ -18,7 +18,7 @@ /** * Delete represents a Delete element action. * - * Element types that make this action available should implement [[ElementInterface::getIsDeletable()]] to explicitly state whether they can be + * Element types that make this action available should implement [[ElementInterface::canDelete()]] to explicitly state whether they can be * deleted by the current user. * * @author Pixel & Tonic, Inc. @@ -70,7 +70,7 @@ public function setHardDelete(): void */ public function getTriggerHtml(): ?string { - // Only enable for deletable elements, per getIsDeletable() + // Only enable for deletable elements, per canDelete() Craft::$app->getView()->registerJsWithVars(fn($type) => << { new Craft.ElementActionTrigger({ diff --git a/src/elements/actions/DeleteAssets.php b/src/elements/actions/DeleteAssets.php index 134c7536886..544a8d6782e 100644 --- a/src/elements/actions/DeleteAssets.php +++ b/src/elements/actions/DeleteAssets.php @@ -51,7 +51,7 @@ public function getConfirmationMessage(): ?string */ public function getTriggerHtml(): ?string { - // Only enable for deletable elements, per getIsDeletable() + // Only enable for deletable elements, per canDelete() Craft::$app->getView()->registerJsWithVars(fn($type) => << { new Craft.ElementActionTrigger({ diff --git a/src/elements/actions/DeleteForSite.php b/src/elements/actions/DeleteForSite.php index d095b61e61c..d14afe90858 100644 --- a/src/elements/actions/DeleteForSite.php +++ b/src/elements/actions/DeleteForSite.php @@ -18,7 +18,7 @@ /** * Delete represents a “Delete for site” element action. * - * Element types that make this action available should implement [[ElementInterface::getIsDeletable()]] to explicitly state whether they can be + * Element types that make this action available should implement [[ElementInterface::canDelete()]] to explicitly state whether they can be * deleted by the current user. * * @author Pixel & Tonic, Inc. @@ -41,7 +41,7 @@ class DeleteForSite extends ElementAction */ public function getTriggerHtml(): ?string { - // Only enable for deletable elements, per getIsDeletable() + // Only enable for deletable elements, per canDelete() Craft::$app->getView()->registerJsWithVars(fn($type) => << { new Craft.ElementActionTrigger({ diff --git a/src/elements/conditions/RelatedToConditionRule.php b/src/elements/conditions/RelatedToConditionRule.php index 709ecb91f6f..b8d282bc5a5 100644 --- a/src/elements/conditions/RelatedToConditionRule.php +++ b/src/elements/conditions/RelatedToConditionRule.php @@ -69,6 +69,7 @@ protected function inputHtml(): string { return Html::tag('div', Cp::selectHtml([ + 'id' => 'element-type', 'name' => 'elementType', 'options' => $this->_elementTypeOptions(), 'value' => $this->elementType, diff --git a/src/elements/conditions/addresses/AdministrativeAreaConditionRule.php b/src/elements/conditions/addresses/AdministrativeAreaConditionRule.php index f0ce3b0798a..5b02d3329e5 100644 --- a/src/elements/conditions/addresses/AdministrativeAreaConditionRule.php +++ b/src/elements/conditions/addresses/AdministrativeAreaConditionRule.php @@ -99,6 +99,7 @@ public function matchElement(ElementInterface $element): bool protected function inputHtml(): string { $countrySelect = Cp::selectFieldHtml([ + 'id' => 'country-code', 'name' => 'countryCode', 'options' => Craft::$app->getAddresses()->getCountryRepository()->getList(), 'value' => $this->countryCode, diff --git a/src/helpers/DateTimeHelper.php b/src/helpers/DateTimeHelper.php index e63eebcf22c..423faabca16 100644 --- a/src/helpers/DateTimeHelper.php +++ b/src/helpers/DateTimeHelper.php @@ -489,7 +489,7 @@ public static function toDateInterval(mixed $value): DateInterval|false // Use DateTime::diff() so the years/months/days/hours/minutes values are all populated correctly $now = static::now(); $then = (clone $now)->modify("+$value seconds"); - return $then->diff($now); + return $now->diff($then); } if (is_string($value)) { @@ -555,7 +555,7 @@ public static function isValidIntervalString(string $intervalString): bool */ public static function humanDuration(mixed $dateInterval, ?bool $showSeconds = null): string { - $dateInterval = static::toDateInterval($dateInterval); + $dateInterval = static::toDateInterval($dateInterval) ?: new DateInterval('PT0S'); $secondsOnly = !$dateInterval->y && !$dateInterval->m && !$dateInterval->d && !$dateInterval->h && !$dateInterval->i; if ($showSeconds === null) { @@ -600,7 +600,7 @@ public static function humanDuration(mixed $dateInterval, ?bool $showSeconds = n $timeComponents[] = Craft::t('app', '{num, number} {num, plural, =1{minute} other{minutes}}', ['num' => $minutes]); } - if ($showSeconds && $dateInterval->s) { + if ($showSeconds && ($dateInterval->s || empty($timeComponents))) { $timeComponents[] = Craft::t('app', '{num, number} {num, plural, =1{second} other{seconds}}', ['num' => $dateInterval->s]); } diff --git a/src/services/Assets.php b/src/services/Assets.php index 5c28023fa2f..160c02a8d65 100644 --- a/src/services/Assets.php +++ b/src/services/Assets.php @@ -920,8 +920,10 @@ public function getUserTemporaryUploadFolder(?User $user = null): VolumeFolder $user = Craft::$app->getUser()->getIdentity(); } - if ($user && isset($this->_userTempFolders[$user->id])) { - return $this->_userTempFolders[$user->id]; + $cacheKey = $user->id ?? '__GUEST__'; + + if (isset($this->_userTempFolders[$cacheKey])) { + return $this->_userTempFolders[$cacheKey]; } if ($user) { @@ -945,7 +947,7 @@ public function getUserTemporaryUploadFolder(?User $user = null): VolumeFolder if ($tempVolume) { $path = ($tempSubpath ? "$tempSubpath/" : '') . $folderName; - return $this->_userTempFolders[$user->id] = $this->ensureFolderByFullPathAndVolume($path, $tempVolume); + return $this->_userTempFolders[$cacheKey] = $this->ensureFolderByFullPathAndVolume($path, $tempVolume); } $volumeTopFolder = $this->findFolder([ @@ -980,7 +982,7 @@ public function getUserTemporaryUploadFolder(?User $user = null): VolumeFolder throw new VolumeException('Unable to create directory for temporary volume.'); } - return $this->_userTempFolders[$user->id] = $folder; + return $this->_userTempFolders[$cacheKey] = $folder; } /** diff --git a/src/services/Drafts.php b/src/services/Drafts.php index 60a3edb734e..3adb03ad800 100644 --- a/src/services/Drafts.php +++ b/src/services/Drafts.php @@ -322,7 +322,7 @@ public function applyDraft(ElementInterface $draft): ElementInterface } catch (Throwable $e) { $transaction->rollBack(); - if ($e instanceof InvalidElementException) { + if ($e instanceof InvalidElementException && $draft !== $e->element) { // Add the errors from the duplicated element back onto the draft $draft->addErrors($e->element->getErrors()); } @@ -371,11 +371,10 @@ public function removeDraftData(ElementInterface $draft): void $draft->validate(); } - if ($draft->hasErrors()) { - throw new InvalidElementException($draft, "Draft $draft->id could not be applied because it doesn't validate."); - } - try { + if ($draft->hasErrors()) { + throw new InvalidElementException($draft, "Draft $draft->id could not be applied because it doesn't validate."); + } Craft::$app->getElements()->saveElement($draft, false); Db::delete(Table::DRAFTS, [ 'id' => $draftId, diff --git a/src/services/Gc.php b/src/services/Gc.php index 1d923145144..0eff954a7fb 100644 --- a/src/services/Gc.php +++ b/src/services/Gc.php @@ -23,11 +23,13 @@ use craft\elements\User; use craft\errors\FsException; use craft\fs\Temp; +use craft\helpers\Console; use craft\helpers\DateTimeHelper; use craft\helpers\Db; use craft\records\Volume; use craft\records\VolumeFolder; use DateTime; +use ReflectionMethod; use yii\base\Component; use yii\base\Exception; use yii\base\InvalidConfigException; @@ -96,8 +98,8 @@ public function run(bool $force = false): void return; } - Craft::$app->getDrafts()->purgeUnsavedDrafts(); - Craft::$app->getUsers()->purgeExpiredPendingUsers(); + $this->_purgeUnsavedDrafts(); + $this->_purgePendingUsers(); $this->_deleteStaleSessions(); $this->_deleteStaleAnnouncements(); @@ -128,7 +130,7 @@ public function run(bool $force = false): void $this->deletePartialElements(User::class, Table::CONTENT, 'elementId'); $this->_deleteOrphanedDraftsAndRevisions(); - Craft::$app->getSearch()->deleteOrphanedIndexes(); + $this->_deleteOrphanedSearchIndexes(); // Fire a 'run' event if ($this->hasEventHandlers(self::EVENT_RUN)) { @@ -142,8 +144,8 @@ public function run(bool $force = false): void ]); $this->hardDeleteVolumes(); - $this->removeEmptyTempFolders(); + $this->_gcCache(); } /** @@ -155,6 +157,7 @@ public function hardDeleteVolumes(): void return; } + Console::stdout(" > deleting trashed volumes and their folders ... "); $condition = $this->_hardDeleteCondition(); $volumes = (new Query())->select(['id'])->from([Table::VOLUMES])->where($condition)->all(); @@ -174,6 +177,7 @@ public function hardDeleteVolumes(): void } Volume::deleteAll(['id' => $volumeIds]); + Console::stdout("done\n", Console::FG_GREEN); } /** @@ -200,6 +204,8 @@ public function hardDeleteElements(): void } } + Console::stdout(' > deleting trashed elements ... '); + if ($normalElementTypes) { Db::delete(Table::ELEMENTS, [ 'and', @@ -240,6 +246,8 @@ public function hardDeleteElements(): void $this->db->createCommand($sql, $params)->execute(); } + + Console::stdout("done\n", Console::FG_GREEN); } /** @@ -260,7 +268,9 @@ public function hardDelete(array|string $tables): void } foreach ($tables as $table) { + Console::stdout(" > deleting trashed rows in the `$table` table ... "); Db::delete($table, $condition); + Console::stdout("done\n", Console::FG_GREEN); } } @@ -275,6 +285,9 @@ public function hardDelete(array|string $tables): void */ public function deletePartialElements(string $elementType, string $table, string $fk): void { + /** @var string|ElementInterface $elementType */ + Console::stdout(sprintf(' > deleting partial %s data in the `%s` table ... ', $elementType::lowerDisplayName(), $table)); + $elementsTable = Table::ELEMENTS; if ($this->db->getIsMysql()) { @@ -298,6 +311,29 @@ public function deletePartialElements(string $elementType, string $table, string } $this->db->createCommand($sql, ['type' => $elementType])->execute(); + Console::stdout("done\n", Console::FG_GREEN); + } + + private function _purgeUnsavedDrafts() + { + if ($this->_generalConfig->purgeUnsavedDraftsDuration === 0) { + return; + } + + Console::stdout(' > purging unsaved drafts that have gone stale ... '); + Craft::$app->getDrafts()->purgeUnsavedDrafts(); + Console::stdout("done\n", Console::FG_GREEN); + } + + private function _purgePendingUsers() + { + if ($this->_generalConfig->purgePendingUsersDuration === 0) { + return; + } + + Console::stdout(' > purging pending users with stale activation codes ... '); + Craft::$app->getUsers()->purgeExpiredPendingUsers(); + Console::stdout("done\n", Console::FG_GREEN); } /** @@ -351,29 +387,32 @@ private function _deleteStaleSessions(): void return; } + Console::stdout(' > deleting stale user sessions ... '); $interval = DateTimeHelper::secondsToInterval($this->_generalConfig->purgeStaleUserSessionDuration); $expire = DateTimeHelper::currentUTCDateTime(); $pastTime = $expire->sub($interval); - Db::delete(Table::SESSIONS, ['<', 'dateUpdated', Db::prepareDateForDb($pastTime)]); + Console::stdout("done\n", Console::FG_GREEN); } /** * Deletes any feature announcement rows that have gone stale. - * */ private function _deleteStaleAnnouncements(): void { + Console::stdout(' > deleting stale feature announcements ... '); Db::delete(Table::ANNOUNCEMENTS, ['<', 'dateRead', Db::prepareDateForDb(new DateTime('7 days ago'))]); + Console::stdout("done\n", Console::FG_GREEN); } /** * Deletes any orphaned rows in the `drafts` and `revisions` tables. - * */ private function _deleteOrphanedDraftsAndRevisions(): void { + Console::stdout(' > deleting orphaned drafts and revisions ... '); + $elementsTable = Table::ELEMENTS; foreach (['draftId' => Table::DRAFTS, 'revisionId' => Table::REVISIONS] as $fk => $table) { @@ -396,6 +435,51 @@ private function _deleteOrphanedDraftsAndRevisions(): void $this->db->createCommand($sql)->execute(); } + + Console::stdout("done\n", Console::FG_GREEN); + } + + private function _deleteOrphanedSearchIndexes(): void + { + Console::stdout(' > deleting orphaned search indexes ... '); + Craft::$app->getSearch()->deleteOrphanedIndexes(); + Console::stdout("done\n", Console::FG_GREEN); + } + + private function _gcCache(): void + { + $cache = Craft::$app->getCache(); + + // gc() isn't always implemented, or defined by an interface, + // so we have to be super defensive here :-/ + + if (!method_exists($cache, 'gc')) { + return; + } + + $method = new ReflectionMethod($cache, 'gc'); + + if (!$method->isPublic()) { + return; + } + + $requiredArgs = $method->getNumberOfRequiredParameters(); + $firstArg = $method->getParameters()[0] ?? null; + $hasForceArg = $firstArg && $firstArg->getName() === 'force'; + + if ($requiredArgs > 1 || ($requiredArgs === 1 && !$hasForceArg)) { + return; + } + + Console::stdout(' > garbage-collecting data caches ... '); + + if ($hasForceArg) { + $cache->gc(true); + } else { + $cache->gc(); + } + + Console::stdout("done\n", Console::FG_GREEN); } /** diff --git a/src/templates/_components/fieldtypes/Matrix/settings.twig b/src/templates/_components/fieldtypes/Matrix/settings.twig index 606f66d1cc6..2a9610eda48 100644 --- a/src/templates/_components/fieldtypes/Matrix/settings.twig +++ b/src/templates/_components/fieldtypes/Matrix/settings.twig @@ -11,13 +11,19 @@ {% for blockTypeId, blockType in blockTypes %}
-

+ {% tag 'h4' with { + title: blockType.name, + } %} {{ blockType.name }} {% if blockType.hasFieldErrors %} {% endif %} -

-
{{ blockType.handle }}
+ {% endtag %} + {{ tag('div', { + class: ['smalltext', 'light', 'code'], + text: blockType.handle, + title: blockType.handle, + }) }}
@@ -40,17 +46,25 @@