From 746eaff0300e7cee8a837b10a34efe4f933abf31 Mon Sep 17 00:00:00 2001 From: Brandon Kelly Date: Fri, 10 Jul 2020 17:56:01 -0700 Subject: [PATCH] New field layout designer Resolves #806 Resolves #841 Resolves #913 Resolves #1103 Resolves #1138 Resolves #2644 Resolves #3953 Resolves #4647 Resolves #4738 --- CHANGELOG-v3.5.md | 44 +- CHANGELOG-v3.md | 44 + src/base/ApplicationTrait.php | 31 + src/base/Element.php | 44 +- src/base/FieldLayoutElement.php | 31 + src/base/FieldLayoutElementInterface.php | 44 + src/config/app.php | 2 +- src/controllers/CategoriesController.php | 38 +- src/controllers/EntriesController.php | 48 +- src/controllers/FieldsController.php | 23 + src/controllers/GlobalsController.php | 41 +- src/controllers/SectionsController.php | 2 - src/controllers/SystemSettingsController.php | 13 - src/controllers/TagsController.php | 13 - src/controllers/UsersController.php | 35 +- src/controllers/VolumesController.php | 12 - src/elements/Entry.php | 25 - src/events/DefineFieldLayoutFieldsEvent.php | 25 + src/fieldlayoutelements/BaseField.php | 410 ++++++ src/fieldlayoutelements/CustomField.php | 217 +++ src/fieldlayoutelements/EntryTitleField.php | 51 + src/fieldlayoutelements/Heading.php | 67 + src/fieldlayoutelements/HorizontalRule.php | 43 + src/fieldlayoutelements/StandardField.php | 154 ++ src/fieldlayoutelements/StandardTextField.php | 146 ++ src/fieldlayoutelements/Tip.php | 103 ++ src/fieldlayoutelements/TitleField.php | 63 + src/migrations/Install.php | 3 +- .../m200619_212955_title_instructions.php | 30 - ...200619_215137_title_translation_method.php | 2 +- .../m200620_230205_field_layout_changes.php | 102 ++ src/models/EntryType.php | 16 +- src/models/FieldLayout.php | 343 +++-- src/models/FieldLayoutForm.php | 82 ++ src/models/FieldLayoutFormTab.php | 39 + src/models/FieldLayoutTab.php | 138 +- src/records/EntryType.php | 2 - src/records/FieldLayoutTab.php | 1 + src/services/Fields.php | 127 +- src/services/ProjectConfig.php | 1 - src/services/Sections.php | 9 - .../_components/widgets/QuickPost/body.html | 8 +- .../_includes/fieldlayoutdesigner.html | 137 +- src/templates/_includes/forms.html | 12 + src/templates/_includes/forms/field.html | 2 +- .../_includes/forms/fieldLayoutDesigner.html | 169 +++ src/templates/_layouts/element.html | 22 +- src/templates/assets/_edit.html | 16 - src/templates/categories/_edit.html | 26 +- src/templates/entries/_edit.html | 6 +- src/templates/entries/_fields.html | 20 - src/templates/entries/_titlefield.html | 21 - src/templates/globals/_edit.html | 13 +- .../settings/assets/volumes/_edit.html | 168 ++- src/templates/settings/categories/_edit.html | 191 ++- src/templates/settings/fields/_edit.html | 2 +- src/templates/settings/globals/_edit.html | 64 +- .../settings/sections/_entrytypes/edit.html | 36 +- src/templates/settings/tags/_edit.html | 65 +- src/templates/settings/users/fields.html | 6 +- src/templates/users/_accountfields.html | 50 +- src/templates/users/_edit.html | 16 +- src/web/assets/cp/dist/css/craft.css | 211 ++- src/web/assets/cp/dist/css/craft.css.map | 6 +- src/web/assets/cp/dist/fonts/Craft.svg | 2 + src/web/assets/cp/dist/fonts/Craft.ttf | Bin 23340 -> 23756 bytes src/web/assets/cp/dist/fonts/Craft.woff | Bin 23420 -> 23836 bytes .../cp/dist/images/fieldlayoutform-bg.png | Bin 153 -> 0 bytes .../cp/dist/images/fieldlayoutform-bg_2x.png | Bin 199 -> 0 bytes src/web/assets/cp/dist/js/Craft.min.js | 2 +- src/web/assets/cp/dist/js/Craft.min.js.map | 2 +- .../assets/cp/src/craft-font/selection.json | 2 +- src/web/assets/cp/src/css/_cp.scss | 47 + src/web/assets/cp/src/css/_fld.scss | 386 +++-- src/web/assets/cp/src/css/_main.scss | 77 +- .../assets/cp/src/js/FieldLayoutDesigner.js | 1310 ++++++++++------- src/web/assets/cp/src/js/FieldToggle.js | 58 +- src/web/assets/cp/src/js/LightSwitch.js | 2 +- src/web/assets/cp/src/js/Listbox.js | 111 ++ src/web/assets/cp/src/js/SlidePicker.js | 154 ++ src/web/assets/cp/src/js/UI.js | 6 +- .../assets/dashboard/dist/Dashboard.min.js | 2 +- .../dashboard/dist/Dashboard.min.js.map | 2 +- src/web/assets/dashboard/dist/dashboard.css | 52 +- .../assets/dashboard/dist/dashboard.css.map | 2 +- src/web/assets/dashboard/src/Dashboard.js | 157 +- src/web/assets/dashboard/src/dashboard.scss | 85 +- .../editentry/dist/EntryTypeSwitcher.min.js | 2 +- .../dist/EntryTypeSwitcher.min.js.map | 2 +- .../assets/editentry/src/EntryTypeSwitcher.js | 12 +- src/web/twig/Extension.php | 1 + 91 files changed, 4412 insertions(+), 1995 deletions(-) create mode 100644 src/base/FieldLayoutElement.php create mode 100644 src/base/FieldLayoutElementInterface.php create mode 100644 src/events/DefineFieldLayoutFieldsEvent.php create mode 100644 src/fieldlayoutelements/BaseField.php create mode 100644 src/fieldlayoutelements/CustomField.php create mode 100644 src/fieldlayoutelements/EntryTitleField.php create mode 100644 src/fieldlayoutelements/Heading.php create mode 100644 src/fieldlayoutelements/HorizontalRule.php create mode 100644 src/fieldlayoutelements/StandardField.php create mode 100644 src/fieldlayoutelements/StandardTextField.php create mode 100644 src/fieldlayoutelements/Tip.php create mode 100644 src/fieldlayoutelements/TitleField.php delete mode 100644 src/migrations/m200619_212955_title_instructions.php create mode 100644 src/migrations/m200620_230205_field_layout_changes.php create mode 100644 src/models/FieldLayoutForm.php create mode 100644 src/models/FieldLayoutFormTab.php create mode 100644 src/templates/_includes/forms/fieldLayoutDesigner.html delete mode 100644 src/templates/entries/_fields.html delete mode 100644 src/templates/entries/_titlefield.html delete mode 100644 src/web/assets/cp/dist/images/fieldlayoutform-bg.png delete mode 100644 src/web/assets/cp/dist/images/fieldlayoutform-bg_2x.png create mode 100644 src/web/assets/cp/src/js/Listbox.js create mode 100644 src/web/assets/cp/src/js/SlidePicker.js diff --git a/CHANGELOG-v3.5.md b/CHANGELOG-v3.5.md index 3faa5d5c0be..940f3d79b63 100644 --- a/CHANGELOG-v3.5.md +++ b/CHANGELOG-v3.5.md @@ -6,12 +6,16 @@ ### Added - Added the Project Config utility, which can be used to perform project config actions, and view a dump of the stored project config. ([#4371](https://github.com/craftcms/cms/issues/4371)) +- It’s now possible to customize the labels and author instructions for all fields (including Title fields), from within field layout designers. ([#806](https://github.com/craftcms/cms/issues/806), [#841](https://github.com/craftcms/cms/issues/841)) +- It’s now possible to set Title fields’ positions within field layout designers. ([#3953](https://github.com/craftcms/cms/issues/3953)) +- It’s now possible to set field widths to 25%, 50%, 75%, or 100%, and fields will be positioned next to each other when there’s room. ([#2644](https://github.com/craftcms/cms/issues/2644)) +- It’s now possible to add headings, tips, warnings, and horizontal rules to field layouts. ([#1103](https://github.com/craftcms/cms/issues/1103), [#1138](https://github.com/craftcms/cms/issues/1138), [#4738](https://github.com/craftcms/cms/issues/4738)) +- It’s now possible to search for fields from within field layout designers. ([#913](https://github.com/craftcms/cms/issues/913)) - Added the “Use shapes to represent statuses” user preference. ([#3293](https://github.com/craftcms/cms/issues/3293)) - Added the “Underline links” user preference. ([#6153](https://github.com/craftcms/cms/issues/6153)) - Added the “Suspend by default” user registration setting. ([#5830](https://github.com/craftcms/cms/issues/5830)) - Added the ability to disable sites on the front end. ([#3005](https://github.com/craftcms/cms/issues/3005)) - Soft-deleted elements now have a “Delete permanently” element action. ([#4420](https://github.com/craftcms/cms/issues/4420)) -- Entry types can now specify custom Title field instructions. ([#1518](https://github.com/craftcms/cms/issues/1518)) - Entry types can now change the Title field’s translation method, similar to how custom fields’ translation methods. ([#2856](https://github.com/craftcms/cms/issues/2856)) - Entry draft forms no longer have a primary action, and the Ctrl/Command + S keyboard shortcut now forces a resave of the draft, rather than publishing it. ([#6199](https://github.com/craftcms/cms/issues/6199)) - Edit Entry pages now support a Shift + Ctrl/Command + S keyboard shortcut for saving the entry and creating a new one. ([#2851](https://github.com/craftcms/cms/issues/2851)) @@ -34,6 +38,7 @@ - Added the `|diff` Twig filter. - Added the `|explodeClass` Twig filter, which converts class names into an array. - Added the `|explodeStyle` Twig filter, which converts CSS styles into an array of property/value pairs. +- Added the `|namespaceAttributes` Twig filter, which namespaces `id`, `for`, and other attributes, but not `name`. - Added the `|push` Twig filter, which returns a new array with one or more items appended to it. - Added the `|unshift` Twig filter, which returns a new array with one or more items prepended to it. - Added the `|where` Twig filter. @@ -55,6 +60,8 @@ - Added the “Prettify” and “History” buttons to the GraphiQL IDE. - Added the Explorer plugin to GraphiQL. - Added support for external subnav links in the global control panel nav. +- Added the `fieldLayoutDesigner()` and `fieldLayoutDesignerField()` macros to the `_includes/forms.html` control panel template. +- Added the `_includes/forms/fieldLayoutDesigner.html` control panel template. - Added the `_layouts/components/form-action-menu.twig` control panel template. - Added the `parseRefs` GraphQL directive. ([#6200](https://github.com/craftcms/cms/issues/6200)) - Added the `prev` and `next` fields for entries, categories and assets when querying elements via GraphQL. ([#5571](https://github.com/craftcms/cms/issues/5571)) @@ -88,6 +95,8 @@ - Added `craft\base\Field::searchKeywords()`. - Added `craft\base\FieldInterface::getContentGqlMutationArgumentType()`. - Added `craft\base\FieldInterface::getContentGqlQueryArgumentType()`. +- Added `craft\base\FieldLayoutElement`. +- Added `craft\base\FieldLayoutElementInterface`. - Added `craft\base\Model::EVENT_DEFINE_EXTRA_FIELDS`. - Added `craft\base\Model::EVENT_DEFINE_FIELDS`. - Added `craft\base\Volume::getFieldLayout()`. @@ -98,6 +107,7 @@ - Added `craft\console\controllers\MigrateController::EVENT_REGISTER_MIGRATOR`. - Added `craft\controllers\AppController::actionBrokenImage()`. - Added `craft\controllers\BaseEntriesController::enforceSitePermissions()`. +- Added `craft\controllers\FieldsController::actionRenderLayoutElementSelector()`. - Added `craft\controllers\UtilitiesController::actionProjectConfigPerformAction()`. - Added `craft\db\MigrationManager::TRACK_CONTENT`. - Added `craft\db\MigrationManager::TRACK_CRAFT`. @@ -131,6 +141,7 @@ - Added `craft\events\DefineAttributeKeywordsEvent`. - Added `craft\events\DefineFieldHtmlEvent`. - Added `craft\events\DefineFieldKeywordsEvent`. +- Added `craft\events\DefineFieldLayoutFieldEvent`. - Added `craft\events\DefineFieldsEvent`. - Added `craft\events\EagerLoadElementsEvent`. - Added `craft\events\RegisterElementFieldLayoutsEvent`. @@ -138,6 +149,15 @@ - Added `craft\events\RegisterGqlSchemaComponentsEvent`. - Added `craft\events\RegisterMigratorEvent`. - Added `craft\events\SetEagerLoadedElementsEvent`. +- Added `craft\fieldlayoutelements\BaseField`. +- Added `craft\fieldlayoutelements\CustomField`. +- Added `craft\fieldlayoutelements\EntryTitleField`. +- Added `craft\fieldlayoutelements\Heading`. +- Added `craft\fieldlayoutelements\HorizontalRule`. +- Added `craft\fieldlayoutelements\StandardField`. +- Added `craft\fieldlayoutelements\StandardTextField`. +- Added `craft\fieldlayoutelements\Tip`. +- Added `craft\fieldlayoutelements\TitleField`. - Added `craft\fields\BaseOptionsField::getContentGqlMutationArgumentType()`. - Added `craft\fields\BaseRelationField::getContentGqlMutationArgumentType()`. - Added `craft\fields\Date::getContentGqlMutationArgumentType()`. @@ -209,6 +229,20 @@ - Added `craft\helpers\MailerHelper::settingsReport()`. - Added `craft\helpers\ProjectConfig::splitConfigIntoComponents()`. - Added `craft\helpers\Queue`. +- Added `craft\models\FieldLayout::createForm()`. +- Added `craft\models\FieldLayout::EVENT_DEFINE_STANDARD_FIELDS`. +- Added `craft\models\FieldLayout::getAvailableCustomFields()`. +- Added `craft\models\FieldLayout::getAvailableStandardFields()`. +- Added `craft\models\FieldLayout::getAvailableUiElements()`. +- Added `craft\models\FieldLayout::getField()`. +- Added `craft\models\FieldLayout::isFieldIncluded()`. +- Added `craft\models\FieldLayoutForm`. +- Added `craft\models\FieldLayoutFormTab`. +- Added `craft\models\FieldLayoutTab::$elements`. +- Added `craft\models\FieldLayoutTab::createFromConfig()`. +- Added `craft\models\FieldLayoutTab::getConfig()`. +- Added `craft\models\FieldLayoutTab::getElementConfigs()`. +- Added `craft\models\FieldLayoutTab::updateConfig()`. - Added `craft\models\Section::PROPAGATION_METHOD_CUSTOM`. - Added `craft\models\Site::$enabled`. - Added `craft\queue\jobs\PruneRevisions`. @@ -228,6 +262,7 @@ - Added `craft\services\Elements::invalidateCachesForElementType()`. - Added `craft\services\Elements::startCollectingCacheTags()`. - Added `craft\services\Elements::stopCollectingCacheTags()`. +- Added `craft\services\Fields::createLayoutElement()`. - Added `craft\services\Fields::getLayoutsByElementType()`. - Added `craft\services\Gql::getAllSchemaComponents()`. - Added `craft\services\Images::getSupportsWebP()`. ([#5853](https://github.com/craftcms/cms/issues/5853)) @@ -251,6 +286,8 @@ - Added the `_includes/forms/password.html` control panel template. - Added the `_includes/forms/copytext.html` control panel template. - Added the `copytext` and `copytextField` macros to the `_includes/forms.html` control panel template. +- Added the `Craft.Listbox` JavaScript class. +- Added the `Craft.SlidePicker` JavaScript class. - Added the `Craft.removeLocalStorage()`, `getCookie()`, `setCookie()`, and `removeCookie()` JavaScript methods. - Added the `Craft.submitForm()` JavaScript method. - Added the `Craft.cp.getSiteId()` and `setSiteId()` JavaScript methods. @@ -351,6 +388,7 @@ - Deprecated `craft\helpers\Stringy`. - Deprecated `craft\queue\jobs\DeleteStaleTemplateCaches`. - Deprecated `craft\services\ElementIndexes::getAvailableTableFields()`. `getSourceTableAttributes()` should be used instead. +- Deprecated `craft\services\Fields::assembleLayout()`. - Deprecated `craft\services\Gql::getAllPermissions()`. `craft\services\Gql::getAllSchemaComponents()` should be used instead. - Deprecated `craft\services\TemplateCaches::deleteAllCaches()`. `craft\services\Elements::invalidateAllCaches()` should be used instead. - Deprecated `craft\services\TemplateCaches::deleteCacheById()`. @@ -375,10 +413,13 @@ - Removed the [Interactive Shell Extension for Yii 2](https://github.com/yiisoft/yii2-shell), as it’s now a dev dependency of the `craftcms/craft` project instead. ([#5783](https://github.com/craftcms/cms/issues/5783)) - Removed support for the `import` directive in project config files. - Removed the `cacheElementQueries` config setting. +- Removed the `entries/_fields.html` control panel template. +- Removed the `entries/_titlefield.html` control panel template. - Removed `craft\controllers\UtilitiesController::actionDbBackupPerformAction()`. - Removed `craft\db\MigrationManager::TYPE_APP`. - Removed `craft\db\MigrationManager::TYPE_CONTENT`. - Removed `craft\db\MigrationManager::TYPE_PLUGIN`. +- Removed `craft\models\EntryType::$titleLabel`. - Removed `craft\models\Info::$configMap`. - Removed `craft\records\Migration`. - Removed `craft\records\Plugin::getMigrations()`. @@ -391,6 +432,7 @@ - Fixed a bug where it was impossible to filter elements using a Lightswitch field using the GraphQL API. ([#5930](https://github.com/craftcms/cms/issues/5930)) - Fixed an error that could occur when saving template caches. ([#2674](https://github.com/craftcms/cms/issues/2674)) - When previewing an image asset on a non-public volume, the image is no longer published to the `cpresources` folder. ([#6093](https://github.com/craftcms/cms/issues/6093) +- Fixed a bug where Entry Edit pages would start showing a tab bar after switching entry types, even if the new entry type only had one content tab. ### Security - The `_includes/forms/checkbox.html`, `checkboxGroup.html`, and `checkboxSelect.html` control panel templates now HTML-encode checkbox labels by default, preventing possible XSS vulnerabilities. If HTML code was desired, it must be passed through the new `raw()` function first. diff --git a/CHANGELOG-v3.md b/CHANGELOG-v3.md index ee3023e38d6..7a9902cfe08 100644 --- a/CHANGELOG-v3.md +++ b/CHANGELOG-v3.md @@ -4,6 +4,11 @@ ### Added - Added the Project Config utility, which can be used to perform project config actions, and view a dump of the stored project config. ([#4371](https://github.com/craftcms/cms/issues/4371)) +- It’s now possible to customize the labels and author instructions for all fields (including Title fields), from within field layout designers. ([#806](https://github.com/craftcms/cms/issues/806), [#841](https://github.com/craftcms/cms/issues/841)) +- It’s now possible to set Title fields’ positions within field layout designers. ([#3953](https://github.com/craftcms/cms/issues/3953)) +- It’s now possible to set field widths to 25%, 50%, 75%, or 100%, and fields will be positioned next to each other when there’s room. ([#2644](https://github.com/craftcms/cms/issues/2644)) +- It’s now possible to add headings, tips, warnings, and horizontal rules to field layouts. ([#1103](https://github.com/craftcms/cms/issues/1103), [#1138](https://github.com/craftcms/cms/issues/1138), [#4738](https://github.com/craftcms/cms/issues/4738)) +- It’s now possible to search for fields from within field layout designers. ([#913](https://github.com/craftcms/cms/issues/913)) - Entry types can now specify custom Title field instructions. ([#1518](https://github.com/craftcms/cms/issues/1518)) - Entry types can now change the Title field’s translation method, similar to how custom fields’ translation methods. ([#2856](https://github.com/craftcms/cms/issues/2856)) - User groups can now have descriptions. ([#4893](https://github.com/craftcms/cms/issues/4893)) @@ -11,22 +16,53 @@ - Added the `parseRefs` GraphQL directive. ([#6200](https://github.com/craftcms/cms/issues/6200)) - Added the `prev` and `next` fields for entries, categories and assets when querying elements via GraphQL. ([#5571](https://github.com/craftcms/cms/issues/5571)) - Added the `imageEditorRatios` config setting, making it possible to customize the list of available aspect ratios in the image editor. ([#6201](https://github.com/craftcms/cms/issues/6201)) +- Added the `|namespaceAttributes` Twig filter, which namespaces `id`, `for`, and other attributes, but not `name`. +- Added the `fieldLayoutDesigner()` and `fieldLayoutDesignerField()` macros to the `_includes/forms.html` control panel template. +- Added the `_includes/forms/fieldLayoutDesigner.html` control panel template. - Added `craft\base\ElementInterface::getIsTitleTranslatable()`. - Added `craft\base\ElementInterface::getTitleTranslationDescription()`. - Added `craft\base\ElementInterface::getTitleTranslationKey()`. - Added `craft\base\ElementInterface::isFieldEmpty()`. +- Added `craft\base\FieldLayoutElement`. +- Added `craft\base\FieldLayoutElementInterface`. - Added `craft\base\Model::EVENT_DEFINE_EXTRA_FIELDS`. - Added `craft\base\Model::EVENT_DEFINE_FIELDS`. +- Added `craft\controllers\FieldsController::actionRenderLayoutElementSelector()`. - Added `craft\controllers\UtilitiesController::actionProjectConfigPerformAction()`. - Added `craft\elements\Asset::getVolumeId()`. - Added `craft\elements\Asset::setVolumeId()`. +- Added `craft\events\DefineFieldLayoutFieldEvent`. - Added `craft\events\DefineFieldsEvent`. +- Added `craft\fieldlayoutelements\BaseField`. +- Added `craft\fieldlayoutelements\CustomField`. +- Added `craft\fieldlayoutelements\EntryTitleField`. +- Added `craft\fieldlayoutelements\Heading`. +- Added `craft\fieldlayoutelements\HorizontalRule`. +- Added `craft\fieldlayoutelements\StandardField`. +- Added `craft\fieldlayoutelements\StandardTextField`. +- Added `craft\fieldlayoutelements\Tip`. +- Added `craft\fieldlayoutelements\TitleField`. - Added `craft\gql\base\InterfaceType::resolveElementTypeName()`. - Added `craft\gql\GqlEntityRegistry::prefixTypeName()`. - Added `craft\helpers\App::dbMutexConfig()`. - Added `craft\helpers\ElementHelper::translationDescription()`. - Added `craft\helpers\ElementHelper::translationKey()`. - Added `craft\helpers\ProjectConfig::splitConfigIntoComponents()`. +- Added `craft\models\FieldLayout::createForm()`. +- Added `craft\models\FieldLayout::EVENT_DEFINE_STANDARD_FIELDS`. +- Added `craft\models\FieldLayout::getAvailableCustomFields()`. +- Added `craft\models\FieldLayout::getAvailableStandardFields()`. +- Added `craft\models\FieldLayout::getAvailableUiElements()`. +- Added `craft\models\FieldLayout::getField()`. +- Added `craft\models\FieldLayout::isFieldIncluded()`. +- Added `craft\models\FieldLayoutForm`. +- Added `craft\models\FieldLayoutFormTab`. +- Added `craft\models\FieldLayoutTab::$elements`. +- Added `craft\models\FieldLayoutTab::createFromConfig()`. +- Added `craft\models\FieldLayoutTab::getConfig()`. +- Added `craft\models\FieldLayoutTab::getElementConfigs()`. +- Added `craft\models\FieldLayoutTab::updateConfig()`. +- Added `craft\services\Fields::createLayoutElement()`. - Added `craft\services\Path::getProjectConfigPath()`. - Added `craft\services\ProjectConfig::$folderName`. ([#5982](https://github.com/craftcms/cms/issues/5982)) - Added `craft\web\Controller::setFailFlash()`. @@ -35,6 +71,8 @@ - Added `craft\web\Request::getIsJson()`. - Added `craft\web\Request::getMimeType()`. - Added `craft\web\View::$allowEval()`, which determines whether calling `evaluateDynamicContent()` should be allowed. ([#6185](https://github.com/craftcms/cms/pull/6185)) +- Added the `Craft.Listbox` JavaScript class. +- Added the `Craft.SlidePicker` JavaScript class. ### Changed - Craft now stores project config files in a new `config/project/` folder, regardless of whether the (deprecated) `useProjectConfigFile` config setting is enabled, and syncing new project config file changes is now optional. @@ -55,11 +93,15 @@ - Deprecated `craft\elements\db\ElementQuery::$enabledForSite`. - Deprecated `craft\elements\db\ElementQuery::enabledForSite()`. - Deprecated `craft\helpers\App::mutexConfig()`. +- Deprecated `craft\services\Fields::assembleLayout()`. ### Removed - Removed support for the `import` directive in project config files. +- Removed the `entries/_fields.html` control panel template. +- Removed the `entries/_titlefield.html` control panel template. - Removed `craft\models\Info::$configMap`. - Removed `craft\services\ProjectConfig::$filename`. +- Removed `craft\models\EntryType::$titleLabel`. ### Fixed - Fixed an error that occurred when using the `gqlTypePrefix` config setting. @@ -69,6 +111,8 @@ - Fixed a 403 Forbidden error that occurred when clicking on the GraphQL nav item, if the `allowAdminChanges` config setting was disabled. ([#6242](https://github.com/craftcms/cms/issues/6242)) - Fixed a bug where `data-params` and `data-param` attributes on `.formsubmit` elements weren’t being respected. - Fix a bug where it was impossible to upload an asset using GraphQL mutations. ([#6322](https://github.com/craftcms/cms/pull/6322)) +- Fixed a bug where Entry Edit pages would start showing a tab bar after switching entry types, even if the new entry type only had one content tab. +- Fixed a bug where some fields were growing wider than they were supposed to within element editor HUDs. ### Security - `craft\web\View::evaluateDynamicContent()` can no longer be called by default. ([#6185](https://github.com/craftcms/cms/pull/6185)) diff --git a/src/base/ApplicationTrait.php b/src/base/ApplicationTrait.php index b22d278e9ca..cd12db1f314 100644 --- a/src/base/ApplicationTrait.php +++ b/src/base/ApplicationTrait.php @@ -14,15 +14,22 @@ use craft\db\MigrationManager; use craft\db\Query; use craft\db\Table; +use craft\elements\Asset; +use craft\elements\Category; +use craft\elements\Entry; use craft\errors\DbConnectException; use craft\errors\SiteNotFoundException; use craft\errors\WrongEditionException; +use craft\events\DefineFieldLayoutFieldsEvent; use craft\events\EditionChangeEvent; +use craft\fieldlayoutelements\EntryTitleField; +use craft\fieldlayoutelements\TitleField; use craft\helpers\App; use craft\helpers\Db; use craft\i18n\Formatter; use craft\i18n\I18N; use craft\i18n\Locale; +use craft\models\FieldLayout; use craft\models\Info; use craft\queue\Queue; use craft\queue\QueueInterface; @@ -1386,6 +1393,9 @@ private function _preInit() */ private function _postInit() { + // Register field layout listeners + $this->_registerFieldLayoutListener(); + // Register all the listeners for config items $this->_registerConfigListeners(); @@ -1481,6 +1491,27 @@ private function _getFallbackLanguage(): string return $this->sourceLanguage; } + /** + * Register event listeners for field layouts. + */ + private function _registerFieldLayoutListener() + { + Event::on(FieldLayout::class, FieldLayout::EVENT_DEFINE_STANDARD_FIELDS, function(DefineFieldLayoutFieldsEvent $event) { + /** @var FieldLayout $fieldLayout */ + $fieldLayout = $event->sender; + + switch ($fieldLayout->type) { + case Asset::class: + case Category::class: + $event->fields[] = new TitleField(); + break; + case Entry::class: + $event->fields[] = new EntryTitleField(); + break; + } + }); + } + /** * Register event listeners for config changes. */ diff --git a/src/base/Element.php b/src/base/Element.php index de54028ac36..408c012ecf0 100644 --- a/src/base/Element.php +++ b/src/base/Element.php @@ -35,6 +35,7 @@ use craft\events\SetEagerLoadedElementsEvent; use craft\events\SetElementRouteEvent; use craft\events\SetElementTableAttributeHtmlEvent; +use craft\fieldlayoutelements\BaseField; use craft\helpers\ArrayHelper; use craft\helpers\Db; use craft\helpers\ElementHelper; @@ -57,7 +58,6 @@ use yii\base\Event; use yii\base\Exception; use yii\base\InvalidConfigException; -use yii\base\InvalidValueException; use yii\validators\NumberValidator; use yii\validators\Validator; @@ -1574,8 +1574,14 @@ public function attributeLabels() $layout = $this->getFieldLayout(); if ($layout !== null) { - foreach ($layout->getFields() as $field) { - $labels[$field->handle] = Craft::t('site', $field->name); + foreach ($layout->getTabs() as $tab) { + if ($tab->elements) { + foreach ($tab->elements as $element) { + if ($element instanceof BaseField && ($label = $element->label()) !== null) { + $labels[$element->attribute()] = $label; + } + } + } } } } @@ -3055,32 +3061,20 @@ protected function tableAttributeHtml(string $attribute): string */ public function getEditorHtml(): string { - $html = ''; - $fieldLayout = $this->getFieldLayout(); - $view = Craft::$app->getView(); - - if ($fieldLayout) { - $namespace = $view->getNamespace(); - $view->setNamespace($view->namespaceInputName('fields')); - $view->setIsDeltaRegistrationActive(true); - - foreach ($fieldLayout->getFields() as $field) { - $fieldHtml = $view->renderTemplate('_includes/field', [ - 'element' => $this, - 'field' => $field, - 'required' => $field->required, - 'registerDeltas' => true, - ]); + if (!$fieldLayout) { + return ''; + } - $html .= Html::namespaceHtml($fieldHtml, 'fields'); + $html = ''; + foreach ($fieldLayout->getTabs() as $tab) { + foreach ($tab->elements as $element) { + if ($element instanceof BaseField) { + $html .= $element->formHtml($this); + } } - - $view->setNamespace($namespace); - $view->setIsDeltaRegistrationActive(false); - - $html .= Html::hiddenInput('fieldLayoutId', $fieldLayout->id); } + $html .= Html::hiddenInput('fieldLayoutId', $fieldLayout->id); return $html; } diff --git a/src/base/FieldLayoutElement.php b/src/base/FieldLayoutElement.php new file mode 100644 index 00000000000..09c91af48a6 --- /dev/null +++ b/src/base/FieldLayoutElement.php @@ -0,0 +1,31 @@ + + * @since 3.5.0 + */ +abstract class FieldLayoutElement extends BaseObject implements FieldLayoutElementInterface +{ + use ArrayableTrait; + + /** + * @inheritdoc + */ + public function settingsHtml() + { + return null; + } +} diff --git a/src/base/FieldLayoutElementInterface.php b/src/base/FieldLayoutElementInterface.php new file mode 100644 index 00000000000..d9ca91c0d67 --- /dev/null +++ b/src/base/FieldLayoutElementInterface.php @@ -0,0 +1,44 @@ + + * @since 3.5.0 + */ +interface FieldLayoutElementInterface extends Arrayable +{ + /** + * Returns the selector HTML that should be displayed within field layout designers. + * + * @return string + */ + public function selectorHtml(): string; + + /** + * Returns the settings HTML for the layout element. + * + * @return string|null + */ + public function settingsHtml(); + + /** + * Returns the element’s form HTMl. + * + * Return `null` if the element should not be present within the form. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return string|null + */ + public function formHtml(ElementInterface $element = null, bool $static = false); +} diff --git a/src/config/app.php b/src/config/app.php index 76d10cb7a9f..539f2b3b681 100644 --- a/src/config/app.php +++ b/src/config/app.php @@ -4,7 +4,7 @@ 'id' => 'CraftCMS', 'name' => 'Craft CMS', 'version' => '3.5.0-beta.3', - 'schemaVersion' => '3.5.7', + 'schemaVersion' => '3.5.8', 'minVersionRequired' => '2.6.2788', 'basePath' => dirname(__DIR__), // Defines the @app alias 'runtimePath' => '@storage/runtime', // Defines the @runtime alias diff --git a/src/controllers/CategoriesController.php b/src/controllers/CategoriesController.php index 0c335849cf0..39e35fe8cb8 100644 --- a/src/controllers/CategoriesController.php +++ b/src/controllers/CategoriesController.php @@ -106,17 +106,6 @@ public function actionEditCategoryGroup(int $groupId = null, CategoryGroup $cate $variables['title'] = Craft::t('app', 'Create a new category group'); } - $variables['tabs'] = [ - 'settings' => [ - 'label' => Craft::t('app', 'Settings'), - 'url' => '#categorygroup-settings' - ], - 'fieldLayout' => [ - 'label' => Craft::t('app', 'Field Layout'), - 'url' => '#categorygroup-fieldlayout' - ] - ]; - $variables['groupId'] = $groupId; $variables['categoryGroup'] = $categoryGroup; @@ -704,29 +693,10 @@ private function _prepEditCategoryVariables(array &$variables) } } - // Define the content tabs - // --------------------------------------------------------------------- - - $variables['tabs'] = []; - - foreach ($variables['group']->getFieldLayout()->getTabs() as $index => $tab) { - // Do any of the fields on this tab have errors? - $hasErrors = false; - - if ($variables['category']->hasErrors()) { - foreach ($tab->getFields() as $field) { - if ($hasErrors = $variables['category']->hasErrors($field->handle . '.*')) { - break; - } - } - } - - $variables['tabs'][] = [ - 'label' => Craft::t('site', $tab->name), - 'url' => '#' . $tab->getHtmlId(), - 'class' => $hasErrors ? 'error' : null - ]; - } + // Prep the form tabs & content + $form = $variables['group']->getFieldLayout()->createForm($variables['category']); + $variables['tabs'] = $form->getTabMenu(); + $variables['fieldsHtml'] = $form->render(); } /** diff --git a/src/controllers/EntriesController.php b/src/controllers/EntriesController.php index 471ff5b91b7..6ef5bbaeee6 100644 --- a/src/controllers/EntriesController.php +++ b/src/controllers/EntriesController.php @@ -9,8 +9,6 @@ use Craft; use craft\base\Element; -use craft\db\Query; -use craft\db\Table; use craft\elements\Entry; use craft\elements\User; use craft\errors\InvalidElementException; @@ -278,17 +276,17 @@ public function actionSwitchEntryType(): Response } $view = $this->getView(); - $tabsHtml = !empty($variables['tabs']) ? $view->renderTemplate('_includes/tabs', $variables) : null; - $fieldsHtml = $view->renderTemplate('entries/_fields', $variables); - $headHtml = $view->getHeadHtml(); - $bodyHtml = $view->getBodyHtml(); - - return $this->asJson(compact( - 'tabsHtml', - 'fieldsHtml', - 'headHtml', - 'bodyHtml' - )); + $form = $variables['entryType']->getFieldLayout()->createForm($variables['entry']); + $tabs = $form->getTabMenu(); + + return $this->asJson([ + 'tabsHtml' => count($tabs) > 1 ? $view->renderTemplate('_includes/tabs', [ + 'tabs' => $tabs, + ]) : null, + 'fieldsHtml' => $form->render(), + 'headHtml' => $view->getHeadHtml(), + 'bodyHtml' => $view->getBodyHtml(), + ]); } /** @@ -620,30 +618,6 @@ private function _prepEditEntryVariables(array &$variables) // Prevent the last entry type's field layout from being used $variables['entry']->fieldLayoutId = null; - // Define the content tabs - // --------------------------------------------------------------------- - - $variables['tabs'] = []; - - foreach ($variables['entryType']->getFieldLayout()->getTabs() as $index => $tab) { - // Do any of the fields on this tab have errors? - $hasErrors = false; - - if ($variables['entry']->hasErrors()) { - foreach ($tab->getFields() as $field) { - if ($hasErrors = $variables['entry']->hasErrors($field->handle . '.*')) { - break; - } - } - } - - $variables['tabs'][] = [ - 'label' => Craft::t('site', $tab->name), - 'url' => '#' . $tab->getHtmlId(), - 'class' => $hasErrors ? 'error' : null - ]; - } - return null; } diff --git a/src/controllers/FieldsController.php b/src/controllers/FieldsController.php index 46902621b29..095d1bb633d 100644 --- a/src/controllers/FieldsController.php +++ b/src/controllers/FieldsController.php @@ -10,13 +10,16 @@ use Craft; use craft\base\Field; use craft\base\FieldInterface; +use craft\base\FieldLayoutElementInterface; use craft\fields\MissingField; use craft\fields\PlainText; use craft\helpers\ArrayHelper; +use craft\helpers\Component; use craft\helpers\UrlHelper; use craft\models\FieldGroup; use craft\web\assets\fieldsettings\FieldSettingsAsset; use craft\web\Controller; +use yii\web\BadRequestHttpException; use yii\web\NotFoundHttpException; use yii\web\Response; use yii\web\ServerErrorHttpException; @@ -337,4 +340,24 @@ public function actionDeleteField(): Response return $this->asJson(['success' => $success]); } + + // Field Layouts + // ------------------------------------------------------------------------- + + /** + * Renders a field layout element’s selector HTML. + * + * @return Response + * @throws BadRequestHttpException + * @since 3.5.0 + */ + public function actionRenderLayoutElementSelector(): Response + { + $config = Craft::$app->getRequest()->getRequiredBodyParam('config'); + $element = Craft::$app->getFields()->createLayoutElement($config); + + return $this->asJson([ + 'html' => $element->selectorHtml(), + ]); + } } diff --git a/src/controllers/GlobalsController.php b/src/controllers/GlobalsController.php index f5e98f6c918..e8f57f50b9c 100644 --- a/src/controllers/GlobalsController.php +++ b/src/controllers/GlobalsController.php @@ -201,40 +201,17 @@ public function actionEditContent(string $globalSetHandle, string $siteHandle = $globalSet = $editableGlobalSets[$globalSetHandle]; } - // Body class - $bodyClass = 'edit-global-set site--' . $site->handle; - - // Define the content tabs - // --------------------------------------------------------------------- - - $tabs = []; - - foreach ($globalSet->getFieldLayout()->getTabs() as $index => $tab) { - // Do any of the fields on this tab have errors? - $hasErrors = false; - - if ($globalSet->hasErrors()) { - foreach ($tab->getFields() as $field) { - if ($hasErrors = $globalSet->hasErrors($field->handle . '.*')) { - break; - } - } - } - - $tabs[] = [ - 'label' => Craft::t('site', $tab->name), - 'url' => '#' . $tab->getHtmlId(), - 'class' => $hasErrors ? 'error' : null - ]; - } + // Prep the form tabs & content + $form = $globalSet->getFieldLayout()->createForm($globalSet); // Render the template! - return $this->renderTemplate('globals/_edit', compact( - 'bodyClass', - 'editableGlobalSets', - 'globalSet', - 'tabs' - )); + return $this->renderTemplate('globals/_edit', [ + 'bodyClass' => 'edit-global-set site--' . $site->handle, + 'editableGlobalSets' => $editableGlobalSets, + 'globalSet' => $globalSet, + 'tabs' => $form->getTabMenu(), + 'fieldsHtml' => $form->render(), + ]); } /** diff --git a/src/controllers/SectionsController.php b/src/controllers/SectionsController.php index bf03eec83dc..b71f56d1118 100644 --- a/src/controllers/SectionsController.php +++ b/src/controllers/SectionsController.php @@ -350,8 +350,6 @@ public function actionSaveEntryType() $entryType->name = Craft::$app->getRequest()->getBodyParam('name', $entryType->name); $entryType->handle = Craft::$app->getRequest()->getBodyParam('handle', $entryType->handle); $entryType->hasTitleField = (bool)Craft::$app->getRequest()->getBodyParam('hasTitleField', $entryType->hasTitleField); - $entryType->titleLabel = Craft::$app->getRequest()->getBodyParam('titleLabel', $entryType->titleLabel); - $entryType->titleInstructions = Craft::$app->getRequest()->getBodyParam('titleInstructions', $entryType->titleInstructions); $entryType->titleTranslationMethod = Craft::$app->getRequest()->getBodyParam('titleTranslationMethod', $entryType->titleTranslationMethod); $entryType->titleTranslationKeyFormat = Craft::$app->getRequest()->getBodyParam('titleTranslationKeyFormat', $entryType->titleTranslationKeyFormat); $entryType->titleFormat = Craft::$app->getRequest()->getBodyParam('titleFormat', $entryType->titleFormat); diff --git a/src/controllers/SystemSettingsController.php b/src/controllers/SystemSettingsController.php index 9c4fdf2f583..7c95650e329 100644 --- a/src/controllers/SystemSettingsController.php +++ b/src/controllers/SystemSettingsController.php @@ -304,25 +304,12 @@ public function actionEditGlobalSet(int $globalSetId = null, GlobalSet $globalSe ] ]; - // Tabs - $tabs = [ - 'settings' => [ - 'label' => Craft::t('app', 'Settings'), - 'url' => '#set-settings' - ], - 'fieldlayout' => [ - 'label' => Craft::t('app', 'Field Layout'), - 'url' => '#set-fieldlayout' - ] - ]; - // Render the template! return $this->renderTemplate('settings/globals/_edit', [ 'globalSetId' => $globalSetId, 'globalSet' => $globalSet, 'title' => $title, 'crumbs' => $crumbs, - 'tabs' => $tabs ]); } diff --git a/src/controllers/TagsController.php b/src/controllers/TagsController.php index a226f2e283b..b54e546ca68 100644 --- a/src/controllers/TagsController.php +++ b/src/controllers/TagsController.php @@ -87,24 +87,11 @@ public function actionEditTagGroup(int $tagGroupId = null, TagGroup $tagGroup = ] ]; - // Tabs - $tabs = [ - 'settings' => [ - 'label' => Craft::t('app', 'Settings'), - 'url' => '#taggroup-settings' - ], - 'fieldLayout' => [ - 'label' => Craft::t('app', 'Field Layout'), - 'url' => '#taggroup-fieldlayout' - ] - ]; - return $this->renderTemplate('settings/tags/_edit', [ 'tagGroupId' => $tagGroupId, 'tagGroup' => $tagGroup, 'title' => $title, 'crumbs' => $crumbs, - 'tabs' => $tabs ]); } diff --git a/src/controllers/UsersController.php b/src/controllers/UsersController.php index a3c6ebca1cc..045fb495ce5 100644 --- a/src/controllers/UsersController.php +++ b/src/controllers/UsersController.php @@ -776,6 +776,12 @@ public function actionEditUser($userId = null, User $user = null, array $errors $title = Craft::t('app', 'Register a new user'); } + // Prep the form tabs & content + // --------------------------------------------------------------------- + + $form = $user->getFieldLayout()->createForm($user, false, [ + 'tabIdPrefix' => 'profile', + ]); $selectedTab = 'account'; $tabs = [ @@ -785,29 +791,7 @@ public function actionEditUser($userId = null, User $user = null, array $errors ] ]; - foreach ($user->getFieldLayout()->getTabs() as $index => $tab) { - // Skip if the tab doesn't have any fields - if (empty($tab->getFields())) { - continue; - } - - // Do any of the fields on this tab have errors? - $hasErrors = false; - - if ($user->hasErrors()) { - foreach ($tab->getFields() as $field) { - if ($hasErrors = $user->hasErrors($field->handle . '.*')) { - break; - } - } - } - - $tabs['profile' . $index] = [ - 'label' => Craft::t('site', $tab->name), - 'url' => '#profile-' . $tab->getHtmlId(), - 'class' => $hasErrors ? 'error' : null - ]; - } + $tabs += $form->getTabMenu(); // Show the permission tab for the users that can change them on Craft Pro editions if ( @@ -862,6 +846,8 @@ public function actionEditUser($userId = null, User $user = null, array $errors } } + $fieldsHtml = $form->render(false); + // Load the resources and render the page // --------------------------------------------------------------------- @@ -885,7 +871,8 @@ public function actionEditUser($userId = null, User $user = null, array $errors 'bodyClass', 'title', 'tabs', - 'selectedTab' + 'selectedTab', + 'fieldsHtml' )); } diff --git a/src/controllers/VolumesController.php b/src/controllers/VolumesController.php index c9bc125875f..023ceaec1df 100644 --- a/src/controllers/VolumesController.php +++ b/src/controllers/VolumesController.php @@ -136,17 +136,6 @@ public function actionEditVolume(int $volumeId = null, VolumeInterface $volume = ], ]; - $tabs = [ - 'settings' => [ - 'label' => Craft::t('app', 'Settings'), - 'url' => '#assetvolume-settings' - ], - 'fieldlayout' => [ - 'label' => Craft::t('app', 'Field Layout'), - 'url' => '#assetvolume-fieldlayout' - ], - ]; - return $this->renderTemplate('settings/assets/volumes/_edit', [ 'volumeId' => $volumeId, 'volume' => $volume, @@ -157,7 +146,6 @@ public function actionEditVolume(int $volumeId = null, VolumeInterface $volume = 'volumeInstances' => $volumeInstances, 'title' => $title, 'crumbs' => $crumbs, - 'tabs' => $tabs ]); } diff --git a/src/elements/Entry.php b/src/elements/Entry.php index 77bb4038534..d03d4c768c2 100644 --- a/src/elements/Entry.php +++ b/src/elements/Entry.php @@ -734,21 +734,6 @@ public function datetimeAttributes(): array return $attributes; } - /** - * @inheritdoc - */ - public function attributeLabels() - { - $labels = parent::attributeLabels(); - - // Use the entry type's title label - if ($titleLabel = $this->getType()->titleLabel) { - $labels['title'] = Craft::t('site', $titleLabel); - } - - return $labels; - } - /** * @inheritdoc */ @@ -1299,16 +1284,6 @@ public function getEditorHtml(): string } } - // Get the entry type - $entryType = $this->getType(); - - // Show the Title field? - if ($entryType->hasTitleField) { - $html .= $view->renderTemplate('entries/_titlefield', [ - 'entry' => $this - ]); - } - // Render the custom fields $html .= parent::getEditorHtml(); diff --git a/src/events/DefineFieldLayoutFieldsEvent.php b/src/events/DefineFieldLayoutFieldsEvent.php new file mode 100644 index 00000000000..fae76f7d386 --- /dev/null +++ b/src/events/DefineFieldLayoutFieldsEvent.php @@ -0,0 +1,25 @@ + + * @since 3.5.0 + */ +class DefineFieldLayoutFieldsEvent extends Event +{ + /** + * @var BaseField[] The fields that should be available to the field layout designer. + */ + public $fields = []; +} diff --git a/src/fieldlayoutelements/BaseField.php b/src/fieldlayoutelements/BaseField.php new file mode 100644 index 00000000000..24ebf307f05 --- /dev/null +++ b/src/fieldlayoutelements/BaseField.php @@ -0,0 +1,410 @@ + + * @since 3.5.0 + */ +abstract class BaseField extends FieldLayoutElement +{ + /** + * @var string|null The field’s label + */ + public $label; + + /** + * @var string|null The field’s instructions + */ + public $instructions; + + /** + * @var string|null The field’s tip text + */ + public $tip; + + /** + * @var string|null The field’s warning text + */ + public $warning; + + /** + * @var bool Whether the field is required. + */ + public $required = false; + + /** + * @var int The width (%) of the field + */ + public $width = 100; + + /** + * Returns the element attribute this field is for. + * + * @return string + */ + abstract public function attribute(): string; + + /** + * Returns the field’s value. + * + * @param ElementInterface|null $element + * @return mixed + */ + protected function value(ElementInterface $element = null) + { + return $element->{$this->attribute()} ?? null; + } + + /** + * Returns the field’s validation errors. + * + * @param ElementInterface|null $element + * @return string[] + */ + protected function errors(ElementInterface $element = null): array + { + if (!$element) { + return []; + } + return $element->getErrors($this->attribute()); + } + + /** + * Returns whether the field *must* be present within the layout. + * + * @return bool + */ + public function mandatory(): bool + { + return false; + } + + /** + * Returns whether the field can optionally be marked as required. + * + * @return bool + */ + public function requirable(): bool + { + return false; + } + + /** + * @inheritdoc + */ + public function selectorHtml(): string + { + return Html::tag('div', $this->selectorInnerHtml(), [ + 'class' => 'fld-field', + 'data' => [ + 'mandatory' => $this->mandatory(), + 'requirable' => $this->requirable(), + ], + ]); + } + + /** + * Returns the selector’s inner HTML. + * + * @return string + */ + protected function selectorInnerHtml(): string + { + $innerHtml = Html::tag('h4', $this->label()); + + if ($type = $this->fieldType()) { + $innerHtml .= Html::tag('div', $type, [ + 'class' => ['smalltext', 'light'], + ]); + } + + return Html::tag('div', $innerHtml, [ + 'class' => ['field-name'], + ]); + } + + /** + * @inheritdoc + */ + public function settingsHtml() + { + $view = Craft::$app->getView(); + return + $view->renderTemplateMacro('_includes/forms', 'textField', [ + [ + 'label' => Craft::t('app', 'Label'), + 'id' => 'label', + 'name' => 'label', + 'value' => $this->label, + 'placeholder' => $this->defaultLabel(), + ], + ]) . + $view->renderTemplateMacro('_includes/forms', 'textareaField', [ + [ + 'label' => Craft::t('app', 'Instructions'), + 'class' => 'nicetext', + 'id' => 'instructions', + 'name' => 'instructions', + 'value' => $this->instructions, + 'placeholder' => $this->defaultInstructions(), + ], + ]); + } + + /** + * @inheritdoc + */ + public function formHtml(ElementInterface $element = null, bool $static = false) + { + $inputHtml = $this->inputHtml($element, $static); + if ($inputHtml === null) { + return null; + } + + $statusClass = $this->statusClass(); + + return Craft::$app->getView()->renderTemplate('_includes/forms/field', [ + 'id' => $this->id(), + 'fieldClass' => 'width-' . ($this->width ?? 100), + 'fieldAttributes' => $this->containerAttributes($element, $static), + 'inputAttributes' => $this->inputContainerAttributes($element, $static), + 'labelAttributes' => $this->labelAttributes($element, $static), + 'status' => $statusClass ? [$statusClass, $this->statusLabel() ?? ucfirst($statusClass)] : null, + 'label' => $this->label ? Craft::t('site', $this->label) : $this->defaultLabel($element, $static), + 'altLabel' => Html::tag('code', $this->attribute()), + 'required' => !$static && $this->required, + 'instructions' => $this->_instructions($element, $static), + 'input' => $inputHtml, + 'tip' => $this->tip($element, $static), + 'warning' => $this->warning($element, $static), + 'orientation' => $this->orientation($element, $static), + 'translatable' => $this->translatable($element, $static), + 'translationDescription' => $this->translationDescription($element, $static), + 'errors' => !$static ? $this->errors($element) : [], + ]); + } + + /** + * Returns the search keywords for this layout element. + * + * @return string[] + */ + public function keywords(): array + { + return array_filter([ + $this->label, + $this->defaultLabel(), + $this->fieldType(), + ]); + } + + /** + * Returns the `id` of the input. + * + * @return string + */ + protected function id(): string + { + return $this->attribute(); + } + + /** + * Returns field container HTML attributes. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return array + */ + protected function containerAttributes(ElementInterface $element = null, bool $static = false): array + { + return []; + } + + /** + * Returns input container HTML attributes. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return array + */ + protected function inputContainerAttributes(ElementInterface $element = null, bool $static = false): array + { + return []; + } + + /** + * Returns label HTML attributes. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return array + */ + protected function labelAttributes(ElementInterface $element = null, bool $static = false): array + { + return []; + } + + /** + * Returns the field’s status class. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return string|null + */ + protected function statusClass(ElementInterface $element = null, bool $static = false) + { + return null; + } + + /** + * Returns the field’s status label. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return string|null + */ + protected function statusLabel(ElementInterface $element = null, bool $static = false) + { + return null; + } + + /** + * Returns the field’s label. + * + * @return string|null + */ + public function label() + { + return $this->label ?: $this->defaultLabel(); + } + + /** + * Returns the field’s default label, which will be used if [[label]] is null. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return string|null + */ + protected function defaultLabel(ElementInterface $element = null, bool $static = false) + { + return null; + } + + /** + * Returns the field’s type. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return string|null + */ + protected function fieldType(ElementInterface $element = null, bool $static = false) + { + return null; + } + + /** + * Returns the field’s instructions. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return string|null + */ + private function _instructions(ElementInterface $element = null, bool $static = false) + { + return $this->instructions ? Craft::t('site', $this->instructions) : $this->defaultInstructions($element, $static); + } + + /** + * Returns the field’s default instructions, which will be used if [[instructions]] is null. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return string|null + */ + protected function defaultInstructions(ElementInterface $element = null, bool $static = false) + { + return null; + } + + /** + * Returns the field’s input HTML. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return string|null + */ + abstract protected function inputHtml(ElementInterface $element = null, bool $static = false); + + /** + * Returns the field’s tip text. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return string|null + */ + protected function tip(ElementInterface $element = null, bool $static = false) + { + return $this->tip ? Craft::t('site', $this->tip) : null; + } + + /** + * Returns the field’s warning text. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return string|null + */ + protected function warning(ElementInterface $element = null, bool $static = false) + { + return $this->warning ? Craft::t('site', $this->warning) : null; + } + + /** + * Returns the field’s orientation (`ltr` or `rtl`). + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return string + */ + protected function orientation(ElementInterface $element = null, bool $static = false): string + { + return Craft::$app->getLocale()->getOrientation(); + } + + /** + * Returns whether the field is translatable. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return bool + */ + protected function translatable(ElementInterface $element = null, bool $static = false): bool + { + return false; + } + + /** + * Returns the descriptive text for how this field is translatable. + * + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @return string|null + */ + protected function translationDescription(ElementInterface $element = null, bool $static = false) + { + return null; + } +} diff --git a/src/fieldlayoutelements/CustomField.php b/src/fieldlayoutelements/CustomField.php new file mode 100644 index 00000000000..4e0d413c179 --- /dev/null +++ b/src/fieldlayoutelements/CustomField.php @@ -0,0 +1,217 @@ + + * @since 3.5.0 + */ +class CustomField extends BaseField +{ + /** + * @var FieldInterface The custom field this layout field is based on. + */ + private $_field; + + /** + * @inheritdoc + * @param FieldInterface|null $field + */ + public function __construct(FieldInterface $field = null, $config = []) + { + $this->_field = $field; + parent::__construct($config); + } + + /** + * @inheritdoc + */ + public function attribute(): string + { + return $this->_field->handle; + } + + /** + * @inheritdoc + */ + public function requirable(): bool + { + return true; + } + + /** + * Sets the custom field this layout field is based on. + * + * @param FieldInterface $field + */ + public function setField(FieldInterface $field) + { + $this->_field = $field; + } + + /** + * Returns the UID of the field this layout field is based on. + * + * @return string + */ + public function getFieldUid(): string + { + return $this->_field->uid; + } + + /** + * Sets the UID of the field this layout field is based on. + * + * @param string $uid + * @throws InvalidArgumentException if $uid is invalid + */ + public function setFieldUid(string $uid) + { + if (($field = \Craft::$app->getFields()->getFieldByUid($uid)) === null) { + throw new InvalidArgumentException("Invalid field UID: $uid"); + } + $this->_field = $field; + } + + /** + * @inheritdoc + */ + public function fields() + { + $fields = parent::fields(); + $fields['fieldUid'] = 'fieldUid'; + return $fields; + } + + /** + * @inheritdoc + */ + protected function containerAttributes(ElementInterface $element = null, bool $static = false): array + { + return [ + 'id' => "{$this->_field->handle}-field", + 'data' => [ + 'type' => get_class($this->_field), + ] + ]; + } + + /** + * @inheritdoc + */ + protected function labelAttributes(ElementInterface $element = null, bool $static = false): array + { + return [ + 'id' => "{$this->_field->handle}-label", + 'for' => $this->_field->handle, + ]; + } + + /** + * @inheritdoc + */ + protected function defaultLabel(ElementInterface $element = null, bool $static = false) + { + return Craft::t('site', $this->_field->name); + } + + /** + * @inheritdoc + */ + protected function fieldType(ElementInterface $element = null, bool $static = false) + { + return $this->_field::displayName(); + } + + /** + * @inheritdoc + */ + protected function defaultInstructions(ElementInterface $element = null, bool $static = false) + { + return $this->_field->instructions ? Craft::t('site', $this->_field->instructions) : null; + } + + /** + * @inheritdoc + */ + public function formHtml(ElementInterface $element = null, bool $static = false) + { + $view = Craft::$app->getView(); + $registerDeltas = $view->getIsDeltaRegistrationActive(); + $namespace = $view->getNamespace(); + $view->setIsDeltaRegistrationActive(!$static); + $view->setNamespace($view->namespaceInputName('fields')); + + $html = Html::namespaceHtml(parent::formHtml($element, $static), 'fields'); + + $view->setIsDeltaRegistrationActive($registerDeltas); + $view->setNamespace($namespace); + + return $html; + } + + /** + * @inheritdoc + */ + protected function inputHtml(ElementInterface $element = null, bool $static = false): string + { + $value = $element ? $element->getFieldValue($this->_field->handle) : $this->_field->normalizeValue(null); + + if ($static) { + return $this->_field->getStaticHtml($value, $element); + } + + $view = Craft::$app->getView(); + $view->registerDeltaName($this->_field->handle); + if (empty($element->id) || $element->isFieldEmpty($this->_field->handle)) { + $view->setInitialDeltaValue($this->_field->handle, null); + } + return $this->_field->getInputHtml($value, $element); + } + + /** + * @inheritdoc + */ + protected function orientation(ElementInterface $element = null, bool $static = false): string + { + if (!$element || !$this->_field->getIsTranslatable($element)) { + return parent::orientation(); + } + + $site = $element->getSite(); + $locale = Craft::$app->getI18n()->getLocaleById($site->language); + return $locale->getOrientation(); + } + + /** + * @inheritdoc + */ + protected function translatable(ElementInterface $element = null, bool $static = false): bool + { + return $this->_field->getIsTranslatable($element); + } + + /** + * @inheritdoc + */ + protected function translationDescription(ElementInterface $element = null, bool $static = false) + { + return $this->_field->getTranslationDescription($element); + } +} diff --git a/src/fieldlayoutelements/EntryTitleField.php b/src/fieldlayoutelements/EntryTitleField.php new file mode 100644 index 00000000000..0a35b244e44 --- /dev/null +++ b/src/fieldlayoutelements/EntryTitleField.php @@ -0,0 +1,51 @@ + + * @since 3.5.0 + */ +class EntryTitleField extends TitleField +{ + + /** + * @inheritdoc + */ + protected function selectorInnerHtml(): string + { + return + Html::tag('span', '', [ + 'class' => ['fld-title-field-icon', 'fld-field-hidden', 'hidden'], + ]) . + parent::selectorInnerHtml(); + } + + /** + * @inheritdoc + */ + public function inputHtml(ElementInterface $element = null, bool $static = false) + { + if (!$element instanceof Entry) { + throw new InvalidArgumentException('EntryTitleField can only be used in entry field layouts.'); + } + + if (!$element->getType()->hasTitleField && !$element->hasErrors('title')) { + return null; + } + + return parent::inputHtml($element, $static); + } +} diff --git a/src/fieldlayoutelements/Heading.php b/src/fieldlayoutelements/Heading.php new file mode 100644 index 00000000000..5a6f10793ad --- /dev/null +++ b/src/fieldlayoutelements/Heading.php @@ -0,0 +1,67 @@ + + * @since 3.5.0 + */ +class Heading extends FieldLayoutElement +{ + /** + * @var string The heading text + */ + public $heading; + + /** + * @inheritdoc + */ + public function selectorHtml(): string + { + $text = Html::tag('div', Html::encode($this->heading ?: Craft::t('app', 'Heading')), [ + 'class' => 'fld-element-label', + ]); + + return << +
+ $text + +HTML; + } + + /** + * @inheritdoc + */ + public function settingsHtml() + { + return Craft::$app->getView()->renderTemplateMacro('_includes/forms', 'textField', [ + [ + 'label' => Craft::t('app', 'Heading'), + 'id' => 'heading', + 'name' => 'heading', + 'value' => $this->heading, + ] + ]); + } + + /** + * @inheritdoc + */ + public function formHtml(ElementInterface $element = null, bool $static = false) + { + return Html::tag('h2', Html::encode($this->heading)); + } +} diff --git a/src/fieldlayoutelements/HorizontalRule.php b/src/fieldlayoutelements/HorizontalRule.php new file mode 100644 index 00000000000..2bfcc666be2 --- /dev/null +++ b/src/fieldlayoutelements/HorizontalRule.php @@ -0,0 +1,43 @@ + + * @since 3.5.0 + */ +class HorizontalRule extends FieldLayoutElement +{ + /** + * @inheritdoc + */ + public function selectorHtml(): string + { + $label = Craft::t('app', 'Horizontal Rule'); + return << +
$label
+ +HTML; + } + + /** + * @inheritdoc + */ + public function formHtml(ElementInterface $element = null, bool $static = false) + { + return Html::tag('hr'); + } +} diff --git a/src/fieldlayoutelements/StandardField.php b/src/fieldlayoutelements/StandardField.php new file mode 100644 index 00000000000..58d3714d64f --- /dev/null +++ b/src/fieldlayoutelements/StandardField.php @@ -0,0 +1,154 @@ + + * @since 3.5.0 + */ +abstract class StandardField extends BaseField +{ + /** + * @var bool Whether the field *must* be present within the layout. + */ + public $mandatory = false; + + /** + * @var bool Whether the field can optionally be marked as required. + */ + public $requirable = false; + + /** + * @var string the element attribute this field is for. + */ + public $attribute; + + /** + * @var string|null The input’s `id` attribute value. + * + * If this is not set, [[attribute()]] will be used by default. + */ + public $id; + + /** + * @var array HTML attributes for the field container + */ + public $containerAttributes = []; + + /** + * @var array HTML attributes for the input container + */ + public $inputContainerAttributes = []; + + /** + * @var string|null The ID of the field label + */ + public $labelAttributes = []; + + /** + * @var string|null The field’s orientation (`ltr` or `rtl`) + */ + public $orientation; + + /** + * @var bool Whether the field is translatable + */ + public $translatable = false; + + /** + * @inheritdoc + */ + public function attribute(): string + { + return $this->attribute; + } + + /** + * @inheritdoc + */ + public function mandatory(): bool + { + return $this->mandatory; + } + + /** + * @inheritdoc + */ + public function requirable(): bool + { + return $this->requirable; + } + + /** + * @inheritdoc + */ + protected function id(): string + { + return $this->id ?? parent::id(); + } + + /** + * @inheritdoc + */ + protected function containerAttributes(ElementInterface $element = null, bool $static = false): array + { + return $this->containerAttributes; + } + + /** + * @inheritdoc + */ + protected function inputContainerAttributes(ElementInterface $element = null, bool $static = false): array + { + return $this->inputContainerAttributes; + } + + /** + * @inheritdoc + */ + protected function labelAttributes(ElementInterface $element = null, bool $static = false): array + { + return $this->labelAttributes; + } + + /** + * @inheritdoc + */ + protected function tip(ElementInterface $element = null, bool $static = false) + { + return $this->tip; + } + + /** + * @inheritdoc + */ + protected function warning(ElementInterface $element = null, bool $static = false) + { + return $this->warning; + } + + /** + * @inheritdoc + */ + protected function orientation(ElementInterface $element = null, bool $static = false): string + { + return $this->orientation ?? parent::orientation(); + } + + /** + * @inheritdoc + */ + protected function translatable(ElementInterface $element = null, bool $static = false): bool + { + return $this->translatable; + } +} diff --git a/src/fieldlayoutelements/StandardTextField.php b/src/fieldlayoutelements/StandardTextField.php new file mode 100644 index 00000000000..b97efefc2b7 --- /dev/null +++ b/src/fieldlayoutelements/StandardTextField.php @@ -0,0 +1,146 @@ + + * @since 3.5.0 + */ +class StandardTextField extends StandardField +{ + /** + * @var string The input type + */ + public $type = 'text'; + + /** + * @var string|bool|null The input’s `autocomplete` attribute value. + * + * This can be set to `true` (`"on"`), `false` ("off")`, or any other allowed `autocomplete` value. + * + * If this is explicitly set to `null`, the input won’t get an `autocomplete` attribute. + */ + public $autocomplete = false; + + /** + * @var string|string[]|null The input’s `class` attribute value. + */ + public $class; + + /** + * @var int|null The input’s `size` attribute value. + */ + public $size; + + /** + * @var string|null The input’s `name` attribute value. + * + * If this is not set, [[attribute()]] will be used by default. + */ + public $name; + + /** + * @var int|null The input’s `maxlength` attribute value. + */ + public $maxlength; + + /** + * @var bool Whether the input should get an `autofocus` attribute. + */ + public $autofocus = false; + + /** + * @var bool Whether the input should support autocorrect. + */ + public $autocorrect = true; + + /** + * @var bool Whether the input should support auto-capitalization. + */ + public $autocapitalize = true; + + /** + * @var bool Whether the input should get a `disabled` attribute. + */ + public $disabled = false; + + /** + * @var bool Whether the input should get a `readonly` attribute. + */ + public $readonly = false; + + /** + * @var string|null The input’s `title` attribute value. + */ + public $title; + + /** + * @var string|null The input’s `placeholder` attribute value. + */ + public $placeholder; + + /** + * @var int|null The input’s `step` attribute value. + */ + public $step; + + /** + * @var int|null The input’s `min` attribute value. + */ + public $min; + + /** + * @var int|null The input’s `max` attribute value. + */ + public $max; + + /** + * @inheritdoc + */ + public function fields() + { + $fields = parent::fields(); + + // Don't include the value + unset($fields['value']); + + return $fields; + } + + /** + * @inheritdoc + */ + protected function inputHtml(ElementInterface $element = null, bool $static = false) + { + return Craft::$app->getView()->renderTemplate('_includes/forms/text', [ + 'type' => $this->type, + 'autocomplete' => $this->autocomplete, + 'class' => $this->class, + 'id' => $this->id(), + 'size' => $this->size, + 'name' => $this->name ?? $this->attribute(), + 'value' => $this->value($element), + 'maxlength' => $this->maxlength, + 'autofocus' => $this->autofocus, + 'autocorrect' => $this->autocorrect, + 'autocapitalize' => $this->autocapitalize, + 'disabled' => $static || $this->disabled, + 'readonly' => $this->readonly, + 'title' => $this->title, + 'placeholder' => $this->placeholder, + 'step' => $this->step, + 'min' => $this->min, + 'max' => $this->max, + ]); + } +} diff --git a/src/fieldlayoutelements/Tip.php b/src/fieldlayoutelements/Tip.php new file mode 100644 index 00000000000..725a97ff6d5 --- /dev/null +++ b/src/fieldlayoutelements/Tip.php @@ -0,0 +1,103 @@ + + * @since 3.5.0 + */ +class Tip extends FieldLayoutElement +{ + const STYLE_TIP = 'tip'; + const STYLE_WARNING = 'warning'; + + /** + * @var string The tip text + */ + public $tip; + + /** + * @var string The tip style (`tip` or `warning`) + */ + public $style = self::STYLE_TIP; + + /** + * @inheritdoc + */ + public function selectorHtml(): string + { + $icon = Html::tag('div', '', [ + 'class' => array_filter([ + 'fld-element-icon', + !$this->_isTip() ? 'fld-tip-warning' : null, + ]), + ]); + $text = Html::tag('div', $this->_isTip() ? Craft::t('app', 'Tip') : Craft::t('app', 'Warning'), [ + 'class' => 'fld-element-label', + ]); + + return << + $icon + $text + +HTML; + } + + /** + * @inheritdoc + */ + public function settingsHtml() + { + return Craft::$app->getView()->renderTemplateMacro('_includes/forms', 'textareaField', [ + [ + 'label' => $this->_isTip() ? Craft::t('app', 'Tip') : Craft::t('app', 'Warning'), + 'instructions' => Craft::t('app', 'Can contain Markdown formatting.'), + 'class' => 'nicetext', + 'id' => 'tip', + 'name' => 'tip', + 'value' => $this->tip, + ] + ]); + } + + /** + * @inheritdoc + */ + public function formHtml(ElementInterface $element = null, bool $static = false) + { + $noteClass = $this->_isTip() ? self::STYLE_TIP : self::STYLE_WARNING; + $tip = Markdown::process(Html::encode($this->tip)); + + return << +
+ $tip +
+ +HTML; + } + + /** + * Returns whether this should have a tip style. + * + * @return bool + */ + private function _isTip(): bool + { + return $this->style !== self::STYLE_WARNING; + } +} diff --git a/src/fieldlayoutelements/TitleField.php b/src/fieldlayoutelements/TitleField.php new file mode 100644 index 00000000000..489051b511f --- /dev/null +++ b/src/fieldlayoutelements/TitleField.php @@ -0,0 +1,63 @@ + + * @since 3.5.0 + */ +class TitleField extends StandardTextField +{ + /** + * @inheritdoc + */ + public $mandatory = true; + + /** + * @inheritdoc + */ + public $attribute = 'title'; + + /** + * @inheritdoc + */ + public $translatable = true; + + /** + * @inheritdoc + */ + public $maxlength = 255; + + /** + * @inheritdoc + */ + public function fields() + { + $fields = parent::fields(); + unset( + $fields['mandatory'], + $fields['attribute'], + $fields['translatable'], + $fields['maxlength'] + ); + return $fields; + } + + /** + * @inheritdoc + */ + public function defaultLabel(ElementInterface $element = null, bool $static = false) + { + return Craft::t('app', 'Title'); + } +} diff --git a/src/migrations/Install.php b/src/migrations/Install.php index 669ddeaee75..2c9e10ae66f 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -315,8 +315,6 @@ public function createTables() 'name' => $this->string()->notNull(), 'handle' => $this->string()->notNull(), 'hasTitleField' => $this->boolean()->defaultValue(true)->notNull(), - 'titleLabel' => $this->string()->defaultValue('Title'), - 'titleInstructions' => $this->text(), 'titleTranslationMethod' => $this->string()->notNull()->defaultValue(Field::TRANSLATION_METHOD_SITE), 'titleTranslationKeyFormat' => $this->text(), 'titleFormat' => $this->string(), @@ -356,6 +354,7 @@ public function createTables() 'id' => $this->primaryKey(), 'layoutId' => $this->integer()->notNull(), 'name' => $this->string()->notNull(), + 'elements' => $this->text(), 'sortOrder' => $this->smallInteger()->unsigned(), 'dateCreated' => $this->dateTime()->notNull(), 'dateUpdated' => $this->dateTime()->notNull(), diff --git a/src/migrations/m200619_212955_title_instructions.php b/src/migrations/m200619_212955_title_instructions.php deleted file mode 100644 index b92fb0431bc..00000000000 --- a/src/migrations/m200619_212955_title_instructions.php +++ /dev/null @@ -1,30 +0,0 @@ -addColumn(Table::ENTRYTYPES, 'titleInstructions', $this->text()->after('titleLabel')); - } - - /** - * @inheritdoc - */ - public function safeDown() - { - echo "m200619_212955_title_instructions cannot be reverted.\n"; - return false; - } -} diff --git a/src/migrations/m200619_215137_title_translation_method.php b/src/migrations/m200619_215137_title_translation_method.php index 175f9f0891b..6829796cbe6 100644 --- a/src/migrations/m200619_215137_title_translation_method.php +++ b/src/migrations/m200619_215137_title_translation_method.php @@ -17,7 +17,7 @@ class m200619_215137_title_translation_method extends Migration */ public function safeUp() { - $this->addColumn(Table::ENTRYTYPES, 'titleTranslationMethod', $this->string()->notNull()->defaultValue(Field::TRANSLATION_METHOD_SITE)->after('titleInstructions')); + $this->addColumn(Table::ENTRYTYPES, 'titleTranslationMethod', $this->string()->notNull()->defaultValue(Field::TRANSLATION_METHOD_SITE)->after('hasTitleField')); $this->addColumn(Table::ENTRYTYPES, 'titleTranslationKeyFormat', $this->text()->after('titleTranslationMethod')); } diff --git a/src/migrations/m200620_230205_field_layout_changes.php b/src/migrations/m200620_230205_field_layout_changes.php new file mode 100644 index 00000000000..d32e5131d43 --- /dev/null +++ b/src/migrations/m200620_230205_field_layout_changes.php @@ -0,0 +1,102 @@ +addColumn(Table::FIELDLAYOUTTABS, 'elements', $this->text()->after('name')); + + $this->dropColumn(Table::ENTRYTYPES, 'titleLabel'); + if ($this->db->columnExists(Table::ENTRYTYPES, 'titleInstructions')) { + $this->dropColumn(Table::ENTRYTYPES, 'titleInstructions'); + } + + // Don't make the same config changes twice + $projectConfig = Craft::$app->getProjectConfig(); + $schemaVersion = $projectConfig->get('system.schemaVersion', true); + if (version_compare($schemaVersion, '3.5.8', '>=')) { + return; + } + + $this->_updateEntryTypeFieldLayouts($projectConfig); + } + + /** + * Adds a Title field to all entry type field layouts, configuring them with the prior custom title label & instructions. + * + * @param ProjectConfig $projectConfig + */ + private function _updateEntryTypeFieldLayouts(ProjectConfig $projectConfig) + { + foreach ($projectConfig->get('sections') ?? [] as $sectionUid => $sectionConfig) { + if (empty($sectionConfig['entryTypes'])) { + continue; + } + + foreach ($sectionConfig['entryTypes'] as $entryTypeUid => $entryTypeConfig) { + if (empty($entryTypeConfig['fieldLayouts'])) { + continue; + } + + foreach ($entryTypeConfig['fieldLayouts'] as $fieldLayoutUid => &$fieldLayoutConfig) { + // Make sure there's at least one tab + if (empty($fieldLayoutConfig['tabs'])) { + $fieldLayoutConfig['tabs'] = [ + 'name' => 'Content', + 'sortOrder' => 1, + ]; + } + + // Update the tab configs to the new format + foreach ($fieldLayoutConfig['tabs'] as &$tabConfig) { + FieldLayoutTab::updateConfig($tabConfig); + } + + // Add the Title field to the first tab + $firstTab = &$fieldLayoutConfig['tabs'][0]; + if (!isset($firstTab['elements'])) { + $firstTab['elements'] = []; + } + array_unshift($firstTab['elements'], [ + 'type' => EntryTitleField::class, + 'label' => $entryTypeConfig['titleLabel'] ?? null, + 'instructions' => $entryTypeConfig['titleInstructions'] ?? null, + ]); + } + + unset( + $entryTypeConfig['titleLabel'], + $entryTypeConfig['titleInstructions'] + ); + + $projectConfig->set("sections.$sectionUid.entryTypes.$entryTypeUid", $entryTypeConfig); + } + } + } + + /** + * @inheritdoc + */ + public function safeDown() + { + echo "m200620_230205_field_layout_changes cannot be reverted.\n"; + return false; + } +} diff --git a/src/models/EntryType.php b/src/models/EntryType.php index b349f34cfb0..1a92696b2d3 100644 --- a/src/models/EntryType.php +++ b/src/models/EntryType.php @@ -57,17 +57,6 @@ class EntryType extends Model */ public $hasTitleField = true; - /** - * @var string Title label - */ - public $titleLabel = 'Title'; - - /** - * @var string|null Title instructions - * @since 3.5.0 - */ - public $titleInstructions; - /** * @var string Title translation method * @since 3.5.0 @@ -112,7 +101,6 @@ public function attributeLabels() 'handle' => Craft::t('app', 'Handle'), 'name' => Craft::t('app', 'Name'), 'titleFormat' => Craft::t('app', 'Title Format'), - 'titleLabel' => Craft::t('app', 'Title Field Label'), ]; } @@ -145,9 +133,7 @@ protected function defineRules(): array 'comboNotUnique' => Craft::t('yii', '{attribute} "{value}" has already been taken.'), ]; - if ($this->hasTitleField) { - $rules[] = [['titleLabel'], 'required']; - } else { + if (!$this->hasTitleField) { $rules[] = [['titleFormat'], 'required']; } diff --git a/src/models/FieldLayout.php b/src/models/FieldLayout.php index 08e10576268..4ed5fb73a51 100644 --- a/src/models/FieldLayout.php +++ b/src/models/FieldLayout.php @@ -8,8 +8,17 @@ namespace craft\models; use Craft; +use craft\base\ElementInterface; use craft\base\FieldInterface; +use craft\base\FieldLayoutElementInterface; use craft\base\Model; +use craft\events\DefineFieldLayoutFieldsEvent; +use craft\fieldlayoutelements\BaseField; +use craft\fieldlayoutelements\CustomField; +use craft\fieldlayoutelements\Heading; +use craft\fieldlayoutelements\HorizontalRule; +use craft\fieldlayoutelements\Tip; +use yii\base\InvalidArgumentException; /** * FieldLayout model class. @@ -19,13 +28,47 @@ */ class FieldLayout extends Model { + /** + * @event DefineFieldLayoutElementsEvent The event that is triggered when defining the standard (not custom) fields for the layout. + * @see getAvailableStandardFields() + * @since 3.5.0 + */ + const EVENT_DEFINE_STANDARD_FIELDS = 'defineStandardFields'; + + /** + * Creates a new field layout from the given config. + * + * @param array $config + * @return self + * @since 3.1.0 + */ + public static function createFromConfig(array $config): self + { + $layout = new self(); + $tabs = []; + + if (!empty($config['tabs']) && is_array($config['tabs'])) { + foreach ($config['tabs'] as $tabConfig) { + $tab = FieldLayoutTab::createFromConfig($tabConfig); + + // Ignore empty tabs + if (!empty($tab->elements)) { + $tabs[] = $tab; + } + } + } + + $layout->setTabs($tabs); + return $layout; + } + /** * @var int|null ID */ public $id; /** - * @var string|null Type + * @var string|null The element type */ public $type; @@ -35,15 +78,36 @@ class FieldLayout extends Model public $uid; /** - * @var + * @var BaseField[][] + * @see getAvailableCustomFields() + */ + private $_availableCustomFields; + + /** + * @var BaseField[][] + * @see getAvailableStandardFields() + */ + private $_availableStandardFields; + + /** + * @var FieldLayoutTab[] */ private $_tabs; /** - * @var + * @var BaseField[] + * @see getTabs() + * @see isFieldIncluded() */ private $_fields; + /** + * @var FieldInterface[] + * @see getFields() + * @see setFields() + */ + private $_customFields; + /** * @inheritdoc */ @@ -65,109 +129,205 @@ public function getTabs(): array return $this->_tabs; } - if (!$this->id) { - return []; + $this->_fields = []; + + if ($this->id) { + $this->_tabs = Craft::$app->getFields()->getLayoutTabsById($this->id); + + // Take stock of all the selected layout elements + foreach ($this->_tabs as $tab) { + foreach ($tab->elements as $element) { + if ($element instanceof BaseField) { + $this->_fields[$element->attribute()] = $element; + } + } + } + } else { + $this->_tabs = []; } - return $this->_tabs = Craft::$app->getFields()->getLayoutTabsById($this->id); + // Make sure that we aren't missing any mandatory fields + /** @var BaseField[] $missingFields */ + $missingFields = []; + foreach ($this->getAvailableStandardFields() as $field) { + if ($field->mandatory() && !isset($this->_fields[$field->attribute()])) { + $missingFields[$field->attribute()] = $field; + $this->_fields[$field->attribute()] = $field; + } + } + + if (!empty($missingFields)) { + // Make sure there's at least one tab + $tab = reset($this->_tabs); + if (!$tab) { + $this->_tabs[] = $tab = new FieldLayoutTab([ + 'layoutId' => $this->id, + 'name' => Craft::t('app', 'Content'), + 'sortOrder' => 1, + 'elements' => [], + ]); + } + array_unshift($tab->elements, ...array_values($missingFields)); + } + + return $this->_tabs; } /** - * Return the field layout config or null if no fields configured. + * Sets the layout’s tabs. * - * @return array|null - * @since 3.1.0 + * @param array|FieldLayoutTab[] $tabs An array of the layout’s tabs, which can either be FieldLayoutTab + * objects or arrays defining the tab’s attributes. */ - public function getConfig() + public function setTabs($tabs) { - $output = []; + $this->_tabs = []; + foreach ($tabs as $tab) { + if (is_array($tab)) { + $tab = new FieldLayoutTab($tab); + } + $tab->setLayout($this); + $this->_tabs[] = $tab; + } + } - foreach ($this->getTabs() as $tab) { - $tabData = [ - 'name' => $tab->name, - 'sortOrder' => (int)$tab->sortOrder, - ]; - - foreach ($tab->getFields() as $field) { - $tabData['fields'][$field->uid] = [ - 'required' => (bool)$field->required, - 'sortOrder' => (int)$field->sortOrder - ]; + /** + * Returns the available fields, grouped by field group name. + * + * @return BaseField[][] + * @since 3.5.0 + */ + public function getAvailableCustomFields(): array + { + if ($this->_availableCustomFields === null) { + $this->_availableCustomFields = []; + + foreach (Craft::$app->getFields()->getAllGroups() as $group) { + $groupName = Craft::t('site', $group->name); + foreach ($group->getFields() as $field) { + $this->_availableCustomFields[$groupName][] = new CustomField($field); + } } - $output['tabs'][] = $tabData; } - return $output ?: null; + return $this->_availableCustomFields; } /** - * Return a field layout created from config data. + * Returns the available standard fields. * - * @param array $config Config data to use. - * @return self - * @since 3.1.0 + * @return BaseField[] + * @since 3.5.0 */ - public static function createFromConfig(array $config): self + public function getAvailableStandardFields(): array { - $layout = new self(); + if ($this->_availableStandardFields === null) { + $event = new DefineFieldLayoutFieldsEvent(); + $this->trigger(self::EVENT_DEFINE_STANDARD_FIELDS, $event); + $this->_availableStandardFields = $event->fields; + } + return $this->_availableStandardFields; + } - $tabs = []; - $fieldService = Craft::$app->getFields(); + /** + * Returns the layout elements that are available to the field layout, grouped by the type name and (optionally) group name. + * + * @return FieldLayoutElementInterface[] + * @since 3.5.0 + */ + public function getAvailableUiElements(): array + { + return [ + new Heading(), + new Tip([ + 'style' => Tip::STYLE_TIP, + ]), + new Tip([ + 'style' => Tip::STYLE_WARNING, + ]), + new HorizontalRule(), + ]; + } - if (!empty($config['tabs']) && is_array($config['tabs'])) { - foreach ($config['tabs'] as $tab) { - $layoutTab = new FieldLayoutTab(); - $layoutTab->name = $tab['name']; - $layoutTab->sortOrder = $tab['sortOrder']; - - if (!empty($tab['fields']) && is_array($tab['fields'])) { - $layoutFields = []; - - foreach ($tab['fields'] as $uid => $field) { - $createdField = $fieldService->getFieldByUid($uid); - - if ($createdField) { - $createdField->sortOrder = $field['sortOrder']; - $createdField->required = $field['required']; - $layoutFields[] = $createdField; - } - } + /** + * Returns whether a field is included in the layout by its attribute. + * + * @param string $attribute + * @return bool + * @since 3.5.0 + */ + public function isFieldIncluded(string $attribute): bool + { + $this->getTabs(); + return isset($this->_fields[$attribute]); + } - $layoutTab->setFields($layoutFields); - $tabs[] = $layoutTab; - } + /** + * Returns a field that’s included in the layout by its attribute. + * + * @param string $attribute + * @return BaseField + * @throws InvalidArgumentException if the field isn’t included + * @since 3.5.0 + */ + public function getField(string $attribute): BaseField + { + $this->getTabs(); + if (!isset($this->_fields[$attribute])) { + throw new InvalidArgumentException("Invalid field: $attribute"); + } + return $this->_fields[$attribute]; + } + + /** + * Return the field layout config or null if no fields configured. + * + * @return array|null + * @since 3.1.0 + */ + public function getConfig() + { + $tabConfigs = []; + + foreach ($this->getTabs() as $tab) { + $tabConfig = $tab->getConfig(); + if ($tabConfig) { + $tabConfigs[] = $tabConfig; } } - if (!empty($tabs)) { - $layout->setTabs($tabs); + if (empty($tabConfigs)) { + return null; } - return $layout; + return [ + 'tabs' => $tabConfigs, + ]; } /** - * Returns the layout’s fields. + * Returns the custom fields included in the layout. * - * @return FieldInterface[] The layout’s fields. + * @return FieldInterface[] */ public function getFields(): array { - if ($this->_fields !== null) { - return $this->_fields; + if ($this->_customFields !== null) { + return $this->_customFields; } if (!$this->id) { return []; } - return $this->_fields = Craft::$app->getFields()->getFieldsByLayoutId($this->id); + return $this->_customFields = Craft::$app->getFields()->getFieldsByLayoutId($this->id); } /** - * Returns the layout’s fields’ IDs. + * Returns the IDs of the custom fields included in the layout. * - * @return array The layout’s fields’ IDs. + * @return int[] + * @deprecated in 3.5.0. */ public function getFieldIds(): array { @@ -181,7 +341,7 @@ public function getFieldIds(): array } /** - * Returns a field by its handle. + * Returns a custom field by its handle. * * @param string $handle The field handle. * @return FieldInterface|null @@ -198,34 +358,47 @@ public function getFieldByHandle(string $handle) } /** - * Sets the layout’s tabs. + * Sets the custom fields included in this layout. * - * @param array|FieldLayoutTab[] $tabs An array of the layout’s tabs, which can either be FieldLayoutTab - * objects or arrays defining the tab’s attributes. + * @param FieldInterface[] $fields */ - public function setTabs($tabs) + public function setFields(array $fields) { - $this->_tabs = []; - - foreach ($tabs as $tab) { - if (is_array($tab)) { - $tab = new FieldLayoutTab($tab); - } - - $tab->setLayout($this); - $this->_tabs[] = $tab; - } + $this->_customFields = $fields; } /** - * Sets the layout's fields. + * Creates a new [[FieldLayoutForm]] object for the given element. * - * @param FieldInterface[] $fields An array of the layout’s fields, which can either be - * FieldLayoutFieldModel objects or arrays defining the tab’s - * attributes. + * @param ElementInterface|null $element The element the form is being rendered for + * @param bool $static Whether the form should be static (non-interactive) + * @param array $config The [[FieldLayoutForm]] config + * @return FieldLayoutForm + * @since 3.5.0 */ - public function setFields(array $fields) - { - $this->_fields = $fields; + public function createForm(ElementInterface $element = null, bool $static = false, array $config = []): FieldLayoutForm { + $form = new FieldLayoutForm($config); + + foreach ($this->getTabs() as $tab) { + $tabHtml = []; + + foreach ($tab->elements as $formElement) { + $elementHtml = $formElement->formHtml($element, $static); + if ($elementHtml !== null) { + $tabHtml[] = $elementHtml; + } + } + + if (!empty($tabHtml)) { + $form->tabs[] = new FieldLayoutFormTab([ + 'name' => Craft::t('site', $tab->name), + 'id' => $tab->getHtmlId(), + 'hasErrors' => $element && $tab->elementHasErrors($element), + 'content' => implode("\n", $tabHtml), + ]); + } + } + + return $form; } } diff --git a/src/models/FieldLayoutForm.php b/src/models/FieldLayoutForm.php new file mode 100644 index 00000000000..f937a5a94c4 --- /dev/null +++ b/src/models/FieldLayoutForm.php @@ -0,0 +1,82 @@ + + * @since 3.5.0 + */ +class FieldLayoutForm extends Model +{ + /** + * @var FieldLayoutFormTab[] The form’s tabs. + */ + public $tabs = []; + + /** + * @var string|null The prefix that should be applied to the tab’s HTML IDs. + */ + public $tabIdPrefix; + + /** + * Returns the tab menu config. + * + * @return array + */ + public function getTabMenu(): array + { + $menu = []; + foreach ($this->tabs as $tab) { + $tabId = $this->_tabId($tab->id); + $menu[$tabId] = [ + 'label' => $tab->name, + 'url' => "#$tabId", + 'class' => $tab->hasErrors ? 'error' : null, + ]; + } + return $menu; + } + + /** + * Renders the form content. + * + * @param bool $showFirst Whether the first tab should be shown initially + * @return string + */ + public function render(bool $showFirst = true): string + { + $html = []; + foreach ($this->tabs as $i => $tab) { + $show = $showFirst && $i === 0; + $html[] = Html::tag('div', $tab->content, [ + 'id' => $this->_tabId($tab->id), + 'class' => array_filter([ + 'flex-fields', + !$show ? 'hidden' : null, + ]), + ]); + } + return implode("\n", $html); + } + + /** + * Returns a tab’s prefixed HTML ID. + * + * @param string $tabId + * @return string + */ + private function _tabId(string $tabId): string + { + return ($this->tabIdPrefix ? "$this->tabIdPrefix-" : '') . $tabId; + } +} diff --git a/src/models/FieldLayoutFormTab.php b/src/models/FieldLayoutFormTab.php new file mode 100644 index 00000000000..9ec76403323 --- /dev/null +++ b/src/models/FieldLayoutFormTab.php @@ -0,0 +1,39 @@ + + * @since 3.5.0 + */ +class FieldLayoutFormTab extends Model +{ + /** + * @var string The tab’s name. + */ + public $name; + + /** + * @var string The tab’s HTML ID. + */ + public $id; + + /** + * @var bool Whether the tab has any validation errors. + */ + public $hasErrors = false; + + /** + * @var string The tab’s HTML content. + */ + public $content; +} diff --git a/src/models/FieldLayoutTab.php b/src/models/FieldLayoutTab.php index 2ebffb3c6d0..6c87b9c88cd 100644 --- a/src/models/FieldLayoutTab.php +++ b/src/models/FieldLayoutTab.php @@ -10,8 +10,13 @@ use Craft; use craft\base\ElementInterface; use craft\base\FieldInterface; +use craft\base\FieldLayoutElementInterface; use craft\base\Model; +use craft\fieldlayoutelements\CustomField; +use craft\helpers\ArrayHelper; +use craft\helpers\Json; use craft\helpers\StringHelper; +use yii\base\InvalidArgumentException; use yii\base\InvalidConfigException; /** @@ -22,6 +27,46 @@ */ class FieldLayoutTab extends Model { + /** + * Creates a new field layout tab from the given config. + * + * @param array $config + * @return self + * @since 3.5.0 + */ + public static function createFromConfig(array $config): self + { + static::updateConfig($config); + return new self($config); + } + + /** + * Updates a field layout tab’s config to the new format. + * + * @param array $config + * @since 3.5.0 + */ + public static function updateConfig(array &$config) + { + if (!array_key_exists('fields', $config)) { + return; + } + + $config['elements'] = []; + + ArrayHelper::multisort($config['fields'], 'sortOrder'); + foreach ($config['fields'] as $fieldUid => $fieldConfig) { + $config['elements'][] = [ + 'type' => CustomField::class, + 'fieldUid' => $fieldUid, + 'required' => (bool)$fieldConfig['required'], + ]; + } + + unset($config['fields']); + } + + /** * @var int|null ID */ @@ -37,6 +82,12 @@ class FieldLayoutTab extends Model */ public $name; + /** + * @var FieldLayoutElementInterface[]|null The tab’s layout elements + * @since 3.5.0 + */ + public $elements; + /** * @var int|null Sort order */ @@ -57,6 +108,39 @@ class FieldLayoutTab extends Model */ private $_fields; + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + + if ($this->elements === null) { + $this->elements = []; + foreach ($this->getFields() as $field) { + $this->elements[] = new CustomField($field, [ + 'required' => $field->required, + ]); + } + } else { + if (is_string($this->elements)) { + $this->elements = Json::decode($this->elements); + } + $fieldsService = Craft::$app->getFields(); + foreach ($this->elements as $i => $element) { + if (is_array($element)) { + try { + $this->elements[$i] = $fieldsService->createLayoutElement($element); + } catch (InvalidArgumentException $e) { + Craft::warning('Invalid field layout element config: ' . $e->getMessage(), __METHOD__); + Craft::$app->getErrorHandler()->logException($e); + unset($this->elements[$i]); + } + } + } + } + } + /** * @inheritdoc */ @@ -69,6 +153,40 @@ protected function defineRules(): array return $rules; } + /** + * Return the tab config. + * + * @return array|null + * @since 3.5.0 + */ + public function getConfig() + { + if (empty($this->elements)) { + return null; + } + + return [ + 'name' => $this->name, + 'sortOrder' => (int)$this->sortOrder, + 'elements' => $this->getElementConfigs(), + ]; + } + + /** + * Returns the tab’s elements’ configs. + * + * @return array[] + * @since 3.5.0 + */ + public function getElementConfigs(): array + { + $elementConfigs = []; + foreach ($this->elements as $element) { + $elementConfigs[] = ['type' => get_class($element)] + $element->toArray(); + } + return $elementConfigs; + } + /** * Returns the tab’s layout. * @@ -103,9 +221,9 @@ public function setLayout(FieldLayout $layout) } /** - * Returns the tab’s fields. + * Returns the custom fields included in this tab. * - * @return FieldInterface[] The tab’s fields. + * @return FieldInterface[] */ public function getFields(): array { @@ -127,17 +245,25 @@ public function getFields(): array } /** - * Sets the tab’s fields. + * Sets the custom fields included in this tab. * - * @param FieldInterface[] $fields The tab’s fields. + * @param FieldInterface[] $fields */ public function setFields(array $fields) { + ArrayHelper::multisort($fields, 'sortOrder'); $this->_fields = $fields; + + $this->elements = []; + foreach ($this->_fields as $field) { + $this->elements[] = new CustomField($field, [ + 'required' => $field->required, + ]); + } } /** - * Returns the tab’s anchor name. + * Returns the tab’s HTML ID. * * @return string */ @@ -147,7 +273,7 @@ public function getHtmlId(): string } /** - * Returns whether the given element has any validation errors within the fields in this tab. + * Returns whether the given element has any validation errors for the custom fields included in this tab. * * @param ElementInterface $element * @return bool diff --git a/src/records/EntryType.php b/src/records/EntryType.php index 4a974f237e5..4513cf85568 100644 --- a/src/records/EntryType.php +++ b/src/records/EntryType.php @@ -21,8 +21,6 @@ * @property string $name Name * @property string $handle Handle * @property bool $hasTitleField Has title field - * @property string|null $titleLabel Title label - * @property string|null $titleInstructions Title instructions * @property string $titleTranslationMethod Title translation method * @property string|null $titleTranslationKeyFormat Title translation key format * @property string|null $titleFormat Title format diff --git a/src/records/FieldLayoutTab.php b/src/records/FieldLayoutTab.php index 24556cd86ab..8947e9830d4 100644 --- a/src/records/FieldLayoutTab.php +++ b/src/records/FieldLayoutTab.php @@ -17,6 +17,7 @@ * @property int $id ID * @property int $layoutId Layout ID * @property string $name Name + * @property string|null $elements Layout elements * @property int $sortOrder Sort order * @property FieldLayout $layout Layout * @property FieldLayoutField[] $fields Fields diff --git a/src/services/Fields.php b/src/services/Fields.php index aa360f0232f..65da5410cc7 100644 --- a/src/services/Fields.php +++ b/src/services/Fields.php @@ -10,6 +10,7 @@ use Craft; use craft\base\Field; use craft\base\FieldInterface; +use craft\base\FieldLayoutElementInterface; use craft\behaviors\CustomFieldBehavior; use craft\db\Query; use craft\db\Table; @@ -20,6 +21,7 @@ use craft\events\FieldGroupEvent; use craft\events\FieldLayoutEvent; use craft\events\RegisterComponentTypesEvent; +use craft\fieldlayoutelements\CustomField; use craft\fields\Assets as AssetsField; use craft\fields\Categories as CategoriesField; use craft\fields\Checkboxes as CheckboxesField; @@ -55,6 +57,8 @@ use craft\records\FieldLayoutTab as FieldLayoutTabRecord; use yii\base\Component; use yii\base\Exception; +use yii\base\InvalidArgumentException; +use yii\web\BadRequestHttpException; /** * Fields service. @@ -1142,24 +1146,91 @@ public function getFieldsByLayoutId(int $layoutId): array return $fields; } + /** + * Creates a field layout element instance from its config. + * + * @param array $config + * @return FieldLayoutElementInterface + * @throws InvalidArgumentException if `$config['type']` does not implement [[FieldLayoutElementInterface]] + * @since 3.5.0 + */ + public function createLayoutElement(array $config): FieldLayoutElementInterface + { + $type = ArrayHelper::remove($config, 'type'); + + if (!$type || !is_subclass_of($type, FieldLayoutElementInterface::class)) { + throw new InvalidArgumentException("Invalid field layout element class: $type"); + } + + $element = new $type(); + Craft::configure($element, $config); + return $element; + } + /** * Assembles a field layout from post data. * * @param string|null $namespace The namespace that the form data was posted in, if any * @return FieldLayout The field layout + * @throws BadRequestHttpException */ public function assembleLayoutFromPost(string $namespace = null): FieldLayout { $paramPrefix = ($namespace ? rtrim($namespace, '.') . '.' : ''); $request = Craft::$app->getRequest(); + $layoutId = $request->getBodyParam("{$paramPrefix}fieldLayoutId"); + $elementPlacements = $request->getBodyParam("{$paramPrefix}elementPlacements"); + + if ($elementPlacements === null) { + // the JS probably didn't get fully initialized, so just go with the existing field layout if there is one + if ($layoutId) { + return $this->getLayoutById($layoutId); + } + return new FieldLayout(); + } + + $elementConfigs = $request->getBodyParam("{$paramPrefix}elementConfigs", []); + + $layout = new FieldLayout(); + $layout->id = $layoutId; + $tabs = []; + $fields = []; + $tabSortOrder = 0; + + foreach ($elementPlacements as $tabName => $elementKeys) { + $tab = $tabs[] = new FieldLayoutTab(); + $tab->name = urldecode($tabName); + $tab->sortOrder = ++$tabSortOrder; + $tab->elements = []; - $postedFieldLayout = $request->getBodyParam($paramPrefix . 'fieldLayout', []); - $requiredFields = $request->getBodyParam($paramPrefix . 'requiredFields', []); + foreach ($elementKeys as $i => $elementKey) { + $elementConfig = Json::decode($elementConfigs[$elementKey]); - $fieldLayout = $this->assembleLayout($postedFieldLayout, $requiredFields); - $fieldLayout->id = $request->getBodyParam($paramPrefix . 'fieldLayoutId'); + try { + $element = $this->createLayoutElement($elementConfig); + } catch (InvalidArgumentException $e) { + throw new BadRequestHttpException($e->getMessage(), 0, $e); + } + + $tab->elements[] = $element; - return $fieldLayout; + if ($element instanceof CustomField) { + $fieldUid = $element->getFieldUid(); + $field = $this->getFieldByUid($fieldUid); + if (!$field) { + throw new BadRequestHttpException("Invalid field UUID: $fieldUid"); + } + $field->required = (bool)($elementConfig['required'] ?? false); + $field->sortOrder = ($i + 1); + $fields[] = $field; + } + } + } + + $layout->setTabs($tabs); + $layout->setFields($fields); + + return $layout; } /** @@ -1168,6 +1239,7 @@ public function assembleLayoutFromPost(string $namespace = null): FieldLayout * @param array $postedFieldLayout The post data for the field layout * @param array $requiredFields The field IDs that should be marked as required in the field layout * @return FieldLayout The field layout + * @deprecated in 3.5.0. */ public function assembleLayout(array $postedFieldLayout, array $requiredFields = []): FieldLayout { @@ -1199,7 +1271,6 @@ public function assembleLayout(array $postedFieldLayout, array $requiredFields = foreach ($postedFieldLayout as $tabName => $fieldIds) { $tabFields = []; - $tabSortOrder++; foreach ($fieldIds as $fieldSortOrder => $fieldId) { if (!isset($allFieldsById[$fieldId])) { @@ -1216,7 +1287,7 @@ public function assembleLayout(array $postedFieldLayout, array $requiredFields = $tab = new FieldLayoutTab(); $tab->name = urldecode($tabName); - $tab->sortOrder = $tabSortOrder; + $tab->sortOrder = ++$tabSortOrder; $tab->setFields($tabFields); $tabs[] = $tab; @@ -1246,11 +1317,6 @@ public function saveLayout(FieldLayout $layout, bool $runValidation = true): boo $isNewLayout = !$layout->id; - // Make sure the tabs/fields are memoized on the layout - foreach ($layout->getTabs() as $tab) { - $tab->getFields(); - } - // Fire a 'beforeSaveFieldLayout' event if ($this->hasEventHandlers(self::EVENT_BEFORE_SAVE_FIELD_LAYOUT)) { $this->trigger(self::EVENT_BEFORE_SAVE_FIELD_LAYOUT, new FieldLayoutEvent([ @@ -1323,18 +1389,29 @@ public function saveLayout(FieldLayout $layout, bool $runValidation = true): boo } else { $tabRecord->name = $tab->name; } + $tabRecord->elements = $tab->getElementConfigs(); $tabRecord->save(false); $tab->id = $tabRecord->id; $tab->uid = $tabRecord->uid; - foreach ($tab->getFields() as $field) { - $fieldRecord = new FieldLayoutFieldRecord(); - $fieldRecord->layoutId = $layout->id; - $fieldRecord->tabId = $tab->id; - $fieldRecord->fieldId = $field->id; - $fieldRecord->required = (bool)$field->required; - $fieldRecord->sortOrder = $field->sortOrder; - $fieldRecord->save(false); + foreach ($tab->elements as $i => $element) { + if ($element instanceof CustomField) { + $fieldUid = $element->getFieldUid(); + $field = $this->getFieldByUid($fieldUid); + + if (!$field) { + Craft::warning("Invalid field UUID: $fieldUid", __METHOD__); + continue; + } + + $fieldRecord = new FieldLayoutFieldRecord(); + $fieldRecord->layoutId = $layout->id; + $fieldRecord->tabId = $tab->id; + $fieldRecord->fieldId = $field->id; + $fieldRecord->required = (bool)$element->required; + $fieldRecord->sortOrder = $i; + $fieldRecord->save(false); + } } } @@ -1684,7 +1761,7 @@ private function _createLayoutQuery(): Query */ private function _createLayoutTabQuery(): Query { - return (new Query()) + $query = (new Query()) ->select([ 'id', 'layoutId', @@ -1694,6 +1771,14 @@ private function _createLayoutTabQuery(): Query ]) ->from([Table::FIELDLAYOUTTABS]) ->orderBy(['sortOrder' => SORT_ASC]); + + // todo: remove schema version condition after next beakpoint + $schemaVersion = Craft::$app->getInstalledSchemaVersion(); + if (version_compare($schemaVersion, '3.5.8', '>=')) { + $query->addSelect(['elements']); + } + + return $query; } /** diff --git a/src/services/ProjectConfig.php b/src/services/ProjectConfig.php index a4147fe1a75..4cf3fa9c4d9 100644 --- a/src/services/ProjectConfig.php +++ b/src/services/ProjectConfig.php @@ -1838,7 +1838,6 @@ private function _getEntryTypeData(): array 'entrytypes.name', 'entrytypes.handle', 'entrytypes.hasTitleField', - 'entrytypes.titleLabel', 'entrytypes.titleFormat', 'entrytypes.sortOrder', 'entrytypes.uid', diff --git a/src/services/Sections.php b/src/services/Sections.php index 73cb55b378e..f72016c39c2 100644 --- a/src/services/Sections.php +++ b/src/services/Sections.php @@ -542,12 +542,9 @@ public function saveSection(Section $section, bool $runValidation = true): bool if ($section->type === Section::TYPE_SINGLE) { $entryType->hasTitleField = false; - $entryType->titleLabel = null; - $entryType->titleInstructions = null; $entryType->titleFormat = '{section.name|raw}'; } else { $entryType->hasTitleField = true; - $entryType->titleLabel = Craft::t('app', 'Title'); $entryType->titleFormat = null; } @@ -1162,8 +1159,6 @@ public function saveEntryType(EntryType $entryType, bool $runValidation = true): 'name' => $entryType->name, 'handle' => $entryType->handle, 'hasTitleField' => (bool)$entryType->hasTitleField, - 'titleLabel' => $entryType->titleLabel, - 'titleInstructions' => $entryType->titleInstructions, 'titleTranslationMethod' => $entryType->titleTranslationMethod, 'titleTranslationKeyFormat' => $entryType->titleTranslationKeyFormat, 'titleFormat' => $entryType->titleFormat, @@ -1226,8 +1221,6 @@ public function handleChangedEntryType(ConfigEvent $event) $entryTypeRecord->name = $data['name']; $entryTypeRecord->handle = $data['handle']; $entryTypeRecord->hasTitleField = $data['hasTitleField']; - $entryTypeRecord->titleLabel = $data['titleLabel']; - $entryTypeRecord->titleInstructions = $data['titleInstructions'] ?? null; $entryTypeRecord->titleTranslationMethod = $data['titleTranslationMethod'] ?? ''; $entryTypeRecord->titleTranslationKeyFormat = $data['titleTranslationKeyFormat'] ?? null; $entryTypeRecord->titleFormat = $data['titleFormat']; @@ -1657,7 +1650,6 @@ private function _createEntryTypeQuery() 'name', 'handle', 'hasTitleField', - 'titleLabel', 'titleFormat', 'uid', ]) @@ -1670,7 +1662,6 @@ private function _createEntryTypeQuery() } if (version_compare($schemaVersion, '3.5.4', '>=')) { $query->addSelect([ - 'titleInstructions', 'titleTranslationMethod', 'titleTranslationKeyFormat', ]); diff --git a/src/templates/_components/widgets/QuickPost/body.html b/src/templates/_components/widgets/QuickPost/body.html index 48cb2c2842f..ad84c55d8cf 100644 --- a/src/templates/_components/widgets/QuickPost/body.html +++ b/src/templates/_components/widgets/QuickPost/body.html @@ -1,14 +1,16 @@ {% import "_includes/forms" as forms %} {% set fieldNamespace = 'fields'~random() %} - +{% set fieldLayout = entryType.getFieldLayout() %}
{{ hiddenInput('fieldsLocation', fieldNamespace) }} {{ csrfInput() }} {% if section.type != 'single' and entryType.hasTitleField %} + {% set titleField = fieldLayout.getField('title') %} {{ forms.textField({ - label: entryType.titleLabel|t('app'), + label: titleField.label|t('site') ?: 'Title'|t('app'), + instructions: titleField.instructions|t('site'), altLabel: tag('code', {text: 'title'}), id: 'title', name: 'title', @@ -18,7 +20,7 @@ {% endif %} {% namespace fieldNamespace %} - {% for field in entryType.getFieldLayout().getFields() %} + {% for field in fieldLayout.getFields() %} {% if field.required or field.id in widget.fields %} {% include "_includes/field" with { field: field, diff --git a/src/templates/_includes/fieldlayoutdesigner.html b/src/templates/_includes/fieldlayoutdesigner.html index d4809637d9e..9a8a541fe1d 100644 --- a/src/templates/_includes/fieldlayoutdesigner.html +++ b/src/templates/_includes/fieldlayoutdesigner.html @@ -1,128 +1,9 @@ -{% set customizableTabs = customizableTabs ?? true %} -{% if not customizableTabs and pretendTabName is not defined %} - {% set pretendTabName = "Content"|t('app') %} -{% endif %} - -{% set groups = craft.app.fields.getAllGroups() %} - -{% do view.registerTranslations('app', [ - "Rename", - "Delete", - "Make required", - "Make not required", - "Remove", - "Give your tab a name.", -]) %} - -{% macro tab(tabName, tabFields, context) %} -
-
-
- {{ tabName }} - {% if context.customizableTabs %} - - {% endif %} -
-
-
- {% for field in tabFields %} -
- - {{ field.name|t('site') }} - {{ hiddenInput("fieldLayout[#{tabName|e('url')}][]", field.id, {class: 'id-input'}) }} - {% if field.required %}{{ hiddenInput('requiredFields[]', field.id, {class: 'required-input'}) }}{% endif %} -
- {% endfor %} -
-
-{% endmacro %} - - - -
- {% if fieldLayout and fieldLayout.id %} - {{ hiddenInput('fieldLayoutId', fieldLayout.id) }} - {% endif %} - -

{{ "Design your field layout"|t('app') }}

- - {% if instructions is defined %} -
{{ instructions|md }}
- {% endif %} - -
- {% if fieldLayout %} - {% if customizableTabs %} - {% for tab in fieldLayout.getTabs() %} - {{ _self.tab(tab.name, tab.getFields(), _context) }} - {% endfor %} - {% else %} - {{ _self.tab(pretendTabName, fieldLayout.getFields(), _context) }} - {% endif %} - {% endif %} -
- - {% if customizableTabs %} -
-
{{ "New Tab"|t('app') }}
-

{{ "…Or use one of your field groups as a starting point:"|t('app') }}

-
- {% endif %} - -
- {% for group in groups %} - {% set totalUnselected = 0 %} - {% for field in group.fields %} - {% if not fieldLayout or field.id not in fieldLayout.getFieldIds() %} - {% set totalUnselected = totalUnselected + 1 %} - {% endif %} - {% endfor %} - -
-
-
{{ group.name }}
-
-
- {% for field in group.fields %} - {% set selected = fieldLayout and field.id in fieldLayout.getFieldIds() %} -
- {{ field.name|t('site') }} -
- {% endfor %} -
-
- {% endfor %} -
- -
- -{% set jsSettings = { - customizableTabs: customizableTabs, - fieldInputName: 'fieldLayout[__TAB_NAME__][]'|namespaceInputName, - requiredFieldInputName: 'requiredFields[]'|namespaceInputName, -} %} - -{% js %} - var initFLD = function() { - new Craft.FieldLayoutDesigner("#{{ 'fieldlayoutform'|namespaceInputId }}", {{ jsSettings|json_encode|raw }}); - }; - - {% if tab is defined %} - - var $fldTab = $('#{{ "tab-#{tab}"|namespaceInputId }}'); - - if ($fldTab.hasClass('sel')) { - initFLD(); - } else { - $fldTab.on('activate.fld', function() { - initFLD(); - $fldTab.off('activate.fld'); - }); - } - - {% else %} - - initFLD(); - - {% endif %} -{% endjs %} +{% from '_includes/forms' import fieldLayoutDesignerField %} + +{{ fieldLayoutDesignerField({ + instructions: instructions ?? null, + customizableTabs: customizableTabs ?? true, + customizableUi: customizableUi ?? false, + pretendTabName: pretendTabName ?? 'Content'|t('app'), + fieldLayout: fieldLayout ?? null, +}) }} diff --git a/src/templates/_includes/forms.html b/src/templates/_includes/forms.html index 28c675465d2..6ae7ef48742 100644 --- a/src/templates/_includes/forms.html +++ b/src/templates/_includes/forms.html @@ -106,6 +106,11 @@ {% endmacro %} +{% macro fieldLayoutDesigner(config) %} + {% include "_includes/forms/fieldLayoutDesigner" with config only %} +{% endmacro %} + + {# Fields #} @@ -262,6 +267,13 @@ {% endmacro %} +{% macro fieldLayoutDesignerField(config) %} + {{ _self.field({ + label: 'Field Layout'|t('app'), + }|merge(config), _self.fieldLayoutDesigner(config)) }} +{% endmacro %} + + {# Other #} diff --git a/src/templates/_includes/forms/field.html b/src/templates/_includes/forms/field.html index 63fa30c3192..ed8fbaae7ce 100644 --- a/src/templates/_includes/forms/field.html +++ b/src/templates/_includes/forms/field.html @@ -9,7 +9,7 @@ {%- set instructions = instructions ?? block('instructions') ?? null %} {%- set tip = tip ?? block('tip') ?? null %} {%- set warning = warning ?? block('warning') ?? null %} -{%- set orientation = (site ? craft.app.i18n.getLocaleById(site.language) : craft.app.locale).getOrientation() %} +{%- set orientation = orientation ?? (site ? craft.app.i18n.getLocaleById(site.language) : craft.app.locale).getOrientation() %} {%- set translatable = translatable ?? (site is not same as(null)) %} {%- set errors = (errors is defined ? errors : null) -%} {%- set fieldClass = (fieldClass ?? [])|explodeClass|merge([ diff --git a/src/templates/_includes/forms/fieldLayoutDesigner.html b/src/templates/_includes/forms/fieldLayoutDesigner.html new file mode 100644 index 00000000000..28bcd6b1ad0 --- /dev/null +++ b/src/templates/_includes/forms/fieldLayoutDesigner.html @@ -0,0 +1,169 @@ +{% import '_includes/forms' as forms %} + +{% set customizableTabs = customizableTabs ?? true %} +{% set customizableUi = customizableUi ?? true %} +{% set pretendTabName = pretendTabName ?? 'Content'|t('app') %} +{% set fieldLayout = fieldLayout ?? create('craft\\models\\FieldLayout') %} + +{% set groups = craft.app.fields.getAllGroups() %} + +{% do view.registerTranslations('app', [ + 'Apply', + 'Delete', + 'Give your tab a name.', + 'Move to the left', + 'Move to the right', + 'Remove', + 'Rename', + 'Required', + '{pct} width', +]) %} + +{% macro tab(tabName, elements, context) %} +
+
+
+ {{ tabName }} + {% if context.customizableTabs %} + + {% endif %} +
+
+
+ {% for element in elements %} + {{ _self.elementSelector(element, false) }} + {% endfor %} +
+
+{% endmacro %} + +{% macro elementSelector(element, forLibrary, attr) %} + {% if element is instance of('craft\\fieldlayoutelements\\BaseField') %} + {% set attr = { + class: [ + not forLibrary and element.required ? 'fld-required', + ]|filter, + data: { + attribute: element.attribute(), + mandatory: element.mandatory(), + requirable: element.requirable(), + keywords: forLibrary ? element.keywords()|join(' ')|lower : false, + }, + }|merge(attr ?? {}, recursive=true) %} + {% endif %} + {% set settingsHtml = element.settingsHtml() %} + {{ element.selectorHtml()|attr({ + class: [ + 'fld-element', + forLibrary ? 'unused', + ]|filter, + data: { + type: className(element), + config: element.toArray(), + 'settings-html': settingsHtml ? settingsHtml|namespaceAttributes("element-#{random()}") ?: false, + }, + }|merge(attr ?? {}, recursive=true)) }} +{% endmacro %} + +{% macro fieldSelectors(groupName, groupFields, context) %} + {% set showGroup = groupFields|contains(f => not context.fieldLayout.isFieldIncluded(f.attribute())) %} +
+
{{ groupName }}
+ {% for field in groupFields %} + {{ _self.elementSelector(field, true, { + class: [ + context.fieldLayout.isFieldIncluded(field.attribute()) ? 'hidden', + ], + }) }} + {% endfor %} +
+{% endmacro %} + +{% if fieldLayout.id %} + {{ hiddenInput('fieldLayoutId', fieldLayout.id) }} +{% endif %} + +
+
+
+ {% if customizableTabs %} + {% for tab in fieldLayout.getTabs() %} + {{ _self.tab(tab.name, tab.elements, _context) }} + {% endfor %} + {% else %} + {% set elements = [] %} + {% for tab in fieldLayout.getTabs() %} + {% for element in tab.elements %} + {% set elements = elements|push(element) %} + {% endfor %} + {% endfor %} + {{ _self.tab(pretendTabName, elements, _context) }} + {% endif %} +
+ + {% if customizableTabs %} +
{{ "New Tab"|t('app') }}
+ {% endif %} +
+ +
+ {% if customizableUi %} +
+
{{ 'Fields'|t('app') }}
+
{{ 'UI Elements'|t('app') }}
+
+ {% endif %} + +
+ + + {{ _self.fieldSelectors('Standard Fields'|t('app'), fieldLayout.getAvailableStandardFields(), _context) }} + + {% for groupName, groupFields in fieldLayout.getAvailableCustomFields() %} + {{ _self.fieldSelectors(groupName, groupFields, _context) }} + {% endfor %} +
+ + {% if customizableUi %} + + {% endif %} +
+
+ +{% set jsSettings = { + customizableTabs: customizableTabs, + customizableUi: customizableUi, + elementPlacementInputName: 'elementPlacements[__TAB_NAME__][]'|namespaceInputName, + elementConfigInputName: 'elementConfigs[__ELEMENT_KEY__]'|namespaceInputName, +} %} + +{% js %} + var initFLD = function() { + new Craft.FieldLayoutDesigner("#{{ 'fieldlayoutform'|namespaceInputId }}", {{ jsSettings|json_encode|raw }}); + }; + + {% if tab is defined %} + var $fldTab = $('#{{ "tab-#{tab}"|namespaceInputId }}'); + + if ($fldTab.hasClass('sel')) { + initFLD(); + } else { + $fldTab.on('activate.fld', function() { + initFLD(); + $fldTab.off('activate.fld'); + }); + } + {% else %} + initFLD(); + {% endif %} +{% endjs %} diff --git a/src/templates/_layouts/element.html b/src/templates/_layouts/element.html index ffe7bf6311d..fb19627bbd1 100644 --- a/src/templates/_layouts/element.html +++ b/src/templates/_layouts/element.html @@ -81,15 +81,10 @@ {% set saveShortcutRedirect = '{cpEditUrl}' %} {% endif %} +{% set form = element.getFieldLayout().createForm(element, isRevision or not canEdit) %} + {% if tabs is not defined %} - {% set tabs = [] %} - {% for tab in element.fieldLayout.tabs ?? [] %} - {% set tabs = tabs|merge([{ - label: tab.name|t('site'), - url: '#' ~ tab.getHtmlId(), - class: tab.elementHasErrors(element) ? 'error', - }]) %} - {% endfor %} + {% set tabs = form.getTabMenu() %} {% endif %} {% set settingsHtml = (block('settings') ?? '')|trim %} @@ -232,16 +227,7 @@ {% block content %}
- {% for tab in element.fieldLayout.tabs ?? [] %} - - {% endfor %} + {{ form.render()|raw }}
{% endblock %} diff --git a/src/templates/assets/_edit.html b/src/templates/assets/_edit.html index 9a4ba413278..ca843fac15c 100644 --- a/src/templates/assets/_edit.html +++ b/src/templates/assets/_edit.html @@ -26,22 +26,6 @@ {% block content %} {{ hiddenInput('assetId', element.id) }} - {{ forms.textField({ - label: 'Title'|t('app'), - altLabel: tag('code', {text: 'title'}), - siteId: element.siteId, - translationDescription: 'This field is translated for each site.'|t('app'), - id: 'title', - name: 'title', - value: element.title, - errors: canEdit and element.getErrors('title'), - first: true, - autofocus: true, - required: canEdit, - disabled: not canEdit, - maxlength: 255 - }) }} - {{ parent() }} {# Give plugins a chance to add other things here #} diff --git a/src/templates/categories/_edit.html b/src/templates/categories/_edit.html index 33d5b4c7691..d06997f1f5f 100644 --- a/src/templates/categories/_edit.html +++ b/src/templates/categories/_edit.html @@ -79,31 +79,7 @@ {% if craft.app.getIsMultiSite() %}{{ hiddenInput('siteId', category.siteId) }}{% endif %}
- {{ forms.textField({ - label: "Title"|t('app'), - altLabel: tag('code', {text: 'title'}), - siteId: category.siteId, - id: 'title', - name: 'title', - value: category.title, - errors: category.getErrors('title'), - first: true, - autofocus: true, - required: true, - maxlength: 255 - }) }} - -
- {% for tab in group.getFieldLayout().getTabs() %} - - {% endfor %} -
+ {{ fieldsHtml|raw }}
{# Give plugins a chance to add other things here #} diff --git a/src/templates/entries/_edit.html b/src/templates/entries/_edit.html index 59e73806f14..8c5eabc320a 100644 --- a/src/templates/entries/_edit.html +++ b/src/templates/entries/_edit.html @@ -46,11 +46,7 @@ {{ hiddenInput('revisionId', entry.revisionId) }} {% endif %} -
- {% include "entries/_fields" with { - static: isRevision - } %} -
+ {{ parent() }} {# Give plugins a chance to add other things here #} {% hook "cp.entries.edit.content" %} diff --git a/src/templates/entries/_fields.html b/src/templates/entries/_fields.html deleted file mode 100644 index 9f99ca1bdfd..00000000000 --- a/src/templates/entries/_fields.html +++ /dev/null @@ -1,20 +0,0 @@ -{% if entryType.hasTitleField or entry.hasErrors('title') %} - {% include "entries/_titlefield" %} -{% endif %} - -{% do view.setIsDeltaRegistrationActive(true) %} - -
- {% for tab in entryType.getFieldLayout().getTabs() %} - - {% endfor %} -
- -{% do view.setIsDeltaRegistrationActive(false) %} diff --git a/src/templates/entries/_titlefield.html b/src/templates/entries/_titlefield.html deleted file mode 100644 index 0c022ed6279..00000000000 --- a/src/templates/entries/_titlefield.html +++ /dev/null @@ -1,21 +0,0 @@ -{% from "_includes/forms" import textField %} -{% set static = static is defined ? static : false %} -{% set entryType = entry.getType() %} - -{{ textField({ - status: entry.getAttributeStatus('title'), - label: entryType.titleLabel|t('site')|e ?: 'Title'|t('app'), - altLabel: tag('code', {text: 'title'}), - instructions: entryType.titleInstructions|t('site')|e, - siteId: entryType.hasTitleField and entry.getIsTitleTranslatable() ? entry.siteId, - translationDescription: entryType.hasTitleField ? entry.getTitleTranslationDescription(), - id: 'title', - name: 'title', - value: entry.title, - errors: (not static ? entry.getErrors('title')), - first: true, - autofocus: true, - required: not static and entryType.hasTitleField, - disabled: static or not entryType.hasTitleField, - maxlength: 255 -}) }} diff --git a/src/templates/globals/_edit.html b/src/templates/globals/_edit.html index 3b12ae838d6..68e1cdfd45b 100644 --- a/src/templates/globals/_edit.html +++ b/src/templates/globals/_edit.html @@ -31,16 +31,9 @@ {{ hiddenInput('siteId', globalSet.siteId) }} {{ csrfInput() }} - {% if globalSet.getFieldLayout().getFields() | length %} -
- {% for tab in globalSet.getFieldLayout().getTabs() %} - - {% endfor %} + {% if globalSet.getFieldLayout().getTabs()|length %} +
+ {{ fieldsHtml|raw }}
{% else %} {{ "This global set doesn’t have any fields assigned to it in its field layout."|t('app') }} diff --git a/src/templates/settings/assets/volumes/_edit.html b/src/templates/settings/assets/volumes/_edit.html index b31a0ecf63a..b1c70d21fd7 100644 --- a/src/templates/settings/assets/volumes/_edit.html +++ b/src/templates/settings/assets/volumes/_edit.html @@ -6,94 +6,90 @@ {% block content %} - {{ actionInput('volumes/save-volume') }} - {{ redirectInput('settings/assets') }} - {% if not isNewVolume %}{{ hiddenInput('volumeId', volume.id) }}{% endif %} - -
- {{ forms.textField({ - first: true, - label: "Name"|t('app'), - id: 'name', - name: 'name', - value: (volume is defined ? volume.name : null), - errors: (volume is defined ? volume.getErrors('name') : null), - autofocus: true, - required: true, - }) }} - - {{ forms.textField({ - first: true, - label: "Handle"|t('app'), - id: 'handle', - name: 'handle', - class: 'code', - autocorrect: false, - autocapitalize: false, - value: (volume is defined ? volume.handle : null), - errors: (volume is defined ? volume.getErrors('handle') : null), - required: true, - }) }} - - {{ forms.lightswitchField({ - label: "Assets in this volume have public URLs"|t('app'), - name: 'hasUrls', - on: volume.hasUrls, - toggle: "url-field" - }) }} - -
- {{ forms.autosuggestField({ - label: "Base URL"|t('app'), - instructions: "The base URL to the assets in this volume."|t('app'), - id: 'url', - class: ['ltr', 'volume-url'], - name: 'url', - suggestEnvVars: true, - suggestAliases: true, - value: (volume is defined ? volume.url : null), - errors: (volume is defined ? volume.getErrors('url') : null), - required: true, - placeholder: "//example.com/path/to/folder" - }) }} -
- -
- - {{ forms.selectField({ - label: "Volume Type"|t('app'), - instructions: "What type of volume is this?"|t('app'), - id: 'type', - name: 'type', - options: volumeTypeOptions, - value: className(volume), - toggle: true - }) }} - - {{ missingVolumePlaceholder|raw }} - - {% for volumeType in volumeTypes %} - {% set isCurrent = (volumeType == className(volume)) %} - - - {% endfor %} + {{ actionInput('volumes/save-volume') }} + {{ redirectInput('settings/assets') }} + {% if not isNewVolume %}{{ hiddenInput('volumeId', volume.id) }}{% endif %} + + {{ forms.textField({ + first: true, + label: "Name"|t('app'), + id: 'name', + name: 'name', + value: (volume is defined ? volume.name : null), + errors: (volume is defined ? volume.getErrors('name') : null), + autofocus: true, + required: true, + }) }} + + {{ forms.textField({ + first: true, + label: "Handle"|t('app'), + id: 'handle', + name: 'handle', + class: 'code', + autocorrect: false, + autocapitalize: false, + value: (volume is defined ? volume.handle : null), + errors: (volume is defined ? volume.getErrors('handle') : null), + required: true, + }) }} + + {{ forms.lightswitchField({ + label: "Assets in this volume have public URLs"|t('app'), + name: 'hasUrls', + on: volume.hasUrls, + toggle: "url-field" + }) }} + +
+ {{ forms.autosuggestField({ + label: "Base URL"|t('app'), + instructions: "The base URL to the assets in this volume."|t('app'), + id: 'url', + class: ['ltr', 'volume-url'], + name: 'url', + suggestEnvVars: true, + suggestAliases: true, + value: (volume is defined ? volume.url : null), + errors: (volume is defined ? volume.getErrors('url') : null), + required: true, + placeholder: "//example.com/path/to/folder" + }) }} +
+ +
+ + {{ forms.selectField({ + label: "Volume Type"|t('app'), + instructions: "What type of volume is this?"|t('app'), + id: 'type', + name: 'type', + options: volumeTypeOptions, + value: className(volume), + toggle: true + }) }} + + {{ missingVolumePlaceholder|raw }} + + {% for volumeType in volumeTypes %} + {% set isCurrent = (volumeType == className(volume)) %} + + + {% endfor %} - +
+ + {{ forms.fieldLayoutDesignerField({ + fieldLayout: volume.getFieldLayout(), + }) }} {% endblock %} diff --git a/src/templates/settings/categories/_edit.html b/src/templates/settings/categories/_edit.html index 02ad91c14a8..42935f99fa4 100644 --- a/src/templates/settings/categories/_edit.html +++ b/src/templates/settings/categories/_edit.html @@ -13,106 +13,103 @@ {% if categoryGroup.id %}{{ hiddenInput('groupId', categoryGroup.id) }}{% endif %} -
- {{ forms.textField({ - first: true, - label: "Name"|t('app'), - instructions: "What this category group will be called in the control panel."|t('app'), - id: 'name', - name: 'name', - value: categoryGroup.name, - errors: categoryGroup.getErrors('name'), - autofocus: true, - required: true, - }) }} - - {{ forms.textField({ - label: "Handle"|t('app'), - instructions: "How you’ll refer to this category group in the templates."|t('app'), - id: 'handle', - name: 'handle', - class: 'code', - autocorrect: false, - autocapitalize: false, - value: categoryGroup.handle, - errors: categoryGroup.getErrors('handle'), - required: true - }) }} - - {{ forms.textField({ - label: "Max Levels"|t('app'), - instructions: "The maximum number of levels this category group can have. Leave blank if you don’t care."|t('app'), - id: 'maxLevels', - name: 'maxLevels', - value: categoryGroup.maxLevels, - size: 5, - errors: categoryGroup.getErrors('maxLevels') - }) }} - -
- - {% set siteRows = [] %} - {% set siteErrors = categoryGroup.getErrors('siteSettings') %} - - {% for site in craft.app.sites.getAllSites() %} - {% set siteSettings = categoryGroup.siteSettings[site.id] ?? null %} - {% if siteSettings %} - {% for attribute, errors in siteSettings.getErrors() %} - {% set siteErrors = siteErrors|merge(errors) %} - {% endfor %} - {% endif %} - {% set siteRows = siteRows|merge({ - (site.handle): { - heading: site.name|t('site')|e, - uriFormat: { - value: siteSettings.uriFormat ?? null, - hasErrors: siteSettings.hasErrors('uriFormat') ?? false - }, - template: not headlessMode ? { - value: siteSettings.template ?? null, - hasErrors: siteSettings.hasErrors('template') ?? false, - } - } - }) %} - {% endfor %} - - {{ forms.editableTableField({ - label: "Site Settings"|t('app'), - instructions: "Configure the category group’s site-specific settings."|t('app'), - id: 'sites', - name: 'sites', - cols: { - heading: { - type: 'heading', - heading: "Site"|t('app'), - class: 'thin' - }, + {{ forms.textField({ + first: true, + label: "Name"|t('app'), + instructions: "What this category group will be called in the control panel."|t('app'), + id: 'name', + name: 'name', + value: categoryGroup.name, + errors: categoryGroup.getErrors('name'), + autofocus: true, + required: true, + }) }} + + {{ forms.textField({ + label: "Handle"|t('app'), + instructions: "How you’ll refer to this category group in the templates."|t('app'), + id: 'handle', + name: 'handle', + class: 'code', + autocorrect: false, + autocapitalize: false, + value: categoryGroup.handle, + errors: categoryGroup.getErrors('handle'), + required: true + }) }} + + {{ forms.textField({ + label: "Max Levels"|t('app'), + instructions: "The maximum number of levels this category group can have. Leave blank if you don’t care."|t('app'), + id: 'maxLevels', + name: 'maxLevels', + value: categoryGroup.maxLevels, + size: 5, + errors: categoryGroup.getErrors('maxLevels') + }) }} + +
+ + {% set siteRows = [] %} + {% set siteErrors = categoryGroup.getErrors('siteSettings') %} + + {% for site in craft.app.sites.getAllSites() %} + {% set siteSettings = categoryGroup.siteSettings[site.id] ?? null %} + {% if siteSettings %} + {% for attribute, errors in siteSettings.getErrors() %} + {% set siteErrors = siteErrors|merge(errors) %} + {% endfor %} + {% endif %} + {% set siteRows = siteRows|merge({ + (site.handle): { + heading: site.name|t('site')|e, uriFormat: { - type: 'singleline', - heading: "Category URI Format"|t('app'), - info: "What category URIs should look like for the site."|t('app'), - placeholder: "Leave blank if categories don’t have URLs"|t('app'), - code: true + value: siteSettings.uriFormat ?? null, + hasErrors: siteSettings.hasErrors('uriFormat') ?? false }, template: not headlessMode ? { - type: 'template', - heading: "Template"|t('app'), - info: "Which template should be loaded when a category’s URL is requested."|t('app'), - code: true - }, - }|filter, - rows: siteRows, - staticRows: true, - errors: siteErrors|unique - }) }} - -
- - + value: siteSettings.template ?? null, + hasErrors: siteSettings.hasErrors('template') ?? false, + } + } + }) %} + {% endfor %} + + {{ forms.editableTableField({ + label: "Site Settings"|t('app'), + instructions: "Configure the category group’s site-specific settings."|t('app'), + id: 'sites', + name: 'sites', + cols: { + heading: { + type: 'heading', + heading: "Site"|t('app'), + class: 'thin' + }, + uriFormat: { + type: 'singleline', + heading: "Category URI Format"|t('app'), + info: "What category URIs should look like for the site."|t('app'), + placeholder: "Leave blank if categories don’t have URLs"|t('app'), + code: true + }, + template: not headlessMode ? { + type: 'template', + heading: "Template"|t('app'), + info: "Which template should be loaded when a category’s URL is requested."|t('app'), + code: true + }, + }|filter, + rows: siteRows, + staticRows: true, + errors: siteErrors|unique + }) }} + +
+ + {{ forms.fieldLayoutDesignerField({ + fieldLayout: categoryGroup.getFieldLayout(), + }) }} {% endblock %} diff --git a/src/templates/settings/fields/_edit.html b/src/templates/settings/fields/_edit.html index 3dcc2241adf..e50e8e26428 100644 --- a/src/templates/settings/fields/_edit.html +++ b/src/templates/settings/fields/_edit.html @@ -46,7 +46,7 @@ }) }} {{ forms.textareaField({ - label: "Instructions"|t('app'), + label: "Default Instructions"|t('app'), instructions: "Helper text to guide the author."|t('app'), id: 'instructions', class: 'nicetext', diff --git a/src/templates/settings/globals/_edit.html b/src/templates/settings/globals/_edit.html index 528ff7cf1c3..0ed79f0d555 100644 --- a/src/templates/settings/globals/_edit.html +++ b/src/templates/settings/globals/_edit.html @@ -10,40 +10,36 @@ {% if globalSet.id %}{{ hiddenInput('setId', globalSet.id) }}{% endif %} -
- {{ forms.textField({ - first: true, - label: "Name"|t('app'), - instructions: "What this global set will be called in the control panel."|t('app'), - id: 'name', - name: 'name', - value: globalSet.name, - errors: globalSet.getErrors('name'), - autofocus: true, - required: true, - }) }} - - {{ forms.textField({ - label: "Handle"|t('app'), - instructions: "How you’ll refer to this global set in the templates."|t('app'), - id: 'handle', - name: 'handle', - class: 'code', - autocorrect: false, - autocapitalize: false, - value: globalSet.handle, - errors: globalSet.getErrors('handle'), - required: true - }) }} - -
- - + {{ forms.textField({ + first: true, + label: "Name"|t('app'), + instructions: "What this global set will be called in the control panel."|t('app'), + id: 'name', + name: 'name', + value: globalSet.name, + errors: globalSet.getErrors('name'), + autofocus: true, + required: true, + }) }} + + {{ forms.textField({ + label: "Handle"|t('app'), + instructions: "How you’ll refer to this global set in the templates."|t('app'), + id: 'handle', + name: 'handle', + class: 'code', + autocorrect: false, + autocapitalize: false, + value: globalSet.handle, + errors: globalSet.getErrors('handle'), + required: true + }) }} + +
+ + {{ forms.fieldLayoutDesignerField({ + fieldLayout: globalSet.getFieldLayout(), + }) }} {% endblock %} diff --git a/src/templates/settings/sections/_entrytypes/edit.html b/src/templates/settings/sections/_entrytypes/edit.html index 569b38a3185..2abede25e90 100644 --- a/src/templates/settings/sections/_entrytypes/edit.html +++ b/src/templates/settings/sections/_entrytypes/edit.html @@ -49,33 +49,13 @@ first: (section.type == 'single'), label: "Show the Title field"|t('app'), name: 'hasTitleField', - toggle: 'titleLabel-container', - reverseToggle: 'titleFormat-container', + toggle: 'title-container', + reverseToggle: '#titleFormat-container, .fld-title-field-icon', checked: entryType.hasTitleField }) }} - +
+ {% endif %}