diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index 871413917e9..62f69f0200e 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -3,6 +3,7 @@ ### Content Management - It’s now possible to copy custom field values from other sites. ([#14056](https://github.com/craftcms/cms/pull/14056)) - “Related To”, “Not Related To”, “Author”, and relational field condition rules now allow multiple elements to be specified. ([#16121](https://github.com/craftcms/cms/discussions/16121)) +- Added the “Widget Title” setting to Quick Post widgets. ([#16429](https://github.com/craftcms/cms/pull/16429)) - Improved the styling of inline code fragments. ([#16141](https://github.com/craftcms/cms/pull/16141)) - Improved the styling of attribute previews in card view. ([#16324](https://github.com/craftcms/cms/pull/16324)) - Added the “Affiliated Site” user condition rule. ([#16174](https://github.com/craftcms/cms/pull/16174)) @@ -26,6 +27,8 @@ - Improved the accessibility of “More” and “Advanced” toggle triggers. ([#16293]](https://github.com/craftcms/cms/pull/16293)) - Improved the accessibility of the Craft Support widget. ([#16293]](https://github.com/craftcms/cms/pull/16293)) - Improved the accessibility of field translatable indicators and tooltips. +- Progress bars now announce their progress to screen readers. ([#16398](https://github.com/craftcms/cms/pull/16398)) +- Loading spinners within element indexes and inline-editable Matrix fields are now announced to screen readers. ([#16417](https://github.com/craftcms/cms/pull/16417)) ### Administration - Added the “Affiliated Site” native user field. ([#16174](https://github.com/craftcms/cms/pull/16174)) @@ -47,13 +50,12 @@ - After creating a new field from a field layout designer, the field is now immediately added to the field layout tab. ([#16374](https://github.com/craftcms/cms/pull/16374)) - Templates rendered for “Template” field layout UI elements can now call control panel template functions like `elementChip()` and `elementCard()`. ([#16267](https://github.com/craftcms/cms/issues/16267)) - “Template” field layout UI elements now show suggestions for the Template input. -- Element condition builders now always show field handles within the rule selection menu, for admin users. +- Added the `elements/delete-all-of-type` command. ([#16423](https://github.com/craftcms/cms/pull/16423)) - Added the `utils/delete-empty-volume-folders` command. ([#16388](https://github.com/craftcms/cms/issues/16388)) - Improved the error output for nested elements when they can’t be resaved via `resave` commands. - `resave` commands’ `--drafts`, `--provisional-drafts`, and `--revisions` options can now be set to `null`, causing elements to be resaved regardless of whether they’re drafts/provisional drafts/revisions. - Added the `systemTemplateCss` config setting. ([#16344](https://github.com/craftcms/cms/pull/16344)) - The `loginPath`, `logoutPath`, `setPasswordPath`, and `verifyEmailPath` config settings are now respected in headless mode. ([#16344](https://github.com/craftcms/cms/pull/16344)) -- Removed the “Show field handles in edit forms” admin user preference. ([#16415](https://github.com/craftcms/cms/pull/16415)) ### Development - Added support for fallback element partial templates, e.g. `_partials/entry.twig` as opposed to `_partials/entry/typeHandle.twig`. ([#16125](https://github.com/craftcms/cms/pull/16125)) @@ -66,14 +68,17 @@ - It’s now possible to set Link field values to arrays with `value` keys set to element instances or IDs. ([#16255](https://github.com/craftcms/cms/pull/16255)) - The `duration` Twig filter now has a `language` argument. ([#16332](https://github.com/craftcms/cms/pull/16332)) - The `indexOf` Twig filter now has a `default` argument, which can be any integer or `null`. (`-1` by default for backwards compatibility.) +- `{% cache %}` tags now cache any JavaScript import map entries registered via `craft\web\View::registerJsImport()` within them. - The `{% requireAdmin %}` tag now supports passing a boolean value, which determines whether administrative changes must be allowed (defaults to `true`). - It’s now possible to reference custom field handles in element queries’ `where` params. ([#16318](https://github.com/craftcms/cms/pull/16318)) - Number fields’ scalar values now return an integer if Decimals is set to `0`, and a number formatted with the correct decimal points when using MySQL. ([16369](https://github.com/craftcms/cms/issues/16369)) - Added support for specifying the current site via an `X-Craft-Site` header set to a site ID or handle. ([#16367](https://github.com/craftcms/cms/pull/16367)) +- Added the `disable2fa` config setting. ([#16426](https://github.com/craftcms/cms/pull/16426)) - Added support for defining redirects from `config/redirects.php`. ([#16355](https://github.com/craftcms/cms/pull/16355)) - Deprecated the `ucfirst` Twig filter. `capitalize` should be used instead. ### Extensibility +- Added `craft\attributes\EnvName`. - Added `craft\base\ConfigurableComponentInterface::getReadOnlySettingsHtml()`. ([#16265](https://github.com/craftcms/cms/pull/16265)) - Added `craft\base\CrossSiteCopyableFieldInterface`. ([#14056](https://github.com/craftcms/cms/pull/14056)) - Added `craft\base\Element::EVENT_DEFINE_ALT_ACTIONS`. ([#16294](https://github.com/craftcms/cms/pull/16294)) @@ -130,11 +135,16 @@ - Added `craft\web\User::getImpersonator()`. - Added `craft\web\User::getImpersonatorId()`. - Added `craft\web\User::setImpersonatorId()`. +- Added `craft\web\View::clearJsImportBuffer()`. ([#16414](https://github.com/craftcms/cms/pull/16414)) +- Added `craft\web\View::registerJsImport()`. ([#16414](https://github.com/craftcms/cms/pull/16414)) +- Added `craft\web\View::registerScriptWithVars()`. ([#16414](https://github.com/craftcms/cms/pull/16414)) - Added `craft\web\View::setTwig()`. +- Added `craft\web\View::startJsImportBuffer()`. ([#16414](https://github.com/craftcms/cms/pull/16414)) - Added `craft\web\twig\variables\Cp::EVENT_REGISTER_READ_ONLY_CP_SETTINGS`. ([#16265](https://github.com/craftcms/cms/pull/16265)) - `GuzzleHttp\Client` is now instantiated via `Craft::createObject()`. ([#16366](https://github.com/craftcms/cms/pull/16366)) - `craft\elements\NestedElementManager::getIndexHtml()` now supports passing `defaultSort` in the `$config` array. ([#16236](https://github.com/craftcms/cms/discussions/16236)) - `craft\elements\conditions\entries\MatrixFieldConditionRule` is now an alias of `FieldConditionRule`. +- `craft\helpers\App::envConfig()` now checks for a `craft\attributes\EnvName` attribute on public properties, which can be used to override the environment variable name (sans prefix) that is associated with the property. - `craft\helpers\Cp::elementIndexHtml()` now supports passing `defaultSort` in the `$config` array, when `sources` is `null`. ([#16236](https://github.com/craftcms/cms/discussions/16236)) - `craft\helpers\Cp::fieldHtml()` now supports passing an `actionMenuItems` array in the config. ([#16415](https://github.com/craftcms/cms/pull/16415)) - `craft\helpers\DateTimeHelper::humanDuration()` now has a `$language` argument. ([#16332](https://github.com/craftcms/cms/pull/16332)) @@ -150,6 +160,7 @@ - `_includes/forms/autosuggest.twig` now supports a `suggestTemplates` variable. - `_includes/forms/colorSelect.twig` now supports `options` and `withBlankOption` variables. - `_includes/forms/selectize.twig` now supports a `color` property in option data, which can be set to a hex value or a color name. +- Added `Craft.IntervalManager`. ([#16398](https://github.com/craftcms/cms/pull/16398)) - Sortable checkbox selects now always display the selected options first on initial render. ### System diff --git a/CHANGELOG.md b/CHANGELOG.md index ed46e4083e1..f4a051e11a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Fixed a bug where the control panel could display a notice about the Craft CMS license belonging to a different domain, even when accessing the control panel from the correct domain. ([#16396](https://github.com/craftcms/cms/issues/16396)) - Fixed a bug where field layout elements’ action menus could have an empty action group. +- Fixed a bug where Single section entries could be duplicated after running the `entry-types/merge` command. ([#16394](https://github.com/craftcms/cms/issues/16394)) +- Fixed a styling bug with the system message modal. ([#16410](https://github.com/craftcms/cms/issues/16410)) ## 5.5.9 - 2025-01-06 diff --git a/packages/craftcms-webpack/Craft.d.ts b/packages/craftcms-webpack/Craft.d.ts index 402ee7afa4f..b97fb0b13e6 100644 --- a/packages/craftcms-webpack/Craft.d.ts +++ b/packages/craftcms-webpack/Craft.d.ts @@ -1,6 +1,10 @@ // Set up interfaces and types interface ProgressBarInterface { - new ($element: JQuery, displaySteps?: boolean): ProgressBarInterface; + new ( + $element: JQuery, + displaySteps?: boolean, + settings?: Object + ): ProgressBarInterface; $progressBar: JQuery; @@ -13,6 +17,14 @@ interface ProgressBarInterface { showProgressBar(): void; } +interface IntervalManagerInterface { + new (settings?: Object): IntervalManagerInterface; + + stop(): void; + + start(): void; +} + type Site = { handle: string; id: number; @@ -25,6 +37,7 @@ declare var Craft: { csrfTokenName?: string; csrfTokenValue?: string; ProgressBar: ProgressBarInterface; + IntervalManager: IntervalManagerInterface; t(category: string, message: string, params?: object): string; sendActionRequest(method: string, action: string, options?: object): Promise; initUiElements($container: JQuery): void; diff --git a/src/attributes/EnvName.php b/src/attributes/EnvName.php new file mode 100644 index 00000000000..71e2d250fa4 --- /dev/null +++ b/src/attributes/EnvName.php @@ -0,0 +1,25 @@ + + * @since 5.6.0 + */ +#[Attribute] +class EnvName +{ + public function __construct( + public readonly string $name, + ) { + } +} diff --git a/src/base/Field.php b/src/base/Field.php index 85acd2aa930..3070e2e0340 100644 --- a/src/base/Field.php +++ b/src/base/Field.php @@ -534,8 +534,9 @@ public function getCpEditUrl(): ?string public function getActionMenuItems(): array { $items = []; + $userSessionService = Craft::$app->getUser(); - if ($this->id && Craft::$app->getUser()->getIsAdmin()) { + if ($this->id && $userSessionService->getIsAdmin()) { $view = Craft::$app->getView(); if (Craft::$app->getConfig()->getGeneral()->allowAdminChanges) { @@ -561,13 +562,14 @@ public function getActionMenuItems(): array } // Copy field handle - $copyId = sprintf('action-copy-handle-%s', mt_rand()); - $items[] = [ - 'id' => $copyId, - 'icon' => 'clipboard', - 'label' => Craft::t('app', 'Copy field handle'), - ]; - $view->registerJsWithVars(fn($id, $attribute) => <<getIdentity()->getPreference('showFieldHandles')) { + $copyId = sprintf('action-copy-handle-%s', mt_rand()); + $items[] = [ + 'id' => $copyId, + 'icon' => 'clipboard', + 'label' => Craft::t('app', 'Copy field handle'), + ]; + $view->registerJsWithVars(fn($id, $attribute) => << { $('#' + $id).on('click', () => { Craft.ui.createCopyTextPrompt({ @@ -577,9 +579,10 @@ public function getActionMenuItems(): array }); })(); JS, [ - $view->namespaceInputId($copyId), - $this->handle, - ]); + $view->namespaceInputId($copyId), + $this->handle, + ]); + } } return $items; diff --git a/src/config/GeneralConfig.php b/src/config/GeneralConfig.php index 91bbe8666eb..ffc94e4c6f9 100644 --- a/src/config/GeneralConfig.php +++ b/src/config/GeneralConfig.php @@ -9,6 +9,7 @@ use Closure; use Craft; +use craft\attributes\EnvName; use craft\helpers\ConfigHelper; use craft\helpers\DateTimeHelper; use craft\helpers\Localization; @@ -944,6 +945,24 @@ class GeneralConfig extends BaseConfig */ public bool $devMode = false; + /** + * @var bool Whether two-step verification features should be disabled. + * + * ::: code + * ```php Static Config + * ->disable2fa() + * ``` + * ```shell Environment Override + * CRAFT_DISABLE_2FA=true + * ``` + * ::: + * + * @group Users + * @since 5.6.0 + */ + #[EnvName('DISABLE_2FA')] + public bool $disable2fa = false; + /** * @var string[]|string|null Array of plugin handles that should be disabled, regardless of what the project config says. * @@ -2694,12 +2713,23 @@ class GeneralConfig extends BaseConfig /** * @var string A private, random, cryptographically-secure key that is used for hashing and encrypting data in [[\craft\services\Security]]. * - * This value should be the same across all environments. If this key ever changes, any data that was encrypted with it will be inaccessible. + * ::: warning + * **Do not** share this key publicly. If exposed, it could lead to a compromised system. + * ::: + * + * In the event that the key is compromised, a new secure key can be generated with the command: + * + * ```sh + * php craft setup/security-key + * ``` + * + * Note that if the key changes, any data that is encrypted with it (e.g. user session cookies) will be inaccessible. * * ```php Static Config * ->securityKey('2cf24dba5...') * ``` * + * @see https://craftcms.com/knowledge-base/securing-craft * @group Security */ public string $securityKey = ''; @@ -4316,6 +4346,30 @@ public function devMode(bool $value = true): self return $this; } + /** + * Whether two-step verification features should be disabled. + * + * ::: code + * ```php Static Config + * ->disable2fa() + * ``` + * ```shell Environment Override + * CRAFT_DISABLE_2FA=true + * ``` + * ::: + * + * @group Users + * @param bool $value + * @return self + * @see $disable2fa + * @since 5.6.0 + */ + public function disable2fa(bool $value = true): self + { + $this->disable2fa = $value; + return $this; + } + /** * Array of plugin handles that should be disabled, regardless of what the project config says. * @@ -6302,7 +6356,17 @@ public function sanitizeSvgUploads(bool $value = true): self /** * A private, random, cryptographically-secure key that is used for hashing and encrypting data in [[\craft\services\Security]]. * - * This value should be the same across all environments. If this key ever changes, any data that was encrypted with it will be inaccessible. + * ::: warning + * **Do not** share this key publicly. If exposed, it could lead to a compromised system. + * ::: + * + * In the event that the key is compromised, a new secure key can be generated with the command: + * + * ```sh + * php craft setup/security-key + * ``` + * + * Note that if the key changes, any data that is encrypted with it (e.g. user session cookies) will be inaccessible. * * ```php * ->securityKey('2cf24dba5...') @@ -6312,6 +6376,7 @@ public function sanitizeSvgUploads(bool $value = true): self * @param string $value * @return self * @see $securityKey + * @see https://craftcms.com/knowledge-base/securing-craft * @since 4.2.0 */ public function securityKey(string $value): self diff --git a/src/console/controllers/ElementsController.php b/src/console/controllers/ElementsController.php index d8682ce254c..e58c3716fc5 100644 --- a/src/console/controllers/ElementsController.php +++ b/src/console/controllers/ElementsController.php @@ -10,8 +10,12 @@ use Craft; use craft\base\ElementInterface; use craft\console\Controller; +use craft\db\Query; +use craft\db\Table; use craft\elements\Entry; +use craft\helpers\Component; use craft\helpers\Console; +use craft\helpers\Db; use craft\models\Section; use yii\console\ExitCode; @@ -28,6 +32,12 @@ class ElementsController extends Controller */ public bool $hard = false; + /** + * @var bool Whether to only do a dry run of the prune elements of type process. + * @since 5.6.0 + */ + public bool $dryRun = false; + /** * @inheritdoc */ @@ -38,6 +48,9 @@ public function options($actionID): array case 'delete': $options[] = 'hard'; break; + case 'delete-all-of-type': + $options[] = 'dryRun'; + break; } return $options; } @@ -90,6 +103,77 @@ public function actionDelete(int $id): int return ExitCode::OK; } + /** + * Deletes all elements of a given type. + * + * @param class-string $type The element type to delete. + * @since 5.6.0 + */ + public function actionDeleteAllOfType(string $type): int + { + // get the elements of that type + $query = (new Query()) + ->select('id') + ->from(Table::ELEMENTS) + ->where(['type' => $type]); + + // exclude single entries + if ($type === Entry::class) { + $singleSections = Craft::$app->getEntries()->getSectionsByType(Section::TYPE_SINGLE); + if (!empty($singleSections)) { + $singleEntryIds = Entry::find() + ->sectionId(array_map(fn(Section $section) => $section->id, $singleSections)) + ->status(null) + ->site('*') + ->unique() + ->ids(); + if (!empty($singleEntryIds)) { + $query->andWhere(['not', ['id' => $singleEntryIds]]); + } + } + } + + $total = $query->count(); + + $isValid = Component::validateComponentClass($type, ElementInterface::class); + if ($isValid) { + $typeLabel = $total == 1 ? $type::lowerDisplayName() : $type::pluralLowerDisplayName(); + } else { + $typeLabel = sprintf('`%s` %s', $type, $total == 1 ? 'element' : 'elements'); + } + + if (!$total) { + $this->stdout(sprintf("%s\n", $this->markdownToAnsi("No $typeLabel found."))); + return ExitCode::OK; + } + + $this->stdout(sprintf("%s\n", $this->markdownToAnsi("$total $typeLabel found."))); + + if (!$this->dryRun && !$this->confirm('Continue?')) { + $this->stdout("Aborting.\n"); + return ExitCode::OK; + } + + foreach (Db::each($query) as $element) { + $elementId = $element['id']; + $message = sprintf('Deleting %s %s', $isValid ? $type::lowerDisplayName() : 'element', $elementId); + $this->do($message, function() use ($elementId) { + if (!$this->dryRun) { + Db::delete(Table::ELEMENTS, [ + 'id' => $elementId, + ]); + Db::delete(Table::SEARCHINDEX, [ + 'elementId' => $elementId, + ]); + } + }); + } + + $dryRunLabel = $this->dryRun ? '**[DRY RUN]** ' : ''; + $this->stdout(sprintf("%s\n", $this->markdownToAnsi("$dryRunLabel$total $typeLabel deleted."))); + return ExitCode::OK; + } + /** * Restores an element by its ID. * diff --git a/src/controllers/UsersController.php b/src/controllers/UsersController.php index ae2a9d92ff6..1b251f45eeb 100644 --- a/src/controllers/UsersController.php +++ b/src/controllers/UsersController.php @@ -248,24 +248,27 @@ public function actionLogin(): ?Response $duration = $generalConfig->userSessionDuration; } - // if user has an active 2SV method, move on to that - $authService = Craft::$app->getAuth(); $userSession = Craft::$app->getUser(); - if (!empty($authService->getActiveMethods($user))) { - $authService->setUser($user, $duration); - if ($this->request->getIsSiteRequest() && !$this->request->getAcceptsJson()) { - $loginPath = $generalConfig->getLoginPath(); - if (!$loginPath) { - $authService->setUser(null); - throw new InvalidConfigException('User requires two-step verification, but the loginPath config setting is disabled.'); + // if user has an active 2SV method, move on to that + if (!$generalConfig->disable2fa) { + $authService = Craft::$app->getAuth(); + if ($authService->hasActiveMethod($user)) { + $authService->setUser($user, $duration); + + if ($this->request->getIsSiteRequest() && !$this->request->getAcceptsJson()) { + $loginPath = $generalConfig->getLoginPath(); + if (!$loginPath) { + $authService->setUser(null); + throw new InvalidConfigException('User requires two-step verification, but the loginPath config setting is disabled.'); + } + return $this->redirect(UrlHelper::siteUrl($loginPath, [ + 'verify' => 1, + ])); } - return $this->redirect(UrlHelper::siteUrl($loginPath, [ - 'verify' => 1, - ])); - } - return $this->runAction('auth-form'); + return $this->runAction('auth-form'); + } } // if we're impersonating, pass the user we're impersonating to the complete method @@ -1311,6 +1314,7 @@ public function actionSavePreferences(): Response if ($user->admin) { $preferences = array_merge($preferences, [ + 'showFieldHandles' => (bool)$this->request->getBodyParam('showFieldHandles', $user->getPreference('showFieldHandles')), 'enableDebugToolbarForSite' => (bool)$this->request->getBodyParam('enableDebugToolbarForSite', $user->getPreference('enableDebugToolbarForSite')), 'enableDebugToolbarForCp' => (bool)$this->request->getBodyParam('enableDebugToolbarForCp', $user->getPreference('enableDebugToolbarForCp')), 'showExceptionView' => (bool)$this->request->getBodyParam('showExceptionView', $user->getPreference('showExceptionView')), diff --git a/src/fieldlayoutelements/BaseField.php b/src/fieldlayoutelements/BaseField.php index e87fb525e45..08267fc6c8c 100644 --- a/src/fieldlayoutelements/BaseField.php +++ b/src/fieldlayoutelements/BaseField.php @@ -85,7 +85,6 @@ abstract public function attribute(): string; * * @return bool * @since 4.5.4 - * @deprecated in 5.6.0 */ public function showAttribute(): bool { diff --git a/src/fieldlayoutelements/CustomField.php b/src/fieldlayoutelements/CustomField.php index 136b19adf49..3c9ef17974a 100644 --- a/src/fieldlayoutelements/CustomField.php +++ b/src/fieldlayoutelements/CustomField.php @@ -77,6 +77,14 @@ public function attribute(): string return $this->handle ?? $this->_field->handle; } + /** + * @inheritdoc + */ + public function showAttribute(): bool + { + return true; + } + /** * @inheritdoc * @since 3.5.2 diff --git a/src/fields/conditions/FieldConditionRuleTrait.php b/src/fields/conditions/FieldConditionRuleTrait.php index 397e7539118..cd258c0b0b4 100644 --- a/src/fields/conditions/FieldConditionRuleTrait.php +++ b/src/fields/conditions/FieldConditionRuleTrait.php @@ -183,7 +183,7 @@ public function getLabel(): string public function getLabelHint(): ?string { static $showHandles = null; - $showHandles ??= Craft::$app->getUser()->getIsAdmin(); + $showHandles ??= Craft::$app->getUser()->getIdentity()?->getPreference('showFieldHandles') ?? false; return $showHandles ? $this->field()->handle : null; } diff --git a/src/helpers/App.php b/src/helpers/App.php index 7586ed09f80..96295a3ba67 100644 --- a/src/helpers/App.php +++ b/src/helpers/App.php @@ -9,6 +9,7 @@ use Closure; use Craft; +use craft\attributes\EnvName; use craft\behaviors\SessionBehavior; use craft\cache\FileCache; use craft\config\DbConfig; @@ -160,12 +161,23 @@ public static function envConfig(string $class, ?string $envPrefix = null): arra continue; } - $propName = $prop->getName(); - $envName = $envPrefix . strtoupper(StringHelper::toSnakeCase($propName)); - $envValue = static::env($envName); + $envName = null; + + foreach ($prop->getAttributes(EnvName::class) as $attribute) { + /** @var EnvName $envName */ + $envName = $attribute->newInstance(); + $envName = $envName->name; + break; + } + + if (!$envName) { + $envName = strtoupper(StringHelper::toSnakeCase($prop->getName())); + } + + $envValue = static::env(sprintf('%s%s', $envPrefix, $envName)); if ($envValue !== null) { - $envConfig[$propName] = $envValue; + $envConfig[$prop->getName()] = $envValue; } } diff --git a/src/helpers/Cp.php b/src/helpers/Cp.php index 5aa67bab426..e8fe570f3aa 100644 --- a/src/helpers/Cp.php +++ b/src/helpers/Cp.php @@ -1488,6 +1488,15 @@ public static function fieldHtml(string|callable $input, array $config = []): st $errors ? 'has-errors' : null, ]), Html::explodeClass($config['fieldClass'] ?? [])); + $userSessionService = Craft::$app->getUser(); + $showAttribute = ( + ($config['showAttribute'] ?? false) && + $userSessionService->getIsAdmin() && + $userSessionService->getIdentity()->getPreference('showFieldHandles') + ); + $showActionMenu = !empty($config['actionMenuItems']); + $showLabelExtra = $showAttribute || $showActionMenu || isset($config['labelExtra']); + $instructionsHtml = $instructions ? Html::tag('div', preg_replace('/&(\w+);/', '&$1;', Markdown::process(Html::encodeInvalidTags($instructions), 'gfm-comment')), [ 'id' => $instructionsId, @@ -1528,15 +1537,6 @@ public static function fieldHtml(string|callable $input, array $config = []): st : '') . ($translatable ? $translationIconHtml : '') ); - - if (!empty($config['actionMenuItems'])) { - $labelHtml .= static::disclosureMenu($config['actionMenuItems'], [ - 'hiddenLabel' => Craft::t('app', 'Actions'), - 'buttonAttributes' => [ - 'class' => ['action-btn', 'small'], - ], - ]); - } } else { $labelHtml = ''; } @@ -1568,7 +1568,7 @@ public static function fieldHtml(string|callable $input, array $config = []): st ]) . Html::endTag('div') : '') . - (($label || isset($config['labelExtra'])) + (($label || $showLabelExtra) ? ( Html::beginTag('div', ['class' => 'heading']) . ($config['headingPrefix'] ?? '') . @@ -1579,8 +1579,20 @@ public static function fieldHtml(string|callable $input, array $config = []): st 'for' => !$fieldset ? $id : null, ], $config['labelAttributes'] ?? [])) : '') . - (isset($config['labelExtra']) - ? Html::tag('div', '', ['class' => 'flex-grow']) . $config['labelExtra'] + ($showLabelExtra + ? Html::tag('div', '', ['class' => 'flex-grow']) . + ($showActionMenu ? static::disclosureMenu($config['actionMenuItems'], [ + 'hiddenLabel' => Craft::t('app', 'Actions'), + 'buttonAttributes' => [ + 'class' => ['action-btn', 'small'], + ], + ]) : '') . + ($showAttribute ? static::renderTemplate('_includes/forms/copytextbtn.twig', [ + 'id' => "$id-attribute", + 'class' => ['code', 'small', 'light'], + 'value' => $config['attribute'], + ]) : '') . + ($config['labelExtra'] ?? '') : '') . ($config['headingSuffix'] ?? '') . Html::endTag('div') diff --git a/src/services/Assets.php b/src/services/Assets.php index bc89b5d6ec2..b16da2559c6 100644 --- a/src/services/Assets.php +++ b/src/services/Assets.php @@ -336,7 +336,7 @@ public function deleteFoldersByIds(int|array $folderIds, bool $deleteDir = true) $assetQuery = Asset::find()->folderId($allFolderIds); $elementService = Craft::$app->getElements(); - foreach ($assetQuery->each() as $asset) { + foreach (Db::each($assetQuery) as $asset) { /** @var Asset $asset */ $asset->keepFileOnDelete = !$deleteDir; $elementService->deleteElement($asset, true); diff --git a/src/services/Entries.php b/src/services/Entries.php index cb472f5ed07..eb007992d27 100644 --- a/src/services/Entries.php +++ b/src/services/Entries.php @@ -966,7 +966,9 @@ private function _ensureSingleEntry(Section $section, ?array $siteSettings = nul // if we didn't find any, try without the typeId, // in case that changed to something completely new if ($entry === null) { - $entry = $baseEntryQuery->one(); + $entry = $baseEntryQuery + ->typeId(null) + ->one(); if ($entry !== null) { $entry->setTypeId($entryTypeIds[0]); @@ -977,6 +979,7 @@ private function _ensureSingleEntry(Section $section, ?array $siteSettings = nul // try without the typeId with trashed where they were deleted with entry type if ($entry === null) { $entry = $baseEntryQuery + ->typeId(null) ->trashed(null) ->where(['entries.deletedWithEntryType' => true]) ->one(); diff --git a/src/services/TemplateCaches.php b/src/services/TemplateCaches.php index 559b617fb90..88e2b67e3d6 100644 --- a/src/services/TemplateCaches.php +++ b/src/services/TemplateCaches.php @@ -71,7 +71,7 @@ public function getTemplateCache(string $key, bool $global, bool $registerResour return null; } - [$body, $cacheInfo, $bufferedJs, $bufferedScripts, $bufferedCss, $bufferedJsFiles, $bufferedCssFiles, $bufferedHtml, $bufferedMetaTags, $bufferedAssetBundles] = array_pad($data, 10, null); + [$body, $cacheInfo, $bufferedJs, $bufferedScripts, $bufferedCss, $bufferedJsFiles, $bufferedCssFiles, $bufferedHtml, $bufferedMetaTags, $bufferedAssetBundles, $bufferedJsImports] = array_pad($data, 11, null); // If we're actively collecting element cache info, register this cache's tags and duration $elementsService = Craft::$app->getElements(); @@ -95,6 +95,7 @@ public function getTemplateCache(string $key, bool $global, bool $registerResour $bufferedHtml ?? [], $bufferedMetaTags ?? [], $bufferedAssetBundles ?? [], + $bufferedJsImports ?? [] ); } @@ -130,6 +131,7 @@ public function startTemplateCache(bool $withResources = false, bool $global = f $view->startHtmlBuffer(); $view->startMetaTagBuffer(); $view->startAssetBundleBuffer(); + $view->startJsImportBuffer(); } } @@ -167,6 +169,7 @@ public function endTemplateCache(string $key, bool $global, ?string $duration, m $bufferedHtml = $view->clearHtmlBuffer(); $bufferedMetaTags = $view->clearMetaTagBuffer(); $bufferedAssetBundles = $view->clearAssetBundleBuffer(); + $bufferedJsImports = $view->clearJsImportBuffer(); } // If there are any transform generation URLs in the body, don't cache it. @@ -218,6 +221,7 @@ public function endTemplateCache(string $key, bool $global, ?string $duration, m $bufferedHtml, $bufferedMetaTags, $bufferedAssetBundles, + $bufferedJsImports ); } @@ -231,6 +235,7 @@ public function endTemplateCache(string $key, bool $global, ?string $duration, m $bufferedHtml, $bufferedMetaTags, $bufferedAssetBundles, + $bufferedJsImports ); } @@ -309,6 +314,7 @@ private function _registerResources( array $bufferedHtml, array $bufferedMetaTags, array $bufferedAssetBundles, + array $bufferedJsImports, ): void { $view = Craft::$app->getView(); @@ -352,6 +358,10 @@ private function _registerResources( foreach ($bufferedAssetBundles as [$name, $position]) { $view->registerAssetBundle($name, $position); } + + foreach ($bufferedJsImports as $key => $value) { + $view->registerJsImport($key, $value); + } } /** diff --git a/src/templates/_components/widgets/QuickPost/settings.twig b/src/templates/_components/widgets/QuickPost/settings.twig index a20f273ec64..ac1eaa2e22d 100644 --- a/src/templates/_components/widgets/QuickPost/settings.twig +++ b/src/templates/_components/widgets/QuickPost/settings.twig @@ -1,7 +1,6 @@ {% import "_includes/forms" as forms %} {% if sections %} - {% if craft.app.getIsMultiSite() %} {% set editableSites = craft.app.sites.getEditableSites() %} @@ -24,8 +23,8 @@ {% endif %} {% set sectionOptions = [] %} - {% for section in sections %} - {% set sectionOptions = sectionOptions|merge([{ label: section.name|t('site'), value: section.id }]) %} + {% for s in sections %} + {% set sectionOptions = sectionOptions|merge([{ label: s.name|t('site'), value: s.id }]) %} {% endfor %} {{ forms.selectField({ label: "Section"|t('app'), @@ -33,39 +32,57 @@ id: 'section', name: 'section', options: sectionOptions, - value: sectionId, + value: section.id ?? null, toggle: true, targetPrefix: 'section' }) }} - {% for section in sections %} - {% set showSection = ((not sectionId and loop.first) or sectionId == section.id) %} -