Skip to content

Commit ddbf1bb

Browse files
authored
[5.x] Fix CP nav ordering for when preferences are stored in JSON SQL columns (#10809)
1 parent 6db65ff commit ddbf1bb

File tree

4 files changed

+378
-125
lines changed

4 files changed

+378
-125
lines changed

src/CP/Navigation/NavPreferencesNormalizer.php

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Statamic\CP\Navigation;
44

5+
use Illuminate\Support\Collection;
56
use Statamic\Support\Arr;
67
use Statamic\Support\Str;
78

@@ -72,11 +73,12 @@ protected function normalize()
7273
{
7374
$navConfig = collect($this->preferences);
7475

75-
$normalized = collect()->put('reorder', $reorder = $navConfig->get('reorder', false));
76+
$normalized = collect()->put('reorder', (bool) $reorder = $navConfig->get('reorder', false));
7677

7778
$sections = collect($navConfig->get('sections') ?? $navConfig->except('reorder'));
7879

79-
$sections = $sections
80+
$sections = $this
81+
->normalizeToInheritsFromReorder($sections, $reorder)
8082
->prepend($sections->pull('top_level') ?? '@inherit', 'top_level')
8183
->map(fn ($config, $section) => $this->normalizeSectionConfig($config, $section))
8284
->reject(fn ($config) => $config['action'] === '@inherit' && ! $reorder)
@@ -115,15 +117,16 @@ protected function normalizeSectionConfig($sectionConfig, $sectionKey)
115117

116118
$normalized->put('display', $sectionConfig->get('display', false));
117119

118-
$normalized->put('reorder', $reorder = $sectionConfig->get('reorder', false));
120+
$normalized->put('reorder', (bool) $reorder = $sectionConfig->get('reorder', false));
119121

120122
$items = collect($sectionConfig->get('items') ?? $sectionConfig->except([
121123
'action',
122124
'display',
123125
'reorder',
124126
]));
125127

126-
$items = $items
128+
$items = $this
129+
->normalizeToInheritsFromReorder($items, $reorder)
127130
->map(fn ($config, $itemId) => $this->normalizeItemConfig($itemId, $config, $sectionKey))
128131
->keyBy(fn ($config, $itemId) => $this->normalizeItemId($itemId, $config))
129132
->filter()
@@ -187,14 +190,23 @@ protected function normalizeItemConfig($itemId, $itemConfig, $sectionKey, $remov
187190
}
188191
}
189192

190-
// If item has children, normalize those items as well.
191-
if ($children = $normalized->get('children')) {
192-
$normalized->put('children', collect($children)
193-
->map(fn ($childConfig, $childId) => $this->normalizeChildItemConfig($childId, $childConfig, $sectionKey))
194-
->keyBy(fn ($childConfig, $childId) => $this->normalizeItemId($childId, $childConfig))
195-
->all());
193+
// Normalize `reorder` bool.
194+
if ($reorder = $normalized->get('reorder', false)) {
195+
$normalized->put('reorder', (bool) $reorder);
196196
}
197197

198+
// Normalize `children`.
199+
$children = $this
200+
->normalizeToInheritsFromReorder($normalized->get('children', []), $reorder)
201+
->map(fn ($childConfig, $childId) => $this->normalizeChildItemConfig($childId, $childConfig, $sectionKey))
202+
->keyBy(fn ($childConfig, $childId) => $this->normalizeItemId($childId, $childConfig))
203+
->all();
204+
205+
// Only output `children` in normalized output if there are any.
206+
$children
207+
? $normalized->put('children', $children)
208+
: $normalized->forget('children');
209+
198210
$allowedKeys = array_merge(['action'], static::ALLOWED_NAV_ITEM_MODIFICATIONS);
199211

200212
return $normalized->only($allowedKeys)->all();
@@ -234,11 +246,14 @@ protected function normalizeChildItemConfig($itemId, $itemConfig, $sectionKey)
234246
];
235247
}
236248

237-
if (is_array($itemConfig)) {
238-
Arr::forget($itemConfig, 'children');
249+
$normalized = $this->normalizeItemConfig($itemId, $itemConfig, $sectionKey, false);
250+
251+
if (is_array($normalized)) {
252+
Arr::forget($normalized, 'reorder');
253+
Arr::forget($normalized, 'children');
239254
}
240255

241-
return $this->normalizeItemConfig($itemId, $itemConfig, $sectionKey, false);
256+
return $normalized;
242257
}
243258

