Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.x] Fix CP nav ordering for when preferences are stored in JSON SQL columns #10809

Merged
merged 21 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
596320b
From now on, we reject all `@inherit` items when minifying.
jesseleite Sep 17, 2024
fe04239
Rename method.
jesseleite Sep 17, 2024
084b1fa
Rework `@inherit` minification at each level of nav.
jesseleite Sep 17, 2024
355cda1
Save explicit array to `reorder` instead of `true` bool.
jesseleite Sep 18, 2024
5c85884
Return minimum count for use in `reorder` arrays.
jesseleite Sep 18, 2024
599e1b5
Rename method and remove unused `reorderedMinimums` property.
jesseleite Sep 18, 2024
332e46a
Remove unused params.
jesseleite Sep 18, 2024
9315279
Remove `Top Level` from minified sections output.
jesseleite Sep 18, 2024
1aa98ce
This is a bit cleaner.
jesseleite Sep 18, 2024
cb8e001
Reordered sections should be list of keys.
jesseleite Sep 18, 2024
512d8c6
Pass tests again with new `reorder` yaml schema.
jesseleite Sep 18, 2024
5af5c3f
Add helper to merge new `reorder` array style with legacy `@inherit` …
jesseleite Sep 18, 2024
6eb3011
Allow for new `reorder` array style when reordering sections.
jesseleite Sep 18, 2024
9191905
Allow for new `reorder` array style when reordering items.
jesseleite Sep 18, 2024
537c5e8
Allow for new `reorder` array style when reordering deeper child items.
jesseleite Sep 18, 2024
95a0d7b
Fix normalization of `reorder` and `children` on deeper child items.
jesseleite Sep 18, 2024
9fc40bc
Don’t normalize to `reorder: false`.
jesseleite Sep 18, 2024
de07d1e
Pass test again, because method requires array/Collection arg input now.
jesseleite Sep 18, 2024
4ef4e80
Flesh out test coverage for new `reorder: []` array style.
jesseleite Sep 18, 2024
45c540d
Use `assertSame` to ensure object key order.
jesseleite Sep 18, 2024
b1b9db0
Test legacy & array style config examples with reordering of sections…
jesseleite Sep 18, 2024
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
56 changes: 43 additions & 13 deletions src/CP/Navigation/NavPreferencesNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Statamic\CP\Navigation;

use Illuminate\Support\Collection;
use Statamic\Support\Arr;
use Statamic\Support\Str;

Expand Down Expand Up @@ -72,11 +73,12 @@ protected function normalize()
{
$navConfig = collect($this->preferences);

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

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

$sections = $sections
$sections = $this
->normalizeToInheritsFromReorder($sections, $reorder)
->prepend($sections->pull('top_level') ?? '@inherit', 'top_level')
->map(fn ($config, $section) => $this->normalizeSectionConfig($config, $section))
->reject(fn ($config) => $config['action'] === '@inherit' && ! $reorder)
Expand Down Expand Up @@ -115,15 +117,16 @@ protected function normalizeSectionConfig($sectionConfig, $sectionKey)

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

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

$items = collect($sectionConfig->get('items') ?? $sectionConfig->except([
'action',
'display',
'reorder',
]));

$items = $items
$items = $this
->normalizeToInheritsFromReorder($items, $reorder)
->map(fn ($config, $itemId) => $this->normalizeItemConfig($itemId, $config, $sectionKey))
->keyBy(fn ($config, $itemId) => $this->normalizeItemId($itemId, $config))
->filter()
Expand Down Expand Up @@ -187,14 +190,23 @@ protected function normalizeItemConfig($itemId, $itemConfig, $sectionKey, $remov
}
}

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

// Normalize `children`.
$children = $this
->normalizeToInheritsFromReorder($normalized->get('children', []), $reorder)
->map(fn ($childConfig, $childId) => $this->normalizeChildItemConfig($childId, $childConfig, $sectionKey))
->keyBy(fn ($childConfig, $childId) => $this->normalizeItemId($childId, $childConfig))
->all();

// Only output `children` in normalized output if there are any.
$children
? $normalized->put('children', $children)
: $normalized->forget('children');

$allowedKeys = array_merge(['action'], static::ALLOWED_NAV_ITEM_MODIFICATIONS);

