Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
564ba01
5.11 changelog
brandonkelly Jun 9, 2026
f4b5945
Merge branch '5.x' into 5.11
brandonkelly Jun 16, 2026
8278917
UrlHelper::removeParams(), removeAllParams()
brandonkelly Jun 16, 2026
95ef394
Release notes
brandonkelly Jun 16, 2026
3b0c332
Merge pull request #19102 from craftcms/feature/url-params
brandonkelly Jun 16, 2026
e8e7855
Merge branch '5.x' into 5.11
brandonkelly Jun 25, 2026
45ac9aa
craft\web\DbSession
brandonkelly Jun 25, 2026
cf9475b
Merge branch '5.11' of https://github.com/craftcms/cms into 6.x-5.11
brandonkelly Jul 6, 2026
cd5e40a
Merge branch '5.x' into 5.11
brandonkelly Jul 6, 2026
0ccf9a7
Twig 3.28
brandonkelly Jul 6, 2026
dd9b6f1
Release note
brandonkelly Jul 6, 2026
f9213a9
Merge pull request #19211 from craftcms/chore/twig-3.28
brandonkelly Jul 6, 2026
b7dd113
Merge branch '5.11' of https://github.com/craftcms/cms into 6.x-5.11
brandonkelly Jul 6, 2026
80a44d6
Merge branch '5.x' into 5.11
brandonkelly Jul 15, 2026
81cb974
Merge branch '5.x' into 5.11
brandonkelly Jul 28, 2026
ad1ee56
Sort lines
brandonkelly Jul 28, 2026
2ba19b3
reorderNestedElements()
brandonkelly Jul 28, 2026
7af7169
Merge pull request #19331 from craftcms/feature/reorderNestedElements
brandonkelly Jul 28, 2026
3244956
Merge branch '6.x' into 6.x-5.11
brandonkelly Jul 28, 2026
babd09d
Merge branch '5.11' of https://github.com/craftcms/cms into 6.x-5.11
brandonkelly Jul 29, 2026
9cbc869
Merge branch '6.x' into 6.x-5.11
brandonkelly Jul 30, 2026
6f0557b
Merge branch '6.x' into 6.x-5.11
brandonkelly Jul 30, 2026
db3ad48
Merge branch '5.x' into 5.11
brandonkelly Aug 1, 2026
8de14b0
yii2-debug 2.1.28
brandonkelly Aug 1, 2026
4ad6779
Add support for .well-known/passkey-endpoints
brandonkelly Aug 3, 2026
9ae1316
Release note
brandonkelly Aug 3, 2026
b87577d
Merge pull request #19364 from craftcms/feature/well-known-passkey-en…
brandonkelly Aug 3, 2026
0382771
Merge branch '6.x' into 6.x-5.11
brandonkelly Aug 3, 2026
c796b83
Wrong changelog
brandonkelly Aug 3, 2026
76dccf8
Merge branch '5.11' of https://github.com/craftcms/cms into 6.x-5.11
brandonkelly Aug 3, 2026
9fd2b6c
Merge branch '5.x' into 5.11
brandonkelly Aug 4, 2026
fdb997f
Merge branch '5.x' into 5.11
brandonkelly Aug 5, 2026
500ed4b
Merge branch '5.x' into 5.11
brandonkelly Aug 5, 2026
24371bd
Fill in the gaps when converting a LinkData object to an array
brandonkelly Aug 5, 2026
2240a0a
Merge branch '6.x' into 6.x-5.11
brandonkelly Aug 5, 2026
851a1e2
Merge branch '5.11' of https://github.com/craftcms/cms into 6.x-5.11
brandonkelly Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"symfony/yaml": "^7.0|^8.0",
"theiconic/name-parser": "^1.2",
"tpetry/laravel-query-expressions": "^1.5",
"twig/twig": "~3.27.0",
"twig/twig": "~3.28.0",
"voku/portable-ascii": "^2.0",
"web-auth/webauthn-lib": "~5.3.5",
"webonyx/graphql-php": "~15.33.1",
Expand Down
16 changes: 8 additions & 8 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export function useAssetMoveDrag() {
// Snapshot the pre-grab selection first so onDragStop can undo a force
// select of a row that wasn't already part of a selected group.
filter: () => {
preDragSelection = {...(table.value?.getState().rowSelection ?? {})};
preDragSelection = {...table.value?.getState().rowSelection};
const grabbed = dragDrop?.$targetItem;
if (grabbed?.dataset.id) {
const rowId = grabbed.dataset.id;
Expand Down
4 changes: 4 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use CraftCms\Cms\Http\Middleware\RequireEdition;
use CraftCms\Cms\Route\Routes as CraftRoutes;
use CraftCms\Cms\Site\Sites;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Route;

$routes = app(CraftRoutes::class);
Expand Down Expand Up @@ -72,4 +73,7 @@
});
}

