Skip to content

Commit 876219c

Browse files
authored
Merge branch '6.x' into feature/user-permissions
2 parents e1b095c + f723828 commit 876219c

File tree

29 files changed

+1082
-793
lines changed

29 files changed

+1082
-793
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
## Unreleased
44

5+
- Fixed a bug where remove buttons within multi-select Selectize inputs weren’t working if the input wasn’t focusend and fully in view. ([#18079](https://github.com/craftcms/cms/issues/18079))
6+
- Fixed an error that could occur when executing a GraphQL mutation when the `lazyGqlTypes` config setting was enabled. ([#18014](https://github.com/craftcms/cms/issues/18014))
57
- Fixed a bug where font icons weren’t hidden from screen readers. ([#18078](https://github.com/craftcms/cms/pull/18078))
8+
- Fixed a bug where relation fields weren’t handling `:empty:`/`:notempty:` element query params properly if the field had multiple instances within a field layout. ([#18092](https://github.com/craftcms/cms/pull/18092))
69

710
## 5.8.20 - 2025-11-18
811

composer.lock

Lines changed: 724 additions & 681 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rector.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector;
77
use RectorLaravel\Rector\ArrayDimFetch\EnvVariableToEnvHelperRector;
88
use RectorLaravel\Rector\Class_\AnonymousMigrationsRector;
9+
use RectorLaravel\Rector\FuncCall\AppToResolveRector;
910
use RectorLaravel\Rector\MethodCall\ResponseHelperCallToJsonResponseRector;
1011
use RectorLaravel\Rector\MethodCall\UseComponentPropertyWithinCommandsRector;
1112
use RectorLaravel\Set\LaravelSetList;
@@ -28,6 +29,7 @@
2829
EnvVariableToEnvHelperRector::class => [
2930
__DIR__.'/src/Utility/Utilities/PhpInfo.php',
3031
],
32+
AppToResolveRector::class,
3133
])
3234
->withSetProviders(LaravelSetProvider::class)
3335
->withComposerBased(laravel: true)

resources/templates/_includes/forms/selectize.twig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@
213213
},
214214
onChange: onChange,
215215
onInitialize: function () {
216+
// this is needed so that you can delete selected item when the field is not fully in viewport
217+
// see https://github.com/craftcms/cms/issues/18079 for details
218+
{% if multi %}
219+
this.isFocused = true;
220+
{% endif %}
221+
216222
// Copy all ARIA attributes from initial select to selectize
217223
[...$select[0].attributes]
218224
.filter(attr => /^aria-/.test(attr.name))

resources/translations/cy/app.php

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Field/Assets.php

Lines changed: 3 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@
3838
use Illuminate\Support\Collection;
3939
use Illuminate\Validation\Rule;
4040
use Override;
41-
use Twig\Error\RuntimeError;
42-
use yii\base\InvalidConfigException;
4341

4442
use function CraftCms\Cms\t;
4543

@@ -717,7 +715,7 @@ public function getInputSources(?ElementInterface $element = null): array
717715
$sources = array_merge($this->sources);
718716
} else {
719717
$sources = [];
720-
foreach (app(ElementSources::class)->getSources(Asset::class) as $source) {
718+
foreach (resolve(ElementSources::class)->getSources(Asset::class) as $source) {
721719
if ($source['type'] !== ElementSources::TYPE_HEADING) {
722720
$sources[] = $source['key'];
723721
}
@@ -931,60 +929,15 @@ private function _findFolder(string $sourceKey, ?string $subpath, ?ElementInterf
931929
throw new InvalidFsException("Invalid source key: $sourceKey");
932930
}
933931

934-
$assetsService = Craft::$app->getAssets();
935-
$rootFolder = $assetsService->getRootFolderByVolumeId($volume->id);
936-
937-
// Are we looking for the root folder?
938-
$subpath = trim($subpath ?? '', '/');
939-
if ($subpath === '') {
940-
return $rootFolder;
941-
}
942-
943-
$isDynamic = preg_match('/\{|\}/', $subpath);
944-
945-
if ($isDynamic) {
946-
// Prepare the path by parsing tokens and normalizing slashes.
947-
try {
948-
if ($element?->duplicateOf) {
949-
$element = $element->duplicateOf->getCanonical();
950-
}
951-
$renderedSubpath = Craft::$app->getView()->renderObjectTemplate($subpath, $element);
952-
} catch (InvalidConfigException|RuntimeError $e) {
953-
throw new InvalidSubpathException($subpath, null, 0, $e);
954-
}
955-
956-
// Did any of the tokens return null?
957-
if (
958-
$renderedSubpath === '' ||
959-
trim((string) $renderedSubpath, '/') != $renderedSubpath ||
960-
str_contains((string) $renderedSubpath, '//') ||
961-
str($renderedSubpath)->explode('/')->contains(fn (string $segment) => ElementHelper::isTempSlug($segment))
962-
) {
963-
throw new InvalidSubpathException($subpath);
964-
}
965-
966-
// Sanitize the subpath
967-
$subpath = str($renderedSubpath)
968-
->explode('/')
969-
->filter(fn (string $segment): bool => $segment !== ':ignore:')
970-
->map(fn (string $segment): string => FileHelper::sanitizeFilename($segment, [
971-
'asciiOnly' => Cms::config()->convertFilenamesToAscii,
972-
]))
973-
->implode('/');
974-
}
975-
976-
$folder = $assetsService->findFolder([
977-
'volumeId' => $volume->id,
978-
'path' => $subpath.'/',
979-
]);
932+
[$subpath, $folder] = AssetsHelper::resolveSubpath($volume, $subpath, $element);
980933

981934
// Ensure that the folder exists
982935
if (! $folder) {
983936
if (! $createDynamicFolders) {
984937
throw new InvalidSubpathException($subpath);
985938
}
986939

987-
$folder = $assetsService->ensureFolderByFullPathAndVolume($subpath, $volume);
940+
$folder = Craft::$app->getAssets()->ensureFolderByFullPathAndVolume($subpath, $volume);
988941
}
989942

990943
return $folder;

src/Field/BaseRelationField.php

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use craft\elements\db\ElementRelationParamParser;
1919
use craft\elements\ElementCollection;
2020
use craft\events\ElementCriteriaEvent;
21+
use craft\fieldlayoutelements\BaseField;
2122
use craft\fieldlayoutelements\CustomField;
2223
use craft\fields\conditions\RelationalFieldConditionRule;
2324
use craft\helpers\Cp;
@@ -140,8 +141,33 @@ public static function modifyQuery(Builder $query, array $instances, mixed $valu
140141
}
141142

142143
if (isset($value[0]) && in_array($value[0], [':notempty:', ':empty:', 'not :empty:'])) {
143-
$emptyCondition = array_shift($value);
144-
if (in_array($emptyCondition, [':notempty:', 'not :empty:'])) {
144+
$emptyParam = array_shift($value);
145+
146+
if (self::isQueryConditionFieldMultiInstance($instances)) {
147+
// look at the JSON values rather than the `relations` table data
148+
// (see https://github.com/craftcms/cms/issues/17290 + https://github.com/craftcms/cms/pull/18092)
149+
if (in_array($emptyParam, [':notempty:', 'not :empty:'])) {
150+
$query->orWhere(function (Builder $query) use ($instances) {
151+
foreach ($instances as $instance) {
152+
$valueSql = $instance->getValueSql();
153+
$query->orWhere(function (Builder $query) use ($valueSql) {
154+
$query->whereNotNull($valueSql)
155+
->whereNot($valueSql, '[]');
156+
});
157+
}
158+
});
159+
} else {
160+
$query->orWhere(function (Builder $query) use ($instances) {
161+
foreach ($instances as $instance) {
162+
$valueSql = $instance->getValueSql();
163+
$query->where(function (Builder $query) use ($valueSql) {
164+
$query->whereNotNull($valueSql)
165+
->whereNot($valueSql, '[]');
166+
});
167+
}
168+
});
169+
}
170+
} elseif (in_array($emptyParam, [':notempty:', 'not :empty:'])) {
145171
$query->orWhereExists(static::existsQuery($field));
146172
} else {
147173
$query->orWhereNotExists(static::existsQuery($field));
@@ -173,6 +199,26 @@ public static function modifyQuery(Builder $query, array $instances, mixed $valu
173199
return $query;
174200
}
175201

202+
/**
203+
* @param self[] $instances
204+
*/
205+
private static function isQueryConditionFieldMultiInstance(array $instances): bool
206+
{
207+
foreach ($instances as $instance) {
208+
// See if this instance is used multiple times within its field layout
209+
$allInstances = $instance->layoutElement?->getLayout()->getFields(fn (BaseField $field) => (
210+
$field instanceof CustomField &&
211+
$field->getFieldUid() === $instance->uid
212+
));
213+
214+
if ($allInstances && count($allInstances) > 1) {
215+
return true;
216+
}
217+
}
218+
219+
return false;
220+
}
221+
176222
/**
177223
* Returns a query builder-compatible condition for an element query,
178224
* limiting the results to only elements where the given relation field has a value.
@@ -441,7 +487,7 @@ public function validateSources(string $attribute): void
441487
$inputSources = [$inputSources];
442488
}
443489

444-
$elementSources = app(ElementSources::class)
490+
$elementSources = resolve(ElementSources::class)
445491
->getSources(static::elementType())
446492
->whereIn('key', $inputSources);
447493

@@ -1117,7 +1163,7 @@ public function getContentGqlMutationArgumentType(): array
11171163
*/
11181164
protected function gqlFieldArguments(): array
11191165
{
1120-
$elementSourcesService = app(ElementSources::class);
1166+
$elementSourcesService = resolve(ElementSources::class);
11211167
$gqlService = Craft::$app->getGql();
11221168
$fieldLayouts = [];
11231169
$arguments = [];
@@ -1729,7 +1775,7 @@ protected function viewMode(): string
17291775
*/
17301776
protected function availableSources(): array
17311777
{
1732-
return app(ElementSources::class)
1778+
return resolve(ElementSources::class)
17331779
->getSources(static::elementType(), 'modal')
17341780
->where('type', '!=', ElementSources::TYPE_HEADING)
17351781
->values()

src/Field/Field.php

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -587,8 +587,7 @@ protected function actionMenuItems(): array
587587
return $items;
588588
}
589589

590-
$userSessionService = Craft::$app->getUser();
591-
if (! $userSessionService->getIsAdmin()) {
590+
if (! Craft::$app->getUser()->getIsAdmin()) {
592591
return $items;
593592
}
594593

@@ -616,29 +615,6 @@ protected function actionMenuItems(): array
616615
]);
617616
}
618617

619-
// Copy field handle
620-
if (! $userSessionService->getIdentity()->getPreference('showFieldHandles')) {
621-
$copyId = sprintf('action-copy-handle-%s', mt_rand());
622-
$items[] = [
623-
'id' => $copyId,
624-
'icon' => 'clipboard',
625-
'label' => t('Copy field handle'),
626-
];
627-
$view->registerJsWithVars(fn ($id, $attribute) => <<<JS
628-
(() => {
629-
$('#' + $id).on('activate', () => {
630-
Craft.ui.createCopyTextPrompt({
631-
label: Craft.t('app', 'Field Handle'),
632-
value: $attribute,
633-
})
634-
});
635-
})();
636-
JS, [
637-
$view->namespaceInputId($copyId),
638-
$this->handle,
639-
]);
640-
}
641-
642618
return $items;
643619
}
644620

@@ -898,7 +874,7 @@ public function getSortOption(): array
898874
// see https://github.com/craftcms/cms/issues/15609
899875
$db = Craft::$app->getDb();
900876
if ($db->getIsMysql() && is_string($dbType) && DbHelper::parseColumnType($dbType) === Schema::TYPE_TEXT) {
901-
$orderBy = "CAST($orderBy AS CHAR(255))";
877+
$orderBy = new Cast($orderBy, 'CHAR(255)');
902878
}
903879

904880
// The attribute name should match the table attribute name,

src/Support/Facades/UserGroups.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@
88
use Override;
99

1010
/**
11+
* @method static \Illuminate\Support\Collection getAllGroups()
12+
* @method static \Illuminate\Support\Collection getAssignableGroups(\craft\elements\User|null $user = null)
13+
* @method static \CraftCms\Cms\User\Data\UserGroup|null getGroupById(int $groupId)
14+
* @method static \CraftCms\Cms\User\Data\UserGroup|null getGroupByUid(string $uid)
15+
* @method static \CraftCms\Cms\User\Data\UserGroup|null getGroupByHandle(string $groupHandle)
16+
* @method static \CraftCms\Cms\User\Data\UserGroup getTeamGroup()
17+
* @method static \Illuminate\Support\Collection getGroupsByUserId(int $userId)
18+
* @method static void eagerLoadGroups(\craft\elements\User[] $users)
19+
* @method static bool saveGroup(\CraftCms\Cms\User\Data\UserGroup $group)
20+
* @method static void handleChangedUserGroup(\CraftCms\Cms\ProjectConfig\Events\ConfigEvent $event)
21+
* @method static void handleDeletedUserGroup(\CraftCms\Cms\ProjectConfig\Events\ConfigEvent $event)
22+
* @method static bool deleteGroupById(int $groupId)
23+
* @method static bool deleteGroup(\CraftCms\Cms\User\Data\UserGroup $group)
24+
*
1125
* @see \CraftCms\Cms\User\UserGroups
1226
*/
1327
final class UserGroups extends Facade

tests/Http/Controllers/Settings/UserSettingsControllerTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,13 @@
4646
test('require2fa only gets saved when above team edition', function () {
4747
Edition::set(Edition::Solo);
4848

49-
expect(ProjectConfig::get('users.require2fa'))->toBeFalsy(false);
49+
expect(ProjectConfig::get('users.require2fa'))->toBeFalsy();
5050

5151
post(action([UserSettingsController::class, 'store'], [
5252
'require2fa' => true,
5353
]))->assertRedirectBack();
5454

55-
expect(ProjectConfig::get('users.require2fa'))->toBeFalsy(false);
55+
expect(ProjectConfig::get('users.require2fa'))->toBeFalsy();
5656

5757
Edition::set(Edition::Team);
5858

0 commit comments

Comments
 (0)