Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions demo/app/Sharp/Posts/PostList.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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.')
Expand Down Expand Up @@ -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(
'<div>%s</div><div><small>[fr] %s</div>',
Expand Down
15 changes: 14 additions & 1 deletion demo/app/Sharp/SharpMenu.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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');
})
Expand Down
17 changes: 17 additions & 0 deletions docs/guide/building-entity-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
26 changes: 25 additions & 1 deletion docs/guide/building-menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
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).
43 changes: 40 additions & 3 deletions docs/guide/page-alerts.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Create a `buildPageAlert()` method:
```php
class MyShow extends SharpShow
{
// [...]
// ...

protected function buildPageAlert(PageAlert $pageAlert): void
{
Expand All @@ -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
{
Expand All @@ -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();`
:::
:::

## 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)
);
}
}
```

44 changes: 5 additions & 39 deletions resources/js/Layouts/Layout.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import {inject, Ref} from "vue";
import {inject, Ref} from "vue";

export function useMenuBoundaryElement() {
export function useMenuBoundaryElement() {
return inject<Ref<HTMLElement>>('menuBoundary');
}
</script>
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -151,23 +151,7 @@ export function useMenuBoundaryElement() {
</div>
</template>
<template v-else>
<SidebarMenuItem>
<SidebarMenuButton :is-active="childItem.current" as-child>
<component
:is="childItem.isExternalLink ? 'a' : Link"
:href="childItem.url"
v-scroll-into-view.center="childItem.current"
>
<template v-if="childItem.icon">
<Icon :icon="childItem.icon" class="size-4" />
</template>
<span>{{ childItem.label }}</span>
<template v-if="childItem.isExternalLink">
<ExternalLink class="ml-auto size-4 opacity-50" />
</template>
</component>
</SidebarMenuButton>
</SidebarMenuItem>
<MenuItem :item="childItem" />
</template>
</template>
</SidebarMenu>
Expand All @@ -180,25 +164,7 @@ export function useMenuBoundaryElement() {
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton :is-active="item.current" as-child>
<component
:is="item.isExternalLink ? 'a' : Link"
:href="item.url"
v-scroll-into-view.center="item.current"
>
<template v-if="item.icon">
<Icon :icon="item.icon" class="size-4" />
</template>
<span>
{{ item.label }}
</span>
<template v-if="item.isExternalLink">
<ExternalLink class="ml-auto size-4 opacity-50" />
</template>
</component>
</SidebarMenuButton>
</SidebarMenuItem>
<MenuItem :item="item" />
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
Expand Down
8 changes: 4 additions & 4 deletions resources/js/Pages/Show/Show.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -217,13 +218,12 @@
<template v-if="show.config.state">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button class="pointer-events-auto h-8 -mx-2 disabled:opacity-100" variant="ghost" size="sm" :disabled="!show.config.state.authorization"
<Button class="pointer-events-auto h-8 -mx-2 disabled:opacity-100 hover:bg-transparent aria-expanded:bg-transparent" variant="ghost" size="sm" :disabled="!show.config.state.authorization"
:aria-label="__('sharp::show.state_dropdown.aria_label')"
>
<Badge variant="outline">
<StateIcon class="-ml-0.5 mr-1.5" :state-value="show.instanceStateValue" />
<StateBadge :state-value="show.instanceStateValue">
{{ show.instanceStateValue?.label }}
</Badge>
</StateBadge>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
Expand Down
60 changes: 60 additions & 0 deletions resources/js/components/MenuItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { isSharpLink } from '@/utils/url';
import Icon from '@/components/ui/Icon.vue';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Badge } from '@/components/ui/badge';
import { ExternalLink } from 'lucide-vue-next';
import { SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { vScrollIntoView } from '@/directives/scroll-into-view';
import { MenuItemData } from "@/types";

defineProps<{
item: MenuItemData,
}>()
</script>

<template>
<SidebarMenuItem>
<SidebarMenuButton
class="relative"
:is-active="item.current"
as-child
>
<div v-scroll-into-view.center="item.current">
<template v-if="item.icon">
<Icon :icon="item.icon" class="size-4" />
</template>
<span class="flex-1">
<component :is="item.isExternalLink ? 'a' : Link" :href="item.url">
<span class="absolute inset-0 z-1"></span>
{{ item.label }}
</component>
</span>
<template v-if="item.badge != null && item.badge !== ''">
<TooltipProvider>
<Tooltip :delay-duration="0" :disabled="!item.badgeTooltip">
<TooltipTrigger as-child>
<Badge
:as="item.badgeUrl || item.badgeTooltip ? isSharpLink(item.badgeUrl || item.url) ? Link : 'a' : 'div'"
class="-my-px -mr-1"
:class="item.badgeUrl || item.badgeTooltip ? 'relative z-1' : ''"
:href="item.badgeUrl || item.url"
variant="sidebar"
>
{{ item.badge }}
</Badge>
</TooltipTrigger>
<TooltipContent>
{{ item.badgeTooltip }}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>
<template v-if="item.isExternalLink">
<ExternalLink class="size-4 opacity-50" />
</template>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</template>
23 changes: 17 additions & 6 deletions resources/js/components/PageAlert.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
import { PageAlertData } from "@/types";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Info, TriangleAlert, OctagonAlert } from 'lucide-vue-next';
import { Button } from "@/components/ui/button";
import { isSharpLink } from "@/utils/url";
import { Link } from '@inertiajs/vue3';

defineProps<{
pageAlert: PageAlertData,
}>();

</script>

<template>
Expand All @@ -25,11 +29,18 @@
<template v-else>
<Info class="w-4 h-4" />
</template>
<AlertDescription :class="{
'font-medium': pageAlert.level === 'warning' || pageAlert.level === 'danger',
'text-foreground': pageAlert.level === 'warning',
}">
<div v-html="pageAlert.text"></div>
</AlertDescription>
<div class="flex items-center gap-4">
<AlertDescription class="flex-1" :class="{
'font-medium': pageAlert.level === 'warning' || pageAlert.level === 'danger',
'text-foreground': pageAlert.level === 'warning',
}">
<div v-html="pageAlert.text"></div>
</AlertDescription>
<template v-if="pageAlert.buttonLabel">
<Button class="-my-2" :as="isSharpLink(pageAlert.buttonUrl) ? Link : 'a'" size="sm" variant="link" :href="pageAlert.buttonUrl">
{{ pageAlert.buttonLabel }}
</Button>
</template>
</div>
</Alert>
</template>
Loading
Loading