// Signals support for passkeys without leaking the CP URL, per https://www.w3.org/TR/passkey-endpoints/.
Route::get('.well-known/passkey-endpoints', fn () => new JsonResponse((object) []));

Route::fallback(SiteRouteController::class)->name('siteFallback');
54 changes: 54 additions & 0 deletions src/Element/Elements.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use CraftCms\Cms\Component\ComponentHelper;
use CraftCms\Cms\Database\Table;
use CraftCms\Cms\Element\Contracts\ElementInterface;
use CraftCms\Cms\Element\Contracts\NestedElementInterface;
use CraftCms\Cms\Element\Data\EagerLoadPlan;
use CraftCms\Cms\Element\Exceptions\InvalidElementException;
use CraftCms\Cms\Element\Exceptions\UnsupportedSiteException;
Expand Down Expand Up @@ -49,6 +50,7 @@
public function __construct(
private readonly ElementPlaceholders $placeholders,
private readonly ElementTypes $elementTypes,
private readonly ElementCaches $elementCaches,
) {}

/**
Expand Down Expand Up @@ -717,6 +719,58 @@
return app(ElementDeletions::class)->restoreElements($elements);
}

/**
* Reorders nested elements for a given owner element.
*
* @param ElementInterface $owner The owner element
* @param ElementQueryInterface|ElementCollection $nestedElements The owner’s nested elements
* @param array $elementIds The nested element IDs that are being moved, in their new relative order
* @param int $offset The zero-based offset that `$elementIds` should be inserted at, relative to the owner’s
* other nested elements
*/
public function reorderNestedElements(

Check failure on line 731 in src/Element/Elements.php

View workflow job for this annotation

GitHub Actions / Code Quality / Phpstan

Method CraftCms\Cms\Element\Elements::reorderNestedElements() has parameter $nestedElements with generic class CraftCms\Cms\Element\ElementCollection but does not specify its types: TKey, TElement

Check failure on line 731 in src/Element/Elements.php

View workflow job for this annotation

GitHub Actions / Code Quality / Phpstan

Method CraftCms\Cms\Element\Elements::reorderNestedElements() has parameter $elementIds with no value type specified in iterable type array.
ElementInterface $owner,
ElementQueryInterface|ElementCollection $nestedElements,
array $elementIds,
int $offset,
): void {
$elementIds = array_map(fn ($id) => (int) $id, $elementIds);

if ($nestedElements instanceof ElementQueryInterface) {
$oldSortOrders = (clone $nestedElements)
->status(null)
->asArray()
->select(['id', 'sortOrder'])
->pluck('sortOrder', 'id')
->all();
} else {
$oldSortOrders = $nestedElements
->keyBy(fn (ElementInterface $element) => $element->id)
/** @phpstan-ignore-next-line */
->map(fn (NestedElementInterface $element) => $element->getSortOrder())
->all();
}

// Build the full list of IDs in the new sort order
$allIds = array_diff(array_keys($oldSortOrders), $elementIds);
array_splice($allIds, $offset, 0, $elementIds);

// Update all the incorrect sort orders
foreach ($allIds as $i => $id) {
$sortOrder = $i + 1;
if (! isset($oldSortOrders[$id]) || $sortOrder !== $oldSortOrders[$id]) {
DB::table(Table::ELEMENTS_OWNERS)
->where('ownerId', $owner->id)
->where('elementId', $id)
->update([
'sortOrder' => $sortOrder,
]);
}
}

$this->elementCaches->invalidateForElement($owner);
}

