diff --git a/src/Panel/Model.php b/src/Panel/Model.php index a66c3db3b8..9d22673b7f 100644 --- a/src/Panel/Model.php +++ b/src/Panel/Model.php @@ -295,6 +295,15 @@ public function lock(): array|false return $this->model->lock()?->toArray() ?? false; } + /** + * Returns the corresponding model object + * @since 5.0.0 + */ + public function model(): ModelWithContent + { + return $this->model; + } + /** * Returns an array of all actions * that can be performed in the Panel diff --git a/src/Panel/Ui/Button.php b/src/Panel/Ui/Button.php index 2f9f720a9a..11a66bc2ec 100644 --- a/src/Panel/Ui/Button.php +++ b/src/Panel/Ui/Button.php @@ -17,8 +17,8 @@ class Button extends Component { public function __construct( public string $component = 'k-button', - public string|bool|null $current = null, public string|null $class = null, + public string|bool|null $current = null, public string|null $dialog = null, public bool $disabled = false, public string|null $drawer = null, diff --git a/src/Panel/Ui/Buttons/ViewButton.php b/src/Panel/Ui/Buttons/ViewButton.php new file mode 100644 index 0000000000..c1e3c33ab8 --- /dev/null +++ b/src/Panel/Ui/Buttons/ViewButton.php @@ -0,0 +1,157 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://getkirby.com/license + * @since 5.0.0 + * @internal + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ +class ViewButton extends Button +{ + public function __construct( + public string $component = 'k-view-button', + public string|null $class = null, + public string|bool|null $current = null, + public string|null $dialog = null, + public bool $disabled = false, + public string|null $drawer = null, + public bool|null $dropdown = null, + public string|null $icon = null, + public string|null $link = null, + public array|string|null $options = null, + public bool|string $responsive = true, + public string|null $size = 'sm', + public string|null $style = null, + public string|null $target = null, + public string|null $text = null, + public string|null $theme = null, + public string|null $title = null, + public string $type = 'button', + public string|null $variant = 'filled', + ) { + } + + /** + * Creates new view button by looking up + * the button in all areas, if referenced by name + * and resolving to proper instance + */ + public static function factory( + string|array|Closure $button, + string|null $view = null, + array $args = [] + ): static|null { + // referenced by name + if (is_string($button) === true) { + $button = static::find($button, $view); + } + + $button = static::resolve($button, $args); + + if ( + $button === null || + $button instanceof ViewButton + ) { + return $button; + } + + return new static(...static::normalize($button)); + } + + /** + * Finds a view button by name + * among the defined buttons from all areas + */ + public static function find( + string $name, + string|null $view = null + ): array|Closure { + // collect all buttons from areas + $buttons = Panel::buttons(); + + // try to find by full name (view-prefixed) + if ($view && $button = $buttons[$view . '.' . $name] ?? null) { + return $button; + } + + // try to find by just name + if ($button = $buttons[$name] ?? null) { + return $button; + } + + // assume it must be a custom view button component + return ['component' => 'k-view-' . $name . '-button']; + } + + /** + * Transforms an array to be used as + * named arguments in the constructor + * @internal + */ + public static function normalize(array $button): array + { + // if component and props are both not set, assume shortcut + // where props were directly passed on top-level + if ( + isset($button['component']) === false && + isset($button['props']) === false + ) { + return $button; + } + + // flatten array + if ($props = $button['props'] ?? null) { + $button = [...$props, ...$button]; + unset($button['props']); + } + + return $button; + } + + public function props(): array + { + return [ + ...parent::props(), + 'options' => $this->options + ]; + } + + /** + * Transforms a closure to the actual view button + * by calling it with the provided arguments + * @internal + */ + public static function resolve( + Closure|array $button, + array $args = [] + ): static|array|null { + if ($button instanceof Closure) { + $kirby = App::instance(); + $controller = new Controller($button); + $button = $controller->call(data: [ + 'kirby' => $kirby, + 'site' => $kirby->site(), + 'user' => $kirby->user(), + ...$args + ]); + } + + return $button; + } +} diff --git a/src/Panel/Ui/Buttons/ViewButtons.php b/src/Panel/Ui/Buttons/ViewButtons.php new file mode 100644 index 0000000000..e9b5dbd742 --- /dev/null +++ b/src/Panel/Ui/Buttons/ViewButtons.php @@ -0,0 +1,72 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://getkirby.com/license + * @since 5.0.0 + * @internal + */ +class ViewButtons +{ + public function __construct( + public readonly string $view, + public array|null $buttons = null + ) { + $this->buttons ??= App::instance()->option( + 'panel.viewButtons.' . $view + ); + } + + /** + * Sets the default buttons + * + * @return $this + */ + public function defaults(string ...$defaults): static + { + $this->buttons ??= $defaults; + return $this; + } + + /** + * Returns array of button component-props definitions + */ + public function render(array $args = []): array + { + $buttons = A::map( + $this->buttons ?? [], + fn ($button) => + ViewButton::factory($button, $this->view, $args)?->render() + ); + + return array_values(array_filter($buttons)); + } + + /** + * Creates new instance for a view + * with special support for model views + */ + public static function view(string|Model $view): static + { + if ($view instanceof Model) { + $blueprint = $view->model()->blueprint()->buttons(); + $view = $view->model()::CLASS_ALIAS; + } + + return new static( + view: $view, + buttons: $blueprint ?? null + ); + } +} diff --git a/tests/Panel/ModelTest.php b/tests/Panel/ModelTest.php index 2e42267876..41b9973fc7 100644 --- a/tests/Panel/ModelTest.php +++ b/tests/Panel/ModelTest.php @@ -468,6 +468,15 @@ public function testLock() $this->assertSame('unlock', $site->panel()->lock()['state']); } + /** + * @covers ::model + */ + public function testModel() + { + $panel = $this->panel(); + $this->assertInstanceOf(ModelSite::class, $panel->model()); + } + /** * @covers ::props */ diff --git a/tests/Panel/Ui/Buttons/ViewButtonTest.php b/tests/Panel/Ui/Buttons/ViewButtonTest.php new file mode 100644 index 0000000000..e5be38070a --- /dev/null +++ b/tests/Panel/Ui/Buttons/ViewButtonTest.php @@ -0,0 +1,192 @@ +install(); + $this->login(); + } + + /** + * @covers ::factory + */ + public function testFactoryFromClosure() + { + $button = ViewButton::factory( + fn (string $name) => ['component' => 'k-view-' . $name . '-button'], + 'test', + ['name' => 'foo'] + ); + + $this->assertInstanceOf(ViewButton::class, $button); + $this->assertSame('k-view-foo-button', $button->component); + } + + /** + * @covers ::factory + */ + public function testFactoryFromDefinition() + { + $button = ViewButton::factory( + ['component' => 'k-view-test-button'], + 'test' + ); + + $this->assertInstanceOf(ViewButton::class, $button); + $this->assertSame('k-view-test-button', $button->component); + } + + /** + * @covers ::factory + */ + public function testFactoryFromStringName() + { + $app = $this->app->clone([ + 'areas' => [ + 'test' => fn () => [ + 'buttons' => [ + 'test' => ['component' => 'result'], + 'foo' => function () {} + ] + ] + ] + ]); + + // simulate a logged in user + $app->impersonate('test@getkirby.com'); + + $button = ViewButton::factory('test'); + $this->assertInstanceOf(ViewButton::class, $button); + $this->assertSame('result', $button->component); + + // null returned + $button = ViewButton::factory('foo'); + $this->assertNull($button); + } + + /** + * @covers ::find + */ + public function testFind(): void + { + $app = $this->app->clone([ + 'areas' => [ + 'test' => fn () => [ + 'buttons' => [ + 'test.a' => ['component' => 'result-a'], + 'b' => ['component' => 'result-b'] + ] + ] + ] + ]); + + // simulate a logged in user + $app->impersonate('test@getkirby.com'); + + // view-prefixed name + $result = ViewButton::find('a', 'test'); + $this->assertSame(['component' => 'result-a'], $result); + + // generic name + $result = ViewButton::find('b'); + $this->assertSame(['component' => 'result-b'], $result); + + // custom component + $result = ViewButton::find('foo'); + $this->assertSame(['component' => 'k-view-foo-button'], $result); + } + + /** + * @covers ::normalize + */ + public function testNormalize(): void + { + $result = ViewButton::normalize([ + 'icon' => 'add' + ]); + + $this->assertSame(['icon' => 'add'], $result); + + // flatten array + $result = ViewButton::normalize([ + 'component' => 'k-view-foo-button', + 'props' => [ + 'icon' => 'add' + ] + ]); + + $this->assertSame([ + 'icon' => 'add', + 'component' => 'k-view-foo-button', + ], $result); + } + + /** + * @covers ::props + */ + public function testProps() + { + $component = new ViewButton( + icon: 'smile', + size: 'xs', + options: '/my/route', + text: 'Congrats', + theme: 'positive', + variant: 'filled' + ); + + $this->assertSame([ + 'class' => null, + 'style' => null, + 'current' => null, + 'dialog' => null, + 'disabled' => false, + 'drawer' => null, + 'dropdown' => null, + 'icon' => 'smile', + 'link' => null, + 'responsive' => true, + 'size' => 'xs', + 'target' => null, + 'text' => 'Congrats', + 'theme' => 'positive', + 'title' => null, + 'type' => 'button', + 'variant' => 'filled', + 'options' => '/my/route' + ], $component->props()); + } + + /** + * @covers ::resolve + */ + 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-view-test-button']; + }, [ + 'a' => false, + 'b' => 'foo' + ]); + + $this->assertSame('k-view-test-button', $result['component']); + + $result = ViewButton::resolve(['component' => 'k-view-test-button']); + $this->assertSame('k-view-test-button', $result['component']); + } +} diff --git a/tests/Panel/Ui/Buttons/ViewButtonsTest.php b/tests/Panel/Ui/Buttons/ViewButtonsTest.php new file mode 100644 index 0000000000..6ed23d42da --- /dev/null +++ b/tests/Panel/Ui/Buttons/ViewButtonsTest.php @@ -0,0 +1,110 @@ +app([ + 'options' => [ + 'panel' => [ + 'viewButtons' => [ + 'test' => [ + 'a' => ['component' => 'result-a'], + 'b' => ['component' => 'result-b'], + 'c' => ['component' => 'result-c'], + 'z' => function () { + return null; + } + ] + ] + ] + ] + ]); + } + /** + * @covers ::__construct + */ + public function testConstruct() + { + // no buttons + $buttons = new ViewButtons('test', []); + $this->assertCount(0, $buttons->buttons); + + // passed directly + $buttons = new ViewButtons('test', ['a', 'b']); + $this->assertCount(2, $buttons->buttons); + + // from options + $buttons = new ViewButtons('test'); + $this->assertCount(4, $buttons->buttons); + } + + /** + * @covers ::defaults + */ + public function testDefaults() + { + $buttons = new ViewButtons('foo'); + $this->assertCount(0, $buttons->buttons ?? []); + + $buttons->defaults('a', 'b'); + $this->assertCount(2, $buttons->buttons); + } + + /** + * @covers ::render + */ + public function testRender() + { + $buttons = new ViewButtons('test', ['a', 'b']); + $result = $buttons->render(); + + $this->assertCount(2, $result); + $this->assertSame('k-view-a-button', $result[0]['component']); + $this->assertSame('k-view-b-button', $result[1]['component']); + } + + /** + * @covers ::render + */ + public function testRenderFromConfig() + { + $buttons = new ViewButtons('test'); + $result = $buttons->render(); + + $this->assertCount(3, $result); + $this->assertSame('result-a', $result[0]['component']); + $this->assertSame('result-b', $result[1]['component']); + $this->assertSame('result-c', $result[2]['component']); + } + + /** + * @covers ::view + */ + public function testView() + { + // view name + $buttons = ViewButtons::view('page'); + $this->assertCount(0, $buttons->buttons ?? []); + + // view model + $page = new Page([ + 'slug' => 'test', + 'blueprint' => [ + 'buttons' => ['a', 'b'] + ] + ]); + + $buttons = ViewButtons::view($page->panel()); + $this->assertCount(2, $buttons->buttons); + } +}