return $normalized->only($allowedKeys)->all();
Expand Down Expand Up @@ -234,11 +246,14 @@ protected function normalizeChildItemConfig($itemId, $itemConfig, $sectionKey)
];
}

if (is_array($itemConfig)) {
Arr::forget($itemConfig, 'children');
$normalized = $this->normalizeItemConfig($itemId, $itemConfig, $sectionKey, false);

if (is_array($normalized)) {
Arr::forget($normalized, 'reorder');
Arr::forget($normalized, 'children');
}

return $this->normalizeItemConfig($itemId, $itemConfig, $sectionKey, false);
return $normalized;
}

/**
Expand Down Expand Up @@ -272,6 +287,21 @@ protected function itemIsInOriginalSection($itemId, $currentSectionKey)
return Str::startsWith($itemId, "$currentSectionKey::");
}

/**
* Normalize to legacy style inherits from new `reorder: []` array schema, introduced to sidestep ordering issues in SQL.
*/
protected function normalizeToInheritsFromReorder(array|Collection $items, array|bool $reorder): Collection
{
if (! is_array($reorder)) {
return collect($items);
}

return collect($reorder)
->flip()
->map(fn () => '@inherit')
->merge($items);
}

/**
* Get normalized preferences.
*
Expand Down
117 changes: 37 additions & 80 deletions src/CP/Navigation/NavTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ class NavTransformer
protected $coreNav;
protected $submitted;
protected $config;
protected $reorderedMinimums;

/**
* Instantiate nav transformer.
Expand Down Expand Up @@ -59,10 +58,9 @@ protected function removeEmptyCustomSections($submitted)
*/
protected function transform()
{
$this->config['reorder'] = $this->itemsAreReordered(
$this->coreNav->pluck('display_original'),
collect($this->submitted)->pluck('display_original'),
'sections'
$this->config['reorder'] = $this->getReorderedItems(
$this->coreNav->map(fn ($section) => $this->transformSectionKey($section)),
collect($this->submitted)->map(fn ($section) => $this->transformSectionKey($section)),
);

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

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

$transformed['reorder'] = $this->itemsAreReordered(
$transformed['reorder'] = $this->getReorderedItems(
$this->coreNav->pluck('items', 'display_original')->get($displayOriginal, collect())->map->id(),
collect($items)->pluck('id'),
$sectionKey
);

$transformed['items'] = $this->transformItems($items, $sectionKey);
Expand All @@ -134,7 +131,7 @@ protected function transformItems($items, $parentId, $transformingChildItems = f
return collect($items)
->map(fn ($item) => array_merge($item, ['id' => $this->transformItemId($item, $item['id'], $parentId, $items)]))
->keyBy('id')
->map(fn ($item, $itemId) => $this->transformItem($item, $itemId, $parentId, $transformingChildItems))
->map(fn ($item, $itemId) => $this->transformItem($item, $itemId, $transformingChildItems))
->all();
}

Expand Down Expand Up @@ -174,11 +171,10 @@ protected function transformItemId($item, $id, $parentId, $items)
*
* @param array $item
* @param string $itemId
* @param string $parentId
* @param bool $isChild
* @return array
*/
protected function transformItem($item, $itemId, $parentId, $isChild = false)
protected function transformItem($item, $itemId, $isChild = false)
{
$transformed = Arr::get($item, 'manipulations', []);

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

if ($children && $originalHasChildren && ! in_array($transformed['action'], ['@alias', '@create'])) {
$transformed['reorder'] = $this->itemsAreReordered(
$transformed['reorder'] = $this->getReorderedItems(
$originalItem->resolveChildren()->children()->map->id()->all(),
collect($children)->keys()->all(),
$itemId
);
}

Expand Down Expand Up @@ -254,14 +249,12 @@ protected function transformItemUrl($url)
}

/**
* Check if items are being reordered.
* Check if items are being reordered and return minimum list of item keys required to replicate saved order.
*
* @param array $originalList
* @param array $newList
* @param string $parentKey
* @return bool
*/
protected function itemsAreReordered($originalList, $newList, $parentKey)
protected function getReorderedItems($originalList, $newList): bool|array
{
$itemsAreReordered = collect($originalList)
->intersect($newList)
Expand All @@ -271,21 +264,22 @@ protected function itemsAreReordered($originalList, $newList, $parentKey)
->reject(fn ($pair) => $pair->first() === $pair->last())
->isNotEmpty();

if ($itemsAreReordered) {
$this->trackReorderedMinimums($originalList, $newList, $parentKey);
if (! $itemsAreReordered) {
return false;
}

return $itemsAreReordered;
return collect($newList)
->take($this->calculateMinimumItemsForReorder($originalList, $newList))
->all();
}

/**
* Track minimum number of items needed for reorder config.
* Calculate minimum number of items needed for reorder config.
*
* @param array $originalList
* @param array $newList
* @param string $parentKey
*/
protected function trackReorderedMinimums($originalList, $newList, $parentKey)
protected function calculateMinimumItemsForReorder($originalList, $newList): int
{
$continueRejecting = true;

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

$this->reorderedMinimums[$parentKey] = max(1, $minimumItemsCount - 1);
return max(1, $minimumItemsCount - 1);
}

/**
Expand All @@ -326,15 +320,18 @@ protected function findOriginalItem($id)
*/
protected function minify()
{
$this->config['sections'] = collect($this->config['sections'])
->map(fn ($section, $key) => $this->minifySection($section, $key))
$sections = collect($this->config['sections'])
->map(fn ($section) => $this->minifySection($section))
->pipe(fn ($sections) => $this->rejectInherits($sections));

$reorder = collect(Arr::get($this->config, 'reorder') ?: [])
->reject(fn ($section) => $section === 'top_level')
->values()
->all();

if ($this->config['reorder'] === true) {
$this->config['sections'] = $this->rejectUnessessaryInherits($this->config['sections'], 'sections');
} else {
$this->config = $this->rejectAllInherits($this->config['sections']);
}
$this->config = $reorder
? array_filter(compact('reorder', 'sections'))
: $sections;

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

$section['items'] = collect($section['items'])
->map(fn ($item, $key) => $this->minifyItem($item, $key))
->all();
->map(fn ($item) => $this->minifyItem($item))
->pipe(fn ($items) => $this->rejectInherits($items));

if ($section['reorder'] === true) {
$section['items'] = $this->rejectUnessessaryInherits($section['items'], $sectionKey);
} else {
$section['items'] = $this->rejectAllInherits($section['items']);
if (! $section['reorder']) {
Arr::forget($section, 'reorder');
}

Expand All @@ -390,21 +383,16 @@ protected function minifySection($section, $sectionKey)
* Minify tranformed item.
*
* @param array $item
* @param string $itemKey
* @param bool $isChild
* @return array
*/
protected function minifyItem($item, $itemKey, $isChild = false)
protected function minifyItem($item, $isChild = false)
{
$action = Arr::get($item, 'action');

$item['children'] = collect($item['children'] ?? [])
->map(fn ($item, $childId) => $this->minifyItem($item, $childId, true))
->all();
->map(fn ($item) => $this->minifyItem($item, true))
->pipe(fn ($items) => $this->rejectInherits($items));

if ($item['reorder'] === true) {
$item['children'] = $this->rejectUnessessaryInherits($item['children'], $itemKey);
} else {
$item['children'] = $this->rejectAllInherits($item['children']);
if (! $item['reorder']) {
Arr::forget($item, 'reorder');
}

Expand Down Expand Up @@ -432,7 +420,7 @@ protected function minifyItem($item, $itemKey, $isChild = false)
* @param array $items
* @return array
*/
protected function rejectAllInherits($items)
protected function rejectInherits($items)
{
$items = collect($items)->reject(fn ($item) => $item === '@inherit');

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

/**
* Reject unessessary `@inherit`s at end of array.
*
* @param array $items
* @param string $parentKey
* @return array
*/
protected function rejectUnessessaryInherits($items, $parentKey)
{
if (! $reorderedMinimum = $this->reorderedMinimums[$parentKey] ?? false) {
return $items;
}

$keyValuePairs = collect($items)
->map(fn ($item, $key) => ['key' => $key, 'value' => $item])
->values()
->keyBy(fn ($keyValuePair, $index) => $index + 1);

$trailingInherits = $keyValuePairs
->reverse()
->takeUntil(fn ($item) => $item['value'] !== '@inherit');

$modifiedMinimum = $keyValuePairs->count() - $trailingInherits->count();

$actualMinimum = max($reorderedMinimum, $modifiedMinimum);

return collect($items)
->take($actualMinimum)
->all();
}

/**
* Get config.
*
Expand Down
Loading
Loading