// Misc
// -------------------------------------------------------------------------

Expand Down
30 changes: 29 additions & 1 deletion src/Field/Data/LinkData.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
use CraftCms\Cms\Support\Html;
use CraftCms\Cms\Support\Template;
use CraftCms\Cms\Twig\Attributes\AllowedInSandbox;
use Illuminate\Contracts\Support\Arrayable;
use Stringable;
use Twig\Markup;

class LinkData implements Serializable, Stringable
/** @implements Arrayable<string, mixed> */
class LinkData implements Arrayable, Serializable, Stringable
{
/** @var string|null The link’s URL suffix value. */
#[AllowedInSandbox]
Expand Down Expand Up @@ -248,4 +250,30 @@ public function serialize(): array
'filename' => $this->filename,
]);
}

public function toArray(): array
{
return [
'type' => $this->getType(),
'value' => $this->getValue(),
'url' => $this->getUrl(),
'label' => $this->getLabel(),
'filename' => $this->getFilename(),
'link' => (string) $this->getLink(),
'attributes' => $this->getAttributes(),
'defaultLabel' => $this->getLabel(false),
'urlSuffix' => $this->urlSuffix,
'target' => $this->target,
'title' => $this->title,
'class' => $this->class,
'id' => $this->id,
'rel' => $this->rel,
'ariaLabel' => $this->ariaLabel,
'download' => $this->download,
'elementType' => $this->getElement() !== null ? $this->getElement()::class : null,
'elementId' => $this->getElement()?->id,
'elementSiteId' => $this->getElement()?->siteId,
'elementTitle' => $this->getElement() !== null ? (string) $this->getElement() : null,
];
}
}
47 changes: 7 additions & 40 deletions src/Http/Controllers/NestedElementsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@
namespace CraftCms\Cms\Http\Controllers;

use CraftCms\Cms\Database\Table;
use CraftCms\Cms\Element\Contracts\ElementInterface;
use CraftCms\Cms\Element\Contracts\NestedElementInterface;
use CraftCms\Cms\Element\ElementCaches;
use CraftCms\Cms\Element\Elements;
use CraftCms\Cms\Element\Queries\Contracts\ElementQueryInterface;
use CraftCms\Cms\Http\Requests\NestedElementsRequest;
use CraftCms\Cms\Http\RespondsWithFlash;
use Illuminate\Support\Facades\DB;
Expand All @@ -26,45 +22,16 @@ public function __construct(
private Elements $elements,
) {}

