diff --git a/demo/app/Sharp/Posts/PostList.php b/demo/app/Sharp/Posts/PostList.php index 9b0d79cdb..7c6f1c07a 100644 --- a/demo/app/Sharp/Posts/PostList.php +++ b/demo/app/Sharp/Posts/PostList.php @@ -13,6 +13,7 @@ use App\Sharp\Utils\Filters\CategoryFilter; use App\Sharp\Utils\Filters\PeriodFilter; use App\Sharp\Utils\Filters\StateFilter; +use Code16\Sharp\EntityList\Fields\EntityListBadgeField; use Code16\Sharp\EntityList\Fields\EntityListField; use Code16\Sharp\EntityList\Fields\EntityListFieldsContainer; use Code16\Sharp\EntityList\Fields\EntityListStateField; @@ -30,6 +31,10 @@ class PostList extends SharpEntityList protected function buildList(EntityListFieldsContainer $fields): void { $fields + ->addField( + EntityListBadgeField::make('is_draft') + ->setTooltip('This post is draft') + ) ->addField( EntityListField::make('cover') ->setWidth(.1) @@ -73,6 +78,15 @@ public function buildListConfig(): void protected function buildPageAlert(PageAlert $pageAlert): void { + if (auth()->user()->isAdmin() && ($count = Post::where('state', 'draft')->count()) > 0) { + $pageAlert + ->setMessage(sprintf('%d posts are still in draft', $count)) + ->setButton( + 'Show drafts', + LinkToEntityList::make(PostEntity::class)->addFilter(StateFilter::class, 'draft') + ); + } + if (! auth()->user()->isAdmin()) { $pageAlert ->setMessage('As an editor, you can only edit your posts; you can see other posts except those which are still in draft.') @@ -172,6 +186,7 @@ function (Builder $builder) { ); return $this + ->setCustomTransformer('is_draft', fn ($value, Post $instance) => $instance->isDraft()) ->setCustomTransformer('title', function ($value, Post $instance) { return sprintf( '
%s
[fr] %s
', diff --git a/demo/app/Sharp/SharpMenu.php b/demo/app/Sharp/SharpMenu.php index 23040fecc..1d8b2701f 100644 --- a/demo/app/Sharp/SharpMenu.php +++ b/demo/app/Sharp/SharpMenu.php @@ -2,12 +2,15 @@ namespace App\Sharp; +use App\Models\Post; use App\Sharp\Entities\AuthorEntity; use App\Sharp\Entities\CategoryEntity; use App\Sharp\Entities\DemoDashboardEntity; use App\Sharp\Entities\PostEntity; use App\Sharp\Entities\ProfileEntity; use App\Sharp\Entities\TestEntity; +use App\Sharp\Utils\Filters\StateFilter; +use Code16\Sharp\Utils\Links\LinkToEntityList; use Code16\Sharp\Utils\Menu\SharpMenu as BaseSharpMenu; use Code16\Sharp\Utils\Menu\SharpMenuItemSection; use Code16\Sharp\Utils\Menu\SharpMenuUserMenu; @@ -25,7 +28,17 @@ public function build(): self ->addSection('Blog', function (SharpMenuItemSection $section) { $section ->setCollapsible(false) - ->addEntityLink(PostEntity::class, 'Posts', icon: 'lucide-file-text') + ->addEntityLink( + entityKeyOrClassName: PostEntity::class, + label: 'Posts', + icon: 'lucide-file-text', + badge: fn () => Post::query() + ->where('state', 'draft') + ->count() ?: null, + badgeTooltip: 'See draft posts', + badgeLink: LinkToEntityList::make(PostEntity::class) + ->addFilter(StateFilter::class, 'draft'), + ) ->addEntityLink(CategoryEntity::class, 'Categories', icon: 'lucide-tags') ->addEntityLink(AuthorEntity::class, 'Authors', icon: 'lucide-signature'); }) diff --git a/docs/guide/building-entity-list.md b/docs/guide/building-entity-list.md index bb156ef39..d54f6831c 100644 --- a/docs/guide/building-entity-list.md +++ b/docs/guide/building-entity-list.md @@ -55,6 +55,23 @@ To hide the column on small screens, use `->hideOnSmallScreens()`. Sorting columns must be handled in the `getListData()` method, see below. +#### Add a badge field + +The `EntityListBadgeField` allows you to display a badge in the list. It is either a simple dot if the value is `true` or a badge containing the value if it is an integer or a string. + +```php +class ProductList extends SharpEntityList +{ + protected function buildList(EntityListFieldsContainer $fields): void + { + $fields + ->addField( + EntityListBadgeField::make('is_new') + ); + } +} +``` + ### `getListData()` Now the real work: grab and return the actual list data. This method must return an array of `instances` of our `entity`. You can do this however you want, so let's see a generic example: diff --git a/docs/guide/building-menu.md b/docs/guide/building-menu.md index 082fa9bf7..76cebd133 100644 --- a/docs/guide/building-menu.md +++ b/docs/guide/building-menu.md @@ -89,6 +89,30 @@ class MySharpMenu extends Code16\Sharp\Utils\Menu\SharpMenu } ``` +### Handle notification badges + +You can display a notification badge on any link, with a count and a tooltip. You can also, optionally, define a tooltip and a link for the badge (usually to a filtered Entity List). + +Here’s an example of a badge on an Entity List link: + +```php +class MySharpMenu extends Code16\Sharp\Utils\Menu\SharpMenu +{ + public function build(): self + { + return $this + ->addEntityLink( + entityKeyOrClassName: PostEntity::class, + label: 'Posts', + badge: fn () => Post::query()->where('state', 'draft')->count(), + badgeTooltip: 'See draft posts', + badgeLink: LinkToEntityList::make(PostEntity::class) + ->addFilter(StateFilter::class, 'draft'), + ); + } +} +``` + ### Group links in sections Sections are groups that can be collapsed @@ -182,4 +206,4 @@ class MySharpMenu extends Code16\Sharp\Utils\Menu\SharpMenu ### Global menu Filters -If you want to display a filter on all pages, above the menu, useful to scope the entire data set (use cases: multi tenant app, customer selector...), you can define a global filter as described in the [Filters documentation](filters.md#global-menu-filters). \ No newline at end of file +If you want to display a filter on all pages, above the menu, useful to scope the entire data set (use cases: multi tenant app, customer selector...), you can define a global filter as described in the [Filters documentation](filters.md#global-menu-filters). diff --git a/docs/guide/page-alerts.md b/docs/guide/page-alerts.md index 134a89394..0ec69c165 100644 --- a/docs/guide/page-alerts.md +++ b/docs/guide/page-alerts.md @@ -13,7 +13,7 @@ Create a `buildPageAlert()` method: ```php class MyShow extends SharpShow { - // [...] + // ... protected function buildPageAlert(PageAlert $pageAlert): void { @@ -33,7 +33,7 @@ To provide a dynamic message, depending on the actual data of the Show, Entity L ```php class MyShow extends SharpShow { - // [...] + // ... protected function buildPageAlert(PageAlert $pageAlert): void { @@ -53,4 +53,41 @@ The `$data` array passed to the closure is the result of your `find()` (Show, Fo ::: tip If your message is complex to build, you can defer to a blade template to encapsulate the logic, eg: `return view('sharp._post-planned-info', ['data' => $data])->render();` -::: \ No newline at end of file +::: + +## Add a button link + +The `setButton()` method allows you to add a link to your alert: + +```php +class MyShow extends SharpShow +{ + // ... + + protected function buildPageAlert(PageAlert $pageAlert): void + { + $pageAlert + ->setMessage('This page has been edited recently.') + ->setButton('Go to page', route('pages.show', sharp()->context()->instanceId())); + } +} +``` + +You can also pass a `SharpLinkTo` object. It's useful for filtering an Entity List, for example: + +```php +class MyEntityList extends SharpEntityList +{ + // ... + + protected function buildPageAlert(PageAlert $pageAlert): void + { + $pageAlert + ->setMessage('There are new orders to handle.') + ->setButton('See orders', LinkToEntityList::make(MyEntity::class) + ->addFilter('is_new', 1) + ); + } +} +``` + diff --git a/resources/js/Layouts/Layout.vue b/resources/js/Layouts/Layout.vue index 77c66c97a..8b48edf9c 100644 --- a/resources/js/Layouts/Layout.vue +++ b/resources/js/Layouts/Layout.vue @@ -1,7 +1,7 @@ @@ -55,8 +55,8 @@ export function useMenuBoundaryElement() { } from "@/components/ui/sidebar"; import { useEventListener, useStorage } from "@vueuse/core"; import GlobalSearch from "@/components/GlobalSearch.vue"; - import { vScrollIntoView } from "@/directives/scroll-into-view"; import Content from "@/components/Content.vue"; + import MenuItem from "@/components/MenuItem.vue"; const dialogs = useDialogs(); const menu = useMenu(); @@ -151,23 +151,7 @@ export function useMenuBoundaryElement() { @@ -180,25 +164,7 @@ export function useMenuBoundaryElement() { - - - - - - {{ item.label }} - - - - - + diff --git a/resources/js/Pages/Show/Show.vue b/resources/js/Pages/Show/Show.vue index fea5c1f3d..88a06ee60 100644 --- a/resources/js/Pages/Show/Show.vue +++ b/resources/js/Pages/Show/Show.vue @@ -43,6 +43,7 @@ import DropdownChevronDown from "@/components/ui/DropdownChevronDown.vue"; import { useEntityListHighlightedItem } from "@/composables/useEntityListHighlightedItem"; import RootCardHeader from "@/components/ui/RootCardHeader.vue"; + import StateBadge from "@/components/ui/StateBadge.vue"; const props = defineProps<{ show: ShowData, @@ -217,13 +218,12 @@ + + + +
@@ -642,14 +654,14 @@ '--width': field.width === 'fill' ? (100 / visibleFields.length)+'%' : field.width ? field.width : - field.type === 'state' ? 0 : null + field.type === 'state' || field.type === 'badge' ? 0 : null }" > @@ -712,15 +726,14 @@ + +