From db50b140005f142848dde0a407d178e3aec5b2cc Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 17 Aug 2026 18:27:47 +0100 Subject: [PATCH] Layouts: Added logical theme event and guidance for block registration --- .../Providers/ViewTweaksServiceProvider.php | 6 +- app/Theming/ThemeEvents.php | 9 ++ app/View/ViewBlockManager.php | 12 +- dev/docs/logical-theme-system.md | 103 +++++++++++++++++- tests/Theme/LogicalThemeEventsTest.php | 43 ++++++++ 5 files changed, 167 insertions(+), 6 deletions(-) diff --git a/app/App/Providers/ViewTweaksServiceProvider.php b/app/App/Providers/ViewTweaksServiceProvider.php index 3f23ad16c..ae41c1d95 100644 --- a/app/App/Providers/ViewTweaksServiceProvider.php +++ b/app/App/Providers/ViewTweaksServiceProvider.php @@ -3,6 +3,8 @@ namespace BookStack\App\Providers; use BookStack\Entities\BreadcrumbsViewComposer; +use BookStack\Facades\Theme; +use BookStack\Theming\ThemeEvents; use BookStack\Util\DateFormatter; use BookStack\View\ViewBlockManager; use BookStack\View\ViewBlockPreferences; @@ -38,8 +40,10 @@ class ViewTweaksServiceProvider extends ServiceProvider View::composer('entities.breadcrumbs', BreadcrumbsViewComposer::class); // View Globals + $viewBlockManager = $this->app->make(ViewBlockManager::class); View::share('dates', $this->app->make(DateFormatter::class)); - View::share('viewBlocks', $this->app->make(ViewBlockManager::class)); + View::share('viewBlocks', $viewBlockManager); + Theme::dispatch(ThemeEvents::VIEW_BLOCKS_REGISTER, $viewBlockManager); // Custom blade view directives Blade::directive('icon', function ($expression) { diff --git a/app/Theming/ThemeEvents.php b/app/Theming/ThemeEvents.php index 511a9c1de..46fa7c2c0 100644 --- a/app/Theming/ThemeEvents.php +++ b/app/Theming/ThemeEvents.php @@ -180,6 +180,15 @@ class ThemeEvents */ const THEME_REGISTER_VIEWS = 'theme_register_views'; + /** + * View blocks register event. + * Runs once a ViewBlockManager instance is available so that custom blocks can be registered + * for use within user-configurable layouts in the system. + * + * @param \BookStack\View\ViewBlockManager $manager + */ + const VIEW_BLOCKS_REGISTER = 'view_blocks_register'; + /** * Web before middleware action. * Runs before the request is handled but after all other middleware apart from those diff --git a/app/View/ViewBlockManager.php b/app/View/ViewBlockManager.php index fb1fd43cc..94753ae83 100644 --- a/app/View/ViewBlockManager.php +++ b/app/View/ViewBlockManager.php @@ -25,17 +25,21 @@ class ViewBlockManager * Register a block type to be displayed at the given location and position. * @param class-string $blockClass */ - public function register(string $location, string $position, string $blockClass): void + public function register(string $location, string $defaultPosition, string $blockClass): void { if (!isset($this->blocksByLocationAndPosition[$location])) { $this->blocksByLocationAndPosition[$location] = []; } - if (!isset($this->blocksByLocationAndPosition[$location][$position])) { - $this->blocksByLocationAndPosition[$location][$position] = []; + if (!isset($this->blocksByLocationAndPosition[$location][$defaultPosition])) { + $this->blocksByLocationAndPosition[$location][$defaultPosition] = []; } - $this->blocksByLocationAndPosition[$location][$position][] = $blockClass; + if (!is_a($blockClass, ViewBlockInterface::class, true)) { + throw new \InvalidArgumentException('When registering a view block, the block class must implement ViewBlockInterface'); + } + + $this->blocksByLocationAndPosition[$location][$defaultPosition][] = $blockClass; } /** diff --git a/dev/docs/logical-theme-system.md b/dev/docs/logical-theme-system.md index b6342ce54..7093bc00b 100644 --- a/dev/docs/logical-theme-system.md +++ b/dev/docs/logical-theme-system.md @@ -193,4 +193,105 @@ Theme::addSocialDriver('reddit', [ $driver->with(['prompt' => 'select_account']); $driver->scopes(['open_id']); }); -``` \ No newline at end of file +``` + +## Custom View Layout Block Example + +By listening to the `ThemeEvents::VIEW_BLOCKS_REGISTER` event, it's possible to register +custom view blocks which can be shown in a layout in the application. View blocks are typically the sections shown in the sidebar, +or the cards shown on the home view. + +As an example of using this system, we'll define a custom view block which will show on the default home view to state how many +books there are in the system. +To start, we'll need to create a custom class to represent our view block. This must implement `\BookStack\View\ViewBlockInterface`: + +```php +use BookStack\Entities\Queries\BookQueries; +use BookStack\View\ViewBlockInterface; +use BookStack\View\ViewBlockManager; + +class BookTotalBlock implements ViewBlockInterface { + + public function __construct( + protected BookQueries $bookQueries + ) { + } + + public static function getId(): string + { + return 'custom_book_total_block'; + } + + public static function getLabel(): string + { + return 'Total books displays'; + } + + public function getView(array $viewData): string + { + return 'blocks.total-blocks'; + } + + public function withData(array $viewData): array + { + $totalBooks = $this->bookQueries->visibleForList()->count(); + return [ + 'totalBooks' => $totalBooks, + ]; + } +} +``` + +The interface has a few required methods: + +- The `getId` method must provide a unique per-block-type string ID. +- The `getLabel` method must provide a general string label for the block. +- The `getView` method provides a string path to a view file (Can be one custom registered). + - This is provided the available view data at time of render, so it can be dynamic based on context. +- The `withData` method is called when block is being rendered, and it should return an array of data which will be merged with existing view data. + - This is also provided available view data, for use as context. + +In this example, we're also making use of the `BookQueries` internal BookStack class. +You're able to inject any other dependent classes/services via the constructor like this, and BookStack will attempt to auto-resolve them. + +We can then register this block class using the logical theme system like so: + +```php +use BookStack\Facades\Theme; +use BookStack\Theming\ThemeEvents; +use BookStack\View\ViewBlockManager; + +Theme::listen(ThemeEvents::VIEW_BLOCKS_REGISTER, function (ViewBlockManager $manager) { + $manager->register( + 'home-default', // The location/layout where this block will be displayed + 'right', // The default position for the block within the location + BookTotalBlock::class // The block class to register + ); +}); +``` + +The above registration code would typically be within your `functions.php` theme file. +You could also define the above block class in the same file or separate it into its own file +and include it from the `functions.php` via a `require_once()` call. + +Lastly, we'll need to create the view for the registered block. +In our example we return `blocks.total-blocks` from the `getView` method, so our view needs to be located at +`blocks/total-blocks.blade.php` from a view providing directory. +In a theme folder, we can just create it at `blocks/total-blocks.blade.php` within the folder path. +If we were building in a module, we'd need to create this at `views/blocks/total-blocks.blade.php` within our module folder. +For our example, we'll use this content to make use of the data we're passing to the view: + +```html +
+