public function reorder(NestedElementsRequest $request, ElementCaches $elementCaches): Response
public function reorder(NestedElementsRequest $request): Response
{
$request->authorizeReorder();

$nestedElements = $request->nestedElements();

if ($nestedElements instanceof ElementQueryInterface) {
$oldSortOrders = (clone $nestedElements)
->status(null)
->asArray()
->select(['id', 'sortOrder'])
->pluck('sortOrder', 'id')
->all();
} else {
$oldSortOrders = $nestedElements
->keyBy(fn (ElementInterface $element) => $element->id)
/** @phpstan-ignore-next-line */
->map(fn (NestedElementInterface $element) => $element->getSortOrder())
->all();
}

// Build the full list of IDs in the new sort order
$allIds = array_diff(array_keys($oldSortOrders), $request->elementIds());
array_splice($allIds, $request->offset(), 0, $request->elementIds());

// Update all the incorrect sort orders
foreach ($allIds as $i => $id) {
$sortOrder = $i + 1;
if (! isset($oldSortOrders[$id]) || $sortOrder !== $oldSortOrders[$id]) {
DB::table(Table::ELEMENTS_OWNERS)
->where('ownerId', $request->owner()->id)
->where('elementId', $id)
->update([
'sortOrder' => $sortOrder,
]);
}
}

$elementCaches->invalidateForElement($request->owner());
$this->elements->reorderNestedElements(
$request->owner(),
$request->nestedElements(),
$request->elementIds(),
$request->offset(),
);

return $this->asSuccess(t('New {total, plural, =1{position} other{positions}} saved.', [
'total' => count($request->elementIds()),
Expand Down
56 changes: 44 additions & 12 deletions src/Support/Url.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,22 +133,50 @@
* Removes a query string param from a URL.
*/
public static function removeParam(string $url, string $param): string
{
return static::removeParams($url, [$param]);
}

/**
* Removes query string params from a URL.
*/
public static function removeParams(string $url, array $params): string

Check failure on line 143 in src/Support/Url.php

View workflow job for this annotation

GitHub Actions / Code Quality / Phpstan

Method CraftCms\Cms\Support\Url::removeParams() has parameter $params with no value type specified in iterable type array.
{
// Extract any params/fragment from the base URL
[$url, $params, $fragment] = self::_extractParams($url);
[$url, $urlParams, $fragment] = self::_extractParams($url);

// Remove the param
unset($params[$param]);
// Remove the params
foreach ($params as $param) {
unset($urlParams[$param]);
}

// Rebuild
if (($query = static::buildQuery($params)) !== '') {
$url .= '?'.$query;
}
if ($fragment !== null) {
$url .= '#'.$fragment;
return self::_buildUrl($url, $urlParams, $fragment);
}

/**
* Removes all query string params from a URL.
*
* @param string[] $except Any params that should be left alone
*/
public static function removeAllParams(string $url, array $except = []): string
{
// Extract any params/fragment from the base URL
[$url, $params, $fragment] = self::_extractParams($url);

// Remove the params
if (! empty($except)) {
foreach (array_keys($params) as $param) {
if (! in_array($param, $except)) {
unset($params[$param]);
}
}
} else {
$params = [];
}

return $url;
// Rebuild
return self::_buildUrl($url, $params, $fragment);
}

/**
Expand Down Expand Up @@ -244,14 +272,18 @@

/**
* Returns either a control panel or a site URL, depending on the request type.
*
* @param array|string|false|null $params The query params to add to the URL. If `false`, any existing params will be removed.
*/
/** @param array<string, mixed>|string|null $params */
public static function url(string $path = '', array|string|null $params = null, ?string $scheme = null): string
/** @param array<string, mixed>|string|false|null $params */
public static function url(string $path = '', array|string|false|null $params = null, ?string $scheme = null): string
{
// Return $path if it appears to be an absolute URL.
if (static::isFullUrl($path)) {
if ($params) {
$path = static::urlWithParams($path, $params);
} elseif ($params === false) {
$path = static::removeAllParams($path);
}

if ($scheme !== null) {
Expand All @@ -276,7 +308,7 @@
$scheme = 'https';
}

return self::_createUrl($path, $params, $scheme, $cpUrl);
return self::_createUrl($path, $params ?: null, $scheme, $cpUrl);
}

/**
Expand Down
17 changes: 17 additions & 0 deletions tests/Feature/Http/Controllers/NestedElementsControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use CraftCms\Cms\Field\Matrix;
use CraftCms\Cms\Http\Controllers\NestedElementsController;
use CraftCms\Cms\Section\Models\Section as SectionModel;
use CraftCms\Cms\Support\Facades\Elements;
use CraftCms\Cms\Support\Facades\Fields;
use CraftCms\Cms\Support\Facades\Sections;
use CraftCms\Cms\User\Elements\User;
Expand Down Expand Up @@ -241,6 +242,22 @@ function nestedElementsControllerOwnerSortOrders(int $ownerId): array
]);
});

it('reorders nested elements via Elements::reorderNestedElements() directly', function () {
['owner' => $owner, 'field' => $field, 'entryType' => $entryType] = nestedElementsControllerCreateMatrixOwnerFixture();

$first = nestedElementsControllerCreateMatrixNestedEntry($owner, $field, $entryType, 1, 'First');
$second = nestedElementsControllerCreateMatrixNestedEntry($owner, $field, $entryType, 2, 'Second');
$third = nestedElementsControllerCreateMatrixNestedEntry($owner, $field, $entryType, 3, 'Third');

Elements::reorderNestedElements($owner, $owner->{'field:matrixField'}, [$third->id], 0);

expect(nestedElementsControllerOwnerSortOrders($owner->id))->toBe([
$third->id => 1,
$first->id => 2,
$second->id => 3,
]);
});

it('validates destroy params', function () {
$owner = User::findOne();

Expand Down
Loading
Loading