From fcd1d75a254a72966d921177c8fbef6b2ad3ffd6 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sat, 11 Jan 2025 19:52:44 +0100 Subject: [PATCH 1/5] View buttons: i18n + query support --- src/Panel/File.php | 5 +--- src/Panel/Page.php | 5 +--- src/Panel/Site.php | 5 +--- src/Panel/Ui/Button.php | 10 ++++--- src/Panel/Ui/Buttons/LanguagesDropdown.php | 5 ++-- src/Panel/Ui/Buttons/ViewButton.php | 31 ++++++++++++++++++---- src/Panel/Ui/Buttons/ViewButtons.php | 21 ++++++++++----- src/Panel/User.php | 5 +--- tests/Panel/Ui/Buttons/ViewButtonTest.php | 29 +++++++++++--------- tests/Panel/Ui/Buttons/ViewButtonsTest.php | 6 ++--- 10 files changed, 73 insertions(+), 49 deletions(-) diff --git a/src/Panel/File.php b/src/Panel/File.php index af714ae442..3a499e828d 100644 --- a/src/Panel/File.php +++ b/src/Panel/File.php @@ -76,10 +76,7 @@ public function buttons(): array 'preview', 'settings', 'languages' - )->bind([ - 'file' => $this->model(), - 'model' => $this->model() - ])->render(); + )->render(); } /** diff --git a/src/Panel/Page.php b/src/Panel/Page.php index 4b63eee132..f9610236e3 100644 --- a/src/Panel/Page.php +++ b/src/Panel/Page.php @@ -51,10 +51,7 @@ public function buttons(): array 'settings', 'languages', 'status' - )->bind([ - 'page' => $this->model(), - 'model' => $this->model() - ])->render(); + )->render(); } /** diff --git a/src/Panel/Site.php b/src/Panel/Site.php index 95aff7b854..6e777900ce 100644 --- a/src/Panel/Site.php +++ b/src/Panel/Site.php @@ -33,10 +33,7 @@ public function buttons(): array return ViewButtons::view($this)->defaults( 'preview', 'languages' - )->bind([ - 'model' => $this->model(), - 'site' => $this->model() - ])->render(); + )->render(); } /** diff --git a/src/Panel/Ui/Button.php b/src/Panel/Ui/Button.php index 93dac09223..563fc52078 100644 --- a/src/Panel/Ui/Button.php +++ b/src/Panel/Ui/Button.php @@ -2,6 +2,8 @@ namespace Kirby\Panel\Ui; +use Kirby\Toolkit\I18n; + /** * @package Kirby Panel * @author Bastian Allgeier @@ -28,9 +30,9 @@ public function __construct( public string|null $size = null, public string|null $style = null, public string|null $target = null, - public string|null $text = null, + public string|array|null $text = null, public string|null $theme = null, - public string|null $title = null, + public string|array|null $title = null, public string $type = 'button', public string|null $variant = null, ) { @@ -51,9 +53,9 @@ public function props(): array 'responsive' => $this->responsive, 'size' => $this->size, 'target' => $this->target, - 'text' => $this->text, + 'text' => I18n::translate($this->text, $this->text), 'theme' => $this->theme, - 'title' => $this->title, + 'title' => I18n::translate($this->title, $this->title), 'type' => $this->type, 'variant' => $this->variant, ]; diff --git a/src/Panel/Ui/Buttons/LanguagesDropdown.php b/src/Panel/Ui/Buttons/LanguagesDropdown.php index 9f0f8ffe91..f12ea42960 100644 --- a/src/Panel/Ui/Buttons/LanguagesDropdown.php +++ b/src/Panel/Ui/Buttons/LanguagesDropdown.php @@ -25,17 +25,18 @@ class LanguagesDropdown extends ViewButton protected App $kirby; public function __construct( - protected ModelWithContent $model + ModelWithContent $model ) { $this->kirby = $model->kirby(); parent::__construct( component: 'k-languages-dropdown', + model: $model, class: 'k-languages-dropdown', icon: 'translate', // Fiber dropdown endpoint to load options // only when dropdown is opened - options: $this->model->panel()->url(true) . '/languages', + options: $model->panel()->url(true) . '/languages', responsive: 'text', text: Str::upper($this->kirby->language()?->code()) ); diff --git a/src/Panel/Ui/Buttons/ViewButton.php b/src/Panel/Ui/Buttons/ViewButton.php index 2f18cbead2..54d1f3a085 100644 --- a/src/Panel/Ui/Buttons/ViewButton.php +++ b/src/Panel/Ui/Buttons/ViewButton.php @@ -4,6 +4,7 @@ use Closure; use Kirby\Cms\App; +use Kirby\Cms\ModelWithContent; use Kirby\Panel\Panel; use Kirby\Panel\Ui\Button; use Kirby\Toolkit\Controller; @@ -24,6 +25,7 @@ class ViewButton extends Button { public function __construct( public string $component = 'k-view-button', + public readonly ModelWithContent|null $model = null, public array|null $badge = null, public string|null $class = null, public string|bool|null $current = null, @@ -38,9 +40,9 @@ public function __construct( public string|null $size = 'sm', public string|null $style = null, public string|null $target = null, - public string|null $text = null, + public string|array|null $text = null, public string|null $theme = null, - public string|null $title = null, + public string|array|null $title = null, public string $type = 'button', public string|null $variant = 'filled', ) { @@ -54,6 +56,7 @@ public function __construct( public static function factory( string|array|Closure $button, string|null $view = null, + ModelWithContent|null $model = null, array $data = [] ): static|null { // referenced by name @@ -61,7 +64,7 @@ public static function factory( $button = static::find($button, $view); } - $button = static::resolve($button, $data); + $button = static::resolve($button, $model, $data); if ( $button === null || @@ -70,7 +73,7 @@ public static function factory( return $button; } - return new static(...static::normalize($button)); + return new static(...static::normalize($button), model: $model); } /** @@ -125,8 +128,16 @@ public static function normalize(array $button): array public function props(): array { + $resolve = fn ($value) => + $value ? + $this->model?->toSafeString($value) ?? $value : + null; + return [ ...parent::props(), + 'dialog' => $resolve($this->dialog), + 'drawer' => $resolve($this->drawer), + 'link' => $resolve($this->link), 'options' => $this->options ]; } @@ -138,12 +149,22 @@ public function props(): array */ public static function resolve( Closure|array $button, + ModelWithContent|null $model = null, array $data = [] ): static|array|null { if ($button instanceof Closure) { $kirby = App::instance(); $controller = new Controller($button); - $button = $controller->call(data: [ + + if ($model instanceof ModelWithContent) { + $data = [ + 'model' => $model, + $model::CLASS_ALIAS => $model, + ...$data + ]; + } + + $button = $controller->call(data: [ 'kirby' => $kirby, 'site' => $kirby->site(), 'user' => $kirby->user(), diff --git a/src/Panel/Ui/Buttons/ViewButtons.php b/src/Panel/Ui/Buttons/ViewButtons.php index b1c7a78189..f60a87168d 100644 --- a/src/Panel/Ui/Buttons/ViewButtons.php +++ b/src/Panel/Ui/Buttons/ViewButtons.php @@ -3,6 +3,7 @@ namespace Kirby\Panel\Ui\Buttons; use Kirby\Cms\App; +use Kirby\Cms\ModelWithContent; use Kirby\Panel\Model; use Kirby\Toolkit\A; @@ -21,6 +22,7 @@ class ViewButtons { public function __construct( public readonly string $view, + public readonly ModelWithContent|null $model = null, public array|false|null $buttons = null, public array $data = [] ) { @@ -66,9 +68,10 @@ public function render(): array $this->buttons ?? [], fn ($button) => ViewButton::factory( - $button, - $this->view, - $this->data + button: $button, + view: $this->view, + model: $this->model, + data: $this->data )?->render() ); @@ -79,15 +82,19 @@ public function render(): array * Creates new instance for a view * with special support for model views */ - public static function view(string|Model $view): static - { + public static function view( + string|Model $view, + ModelWithContent|null $model = null + ): static { if ($view instanceof Model) { - $blueprint = $view->model()->blueprint()->buttons(); - $view = $view->model()::CLASS_ALIAS; + $model = $view->model(); + $blueprint = $model->blueprint()->buttons(); + $view = $model::CLASS_ALIAS; } return new static( view: $view, + model: $model ?? null, buttons: $blueprint ?? null ); } diff --git a/src/Panel/User.php b/src/Panel/User.php index b86c6ba3ad..d3c35a6792 100644 --- a/src/Panel/User.php +++ b/src/Panel/User.php @@ -50,10 +50,7 @@ public function buttons(): array 'theme', 'settings', 'languages' - )->bind([ - 'model' => $this->model(), - 'user' => $this->model() - ])->render(); + )->render(); } /** diff --git a/tests/Panel/Ui/Buttons/ViewButtonTest.php b/tests/Panel/Ui/Buttons/ViewButtonTest.php index fad9527d2a..b2e07b8632 100644 --- a/tests/Panel/Ui/Buttons/ViewButtonTest.php +++ b/tests/Panel/Ui/Buttons/ViewButtonTest.php @@ -24,9 +24,11 @@ public function setUp(): void public function testFactoryFromClosure() { $button = ViewButton::factory( - fn (string $name) => ['component' => 'k-' . $name . '-view-button'], - 'test', - ['name' => 'foo'] + button: fn (string $name) => [ + 'component' => 'k-' . $name . '-view-button' + ], + view: 'test', + data: ['name' => 'foo'] ); $this->assertInstanceOf(ViewButton::class, $button); @@ -175,15 +177,18 @@ public function testProps() public function testResolve(): void { $test = $this; - $result = ViewButton::resolve(function (string $b, bool $a, App $kirby) use ($test) { - $test->assertFalse($a); - $test->assertSame('foo', $b); - $test->assertInstanceOf(App::class, $kirby); - return ['component' => 'k-test-view-button']; - }, [ - 'a' => false, - 'b' => 'foo' - ]); + $result = ViewButton::resolve( + button: function (string $b, bool $a, App $kirby) use ($test) { + $test->assertFalse($a); + $test->assertSame('foo', $b); + $test->assertInstanceOf(App::class, $kirby); + return ['component' => 'k-test-view-button']; + }, + data: [ + 'a' => false, + 'b' => 'foo' + ] + ); $this->assertSame('k-test-view-button', $result['component']); diff --git a/tests/Panel/Ui/Buttons/ViewButtonsTest.php b/tests/Panel/Ui/Buttons/ViewButtonsTest.php index ac7fdd710d..be4a450e9c 100644 --- a/tests/Panel/Ui/Buttons/ViewButtonsTest.php +++ b/tests/Panel/Ui/Buttons/ViewButtonsTest.php @@ -37,11 +37,11 @@ public function setUp(): void public function testConstruct() { // no buttons - $buttons = new ViewButtons('test', []); + $buttons = new ViewButtons('test', buttons: []); $this->assertCount(0, $buttons->buttons); // passed directly - $buttons = new ViewButtons('test', ['a', 'b']); + $buttons = new ViewButtons('test', buttons: ['a', 'b']); $this->assertCount(2, $buttons->buttons); // from options @@ -82,7 +82,7 @@ public function testDefaults() */ public function testRender() { - $buttons = new ViewButtons('test', ['a', 'b']); + $buttons = new ViewButtons('test', buttons: ['a', 'b']); $result = $buttons->render(); $this->assertCount(2, $result); From 8d8aecd97785cf555aef8c987b51bac590721163 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sat, 11 Jan 2025 21:08:50 +0100 Subject: [PATCH 2/5] View buttons: retrieve component name from key --- src/Panel/Ui/Buttons/ViewButton.php | 15 ++++++++++++++- src/Panel/Ui/Buttons/ViewButtons.php | 26 ++++++++++++++------------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/Panel/Ui/Buttons/ViewButton.php b/src/Panel/Ui/Buttons/ViewButton.php index 54d1f3a085..70264bc7a3 100644 --- a/src/Panel/Ui/Buttons/ViewButton.php +++ b/src/Panel/Ui/Buttons/ViewButton.php @@ -55,6 +55,7 @@ public function __construct( */ public static function factory( string|array|Closure $button, + string|int|null $name = null, string|null $view = null, ModelWithContent|null $model = null, array $data = [] @@ -64,6 +65,7 @@ public static function factory( $button = static::find($button, $view); } + // turn closure into actual button (object or array) $button = static::resolve($button, $model, $data); if ( @@ -73,7 +75,17 @@ public static function factory( return $button; } - return new static(...static::normalize($button), model: $model); + // flatten definition array into list of arguments for this class + $button = static::normalize($button); + + // if button definition has a name, use it for the component name + if (is_string($name) === true) { + // If this specific component does not exist, + // `k-view-buttons` will fall back to `k-view-button` again + $button['component'] ??= 'k-' . $name . '-view-button'; + } + + return new static(...$button, model: $model); } /** @@ -128,6 +140,7 @@ public static function normalize(array $button): array public function props(): array { + // helper for props that support Kirby queries $resolve = fn ($value) => $value ? $this->model?->toSafeString($value) ?? $value : diff --git a/src/Panel/Ui/Buttons/ViewButtons.php b/src/Panel/Ui/Buttons/ViewButtons.php index f60a87168d..f4546d5f6e 100644 --- a/src/Panel/Ui/Buttons/ViewButtons.php +++ b/src/Panel/Ui/Buttons/ViewButtons.php @@ -5,7 +5,6 @@ use Kirby\Cms\App; use Kirby\Cms\ModelWithContent; use Kirby\Panel\Model; -use Kirby\Toolkit\A; /** * Collects view buttons for a specific view @@ -26,6 +25,8 @@ public function __construct( public array|false|null $buttons = null, public array $data = [] ) { + // if no specific buttons are passed, + // use default buttons for this view from config $this->buttons ??= App::instance()->option( 'panel.viewButtons.' . $view ); @@ -64,18 +65,19 @@ public function render(): array return []; } - $buttons = A::map( - $this->buttons ?? [], - fn ($button) => - ViewButton::factory( - button: $button, - view: $this->view, - model: $this->model, - data: $this->data - )?->render() - ); + $buttons = []; + + foreach ($this->buttons ?? [] as $name => $button) { + $buttons[] = ViewButton::factory( + button: $button, + name: $name, + view: $this->view, + model: $this->model, + data: $this->data + )?->render(); + } - return array_values(array_filter($buttons)); + return array_filter($buttons); } /** From 0db0596d100648730f689bae12dff5370b5ceacb Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sat, 11 Jan 2025 21:20:58 +0100 Subject: [PATCH 3/5] ViewButton: support `Language` as model --- config/areas/languages/views.php | 3 +-- src/Cms/Language.php | 5 +++++ src/Panel/Ui/Buttons/ViewButton.php | 12 ++++++++---- src/Panel/Ui/Buttons/ViewButtons.php | 5 +++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/config/areas/languages/views.php b/config/areas/languages/views.php index ccc9ac3cc4..bee79c14cf 100644 --- a/config/areas/languages/views.php +++ b/config/areas/languages/views.php @@ -75,9 +75,8 @@ ], 'props' => [ 'buttons' => fn () => - ViewButtons::view('language') + ViewButtons::view('language', model: $language) ->defaults('preview', 'settings', 'delete') - ->bind(['language' => $language]) ->render(), 'deletable' => $language->isDeletable(), 'code' => Escape::html($language->code()), diff --git a/src/Cms/Language.php b/src/Cms/Language.php index c5053c359a..1ed23f1247 100644 --- a/src/Cms/Language.php +++ b/src/Cms/Language.php @@ -34,6 +34,11 @@ class Language implements Stringable */ use HasSiblings; + /** + * Short human-readable version used in template queries + */ + public const CLASS_ALIAS = 'language'; + /** * The parent Kirby instance */ diff --git a/src/Panel/Ui/Buttons/ViewButton.php b/src/Panel/Ui/Buttons/ViewButton.php index 70264bc7a3..74de7a3704 100644 --- a/src/Panel/Ui/Buttons/ViewButton.php +++ b/src/Panel/Ui/Buttons/ViewButton.php @@ -4,6 +4,7 @@ use Closure; use Kirby\Cms\App; +use Kirby\Cms\Language; use Kirby\Cms\ModelWithContent; use Kirby\Panel\Panel; use Kirby\Panel\Ui\Button; @@ -25,7 +26,7 @@ class ViewButton extends Button { public function __construct( public string $component = 'k-view-button', - public readonly ModelWithContent|null $model = null, + public readonly ModelWithContent|Language|null $model = null, public array|null $badge = null, public string|null $class = null, public string|bool|null $current = null, @@ -57,7 +58,7 @@ public static function factory( string|array|Closure $button, string|int|null $name = null, string|null $view = null, - ModelWithContent|null $model = null, + ModelWithContent|Language|null $model = null, array $data = [] ): static|null { // referenced by name @@ -162,14 +163,17 @@ public function props(): array */ public static function resolve( Closure|array $button, - ModelWithContent|null $model = null, + ModelWithContent|Language|null $model = null, array $data = [] ): static|array|null { if ($button instanceof Closure) { $kirby = App::instance(); $controller = new Controller($button); - if ($model instanceof ModelWithContent) { + if ( + $model instanceof ModelWithContent || + $model instanceof Language + ) { $data = [ 'model' => $model, $model::CLASS_ALIAS => $model, diff --git a/src/Panel/Ui/Buttons/ViewButtons.php b/src/Panel/Ui/Buttons/ViewButtons.php index f4546d5f6e..3c03724d59 100644 --- a/src/Panel/Ui/Buttons/ViewButtons.php +++ b/src/Panel/Ui/Buttons/ViewButtons.php @@ -3,6 +3,7 @@ namespace Kirby\Panel\Ui\Buttons; use Kirby\Cms\App; +use Kirby\Cms\Language; use Kirby\Cms\ModelWithContent; use Kirby\Panel\Model; @@ -21,7 +22,7 @@ class ViewButtons { public function __construct( public readonly string $view, - public readonly ModelWithContent|null $model = null, + public readonly ModelWithContent|Language|null $model = null, public array|false|null $buttons = null, public array $data = [] ) { @@ -86,7 +87,7 @@ public function render(): array */ public static function view( string|Model $view, - ModelWithContent|null $model = null + ModelWithContent|Language|null $model = null ): static { if ($view instanceof Model) { $model = $view->model(); From 9865d329ada4f9d4f824d73238d2971ac6791e39 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sun, 12 Jan 2025 11:51:52 +0100 Subject: [PATCH 4/5] View button: support queries in more props --- src/Panel/Ui/Buttons/ViewButton.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Panel/Ui/Buttons/ViewButton.php b/src/Panel/Ui/Buttons/ViewButton.php index 74de7a3704..5c73b37265 100644 --- a/src/Panel/Ui/Buttons/ViewButton.php +++ b/src/Panel/Ui/Buttons/ViewButton.php @@ -148,10 +148,13 @@ public function props(): array null; return [ - ...parent::props(), - 'dialog' => $resolve($this->dialog), - 'drawer' => $resolve($this->drawer), - 'link' => $resolve($this->link), + ...$props = parent::props(), + 'dialog' => $resolve($props['dialog']), + 'drawer' => $resolve($props['drawer']), + 'icon' => $resolve($props['icon']), + 'link' => $resolve($props['link']), + 'text' => $resolve($props['text']), + 'theme' => $resolve($props['theme']), 'options' => $this->options ]; } From 38637dcbef14f8b6ffea7edb58164156e7b3345a Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sun, 12 Jan 2025 12:04:26 +0100 Subject: [PATCH 5/5] View button: add unit tests --- tests/Panel/Ui/ButtonTest.php | 16 ++++++++ tests/Panel/Ui/Buttons/ViewButtonTest.php | 44 ++++++++++++++++++++-- tests/Panel/Ui/Buttons/ViewButtonsTest.php | 2 + 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/tests/Panel/Ui/ButtonTest.php b/tests/Panel/Ui/ButtonTest.php index 0a0d469bb0..73d7300a7e 100644 --- a/tests/Panel/Ui/ButtonTest.php +++ b/tests/Panel/Ui/ButtonTest.php @@ -44,4 +44,20 @@ public function testProps() 'variant' => 'filled', ], $component->props()); } + + /** + * @covers ::props + */ + public function testPropsWithI18n() + { + $component = new Button( + text: [ + 'en' => 'Congrats', + 'de' => 'Glückwunsch' + ], + ); + + $props = $component->props(); + $this->assertSame('Congrats', $props['text']); + } } diff --git a/tests/Panel/Ui/Buttons/ViewButtonTest.php b/tests/Panel/Ui/Buttons/ViewButtonTest.php index b2e07b8632..543538c108 100644 --- a/tests/Panel/Ui/Buttons/ViewButtonTest.php +++ b/tests/Panel/Ui/Buttons/ViewButtonTest.php @@ -3,6 +3,7 @@ namespace Kirby\Panel\Ui\Buttons; use Kirby\Cms\App; +use Kirby\Cms\Page; use Kirby\Panel\Areas\AreaTestCase; /** @@ -41,8 +42,8 @@ public function testFactoryFromClosure() public function testFactoryFromDefinition() { $button = ViewButton::factory( - ['component' => 'k-test-view-button'], - 'test' + button: ['component' => 'k-test-view-button'], + view: 'test' ); $this->assertInstanceOf(ViewButton::class, $button); @@ -77,6 +78,21 @@ public function testFactoryFromStringName() $this->assertNull($button); } + /** + * @covers ::factory + */ + public function testFactoryWithNameArg() + { + $button = ViewButton::factory( + button: [], + name: 'foo', + view: 'test' + ); + + $this->assertInstanceOf(ViewButton::class, $button); + $this->assertSame('k-foo-view-button', $button->component); + } + /** * @covers ::find */ @@ -97,7 +113,7 @@ public function testFind(): void $app->impersonate('test@getkirby.com'); // view-prefixed name - $result = ViewButton::find('a', 'test'); + $result = ViewButton::find('a', view: 'test'); $this->assertSame(['component' => 'result-a'], $result); // generic name @@ -171,19 +187,39 @@ public function testProps() ], $component->props()); } + /** + * @covers ::props + */ + public function testPropsWithQueries() + { + $model = new Page(['slug' => 'test']); + $component = new ViewButton( + model: $model, + text: 'Page: {{ page.url }}', + link: 'https://getkirby.com/{{ page.slug }}', + ); + + $props = $component->props(); + $this->assertSame('Page: /test', $props['text']); + $this->assertSame('https://getkirby.com/test', $props['link']); + } + /** * @covers ::resolve */ public function testResolve(): void { $test = $this; + $model = new Page(['slug' => 'test']); $result = ViewButton::resolve( - button: function (string $b, bool $a, App $kirby) use ($test) { + button: function (string $b, bool $a, App $kirby, Page $page) use ($test) { $test->assertFalse($a); $test->assertSame('foo', $b); $test->assertInstanceOf(App::class, $kirby); + $test->assertInstanceOf(Page::class, $page); return ['component' => 'k-test-view-button']; }, + model: $model, data: [ 'a' => false, 'b' => 'foo' diff --git a/tests/Panel/Ui/Buttons/ViewButtonsTest.php b/tests/Panel/Ui/Buttons/ViewButtonsTest.php index be4a450e9c..395e122118 100644 --- a/tests/Panel/Ui/Buttons/ViewButtonsTest.php +++ b/tests/Panel/Ui/Buttons/ViewButtonsTest.php @@ -121,6 +121,7 @@ public function testView() // view name $buttons = ViewButtons::view('page'); $this->assertCount(0, $buttons->buttons ?? []); + $this->assertNull($buttons->model); // view model $page = new Page([ @@ -132,5 +133,6 @@ public function testView() $buttons = ViewButtons::view($page->panel()); $this->assertCount(2, $buttons->buttons); + $this->assertSame($page, $buttons->model); } }