Total Books

+
+

+ There are currently {{ $totalBooks }} books in the system! +

+
+
+``` + +This will then show up on the default home grid view, in the right column. +The exact position within that location may change depending on user preferences, but the block will be limited +to the location provided during registration unless it has also been registered for other locations. diff --git a/tests/Theme/LogicalThemeEventsTest.php b/tests/Theme/LogicalThemeEventsTest.php index 2add83868..28f21f209 100644 --- a/tests/Theme/LogicalThemeEventsTest.php +++ b/tests/Theme/LogicalThemeEventsTest.php @@ -394,4 +394,47 @@ CONTENT; $this->artisan('view:clear'); } + + public function test_view_blocks_register_event() + { + $this->usingThemeFolder(function (string $folder) { + $functionsContent = <<<'CONTENT' +register('home-default', 'right', CustomViewBlock::class); +}); +CONTENT; + + $viewDir = theme_path(); + file_put_contents($viewDir . '/functions.php', $functionsContent); + file_put_contents($viewDir . '/custom-block.blade.php', '

Hello there from my custom home block!

'); + + $this->refreshApplication(); + $resp = $this->asEditor()->get('/'); + $html = $this->withHtml($resp); + + $html->assertElementContains('.grid > div:nth-child(3) h2', 'Hello there from my custom home block!'); + }); + } }