244259
/**
@@ -272,6 +287,21 @@ protected function itemIsInOriginalSection($itemId, $currentSectionKey)
272287
return Str::startsWith($itemId, "$currentSectionKey::");
273288
}
274289

290+
/**
291+
* Normalize to legacy style inherits from new `reorder: []` array schema, introduced to sidestep ordering issues in SQL.
292+
*/
293+
protected function normalizeToInheritsFromReorder(array|Collection $items, array|bool $reorder): Collection
294+
{
295+
if (! is_array($reorder)) {
296+
return collect($items);
297+
}
298+
299+
return collect($reorder)
300+
->flip()
301+
->map(fn () => '@inherit')
302+
->merge($items);
303+
}
304+
275305
/**
276306
* Get normalized preferences.
277307
*

src/CP/Navigation/NavTransformer.php

Lines changed: 37 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ class NavTransformer
1212
protected $coreNav;
1313
protected $submitted;
1414
protected $config;
15-
protected $reorderedMinimums;
1615

1716
/**
1817
* Instantiate nav transformer.
@@ -59,10 +58,9 @@ protected function removeEmptyCustomSections($submitted)
5958
*/
6059
protected function transform()
6160
{
62-
$this->config['reorder'] = $this->itemsAreReordered(
63-
$this->coreNav->pluck('display_original'),
64-
collect($this->submitted)->pluck('display_original'),
65-
'sections'
61+
$this->config['reorder'] = $this->getReorderedItems(
62+
$this->coreNav->map(fn ($section) => $this->transformSectionKey($section)),
63+
collect($this->submitted)->map(fn ($section) => $this->transformSectionKey($section)),
6664
);
6765

6866
$this->config['sections'] = collect($this->submitted)
@@ -110,10 +108,9 @@ protected function transformSection($section, $sectionKey)
110108

111109
$items = Arr::get($section, 'items', []);
112110

113-
$transformed['reorder'] = $this->itemsAreReordered(
111+
$transformed['reorder'] = $this->getReorderedItems(
114112
$this->coreNav->pluck('items', 'display_original')->get($displayOriginal, collect())->map->id(),
115113
collect($items)->pluck('id'),
116-
$sectionKey
117114
);
118115

119116
$transformed['items'] = $this->transformItems($items, $sectionKey);
@@ -134,7 +131,7 @@ protected function transformItems($items, $parentId, $transformingChildItems = f
134131
return collect($items)
135132
->map(fn ($item) => array_merge($item, ['id' => $this->transformItemId($item, $item['id'], $parentId, $items)]))
136133
->keyBy('id')
137-
->map(fn ($item, $itemId) => $this->transformItem($item, $itemId, $parentId, $transformingChildItems))
134+
->map(fn ($item, $itemId) => $this->transformItem($item, $itemId, $transformingChildItems))
138135
->all();
139136
}
140137

@@ -174,11 +171,10 @@ protected function transformItemId($item, $id, $parentId, $items)
174171
*
175172
* @param array $item
176173
* @param string $itemId
177-
* @param string $parentId
178174
* @param bool $isChild
179175
* @return array
180176
*/
181-
protected function transformItem($item, $itemId, $parentId, $isChild = false)
177+
protected function transformItem($item, $itemId, $isChild = false)
182178
{
183179
$transformed = Arr::get($item, 'manipulations', []);
184180

@@ -210,10 +206,9 @@ protected function transformItem($item, $itemId, $parentId, $isChild = false)
210206
$transformed['reorder'] = false;
211207

212208
if ($children && $originalHasChildren && ! in_array($transformed['action'], ['@alias', '@create'])) {
213-
$transformed['reorder'] = $this->itemsAreReordered(
209+
$transformed['reorder'] = $this->getReorderedItems(
214210
$originalItem->resolveChildren()->children()->map->id()->all(),
215211
collect($children)->keys()->all(),
216-
$itemId
217212
);
218213
}
219214

@@ -254,14 +249,12 @@ protected function transformItemUrl($url)
254249
}
255250

256251
/**
257-
* Check if items are being reordered.
252+
* Check if items are being reordered and return minimum list of item keys required to replicate saved order.
258253
*
259254
* @param array $originalList
260255
* @param array $newList
261-
* @param string $parentKey
262-
* @return bool
263256
*/
264-
protected function itemsAreReordered($originalList, $newList, $parentKey)
257+
protected function getReorderedItems($originalList, $newList): bool|array
265258
{
266259
$itemsAreReordered = collect($originalList)
267260
->intersect($newList)
@@ -271,21 +264,22 @@ protected function itemsAreReordered($originalList, $newList, $parentKey)
271264
->reject(fn ($pair) => $pair->first() === $pair->last())
272265
->isNotEmpty();
273266

274-
if ($itemsAreReordered) {
275-
$this->trackReorderedMinimums($originalList, $newList, $parentKey);
267+
if (! $itemsAreReordered) {
268+
return false;
276269
}
277270

278-
return $itemsAreReordered;
271+
return collect($newList)
272+
->take($this->calculateMinimumItemsForReorder($originalList, $newList))
273+
->all();
279274
}
280275

281276
/**
282-
* Track minimum number of items needed for reorder config.
277+
* Calculate minimum number of items needed for reorder config.
283278
*
284279
* @param array $originalList
285280
* @param array $newList
286-
* @param string $parentKey
287281
*/
288-
protected function trackReorderedMinimums($originalList, $newList, $parentKey)
282+
protected function calculateMinimumItemsForReorder($originalList, $newList): int
289283
{
290284
$continueRejecting = true;
291285

@@ -301,7 +295,7 @@ protected function trackReorderedMinimums($originalList, $newList, $parentKey)
301295
})
302296
->count();
303297

304-
$this->reorderedMinimums[$parentKey] = max(1, $minimumItemsCount - 1);
298+
return max(1, $minimumItemsCount - 1);
305299
}
306300

307301
/**
@@ -326,15 +320,18 @@ protected function findOriginalItem($id)
326320
*/
327321
protected function minify()
328322
{
329-
$this->config['sections'] = collect($this->config['sections'])
330-
->map(fn ($section, $key) => $this->minifySection($section, $key))
323+
$sections = collect($this->config['sections'])
324+
->map(fn ($section) => $this->minifySection($section))
325+
->pipe(fn ($sections) => $this->rejectInherits($sections));
326+
327+
$reorder = collect(Arr::get($this->config, 'reorder') ?: [])
328+
->reject(fn ($section) => $section === 'top_level')
329+
->values()
331330
->all();
332331

333-
if ($this->config['reorder'] === true) {
334-
$this->config['sections'] = $this->rejectUnessessaryInherits($this->config['sections'], 'sections');
335-
} else {
336-
$this->config = $this->rejectAllInherits($this->config['sections']);
337-
}
332+
$this->config = $reorder
333+
? array_filter(compact('reorder', 'sections'))
334+
: $sections;
338335

339336
// If the config is completely null after minifying, ensure we save an empty array.
340337
// For example, if we're transforming this config for a user's nav preferences,
@@ -351,21 +348,17 @@ protected function minify()
351348
* Minify tranformed section.
352349
*
353350
* @param array $section
354-
* @param string $sectionKey
355351
* @return mixed
356352
*/
357-
protected function minifySection($section, $sectionKey)
353+
protected function minifySection($section)
358354
{
359355
$action = Arr::get($section, 'action');
360356

361357
$section['items'] = collect($section['items'])
362-
->map(fn ($item, $key) => $this->minifyItem($item, $key))
363-
->all();
358+
->map(fn ($item) => $this->minifyItem($item))
359+
->pipe(fn ($items) => $this->rejectInherits($items));
364360

365-
if ($section['reorder'] === true) {
366-
$section['items'] = $this->rejectUnessessaryInherits($section['items'], $sectionKey);
367-
} else {
368-
$section['items'] = $this->rejectAllInherits($section['items']);
361+
if (! $section['reorder']) {
369362
Arr::forget($section, 'reorder');
370363
}
371364

@@ -390,21 +383,16 @@ protected function minifySection($section, $sectionKey)
390383
* Minify tranformed item.
391384
*
392385
* @param array $item
393-
* @param string $itemKey
386+
* @param bool $isChild
394387
* @return array
395388
*/
396-
protected function minifyItem($item, $itemKey, $isChild = false)
389+
protected function minifyItem($item, $isChild = false)
397390
{
398-
$action = Arr::get($item, 'action');
399-
400391
$item['children'] = collect($item['children'] ?? [])
401-
->map(fn ($item, $childId) => $this->minifyItem($item, $childId, true))
402-
->all();
392+
->map(fn ($item) => $this->minifyItem($item, true))
393+
->pipe(fn ($items) => $this->rejectInherits($items));
403394

404-
if ($item['reorder'] === true) {
405-
$item['children'] = $this->rejectUnessessaryInherits($item['children'], $itemKey);
406-
} else {
407-
$item['children'] = $this->rejectAllInherits($item['children']);
395+
if (! $item['reorder']) {
408396
Arr::forget($item, 'reorder');
409397
}
410398

@@ -432,7 +420,7 @@ protected function minifyItem($item, $itemKey, $isChild = false)
432420
* @param array $items
433421
* @return array
434422
*/
435-
protected function rejectAllInherits($items)
423+
protected function rejectInherits($items)
436424
{
437425
$items = collect($items)->reject(fn ($item) => $item === '@inherit');
438426

@@ -443,37 +431,6 @@ protected function rejectAllInherits($items)
443431
return $items->all();
444432
}
445433

446-
/**
447-
* Reject unessessary `@inherit`s at end of array.
448-
*
449-
* @param array $items
450-
* @param string $parentKey
451-
* @return array
452-
*/
453-
protected function rejectUnessessaryInherits($items, $parentKey)
454-
{
455-
if (! $reorderedMinimum = $this->reorderedMinimums[$parentKey] ?? false) {
456-
return $items;
457-
}
458-
459-
$keyValuePairs = collect($items)
460-
->map(fn ($item, $key) => ['key' => $key, 'value' => $item])
461-
->values()
462-
->keyBy(fn ($keyValuePair, $index) => $index + 1);
463-
464-
$trailingInherits = $keyValuePairs
465-
->reverse()
466-
->takeUntil(fn ($item) => $item['value'] !== '@inherit');
467-
468-
$modifiedMinimum = $keyValuePairs->count() - $trailingInherits->count();
469-
470-
$actualMinimum = max($reorderedMinimum, $modifiedMinimum);
471-
472-
return collect($items)
473-
->take($actualMinimum)
474-
->all();
475-
}
476-
477434
/**
478435
* Get config.
479436
*

0 commit comments

Comments
 (0)