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() {
-
-
-
-
-
-
- {{ childItem.label }}
-
-
-
-
-
-
+
@@ -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 @@
-
diff --git a/resources/js/components/MenuItem.vue b/resources/js/components/MenuItem.vue
new file mode 100644
index 000000000..baf9ca962
--- /dev/null
+++ b/resources/js/components/MenuItem.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+