diff --git a/config/areas/site/requests.php b/config/areas/site/requests.php index 1ee80cae11..9658e0a500 100644 --- a/config/areas/site/requests.php +++ b/config/areas/site/requests.php @@ -3,7 +3,7 @@ use Kirby\Cms\App; use Kirby\Cms\Find; use Kirby\Panel\Controller\Changes; -use Kirby\Toolkit\I18n; +use Kirby\Panel\Controller\PageTree; $files = require __DIR__ . '/../files/requests.php'; @@ -125,86 +125,22 @@ ], // Tree Navigation - // @codeCoverageIgnoreStart - // TODO: move to controller class and add unit tests 'tree' => [ 'pattern' => 'site/tree', 'action' => function () { - $kirby = App::instance(); - $request = $kirby->request(); - $move = $request->get('move'); - $move = $move ? Find::parent($move) : null; - $parent = $request->get('parent'); - - if ($parent === null) { - $site = $kirby->site(); - $panel = $site->panel(); - $uuid = $site->uuid()?->toString(); - $url = $site->url(); - $value = $uuid ?? '/'; - - return [ - [ - 'children' => $panel->url(true), - 'disabled' => $move?->isMovableTo($site) === false, - 'hasChildren' => true, - 'icon' => 'home', - 'id' => '/', - 'label' => I18n::translate('view.site'), - 'open' => false, - 'url' => $url, - 'uuid' => $uuid, - 'value' => $value - ] - ]; - } - - $parent = Find::parent($parent); - $pages = []; - - foreach ($parent->childrenAndDrafts()->filterBy('isListable', true) as $child) { - $panel = $child->panel(); - $uuid = $child->uuid()?->toString(); - $url = $child->url(); - $value = $uuid ?? $child->id(); - - $pages[] = [ - 'children' => $panel->url(true), - 'disabled' => $move?->isMovableTo($child) === false, - 'hasChildren' => $child->hasChildren() === true || $child->hasDrafts() === true, - 'icon' => $panel->image()['icon'] ?? null, - 'id' => $child->id(), - 'open' => false, - 'label' => $child->title()->value(), - 'url' => $url, - 'uuid' => $uuid, - 'value' => $value - ]; - } - - return $pages; + return (new PageTree())->children( + parent: App::instance()->request()->get('parent'), + moving: App::instance()->request()->get('move') + ); } ], 'tree.parents' => [ 'pattern' => 'site/tree/parents', 'action' => function () { - $kirby = App::instance(); - $request = $kirby->request(); - $root = $request->get('root'); - $page = $kirby->page($request->get('page')); - $parents = $page?->parents()->flip()->values( - fn ($parent) => $parent->uuid()?->toString() ?? $parent->id() - ) ?? []; - - // if root is included, add the site as top-level parent - if ($root === 'true') { - array_unshift($parents, $kirby->site()->uuid()?->toString() ?? '/'); - } - - return [ - 'data' => $parents - ]; + return (new PageTree())->parents( + page: App::instance()->request()->get('page'), + includeSite: App::instance()->request()->get('root') === 'true', + ); } ] - // @codeCoverageIgnoreEnd ]; diff --git a/src/Panel/Controller/PageTree.php b/src/Panel/Controller/PageTree.php new file mode 100644 index 0000000000..49162397c1 --- /dev/null +++ b/src/Panel/Controller/PageTree.php @@ -0,0 +1,113 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://getkirby.com/license + */ +class PageTree +{ + protected Site $site; + + public function __construct( + ) { + $this->site = App::instance()->site(); + } + + /** + * Returns children for the parent as entries + */ + public function children( + string|null $parent = null, + string|null $moving = null + ): array { + if ($moving !== null) { + $moving = Find::parent($moving); + } + + if ($parent === null) { + return [ + $this->entry($this->site, $moving) + ]; + } + + return Find::parent($parent) + ->childrenAndDrafts() + ->filterBy('isListable', true) + ->values( + fn ($child) => $this->entry($child, $moving) + ); + } + + /** + * Returns the properties to display the site or page + * as an entry in the page tree component + */ + public function entry( + Site|Page $entry, + Page|null $moving = null + ): array { + $panel = $entry->panel(); + $id = $entry->id() ?? '/'; + $uuid = $entry->uuid()?->toString(); + $url = $entry->url(); + $value = $uuid ?? $id; + + return [ + 'children' => $panel->url(true), + 'disabled' => $moving?->isMovableTo($entry) === false, + 'hasChildren' => + $entry->hasChildren() === true || + $entry->hasDrafts() === true, + 'icon' => match (true) { + $entry instanceof Site => 'home', + default => $panel->image()['icon'] ?? null + }, + 'id' => $id, + 'open' => false, + 'label' => match (true) { + $entry instanceof Site => I18n::translate('view.site'), + default => $entry->title()->value() + }, + 'url' => $url, + 'uuid' => $uuid, + 'value' => $value + ]; + } + + /** + * Returns the UUIDs/ids for all parents of the page + */ + public function parents( + string|null $page = null, + bool $includeSite = false, + ): array { + $page = $this->site->page($page); + $parents = $page?->parents()->flip(); + $parents = $parents?->values( + fn ($parent) => $parent->uuid()?->toString() ?? $parent->id() + ); + $parents ??= []; + + if ($includeSite === true) { + array_unshift($parents, $this->site->uuid()?->toString() ?? '/'); + } + + return [ + 'data' => $parents + ]; + } +} diff --git a/tests/Panel/Controller/PageTreeTest.php b/tests/Panel/Controller/PageTreeTest.php new file mode 100644 index 0000000000..ab3f51352f --- /dev/null +++ b/tests/Panel/Controller/PageTreeTest.php @@ -0,0 +1,231 @@ +setUpTmp(); + $this->setUpSingleLanguage(site: [ + 'children' => [ + [ + 'slug' => 'articles', + 'content' => [ + 'title' => 'Blog articles', + 'uuid' => 'articles' + ], + 'children' => [ + [ + 'slug' => 'article-1', + 'content' => [ + 'uuid' => 'article-1' + ], + ], + [ + 'slug' => 'article-2', + 'content' => [ + 'uuid' => 'article-2' + ], + 'children' => [ + ['slug' => 'subarticle'] + ] + ] + ] + ] + ] + ]); + + $this->app->impersonate('kirby'); + $this->tree = new PageTree($this->app->site()); + } + + public function tearDown(): void + { + $this->tearDownTmp(); + App::destroy(); + } + + /** + * @covers ::children + */ + public function testChildrenForSite(): void + { + $children = $this->tree->children(null); + + $this->assertCount(1, $children); + $this->assertSame('site://', $children[0]['value']); + } + + /** + * @covers ::children + */ + public function testChildrenForPage(): void + { + $children = $this->tree->children('/pages/articles'); + + $this->assertCount(2, $children); + $this->assertSame('page://article-1', $children[0]['value']); + $this->assertSame('page://article-2', $children[1]['value']); + $this->assertFalse($children[0]['disabled']); + $this->assertFalse($children[1]['disabled']); + } + + /** + * @covers ::children + */ + public function testChildrenWithMoving(): void + { + $children = $this->tree->children( + '/pages/articles', + '/pages/articles+article-2+subarticle' + ); + + $this->assertCount(2, $children); + $this->assertSame('page://article-1', $children[0]['value']); + $this->assertSame('page://article-2', $children[1]['value']); + $this->assertTrue($children[0]['disabled']); + $this->assertTrue($children[1]['disabled']); + } + + /** + * @covers ::entry + */ + public function testEntryWithSite(): void + { + $entry = $this->tree->entry($this->app->site()); + + $this->assertSame('/site', $entry['children']); + $this->assertFalse($entry['disabled']); + $this->assertTrue($entry['hasChildren']); + $this->assertSame('home', $entry['icon']); + $this->assertSame('/', $entry['id']); + $this->assertFalse($entry['open']); + $this->assertSame('Site', $entry['label']); + $this->assertSame('/', $entry['url']); + $this->assertSame('site://', $entry['uuid']); + $this->assertSame('site://', $entry['value']); + } + + /** + * @covers ::entry + */ + public function testEntryWithPage(): void + { + $entry = $this->tree->entry($this->app->page('articles')); + + $this->assertSame('/pages/articles', $entry['children']); + $this->assertFalse($entry['disabled']); + $this->assertTrue($entry['hasChildren']); + $this->assertSame('page', $entry['icon']); + $this->assertSame('articles', $entry['id']); + $this->assertFalse($entry['open']); + $this->assertSame('Blog articles', $entry['label']); + $this->assertSame('/articles', $entry['url']); + $this->assertSame('page://articles', $entry['uuid']); + $this->assertSame('page://articles', $entry['value']); + } + + /** + * @covers ::entry + */ + public function testEntryWithMoving(): void + { + $entry = $this->tree->entry( + $this->app->page('articles/article-2/subarticle'), + $this->app->page('articles/article-1') + ); + + $this->assertTrue($entry['disabled']); + } + + /** + * @covers ::entry + */ + public function testEntryWhenUuidsDisabled(): void + { + $this->app = $this->app->clone([ + 'options' => [ + 'content' => [ + 'uuid' => false + ] + ] + ]); + + $entry = $this->tree->entry($this->app->page('articles')); + + $this->assertSame('articles', $entry['id']); + $this->assertNull($entry['uuid']); + $this->assertSame('articles', $entry['value']); + } + + /** + * @covers ::parents + */ + public function testParents(): void + { + $parents = $this->tree->parents( + page: 'articles/article-2/subarticle' + ); + + $this->assertSame([ + 'page://articles', + 'page://article-2' + ], $parents['data']); + + $parents = $this->tree->parents( + page: 'articles/article-2/subarticle', + includeSite: true + ); + + $this->assertSame([ + 'site://', + 'page://articles', + 'page://article-2' + ], $parents['data']); + } + + /** + * @covers ::parents + */ + public function testParentsWhenUuidsDisabled(): void + { + $this->app = $this->app->clone([ + 'options' => [ + 'content' => [ + 'uuid' => false + ] + ] + ]); + + $parents = $this->tree->parents( + page: 'articles/article-2/subarticle' + ); + + $this->assertSame([ + 'articles', + 'articles/article-2' + ], $parents['data']); + + $parents = $this->tree->parents( + page: 'articles/article-2/subarticle', + includeSite: true + ); + + $this->assertSame([ + '/', + 'articles', + 'articles/article-2' + ], $parents['data']); + } +}