🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,13 @@
<?php
namespace Livewire\Features\SupportTesting {
use Filament\Notifications\Notification;
class Testable {
public function assertNotified(Notification | string $notification = null): static {}
public function assertNotNotified(Notification | string $notification = null): static {}
}
}
@@ -0,0 +1,35 @@
{
"name": "filament/notifications",
"description": "Easily add beautiful notifications to any Livewire app.",
"license": "MIT",
"homepage": "https://github.com/filamentphp/filament",
"support": {
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"require": {
"php": "^8.2",
"filament/actions": "self.version",
"filament/support": "self.version"
},
"autoload": {
"psr-4": {
"Filament\\Notifications\\": "src"
},
"files": [
"src/Testing/helpers.php"
]
},
"extra": {
"laravel": {
"providers": [
"Filament\\Notifications\\NotificationsServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,490 @@
---
title: Overview
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
## Introduction
Notifications are sent using a `Notification` object that's constructed through a fluent API. Calling the `send()` method on the `Notification` object will dispatch the notification and display it in your application. As the session is used to flash notifications, they can be sent from anywhere in your code, including JavaScript, not just Livewire components.
```php
<?php
namespace App\Livewire;
use Filament\Notifications\Notification;
use Livewire\Component;
class EditPost extends Component
{
public function save(): void
{
// ...
Notification::make()
->title('Saved successfully')
->success()
->send();
}
}
```
<AutoScreenshot name="notifications/success" alt="Success notification" version="4.x" />
## Setting a title
The main message of the notification is shown in the title. You can set the title as follows:
```php
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->send();
```
The title text can contain basic, safe HTML elements. To generate safe HTML with Markdown, you can use the [`Str::markdown()` helper](https://laravel.com/docs/strings#method-str-markdown): `title(Str::markdown('Saved **successfully**'))`
Or with JavaScript:
```js
new FilamentNotification()
.title('Saved successfully')
.send()
```
## Setting an icon
Optionally, a notification can have an [icon](../styling/icons) that's displayed in front of its content. You may also set a color for the icon, which is gray by default:
```php
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->icon('heroicon-o-document-text')
->iconColor('success')
->send();
```
Or with JavaScript:
```js
new FilamentNotification()
.title('Saved successfully')
.icon('heroicon-o-document-text')
.iconColor('success')
.send()
```
<AutoScreenshot name="notifications/icon" alt="Notification with icon" version="4.x" />
Notifications often have a status like `success`, `warning`, `danger` or `info`. Instead of manually setting the corresponding [icons](../styling/icons) and [colors](../styling/colors), there's a `status()` method which you can pass the status. You may also use the dedicated `success()`, `warning()`, `danger()` and `info()` methods instead. So, cleaning up the above example would look like this:
```php
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->success()
->send();
```
Or with JavaScript:
```js
new FilamentNotification()
.title('Saved successfully')
.success()
.send()
```
<AutoScreenshot name="notifications/statuses" alt="Notifications with various statuses" version="4.x" />
## Setting a background color
Notifications have no background color by default. You may want to provide additional context to your notification by setting a color as follows:
```php
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->color('success')
->send();
```
Or with JavaScript:
```js
new FilamentNotification()
.title('Saved successfully')
.color('success')
.send()
```
<AutoScreenshot name="notifications/color" alt="Notification with background color" version="4.x" />
## Setting a duration
By default, notifications are shown for 6 seconds before they're automatically closed. You may specify a custom duration value in milliseconds as follows:
```php
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->success()
->duration(5000)
->send();
```
Or with JavaScript:
```js
new FilamentNotification()
.title('Saved successfully')
.success()
.duration(5000)
.send()
```
If you prefer setting a duration in seconds instead of milliseconds, you can do so:
```php
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->success()
->seconds(5)
->send();
```
Or with JavaScript:
```js
new FilamentNotification()
.title('Saved successfully')
.success()
.seconds(5)
.send()
```
You might want some notifications to not automatically close and require the user to close them manually. This can be achieved by making the notification persistent:
```php
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->success()
->persistent()
->send();
```
Or with JavaScript:
```js
new FilamentNotification()
.title('Saved successfully')
.success()
.persistent()
.send()
```
## Setting body text
Additional notification text can be shown in the `body()`:
```php
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->success()
->body('Changes to the post have been saved.')
->send();
```
The body text can contain basic, safe HTML elements. To generate safe HTML with Markdown, you can use the [`Str::markdown()` helper](https://laravel.com/docs/strings#method-str-markdown): `body(Str::markdown('Changes to the **post** have been saved.'))`
Or with JavaScript:
```js
new FilamentNotification()
.title('Saved successfully')
.success()
.body('Changes to the post have been saved.')
.send()
```
<AutoScreenshot name="notifications/body" alt="Notification with body text" version="4.x" />
## Adding actions to notifications
Notifications support [Actions](../actions/overview), which are buttons that render below the content of the notification. They can open a URL or dispatch a Livewire event. Actions can be defined as follows:
```php
use Filament\Actions\Action;
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->success()
->body('Changes to the post have been saved.')
->actions([
Action::make('view')
->button(),
Action::make('undo')
->color('gray'),
])
->send();
```
Or with JavaScript:
```js
new FilamentNotification()
.title('Saved successfully')
.success()
.body('Changes to the post have been saved.')
.actions([
new FilamentNotificationAction('view')
.button(),
new FilamentNotificationAction('undo')
.color('gray'),
])
.send()
```
<AutoScreenshot name="notifications/actions" alt="Notification with actions" version="4.x" />
You can learn more about how to style action buttons [here](../actions/overview).
### Opening URLs from notification actions
You can open a URL, optionally in a new tab, when clicking on an action:
```php
use Filament\Actions\Action;
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->success()
->body('Changes to the post have been saved.')
->actions([
Action::make('view')
->button()
->url(route('posts.show', $post), shouldOpenInNewTab: true),
Action::make('undo')
->color('gray'),
])
->send();
```
Or with JavaScript:
```js
new FilamentNotification()
.title('Saved successfully')
.success()
.body('Changes to the post have been saved.')
.actions([
new FilamentNotificationAction('view')
.button()
.url('/view')
.openUrlInNewTab(),
new FilamentNotificationAction('undo')
.color('gray'),
])
.send()
```
### Dispatching Livewire events from notification actions
Sometimes you want to execute additional code when a notification action is clicked. This can be achieved by setting a Livewire event which should be dispatched on clicking the action. You may optionally pass an array of data, which will be available as parameters in the event listener on your Livewire component:
```php
use Filament\Actions\Action;
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->success()
->body('Changes to the post have been saved.')
->actions([
Action::make('view')
->button()
->url(route('posts.show', $post), shouldOpenInNewTab: true),
Action::make('undo')
->color('gray')
->dispatch('undoEditingPost', [$post->id]),
])
->send();
```
You can also `dispatchSelf` and `dispatchTo`:
```php
Action::make('undo')
->color('gray')
->dispatchSelf('undoEditingPost', [$post->id])
Action::make('undo')
->color('gray')
->dispatchTo('another_component', 'undoEditingPost', [$post->id])
```
Or with JavaScript:
```js
new FilamentNotification()
.title('Saved successfully')
.success()
.body('Changes to the post have been saved.')
.actions([
new FilamentNotificationAction('view')
.button()
.url('/view')
.openUrlInNewTab(),
new FilamentNotificationAction('undo')
.color('gray')
.dispatch('undoEditingPost'),
])
.send()
```
Similarly, `dispatchSelf` and `dispatchTo` are also available:
```js
new FilamentNotificationAction('undo')
.color('gray')
.dispatchSelf('undoEditingPost')
new FilamentNotificationAction('undo')
.color('gray')
.dispatchTo('another_component', 'undoEditingPost')
```
### Closing notifications from actions
After opening a URL or dispatching an event from your action, you may want to close the notification right away:
```php
use Filament\Actions\Action;
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->success()
->body('Changes to the post have been saved.')
->actions([
Action::make('view')
->button()
->url(route('posts.show', $post), shouldOpenInNewTab: true),
Action::make('undo')
->color('gray')
->dispatch('undoEditingPost', [$post->id])
->close(),
])
->send();
```
Or with JavaScript:
```js
new FilamentNotification()
.title('Saved successfully')
.success()
.body('Changes to the post have been saved.')
.actions([
new FilamentNotificationAction('view')
.button()
.url('/view')
.openUrlInNewTab(),
new FilamentNotificationAction('undo')
.color('gray')
.dispatch('undoEditingPost')
.close(),
])
.send()
```
## Using the JavaScript objects
The JavaScript objects (`FilamentNotification` and `FilamentNotificationAction`) are assigned to `window.FilamentNotification` and `window.FilamentNotificationAction`, so they are available in on-page scripts.
You may also import them in a bundled JavaScript file:
```js
import { Notification, NotificationAction } from '../../vendor/filament/notifications/dist/index.js'
// ...
```
## Closing a notification with JavaScript
Once a notification has been sent, you can close it on demand by dispatching a browser event on the window called `close-notification`.
The event needs to contain the ID of the notification you sent. To get the ID, you can use the `getId()` method on the `Notification` object:
```php
use Filament\Notifications\Notification;
$notification = Notification::make()
->title('Hello')
->persistent()
->send()
$notificationId = $notification->getId()
```
To close the notification, you can dispatch the event from Livewire:
```php
$this->dispatch('close-notification', id: $notificationId);
```
Or from JavaScript, in this case Alpine.js:
```blade
<button x-on:click="$dispatch('close-notification', { id: notificationId })" type="button">
Close Notification
</button>
```
If you are able to retrieve the notification ID, persist it, and then use it to close the notification, that is the recommended approach, as IDs are generated uniquely, and you will not risk closing the wrong notification. However, if it is not possible to persist the random ID, you can pass in a custom ID when sending the notification:
```php
use Filament\Notifications\Notification;
Notification::make('greeting')
->title('Hello')
->persistent()
->send()
```
In this case, you can close the notification by dispatching the event with the custom ID:
```blade
<button x-on:click="$dispatch('close-notification', { id: 'greeting' })" type="button">
Close Notification
</button>
```
Please be aware that if you send multiple notifications with the same ID, you may experience unexpected side effects, so random IDs are recommended.
## Positioning notifications
You can configure the alignment of the notifications in a service provider or middleware, by calling `Notifications::alignment()` and `Notifications::verticalAlignment()`. You can pass `Alignment::Start`, `Alignment::Center`, `Alignment::End`, `VerticalAlignment::Start`, `VerticalAlignment::Center` or `VerticalAlignment::End`:
```php
use Filament\Notifications\Livewire\Notifications;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\VerticalAlignment;
Notifications::alignment(Alignment::Start);
Notifications::verticalAlignment(VerticalAlignment::End);
```
@@ -0,0 +1,202 @@
---
title: Database notifications
---
import Aside from "@components/Aside.astro"
import AutoScreenshot from "@components/AutoScreenshot.astro"
<AutoScreenshot name="notifications/database" alt="Database notifications" version="4.x" />
## Setting up the notifications database table
Before we start, make sure that the [Laravel notifications table](https://laravel.com/docs/notifications#database-prerequisites) is added to your database:
```bash
php artisan make:notifications-table
```
> If you're using PostgreSQL, make sure that the `data` column in the migration is using `json()`: `$table->json('data')`.
> If you're using UUIDs for your `User` model, make sure that your `notifiable` column is using `uuidMorphs()`: `$table->uuidMorphs('notifiable')`.
## Enabling database notifications in a panel
If you'd like to receive database notifications in a panel, you can enable them in the [configuration](../panel-configuration):
```php
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->databaseNotifications();
}
```
## Sending database notifications
There are several ways to send database notifications, depending on which one suits you best.
You may use our fluent API:
```php
use Filament\Notifications\Notification;
$recipient = auth()->user();
Notification::make()
->title('Saved successfully')
->sendToDatabase($recipient);
```
Or, use the `notify()` method:
```php
use Filament\Notifications\Notification;
$recipient = auth()->user();
$recipient->notify(
Notification::make()
->title('Saved successfully')
->toDatabase(),
);
```
> Laravel sends database notifications using the queue. Ensure your queue is running in order to receive the notifications.
Alternatively, use a traditional [Laravel notification class](https://laravel.com/docs/notifications#generating-notifications) by returning the notification from the `toDatabase()` method:
```php
use App\Models\User;
use Filament\Notifications\Notification;
public function toDatabase(User $notifiable): array
{
return Notification::make()
->title('Saved successfully')
->getDatabaseMessage();
}
```
## Moving the database notifications trigger to the panel sidebar
By default, the database notifications trigger is positioned in the topbar. If the topbar is disabled, it is added to the sidebar.
You can choose to always move it to the sidebar by passing a `position` argument to the `databaseNotifications()` method in the [configuration](../panel-configuration):
```php
use Filament\Enums\DatabaseNotificationsPosition;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->databaseNotifications(position: DatabaseNotificationsPosition::Sidebar);
}
```
## Receiving database notifications
Without any setup, new database notifications will only be received when the page is first loaded.
### Polling for new database notifications
Polling is the practice of periodically making a request to the server to check for new notifications. This is a good approach as the setup is simple, but some may say that it is not a scalable solution as it increases server load.
By default, Livewire polls for new notifications every 30 seconds:
```php
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->databaseNotifications()
->databaseNotificationsPolling('30s');
}
```
You may completely disable polling if you wish:
```php
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->databaseNotifications()
->databaseNotificationsPolling(null);
}
```
### Using Echo to receive new database notifications with websockets
Websockets are a more efficient way to receive new notifications in real-time. To set up websockets, you must [configure it](broadcast-notifications#setting-up-websockets-in-a-panel) in the panel first.
Once websockets are set up, you can automatically dispatch a `DatabaseNotificationsSent` event by setting the `isEventDispatched` parameter to `true` when sending the notification. This will trigger the immediate fetching of new notifications for the user:
```php
use Filament\Notifications\Notification;
$recipient = auth()->user();
Notification::make()
->title('Saved successfully')
->sendToDatabase($recipient, isEventDispatched: true);
```
## Marking database notifications as read
There is a button at the top of the modal to mark all notifications as read at once. You may also add [Actions](overview#adding-actions-to-notifications) to notifications, which you can use to mark individual notifications as read. To do this, use the `markAsRead()` method on the action:
```php
use Filament\Actions\Action;
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->success()
->body('Changes to the post have been saved.')
->actions([
Action::make('view')
->button()
->markAsRead(),
])
->send();
```
Alternatively, you may use the `markAsUnread()` method to mark a notification as unread:
```php
use Filament\Actions\Action;
use Filament\Notifications\Notification;
Notification::make()
->title('Saved successfully')
->success()
->body('Changes to the post have been saved.')
->actions([
Action::make('markAsUnread')
->button()
->markAsUnread(),
])
->send();
```
## Opening the database notifications modal
You can open the database notifications modal from anywhere by dispatching an `open-modal` browser event:
```blade
<button
x-data="{}"
x-on:click="$dispatch('open-modal', { id: 'database-notifications' })"
type="button"
>
Notifications
</button>
```
@@ -0,0 +1,72 @@
---
title: Broadcast notifications
---
## Introduction
By default, Filament will send flash notifications via the Laravel session. However, you may wish that your notifications are "broadcast" to a user in real-time, instead. This could be used to send a temporary success notification from a queued job after it has finished processing.
We have a native integration with [Laravel Echo](https://laravel.com/docs/broadcasting#client-side-installation). Make sure Echo is installed, as well as a [server-side websockets integration](https://laravel.com/docs/broadcasting#server-side-installation) like Pusher.
## Sending broadcast notifications
There are several ways to send broadcast notifications, depending on which one suits you best.
You may use our fluent API:
```php
use Filament\Notifications\Notification;
$recipient = auth()->user();
Notification::make()
->title('Saved successfully')
->broadcast($recipient);
```
Or, use the `notify()` method:
```php
use Filament\Notifications\Notification;
$recipient = auth()->user();
$recipient->notify(
Notification::make()
->title('Saved successfully')
->toBroadcast(),
)
```
Alternatively, use a traditional [Laravel notification class](https://laravel.com/docs/notifications#generating-notifications) by returning the notification from the `toBroadcast()` method:
```php
use App\Models\User;
use Filament\Notifications\Notification;
use Illuminate\Notifications\Messages\BroadcastMessage;
public function toBroadcast(User $notifiable): BroadcastMessage
{
return Notification::make()
->title('Saved successfully')
->getBroadcastMessage();
}
```
## Setting up websockets in a panel
The Panel Builder comes with a level of inbuilt support for real-time broadcast and database notifications. However there are a number of areas you will need to install and configure to wire everything up and get it working.
1. If you haven't already, read up on [broadcasting](https://laravel.com/docs/broadcasting) in the Laravel documentation.
2. Install and configure broadcasting to use a [server-side websockets integration](https://laravel.com/docs/broadcasting#server-side-installation) like Pusher.
3. If you haven't already, you will need to publish the Filament package configuration:
```bash
php artisan vendor:publish --tag=filament-config
```
4. Edit the configuration at `config/filament.php` and uncomment the `broadcasting.echo` section - ensuring the settings are correctly configured according to your broadcasting installation.
5. Ensure the [relevant `VITE_*` entries](https://laravel.com/docs/broadcasting#client-pusher-channels) exist in your `.env` file.
6. Clear relevant caches with `php artisan route:clear` and `php artisan config:clear` to ensure your new configuration takes effect.
Your panel should now be connecting to your broadcasting service. For example, if you log into the Pusher debug console you should see an incoming connection each time you load a page.
@@ -0,0 +1,35 @@
.fi-no-database {
@apply flex;
& .fi-modal-window-ctn > .fi-modal-window {
& .fi-modal-heading {
@apply relative inline-block;
& .fi-badge {
@apply absolute start-full -top-1 ms-1 w-max;
}
}
& .fi-modal-header {
& .fi-ac {
@apply mt-2;
}
}
& .fi-modal-content {
@apply -mx-6 -mt-6 gap-y-0 divide-y divide-gray-200 dark:divide-white/10;
}
&:not(.fi-modal-window-has-footer) .fi-modal-content {
@apply -mb-6;
}
&.fi-modal-window-has-footer .fi-modal-content {
@apply border-b border-gray-200 dark:border-white/10;
}
}
& .fi-no-notification-unread-ctn {
@apply before:bg-primary-600 dark:before:bg-primary-500 relative before:absolute before:start-0 before:h-full before:w-0.5;
}
}
@@ -0,0 +1,3 @@
@import './database-notifications.css' layer(components);
@import './notification.css' layer(components);
@import './notifications.css' layer(components);
@@ -0,0 +1,105 @@
.fi-no-notification {
@apply pointer-events-auto invisible flex w-full shrink-0 gap-3 overflow-hidden p-4 transition duration-300;
& .fi-no-notification-icon {
@apply text-gray-400;
&.fi-color {
@apply text-color-400;
}
}
& .fi-no-notification-main {
@apply mt-0.5 grid flex-1 gap-3;
}
& .fi-no-notification-text {
@apply grid gap-1;
}
& .fi-no-notification-title {
@apply text-sm font-medium text-gray-950 dark:text-white;
}
& .fi-no-notification-date {
@apply text-sm text-gray-500 dark:text-gray-400;
}
& .fi-no-notification-body {
@apply overflow-hidden text-sm text-pretty break-words text-gray-500 dark:text-gray-400;
& > p:not(:first-of-type) {
@apply mt-1;
}
}
&:not(.fi-inline) {
@apply flex max-w-sm gap-3 rounded-xl bg-white p-4 shadow-lg ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10;
&.fi-color {
@apply ring-color-600/20 dark:ring-color-400/30;
}
&.fi-transition-leave-end {
@apply scale-95;
}
}
&.fi-color {
background-color: color-mix(in oklab, white 90%, var(--color-400));
@variant dark {
background-color: color-mix(
in oklab,
var(--gray-900) 90%,
var(--color-400)
);
}
}
&.fi-transition-enter-start {
@apply opacity-0;
}
&.fi-transition-leave-end {
@apply opacity-0;
}
}
.fi-no {
&.fi-align-start,
&.fi-align-left {
& .fi-no-notification {
&.fi-transition-enter-start {
@apply -translate-x-12;
}
}
}
&.fi-align-end,
&.fi-align-right {
& .fi-no-notification {
&.fi-transition-enter-start {
@apply translate-x-12;
}
}
}
&.fi-align-center {
&.fi-vertical-align-start {
& .fi-no-notification {
&.fi-transition-enter-start {
@apply -translate-y-12;
}
}
}
&.fi-vertical-align-end {
& .fi-no-notification {
&.fi-transition-enter-start {
@apply translate-y-12;
}
}
}
}
}
@@ -0,0 +1,29 @@
.fi-no {
@apply pointer-events-none fixed inset-4 z-50 mx-auto flex gap-3;
&.fi-align-start,
&.fi-align-left {
@apply items-start;
}
&.fi-align-center {
@apply items-center;
}
&.fi-align-end,
&.fi-align-right {
@apply items-end;
}
&.fi-vertical-align-start {
@apply flex-col-reverse justify-end;
}
&.fi-vertical-align-center {
@apply flex-col justify-center;
}
&.fi-vertical-align-end {
@apply flex-col justify-end;
}
}
@@ -0,0 +1,352 @@
import { v4 as uuid } from 'uuid-browser'
class Notification {
constructor() {
this.id(uuid())
return this
}
id(id) {
this.id = id
return this
}
title(title) {
this.title = title
return this
}
body(body) {
this.body = body
return this
}
actions(actions) {
this.actions = actions
return this
}
status(status) {
this.status = status
return this
}
color(color) {
this.color = color
return this
}
icon(icon) {
this.icon = icon
return this
}
iconColor(color) {
this.iconColor = color
return this
}
duration(duration) {
this.duration = duration
return this
}
seconds(seconds) {
this.duration(seconds * 1000)
return this
}
persistent() {
this.duration('persistent')
return this
}
danger() {
this.status('danger')
return this
}
info() {
this.status('info')
return this
}
success() {
this.status('success')
return this
}
warning() {
this.status('warning')
return this
}
view(view) {
this.view = view
return this
}
viewData(viewData) {
this.viewData = viewData
return this
}
send() {
window.dispatchEvent(
new CustomEvent('notificationSent', {
detail: {
notification: this,
},
}),
)
return this
}
}
class Action {
constructor(name) {
this.name(name)
return this
}
name(name) {
this.name = name
return this
}
color(color) {
this.color = color
return this
}
dispatch(event, data) {
this.event(event)
this.eventData(data)
return this
}
dispatchSelf(event, data) {
this.dispatch(event, data)
this.dispatchDirection = 'self'
return this
}
dispatchTo(component, event, data) {
this.dispatch(event, data)
this.dispatchDirection = 'to'
this.dispatchToComponent = component
return this
}
/**
* @deprecated Use `dispatch()` instead.
*/
emit(event, data) {
this.dispatch(event, data)
return this
}
/**
* @deprecated Use `dispatchSelf()` instead.
*/
emitSelf(event, data) {
this.dispatchSelf(event, data)
return this
}
/**
* @deprecated Use `dispatchTo()` instead.
*/
emitTo(component, event, data) {
this.dispatchTo(component, event, data)
return this
}
dispatchDirection(dispatchDirection) {
this.dispatchDirection = dispatchDirection
return this
}
dispatchToComponent(component) {
this.dispatchToComponent = component
return this
}
event(event) {
this.event = event
return this
}
eventData(data) {
this.eventData = data
return this
}
extraAttributes(attributes) {
this.extraAttributes = attributes
return this
}
icon(icon) {
this.icon = icon
return this
}
iconPosition(position) {
this.iconPosition = position
return this
}
outlined(condition = true) {
this.isOutlined = condition
return this
}
disabled(condition = true) {
this.isDisabled = condition
return this
}
label(label) {
this.label = label
return this
}
close(condition = true) {
this.shouldClose = condition
return this
}
openUrlInNewTab(condition = true) {
this.shouldOpenUrlInNewTab = condition
return this
}
size(size) {
this.size = size
return this
}
url(url) {
this.url = url
return this
}
view(view) {
this.view = view
return this
}
button() {
this.view('filament::components.button.index')
return this
}
grouped() {
this.view('filament::components.dropdown.list.item')
return this
}
iconButton() {
this.view('filament::components.icon-button')
return this
}
link() {
this.view('filament::components.link')
return this
}
}
class ActionGroup {
constructor(actions) {
this.actions(actions)
return this
}
actions(actions) {
this.actions = actions.map((action) => action.grouped())
return this
}
color(color) {
this.color = color
return this
}
icon(icon) {
this.icon = icon
return this
}
iconPosition(position) {
this.iconPosition = position
return this
}
label(label) {
this.label = label
return this
}
tooltip(tooltip) {
this.tooltip = tooltip
return this
}
}
export { Action, ActionGroup, Notification }
@@ -0,0 +1,167 @@
import { once } from 'alpinejs/src/utils/once'
export default (Alpine) => {
Alpine.data('notificationComponent', ({ notification }) => ({
isShown: false,
computedStyle: null,
transitionDuration: null,
transitionEasing: null,
init() {
this.computedStyle = window.getComputedStyle(this.$el)
this.transitionDuration =
parseFloat(this.computedStyle.transitionDuration) * 1000
this.transitionEasing = this.computedStyle.transitionTimingFunction
this.configureTransitions()
this.configureAnimations()
if (
notification.duration &&
notification.duration !== 'persistent'
) {
setTimeout(() => {
if (!this.$el.matches(':hover')) {
this.close()
return
}
this.$el.addEventListener('mouseleave', () => this.close())
}, notification.duration)
}
this.isShown = true
},
configureTransitions() {
const display = this.computedStyle.display
const show = () => {
Alpine.mutateDom(() => {
this.$el.style.setProperty('display', display)
this.$el.style.setProperty('visibility', 'visible')
})
this.$el._x_isShown = true
}
const hide = () => {
Alpine.mutateDom(() => {
this.$el._x_isShown
? this.$el.style.setProperty('visibility', 'hidden')
: this.$el.style.setProperty('display', 'none')
})
}
const toggle = once(
(value) => (value ? show() : hide()),
(value) => {
this.$el._x_toggleAndCascadeWithTransitions(
this.$el,
value,
show,
hide,
)
},
)
Alpine.effect(() => toggle(this.isShown))
},
configureAnimations() {
let animation
Livewire.hook(
'commit',
({ component, commit, succeed, fail, respond }) => {
if (
!component.snapshot.data
.isFilamentNotificationsComponent
) {
return
}
// Calling `el.getBoundingClientRect()` from outside `requestAnimationFrame()` can
// occasionally cause the page to scroll to the top.
requestAnimationFrame(() => {
const getTop = () =>
this.$el.getBoundingClientRect().top
const oldTop = getTop()
respond(() => {
animation = () => {
if (!this.isShown) {
return
}
this.$el.animate(
[
{
transform: `translateY(${
oldTop - getTop()
}px)`,
},
{ transform: 'translateY(0px)' },
],
{
duration: this.transitionDuration,
easing: this.transitionEasing,
},
)
}
this.$el
.getAnimations()
.forEach((animation) => animation.finish())
})
succeed(({ snapshot, effect }) => {
animation()
})
})
},
)
},
close() {
this.isShown = false
setTimeout(
() =>
window.dispatchEvent(
new CustomEvent('notificationClosed', {
detail: {
id: notification.id,
},
}),
),
this.transitionDuration,
)
},
markAsRead() {
window.dispatchEvent(
new CustomEvent('markedNotificationAsRead', {
detail: {
id: notification.id,
},
}),
)
},
markAsUnread() {
window.dispatchEvent(
new CustomEvent('markedNotificationAsUnread', {
detail: {
id: notification.id,
},
}),
)
},
}))
}
@@ -0,0 +1,14 @@
import NotificationComponentAlpinePlugin from './components/notification'
import {
Action as NotificationAction,
ActionGroup as NotificationActionGroup,
Notification,
} from './Notification'
window.FilamentNotificationAction = NotificationAction
window.FilamentNotificationActionGroup = NotificationActionGroup
window.FilamentNotification = Notification
document.addEventListener('alpine:init', () => {
window.Alpine.plugin(NotificationComponentAlpinePlugin)
})
@@ -0,0 +1,25 @@
<?php
return [
'modal' => [
'heading' => 'ማሳወቂያዎች',
'actions' => [
'clear' => [
'label' => 'አፅዳ',
],
'mark_all_as_read' => [
'label' => 'ሁሉም ተነበዋል',
],
],
'empty' => [
'heading' => 'ምንም አዲስ ነገር የለም',
'description' => 'እባክዎ ከትንሽ ቆይታ በኋላ ይመለሱ',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'التنبيهات',
'actions' => [
'clear' => [
'label' => 'مسح',
],
'mark_all_as_read' => [
'label' => 'تحديد الكل كمقروء',
],
],
'empty' => [
'heading' => 'لا توجد تنبيهات',
'description' => 'يرجى التحقق مرة أخرى لاحقاً.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Bildirişlər',
'actions' => [
'clear' => [
'label' => 'Təmizlə',
],
'mark_all_as_read' => [
'label' => 'Hamısını oxunub olaraq qeyd et',
],
],
'empty' => [
'heading' => 'Bildiriş yoxdur',
'description' => 'Zəhmət olmazsa sonra yoxlayın',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Известия',
'actions' => [
'clear' => [
'label' => 'Изчисти',
],
'mark_all_as_read' => [
'label' => 'Маркирай всички като прочетени',
],
],
'empty' => [
'heading' => 'Нямате известия',
'description' => 'Моля проверете отново по-късно.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'বিজ্ঞপ্তি',
'actions' => [
'clear' => [
'label' => 'পরিষ্কার',
],
'mark_all_as_read' => [
'label' => 'পড়া হয়েছে',
],
],
'empty' => [
'heading' => 'কোন বিজ্ঞপ্তি নেই',
'description' => 'পরে আবার চেষ্টা করুন',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Obavijesti',
'actions' => [
'clear' => [
'label' => 'Izbrišite sve',
],
'mark_all_as_read' => [
'label' => 'Označi sve kao pročitano ',
],
],
'empty' => [
'heading' => 'Nema obavijesti',
'description' => 'Molimo provjerite kasnije opet',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Notificacions',
'actions' => [
'clear' => [
'label' => 'Netejar',
],
'mark_all_as_read' => [
'label' => 'Marcar tot com a llegit',
],
],
'empty' => [
'heading' => 'Sense notificacions',
'description' => 'Si us plau, torna a comprovar-ho més tard.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'ئاگادارینامەکان',
'actions' => [
'clear' => [
'label' => 'سرینەوەی هەموو',
],
'mark_all_as_read' => [
'label' => 'نیشانەکردنی هەموو بۆ خوێنراوە',
],
],
'empty' => [
'heading' => 'هیچ ئاگادارینامەیەک نییە',
'description' => 'تکایە دواتر سەیری بکەرەوە',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Moje aktualizace',
'actions' => [
'clear' => [
'label' => 'Odstranit',
],
'mark_all_as_read' => [
'label' => 'Označit vše jako přečtené',
],
],
'empty' => [
'heading' => 'Nemáme pro vás žádné aktulizace',
'description' => 'Zkuste to prosím později',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Hysbysiadau',
'actions' => [
'clear' => [
'label' => 'Clirio',
],
'mark_all_as_read' => [
'label' => 'Nodi pob un fel wedi darllen',
],
],
'empty' => [
'heading' => 'Dim hysbysiad yma',
'description' => 'Gwiriwch eto nes ymlaen',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Notifikationer',
'actions' => [
'clear' => [
'label' => 'Ryd',
],
'mark_all_as_read' => [
'label' => 'Markér alle som læst',
],
],
'empty' => [
'heading' => 'Ingen notifikationer',
'description' => 'Tjek venligst igen senere',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Benachrichtigungen',
'actions' => [
'clear' => [
'label' => 'Alle löschen',
],
'mark_all_as_read' => [
'label' => 'Alle als gelesen markieren',
],
],
'empty' => [
'heading' => 'Keine Benachrichtigungen vorhanden',
'description' => 'Bitte schauen Sie später erneut vorbei',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Ειδοποιήσεις',
'actions' => [
'clear' => [
'label' => 'Καθαρισμός',
],
'mark_all_as_read' => [
'label' => 'Επισήμανση όλων ως αναγνωσμένων',
],
],
'empty' => [
'heading' => 'Δεν υπάρχουν νέες ειδοποιήσεις',
'description' => 'Ελέγξτε ξανά αργότερα.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Notifications',
'actions' => [
'clear' => [
'label' => 'Clear',
],
'mark_all_as_read' => [
'label' => 'Mark all as read',
],
],
'empty' => [
'heading' => 'No notifications',
'description' => 'Please check again later.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Notificaciones',
'actions' => [
'clear' => [
'label' => 'Borrar',
],
'mark_all_as_read' => [
'label' => 'Marcar todas como leídas',
],
],
'empty' => [
'heading' => 'No hay notificaciones',
'description' => 'Por favor, compruebe más tarde',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Jakinarazpenak',
'actions' => [
'clear' => [
'label' => 'Ezabatu',
],
'mark_all_as_read' => [
'label' => 'Denak irakurrita bezala markatu',
],
],
'empty' => [
'heading' => 'Ez dago jakinarazpenik',
'description' => 'Mesedez, egiaztatu geroago',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'پیام‌ها',
'actions' => [
'clear' => [
'label' => 'پاک کردن',
],
'mark_all_as_read' => [
'label' => 'علامت‌گذاری همه به عنوان خوانده‌شده',
],
],
'empty' => [
'heading' => 'شما پیامی ندارید',
'description' => 'لطفا بعدا مراجعه کنید',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Ilmoitukset',
'actions' => [
'clear' => [
'label' => 'Tyhjennä',
],
'mark_all_as_read' => [
'label' => 'Merkitse luetuiksi',
],
],
'empty' => [
'heading' => 'Ei ilmoituksia',
'description' => 'Tarkista myöhemmin uudestaan',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Notifications',
'actions' => [
'clear' => [
'label' => 'Effacer',
],
'mark_all_as_read' => [
'label' => 'Tout marquer comme lu',
],
],
'empty' => [
'heading' => 'Aucune notification',
'description' => 'Veuillez revérifier ultérieurement',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'התראות',
'actions' => [
'clear' => [
'label' => 'נקה',
],
'mark_all_as_read' => [
'label' => 'סמך הכל כנקרא',
],
],
'empty' => [
'heading' => 'אין התראות',
'description' => 'בדוק שוב מאוחר יותר',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Obavijesti',
'actions' => [
'clear' => [
'label' => 'Očisti',
],
'mark_all_as_read' => [
'label' => 'Označi sve kao pročitano',
],
],
'empty' => [
'heading' => 'Nema obavijesti',
'description' => 'Molim te, provjeri ponovno kasnije.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Értesítések',
'actions' => [
'clear' => [
'label' => 'Törlés',
],
'mark_all_as_read' => [
'label' => 'Összes olvasottnak jelölése',
],
],
'empty' => [
'heading' => 'Nincsenek értesítések',
'description' => 'Kérjük, hogy nézz vissza később.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Ծանուցումներ',
'actions' => [
'clear' => [
'label' => 'Ջնջել',
],
'mark_all_as_read' => [
'label' => 'Նշել բոլորը որպես կարդացված',
],
],
'empty' => [
'heading' => 'Ոչ մի ծանուցում',
'description' => 'Խնդրում ենք ավելի ուշ կրկին ստուգել։',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Notifikasi',
'actions' => [
'clear' => [
'label' => 'Bersihkan',
],
'mark_all_as_read' => [
'label' => 'Tandai semua sudah dibaca',
],
],
'empty' => [
'heading' => 'Tidak ada notifikasi',
'description' => 'Silakan periksa kembali nanti',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Notifiche',
'actions' => [
'clear' => [
'label' => 'Pulisci',
],
'mark_all_as_read' => [
'label' => 'Imposta tutto come letto',
],
],
'empty' => [
'heading' => 'Nessuna notifica',
'description' => 'Si prega di controllare più tardi',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => '通知',
'actions' => [
'clear' => [
'label' => 'クリア',
],
'mark_all_as_read' => [
'label' => 'すべて既読にする',
],
],
'empty' => [
'heading' => '通知はありません',
'description' => 'のちほど確認してください',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'შეტყობინებები',
'actions' => [
'clear' => [
'label' => 'გასუფთავება',
],
'mark_all_as_read' => [
'label' => 'ყველას წაკითხულად მონიშვნა',
],
],
'empty' => [
'heading' => 'შეტყობინებები არ არის',
'description' => 'გთხოვთ, შეამოწმოთ მოგვიანებით.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'ដំណឹង',
'actions' => [
'clear' => [
'label' => 'សំអាត',
],
'mark_all_as_read' => [
'label' => 'សម្គាល់ថាបានអានទាំងអស់ហើយ',
],
],
'empty' => [
'heading' => 'គ្នានដំណឹង',
'description' => 'សូមពិនិត្យម្តងទៀតនៅពេលក្រោយ.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => '알림',
'actions' => [
'clear' => [
'label' => '전체 삭제',
],
'mark_all_as_read' => [
'label' => '모두 읽음으로 표시',
],
],
'empty' => [
'heading' => '알림 없음',
'description' => '나중에 다시 확인해 주세요.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'ئاگانامەکان',
'actions' => [
'clear' => [
'label' => 'سرینەوەی هەموو',
],
'mark_all_as_read' => [
'label' => 'نیشانە کردنی هەموو بۆ خوێنراوە',
],
],
'empty' => [
'heading' => 'هیچ ئاگانامەیەک نییە',
'description' => 'تکایە دواتر سەردان بکە',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Pranešimai',
'actions' => [
'clear' => [
'label' => 'Išvalyti',
],
'mark_all_as_read' => [
'label' => 'Pažymėti visus kaip perskaitytus',
],
],
'empty' => [
'heading' => 'Nėra pranešimų',
'description' => 'Patikrinkite vėliau.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Hriattîrna',
'actions' => [
'clear' => [
'label' => 'Then faina',
],
'mark_all_as_read' => [
'label' => 'Chhiar vek tawh ah dah rawh',
],
],
'empty' => [
'heading' => 'Hriattîrna a awmlo',
'description' => 'Nakinah ilo en leh dawn nia.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Paziņojumi',
'actions' => [
'clear' => [
'label' => 'Nodzēst',
],
'mark_all_as_read' => [
'label' => 'Atzīmēt visus kā izlasītus',
],
],
'empty' => [
'heading' => 'Nav jaunu paziņojumu',
'description' => 'Lūdzu, skatiet vēlāk',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Pemberitahuan',
'actions' => [
'clear' => [
'label' => 'Hapus',
],
'mark_all_as_read' => [
'label' => 'Tandai semua sebagai dibaca',
],
],
'empty' => [
'heading' => 'Tiada pemberitahuan di sini',
'description' => 'Sila semak semula kemudian',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Varsler',
'actions' => [
'clear' => [
'label' => 'Tøm',
],
'mark_all_as_read' => [
'label' => 'Merk alle som lest',
],
],
'empty' => [
'heading' => 'Ingen varsler',
'description' => 'Vennligst sjekk senere.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'सूचनाहरू',
'actions' => [
'clear' => [
'label' => 'खाली गर्नुहोस्',
],
'mark_all_as_read' => [
'label' => 'सबै पढेको रूपमा चिन्ह लगाउनुहोस्',
],
],
'empty' => [
'heading' => 'कुनै सूचना छैन',
'description' => 'कृपया पछि फेरि जाँच गर्नुहोस्।',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Meldingen',
'actions' => [
'clear' => [
'label' => 'Wissen',
],
'mark_all_as_read' => [
'label' => 'Alles als gelezen markeren',
],
],
'empty' => [
'heading' => 'Geen meldingen',
'description' => 'Kijk later nog eens.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Powiadomienia',
'actions' => [
'clear' => [
'label' => 'Wyczyść',
],
'mark_all_as_read' => [
'label' => 'Oznacz wszystkie jako przeczytane',
],
],
'empty' => [
'heading' => 'Brak powiadomień',
'description' => 'Zajrzyj ponownie później',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Notificações',
'actions' => [
'clear' => [
'label' => 'Limpar',
],
'mark_all_as_read' => [
'label' => 'Marcar tudo como lido',
],
],
'empty' => [
'heading' => 'Sem notificações',
'description' => 'Por favor, verifique mais tarde.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Notificações',
'actions' => [
'clear' => [
'label' => 'Limpar',
],
'mark_all_as_read' => [
'label' => 'Marcar tudo como lido',
],
],
'empty' => [
'heading' => 'Sem notificações',
'description' => 'Por favor, verifique mais tarde.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Notificări',
'actions' => [
'clear' => [
'label' => 'Ștergere',
],
'mark_all_as_read' => [
'label' => 'Marchează totul ca fiind citit',
],
],
'empty' => [
'heading' => 'Nu există notificări',
'description' => 'Vă rugăm să verificați din nou mai târziu',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Уведомления',
'actions' => [
'clear' => [
'label' => 'Удалить',
],
'mark_all_as_read' => [
'label' => 'Отметить как прочитанное',
],
],
'empty' => [
'heading' => 'Нет уведомлений',
'description' => 'Пожалуйста, проверьте позже',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Notifikácie',
'actions' => [
'clear' => [
'label' => 'Odstrániť',
],
'mark_all_as_read' => [
'label' => 'Označiť všetko ako prečítané',
],
],
'empty' => [
'heading' => 'Žiadne notifikácie',
'description' => 'Skúste to prosím neskôr.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Obvestila',
'actions' => [
'clear' => [
'label' => 'Počisti',
],
'mark_all_as_read' => [
'label' => 'Označi vse kot prebrano',
],
],
'empty' => [
'heading' => 'Ni obvestil',
'description' => 'Prosimo, preverite ponovno kasneje.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Njoftimet',
'actions' => [
'clear' => [
'label' => 'Pastro',
],
'mark_all_as_read' => [
'label' => 'Shënoni të gjitha si të lexuara',
],
],
'empty' => [
'heading' => 'Nuk ka njoftime',
'description' => 'Ju lutemi kontrolloni përsëri më vonë.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Обавештења',
'actions' => [
'clear' => [
'label' => 'Очисти',
],
'mark_all_as_read' => [
'label' => 'Означи све као прочитано',
],
],
'empty' => [
'heading' => 'Без обавештења',
'description' => 'Молим вас, проверите поново касније.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Obaveštenja',
'actions' => [
'clear' => [
'label' => 'Očisti',
],
'mark_all_as_read' => [
'label' => 'Označi sve kao pročitano',
],
],
'empty' => [
'heading' => 'Bez obaveštenja',
'description' => 'Molim vas, proverite ponovo kasnije.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Notiser',
'actions' => [
'clear' => [
'label' => 'Rensa',
],
'mark_all_as_read' => [
'label' => 'Markera alla som lästa',
],
],
'empty' => [
'heading' => 'Inga notiser',
'description' => 'Kolla igen lite senare.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Arifa',
'actions' => [
'clear' => [
'label' => 'Safisha',
],
'mark_all_as_read' => [
'label' => 'Weka alama zote kama zimesomwa',
],
],
'empty' => [
'heading' => 'Hakuna arifa hapa',
'description' => 'Tafadhali angalia tena baadae',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'การแจ้งเตือน',
'actions' => [
'clear' => [
'label' => 'ล้าง',
],
'mark_all_as_read' => [
'label' => 'ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว',
],
],
'empty' => [
'heading' => 'ไม่มีการแจ้งเตือน',
'description' => 'กรุณาตรวจสอบอีกครั้งในภายหลัง',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Bildirimler',
'actions' => [
'clear' => [
'label' => 'Temizle',
],
'mark_all_as_read' => [
'label' => 'Tümünü okundu işaretle',
],
],
'empty' => [
'heading' => 'Bildirim yok',
'description' => 'Lütfen sonra kontrol ediniz',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Сповіщення',
'actions' => [
'clear' => [
'label' => 'Видалити',
],
'mark_all_as_read' => [
'label' => 'Позначити як прочитане',
],
],
'empty' => [
'heading' => 'Немає повідомлень',
'description' => 'Будь ласка, перевірте пізніше',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'نوٹیفکیشنز',
'actions' => [
'clear' => [
'label' => 'صاف کریں',
],
'mark_all_as_read' => [
'label' => 'سب کو پڑھا ہوا نشان لگائیں',
],
],
'empty' => [
'heading' => 'کوئی نوٹیفکیشن نہیں',
'description' => 'براہ کرم بعد میں دوبارہ چیک کریں۔',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Bildirishnomalar',
'actions' => [
'clear' => [
'label' => 'O\'chirish',
],
'mark_all_as_read' => [
'label' => 'O\'qilgan deb belgilash',
],
],
'empty' => [
'heading' => 'Bildirishnomalar mavjud emas',
'description' => 'Iltimos keyinroq tekshiring',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => 'Thông báo',
'actions' => [
'clear' => [
'label' => 'Xóa',
],
'mark_all_as_read' => [
'label' => 'Đánh dấu là đã đọc tất cả',
],
],
'empty' => [
'heading' => 'Không có thông báo',
'description' => 'Vui lòng kiểm tra lại sau.',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => '通知',
'actions' => [
'clear' => [
'label' => '清除',
],
'mark_all_as_read' => [
'label' => '标记为已读',
],
],
'empty' => [
'heading' => '没有通知',
'description' => '请稍后再查看。',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => '通知',
'actions' => [
'clear' => [
'label' => '清除',
],
'mark_all_as_read' => [
'label' => '標記為已讀',
],
],
'empty' => [
'heading' => '沒有通知',
'description' => '請稍後再查看。',
],
],
];
@@ -0,0 +1,28 @@
<?php
return [
'modal' => [
'heading' => '通知',
'actions' => [
'clear' => [
'label' => '清除',
],
'mark_all_as_read' => [
'label' => '標記為已讀',
],
],
'empty' => [
'heading' => '沒有通知',
'description' => '請稍後再查看。',
],
],
];
@@ -0,0 +1,115 @@
@php
use Filament\Support\Enums\Alignment;
use Filament\Support\View\Components\BadgeComponent;
use Illuminate\View\ComponentAttributeBag;
$notifications = $this->getNotifications();
$unreadNotificationsCount = $this->getUnreadNotificationsCount();
$hasNotifications = $notifications->count();
$isPaginated = $notifications instanceof \Illuminate\Contracts\Pagination\Paginator && $notifications->hasPages();
$pollingInterval = $this->getPollingInterval();
@endphp
<div class="fi-no-database">
<x-filament::modal
:alignment="$hasNotifications ? null : Alignment::Center"
close-button
:description="$hasNotifications ? null : __('filament-notifications::database.modal.empty.description')"
:heading="$hasNotifications ? null : __('filament-notifications::database.modal.empty.heading')"
:icon="$hasNotifications ? null : \Filament\Support\Icons\Heroicon::OutlinedBellSlash"
:icon-alias="
$hasNotifications
? null
: \Filament\Notifications\View\NotificationsIconAlias::DATABASE_MODAL_EMPTY_STATE
"
:icon-color="$hasNotifications ? null : 'gray'"
id="database-notifications"
slide-over
:sticky-header="$hasNotifications"
teleport="body"
width="md"
class="fi-no-database"
:attributes="
new \Illuminate\View\ComponentAttributeBag([
'wire:poll.' . $pollingInterval => $pollingInterval ? '' : false,
])
"
>
@if ($trigger = $this->getTrigger())
<x-slot name="trigger">
{{ $trigger->with(['unreadNotificationsCount' => $unreadNotificationsCount]) }}
</x-slot>
@endif
@if ($hasNotifications)
<x-slot name="header">
<div>
<h2 class="fi-modal-heading">
{{ __('filament-notifications::database.modal.heading') }}
@if ($unreadNotificationsCount)
<span
{{
(new ComponentAttributeBag)->color(BadgeComponent::class, 'primary')->class([
'fi-badge fi-size-xs',
])
}}
>
{{ $unreadNotificationsCount }}
</span>
@endif
</h2>
<div class="fi-ac">
@if ($unreadNotificationsCount && $this->markAllNotificationsAsReadAction?->isVisible())
{{ $this->markAllNotificationsAsReadAction }}
@endif
@if ($this->clearNotificationsAction?->isVisible())
{{ $this->clearNotificationsAction }}
@endif
</div>
</div>
</x-slot>
@foreach ($notifications as $notification)
<div
@class([
'fi-no-notification-read-ctn' => ! $notification->unread(),
'fi-no-notification-unread-ctn' => $notification->unread(),
])
>
{{ $this->getNotification($notification)->inline() }}
</div>
@endforeach
@if ($broadcastChannel = $this->getBroadcastChannel())
@script
<script>
window.addEventListener('EchoLoaded', () => {
window.Echo.private(@js($broadcastChannel)).listen(
'.database-notifications.sent',
() => {
setTimeout(
() => $wire.call('$refresh'),
500,
)
},
)
})
if (window.Echo) {
window.dispatchEvent(new CustomEvent('EchoLoaded'))
}
</script>
@endscript
@endif
@if ($isPaginated)
<x-slot name="footer">
<x-filament::pagination :paginator="$notifications" />
</x-slot>
@endif
@endif
</x-filament::modal>
</div>
@@ -0,0 +1,43 @@
@php
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\VerticalAlignment;
@endphp
<div>
<div
@class([
'fi-no',
'fi-align-' . static::$alignment->value,
'fi-vertical-align-' . static::$verticalAlignment->value,
])
role="status"
>
@foreach ($notifications as $notification)
{{ $notification }}
@endforeach
</div>
@if ($broadcastChannel = $this->getBroadcastChannel())
@script
<script>
window.addEventListener('EchoLoaded', () => {
window.Echo.private(@js($broadcastChannel)).notification(
(notification) => {
setTimeout(
() =>
$wire.handleBroadcastNotification(
notification,
),
500,
)
},
)
})
if (window.Echo) {
window.dispatchEvent(new CustomEvent('EchoLoaded'))
}
</script>
@endscript
@endif
</div>
@@ -0,0 +1,40 @@
<?php
namespace Filament\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Notification as BaseNotification;
class BroadcastNotification extends BaseNotification implements ShouldQueue
{
use Queueable;
/**
* @param array<string, mixed> $data
*/
public function __construct(
public array $data,
) {}
/**
* @param Model $notifiable
* @return array<string>
*/
public function via($notifiable): array
{
return ['broadcast'];
}
/**
* @param Model $notifiable
*/
public function toBroadcast($notifiable): BroadcastMessage
{
return (new BroadcastMessage($this->data))
->onConnection($this->connection)
->onQueue($this->queue);
}
}
@@ -0,0 +1,35 @@
<?php
namespace Filament\Notifications;
use Illuminate\Support\Collection as BaseCollection;
use Livewire\Wireable;
class Collection extends BaseCollection implements Wireable
{
/**
* @param array<array<string, mixed>> $items
*/
final public function __construct($items = [])
{
parent::__construct($items);
}
/**
* @return array<array<string, mixed>>
*/
public function toLivewire(): array
{
return $this->toArray();
}
/**
* @param array<array<string, mixed>> $value
*/
public static function fromLivewire($value): static
{
return app(static::class, ['items' => $value])->transform(
fn (array $notification): Notification => Notification::fromArray($notification),
);
}
}
@@ -0,0 +1,20 @@
<?php
namespace Filament\Notifications\Concerns;
trait CanBeInline
{
protected bool $isInline = false;
public function inline(bool $condition = true): static
{
$this->isInline = $condition;
return $this;
}
public function isInline(): bool
{
return $this->isInline;
}
}
@@ -0,0 +1,40 @@
<?php
namespace Filament\Notifications\Concerns;
use Closure;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Support\Enums\Size;
use Illuminate\Support\Arr;
trait HasActions
{
/**
* @var array<Action | ActionGroup> | ActionGroup | Closure
*/
protected array | ActionGroup | Closure $actions = [];
/**
* @param array<Action | ActionGroup> | ActionGroup | Closure $actions
*/
public function actions(array | ActionGroup | Closure $actions): static
{
$this->actions = $actions;
return $this;
}
/**
* @return array<Action | ActionGroup>
*/
public function getActions(): array
{
return array_map(
fn (Action $action) => $action
->defaultView(Action::LINK_VIEW)
->defaultSize(Size::Small),
Arr::wrap($this->evaluate($this->actions)),
);
}
}
@@ -0,0 +1,22 @@
<?php
namespace Filament\Notifications\Concerns;
use Closure;
trait HasBody
{
protected string | Closure | null $body = null;
public function body(string | Closure | null $body): static
{
$this->body = $body;
return $this;
}
public function getBody(): ?string
{
return $this->evaluate($this->body);
}
}
@@ -0,0 +1,22 @@
<?php
namespace Filament\Notifications\Concerns;
use Closure;
trait HasDate
{
protected string | Closure | null $date = null;
public function date(string | Closure | null $date): static
{
$this->date = $date;
return $this;
}
public function getDate(): ?string
{
return $this->evaluate($this->date);
}
}
@@ -0,0 +1,36 @@
<?php
namespace Filament\Notifications\Concerns;
use Closure;
trait HasDuration
{
protected int | string | Closure $duration = 6000;
public function duration(int | string | Closure | null $duration): static
{
$this->duration = $duration ?? 'persistent';
return $this;
}
public function getDuration(): int | string
{
return $this->evaluate($this->duration) ?? 'persistent';
}
public function seconds(float $seconds): static
{
$this->duration((int) ($seconds * 1000));
return $this;
}
public function persistent(): static
{
$this->duration('persistent');
return $this;
}
}
@@ -0,0 +1,28 @@
<?php
namespace Filament\Notifications\Concerns;
use BackedEnum;
use Filament\Notifications\View\NotificationsIconAlias;
use Filament\Support\Concerns\HasIcon as BaseTrait;
use Filament\Support\Facades\FilamentIcon;
use Filament\Support\Icons\Heroicon;
use Illuminate\Contracts\Support\Htmlable;
trait HasIcon
{
use BaseTrait {
getIcon as getBaseIcon;
}
public function getIcon(): string | BackedEnum | Htmlable | null
{
return $this->getBaseIcon() ?? match ($this->getStatus()) {
'danger' => FilamentIcon::resolve(NotificationsIconAlias::NOTIFICATION_DANGER) ?? Heroicon::OutlinedXCircle,
'info' => FilamentIcon::resolve(NotificationsIconAlias::NOTIFICATION_INFO) ?? Heroicon::OutlinedInformationCircle,
'success' => FilamentIcon::resolve(NotificationsIconAlias::NOTIFICATION_SUCCESS) ?? Heroicon::OutlinedCheckCircle,
'warning' => FilamentIcon::resolve(NotificationsIconAlias::NOTIFICATION_WARNING) ?? Heroicon::OutlinedExclamationCircle,
default => null,
};
}
}
@@ -0,0 +1,20 @@
<?php
namespace Filament\Notifications\Concerns;
use Filament\Support\Concerns\HasIconColor as BaseTrait;
trait HasIconColor
{
use BaseTrait {
getIconColor as getBaseIconColor;
}
/**
* @return string | array<string> | null
*/
public function getIconColor(): string | array | null
{
return $this->getBaseIconColor() ?? $this->getStatus();
}
}
@@ -0,0 +1,20 @@
<?php
namespace Filament\Notifications\Concerns;
trait HasId
{
protected string $id;
public function id(string $id): static
{
$this->id = $id;
return $this;
}
public function getId(): string
{
return $this->id;
}
}
@@ -0,0 +1,42 @@
<?php
namespace Filament\Notifications\Concerns;
use Closure;
trait HasStatus
{
protected string | Closure | null $status = null;
public function status(string | Closure | null $status): static
{
$this->status = $status;
return $this;
}
public function getStatus(): ?string
{
return $this->evaluate($this->status);
}
public function danger(): static
{
return $this->status('danger');
}
public function info(): static
{
return $this->status('info');
}
public function success(): static
{
return $this->status('success');
}
public function warning(): static
{
return $this->status('warning');
}
}
@@ -0,0 +1,22 @@
<?php
namespace Filament\Notifications\Concerns;
use Closure;
trait HasTitle
{
protected string | Closure | null $title = null;
public function title(string | Closure | null $title): static
{
$this->title = $title;
return $this;
}
public function getTitle(): ?string
{
return $this->evaluate($this->title);
}
}
@@ -0,0 +1,47 @@
<?php
namespace Filament\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notification as BaseNotification;
class DatabaseNotification extends BaseNotification implements Arrayable, ShouldQueue
{
use Queueable;
/**
* @param array<string, mixed> $data
*/
public function __construct(
public array $data,
) {}
/**
* @param Model $notifiable
* @return array<string>
*/
public function via($notifiable): array
{
return ['database'];
}
/**
* @param Model $notifiable
* @return array<string, mixed>
*/
public function toDatabase($notifiable): array
{
return $this->data;
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return $this->data;
}
}
@@ -0,0 +1,38 @@
<?php
namespace Filament\Notifications\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class DatabaseNotificationsSent implements ShouldBroadcast
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
public function __construct(
protected Model | Authenticatable $user,
) {}
public function broadcastOn(): string
{
if (method_exists($this->user, 'receivesBroadcastNotificationsOn')) {
return new PrivateChannel($this->user->receivesBroadcastNotificationsOn());
}
$userClass = str_replace('\\', '.', $this->user::class);
return new PrivateChannel("{$userClass}.{$this->user->getKey()}");
}
public function broadcastAs(): string
{
return 'database-notifications.sent';
}
}
@@ -0,0 +1,214 @@
<?php
namespace Filament\Notifications\Livewire;
use Carbon\CarbonInterface;
use Filament\Actions\Action;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Notifications\Notification;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Notifications\DatabaseNotificationCollection;
use Livewire\Attributes\On;
use Livewire\Component;
use Livewire\WithPagination;
class DatabaseNotifications extends Component implements HasActions, HasSchemas
{
use InteractsWithActions;
use InteractsWithSchemas;
use WithPagination;
public static bool $isPaginated = true;
public static ?string $trigger = null;
public static ?string $pollingInterval = '30s';
public static ?string $authGuard = null;
#[On('databaseNotificationsSent')]
public function refresh(): void {}
#[On('notificationClosed')]
public function removeNotification(string $id): void
{
$this->getNotificationsQuery()
->where('id', $id)
->delete();
}
#[On('markedNotificationAsRead')]
public function markNotificationAsRead(string $id): void
{
$this->getNotificationsQuery()
->where('id', $id)
->update(['read_at' => now()]);
}
#[On('markedNotificationAsUnread')]
public function markNotificationAsUnread(string $id): void
{
$this->getNotificationsQuery()
->where('id', $id)
->update(['read_at' => null]);
}
public function clearNotifications(): void
{
$this->getNotificationsQuery()->delete();
}
public function markAllNotificationsAsRead(): void
{
$this->getUnreadNotificationsQuery()->update(['read_at' => now()]);
}
public function getNotifications(): DatabaseNotificationCollection | Paginator
{
if (! $this->isPaginated()) {
/** @phpstan-ignore-next-line */
return $this->getNotificationsQuery()->get();
}
return $this->getNotificationsQuery()->simplePaginate(50, pageName: 'database-notifications-page');
}
public function isPaginated(): bool
{
return static::$isPaginated;
}
public function getNotificationsQuery(): Builder | Relation
{
$user = $this->getUser();
if (! $user) {
abort(401);
}
/** @phpstan-ignore-next-line */
return $user->notifications()->where('data->format', 'filament');
}
public function getUnreadNotificationsQuery(): Builder | Relation
{
/** @phpstan-ignore-next-line */
return $this->getNotificationsQuery()->unread();
}
public function getUnreadNotificationsCount(): int
{
return $this->getUnreadNotificationsQuery()->count();
}
public function getPollingInterval(): ?string
{
return static::$pollingInterval;
}
public function getTrigger(): ?View
{
$viewPath = static::$trigger;
if (blank($viewPath)) {
return null;
}
return view($viewPath);
}
public function markAllNotificationsAsReadAction(): Action
{
return Action::make('markAllNotificationsAsRead')
->link()
->label(__('filament-notifications::database.modal.actions.mark_all_as_read.label'))
->extraAttributes(['tabindex' => '-1'])
->action('markAllNotificationsAsRead');
}
public function clearNotificationsAction(): Action
{
return Action::make('clearNotifications')
->link()
->color('danger')
->label(__('filament-notifications::database.modal.actions.clear.label'))
->extraAttributes(['tabindex' => '-1'])
->action('clearNotifications')
->close();
}
public function getUser(): Model | Authenticatable | null
{
return auth(static::$authGuard)->user();
}
public function getBroadcastChannel(): ?string
{
$user = $this->getUser();
if (! $user) {
return null;
}
if (method_exists($user, 'receivesBroadcastNotificationsOn')) {
return $user->receivesBroadcastNotificationsOn();
}
$userClass = str_replace('\\', '.', $user::class);
return "{$userClass}.{$user->getKey()}";
}
public function getNotification(DatabaseNotification $notification): Notification
{
return Notification::fromDatabase($notification)
->date($this->formatNotificationDate($notification->getAttributeValue('created_at')));
}
protected function formatNotificationDate(CarbonInterface $date): string
{
return $date->diffForHumans();
}
public static function trigger(?string $trigger): void
{
static::$trigger = $trigger;
}
public static function pollingInterval(?string $interval): void
{
static::$pollingInterval = $interval;
}
public static function authGuard(?string $guard): void
{
static::$authGuard = $guard;
}
/**
* @return array<string>
*/
public function queryStringHandlesPagination(): array
{
return [];
}
public function render(): View
{
return view('filament-notifications::database-notifications');
}
public function placeholder(): string
{
return '<div>' . $this->getTrigger()?->with(['unreadNotificationsCount' => $this->getUnreadNotificationsCount()])->render() . '</div>';
}
}
@@ -0,0 +1,126 @@
<?php
namespace Filament\Notifications\Livewire;
use Filament\Notifications\Collection;
use Filament\Notifications\Notification;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\VerticalAlignment;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Model;
use Livewire\Attributes\On;
use Livewire\Component;
class Notifications extends Component
{
// Used to check if Livewire messages should trigger notification animations.
public bool $isFilamentNotificationsComponent = true;
public Collection $notifications;
public static Alignment $alignment = Alignment::Right;
public static VerticalAlignment $verticalAlignment = VerticalAlignment::Start;
public static ?string $authGuard = null;
public function mount(): void
{
$this->notifications = new Collection;
$this->pullNotificationsFromSession();
}
#[On('notificationsSent')]
public function pullNotificationsFromSession(): void
{
foreach (session()->pull('filament.notifications') ?? [] as $notification) {
$notification = Notification::fromArray($notification);
$this->pushNotification($notification);
}
}
/**
* @param array<string, mixed> $notification
*/
#[On('notificationSent')]
public function pushNotificationFromEvent(array $notification): void
{
$notification = Notification::fromArray($notification);
$this->pushNotification($notification);
}
#[On('notificationClosed')]
public function removeNotification(string $id): void
{
if (! $this->notifications->has($id)) {
return;
}
$this->notifications->forget($id);
}
/**
* @param array<string, mixed> $notification
*/
public function handleBroadcastNotification(array $notification): void
{
if (($notification['format'] ?? null) !== 'filament') {
return;
}
$this->pushNotification(Notification::fromArray($notification));
}
protected function pushNotification(Notification $notification): void
{
$this->notifications->put(
$notification->getId(),
$notification,
);
}
public function getUser(): Model | Authenticatable | null
{
return auth(static::$authGuard)->user();
}
public function getBroadcastChannel(): ?string
{
$user = $this->getUser();
if (! $user) {
return null;
}
if (method_exists($user, 'receivesBroadcastNotificationsOn')) {
return $user->receivesBroadcastNotificationsOn();
}
$userClass = str_replace('\\', '.', $user::class);
return "{$userClass}.{$user->getKey()}";
}
public static function alignment(Alignment $alignment): void
{
static::$alignment = $alignment;
}
public static function verticalAlignment(VerticalAlignment $alignment): void
{
static::$verticalAlignment = $alignment;
}
public static function authGuard(?string $guard): void
{
static::$authGuard = $guard;
}
public function render(): View
{
return view('filament-notifications::notifications');
}
}
@@ -0,0 +1,404 @@
<?php
namespace Filament\Notifications;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Notifications\Events\DatabaseNotificationsSent;
use Filament\Notifications\Livewire\Notifications;
use Filament\Notifications\View\Components\NotificationComponent;
use Filament\Notifications\View\Components\NotificationComponent\IconComponent;
use Filament\Notifications\View\NotificationsIconAlias;
use Filament\Support\Components\Contracts\HasEmbeddedView;
use Filament\Support\Components\ViewComponent;
use Filament\Support\Concerns\HasColor;
use Filament\Support\Concerns\HasIconSize;
use Filament\Support\Contracts\ScalableIcon;
use Filament\Support\Enums\IconSize;
use Filament\Support\Icons\Heroicon;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\DatabaseNotification as DatabaseNotificationModel;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Js;
use Illuminate\Support\Str;
use Illuminate\View\ComponentAttributeBag;
use PHPUnit\Framework\Assert;
use function Filament\Support\generate_icon_html;
class Notification extends ViewComponent implements Arrayable, HasEmbeddedView
{
use Concerns\CanBeInline;
use Concerns\HasActions;
use Concerns\HasBody;
use Concerns\HasDate;
use Concerns\HasDuration;
use Concerns\HasIcon;
use Concerns\HasIconColor;
use Concerns\HasId;
use Concerns\HasStatus;
use Concerns\HasTitle;
use HasColor;
use HasIconSize;
protected string $viewIdentifier = 'notification';
/**
* @var array<string>
*/
protected array $safeViews = [];
public function __construct(string $id)
{
$this->id($id);
}
public static function make(?string $id = null): static
{
$static = app(static::class, ['id' => $id ?? Str::orderedUuid()]);
$static->configure();
return $static;
}
public function toArray(): array
{
$icon = $this->getIcon();
if ($icon instanceof ScalableIcon) {
$icon = $icon->getIconForSize(IconSize::Large);
} elseif ($icon instanceof BackedEnum) {
$icon = $icon->value;
}
return [
'id' => $this->getId(),
'actions' => array_map(fn (Action | ActionGroup $action): array => $action->toArray(), $this->getActions()),
'body' => $this->getBody(),
'color' => $this->getColor(),
'duration' => $this->getDuration(),
'icon' => $icon,
'iconColor' => $this->getIconColor(),
'status' => $this->getStatus(),
'title' => $this->getTitle(),
'view' => $this->hasView() ? $this->getView() : null,
'viewData' => $this->getViewData(),
];
}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): static
{
$static = static::make($data['id'] ?? Str::random());
// If the container constructs an instance of child class
// instead of the current class, we should run `fromArray()`
// on the child class instead.
if (
($static::class !== self::class) &&
(get_called_class() === self::class)
) {
return $static::fromArray($data);
}
$static->actions(
array_map(
fn (array $action): Action | ActionGroup => match (array_key_exists('actions', $action)) {
true => ActionGroup::fromArray($action),
false => Action::fromArray($action),
},
$data['actions'] ?? [],
),
);
$view = $data['view'] ?? null;
if (filled($view) && ((! $static->hasView()) || ($static->getView() !== $view)) && $static->isViewSafe($view)) {
$static->view($data['view']);
}
$static->viewData($data['viewData'] ?? []);
$static->body($data['body'] ?? null);
$static->color($data['color'] ?? null);
$static->duration($data['duration'] ?? $static->getDuration());
$static->status($data['status'] ?? $static->getStatus());
$static->icon($data['icon'] ?? $static->getIcon());
$static->iconColor($data['iconColor'] ?? $static->getIconColor());
$static->title($data['title'] ?? null);
return $static;
}
protected function isViewSafe(string $view): bool
{
return in_array($view, $this->safeViews, strict: true);
}
/**
* @param string | array<string> $safeViews
*/
public function safeViews(string | array $safeViews): static
{
$this->safeViews = [
...$this->safeViews,
...Arr::wrap($safeViews),
];
return $this;
}
public function send(): static
{
session()->push(
'filament.notifications',
$this->toArray(),
);
return $this;
}
/**
* @param Model | Authenticatable | Collection | array<Model | Authenticatable> $users
*/
public function broadcast(Model | Authenticatable | Collection | array $users): static
{
if (! is_iterable($users)) {
$users = [$users];
}
foreach ($users as $user) {
$user->notify($this->toBroadcast());
}
return $this;
}
/**
* @param Model | Authenticatable | Collection | array<Model | Authenticatable> $users
*/
public function sendToDatabase(Model | Authenticatable | Collection | array $users, bool $isEventDispatched = false): static
{
if (! is_iterable($users)) {
$users = [$users];
}
foreach ($users as $user) {
$user->notify($this->toDatabase());
if ($isEventDispatched) {
DatabaseNotificationsSent::dispatch($user);
}
}
return $this;
}
public function toBroadcast(): BroadcastNotification
{
$data = $this->toArray();
$data['format'] = 'filament';
return app(BroadcastNotification::class, ['data' => $data]);
}
public function toDatabase(): DatabaseNotification
{
return new DatabaseNotification($this->getDatabaseMessage());
}
public function getBroadcastMessage(): BroadcastMessage
{
$data = $this->toArray();
$data['format'] = 'filament';
return new BroadcastMessage($data);
}
/**
* @return array<string, mixed>
*/
public function getDatabaseMessage(): array
{
$data = $this->toArray();
$data['duration'] = 'persistent';
$data['format'] = 'filament';
unset($data['id']);
return $data;
}
public static function fromDatabase(DatabaseNotificationModel $notification): static
{
/** @phpstan-ignore-next-line */
$static = static::fromArray($notification->data);
$static->id($notification->getKey());
return $static;
}
public static function assertNotified(Notification | string | null $notification = null): void
{
$notificationsLivewireComponent = new Notifications;
$notificationsLivewireComponent->mount();
$notifications = $notificationsLivewireComponent->notifications;
$expectedNotification = null;
Assert::assertIsArray($notifications->toArray());
if (is_string($notification)) {
$expectedNotification = $notifications->first(fn (Notification $mountedNotification): bool => $mountedNotification->title === $notification);
}
if ($notification instanceof Notification) {
$expectedNotification = $notifications->first(fn (Notification $mountedNotification, string $key): bool => $mountedNotification->id === $key);
}
if (blank($notification)) {
return;
}
Assert::assertNotNull($expectedNotification, 'A notification was not sent');
if ($notification instanceof Notification) {
Assert::assertSame(
collect($expectedNotification)->except(['id'])->toArray(),
collect($notification->toArray())->except(['id'])->toArray()
);
return;
}
Assert::assertSame($expectedNotification->title, $notification);
}
public static function assertNotNotified(Notification | string | null $notification = null): void
{
$notificationsLivewireComponent = new Notifications;
$notificationsLivewireComponent->mount();
$notifications = $notificationsLivewireComponent->notifications;
$expectedNotification = null;
Assert::assertIsArray($notifications->toArray());
if (is_string($notification)) {
$expectedNotification = $notifications->first(fn (Notification $mountedNotification): bool => $mountedNotification->title === $notification);
}
if ($notification instanceof Notification) {
$expectedNotification = $notifications->first(fn (Notification $mountedNotification, string $key): bool => $mountedNotification->id === $key);
}
if (blank($notification)) {
return;
}
if ($notification instanceof Notification) {
Assert::assertNotSame(
collect($expectedNotification)->except(['id'])->toArray(),
collect($notification->toArray())->except(['id'])->toArray(),
'The notification with the given configuration was sent'
);
return;
}
if ($expectedNotification instanceof Notification) {
Assert::assertNotSame(
$expectedNotification->title,
$notification,
'The notification with the given title was sent'
);
}
}
public function toEmbeddedHtml(): string
{
$status = $this->getStatus();
$title = $this->getTitle();
$hasTitle = filled($title);
$date = $this->getDate();
$hasDate = filled($date);
$body = $this->getBody();
$hasBody = filled($body);
$attributes = (new ComponentAttributeBag)
->merge([
'wire:key' => "{$this->getId()}.notifications.{$this->getId()}",
'x-on:close-notification.window' => "if (\$event.detail.id == '{$this->getId()}') close()",
], escape: false)
->color(NotificationComponent::class, $this->getColor() ?? 'gray')
->class([
'fi-no-notification',
'fi-inline' => $this->isInline,
"fi-status-{$status}" => $status,
]);
ob_start(); ?>
<div
x-data="notificationComponent({ notification: <?= Js::from($this->toArray()) ?> })"
x-transition:enter-start="fi-transition-enter-start"
x-transition:enter-end="fi-transition-enter-end"
x-transition:leave-start="fi-transition-leave-start"
x-transition:leave-end="fi-transition-leave-end"
<?= $attributes ?>
>
<?= generate_icon_html(
$this->getIcon(),
attributes: (new ComponentAttributeBag)->color(IconComponent::class, $this->getIconColor())->class(['fi-no-notification-icon']),
size: $this->getIconSize(),
)?->toHtml() ?>
<div class="fi-no-notification-main">
<?php if ($hasTitle || $hasDate || $hasBody) { ?>
<div class="fi-no-notification-text">
<?php if ($hasTitle) { ?>
<h3 class="fi-no-notification-title">
<?= str($title)->sanitizeHtml() ?>
</h3>
<?php } ?>
<?php if ($hasDate) { ?>
<time class="fi-no-notification-date">
<?= e($date) ?>
</time>
<?php } ?>
<?php if ($hasBody) { ?>
<div class="fi-no-notification-body">
<?= str($body)->sanitizeHtml() ?>
</div>
<?php } ?>
</div>
<?php } ?>
<?php if ($actions = $this->getActions()) { ?>
<div class="fi-ac fi-no-notification-actions">
<?php foreach ($actions as $action) { ?>
<?= $action->toHtml() ?>
<?php } ?>
</div>
<?php } ?>
</div>
<button
type="button"
x-on:click="close"
class="fi-icon-btn fi-no-notification-close-btn"
>
<?= generate_icon_html(Heroicon::XMark, alias: NotificationsIconAlias::NOTIFICATION_CLOSE_BUTTON)->toHtml() ?>
</button>
</div>
<?php return ob_get_clean();
}
}
@@ -0,0 +1,57 @@
<?php
namespace Filament\Notifications;
use Filament\Notifications\Livewire\DatabaseNotifications;
use Filament\Notifications\Livewire\Notifications;
use Filament\Notifications\Testing\TestsNotifications;
use Filament\Support\Assets\Js;
use Filament\Support\Facades\FilamentAsset;
use Livewire\Component;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
use function Livewire\on;
use function Livewire\store;
class NotificationsServiceProvider extends PackageServiceProvider
{
public function configurePackage(Package $package): void
{
$package
->name('filament-notifications')
->hasTranslations()
->hasViews();
}
public function packageBooted(): void
{
FilamentAsset::register([
Js::make('notifications', __DIR__ . '/../dist/index.js'),
], 'filament/notifications');
Livewire::component('database-notifications', DatabaseNotifications::class);
Livewire::component('notifications', Notifications::class);
on('dehydrate', function (Component $component): void {
if (! Livewire::isLivewireRequest()) {
return;
}
if (store($component)->has('redirect')) {
return;
}
if (count(session()->get('filament.notifications') ?? []) <= 0) {
return;
}
$component->dispatch('notificationsSent');
});
Testable::mixin(new TestsNotifications);
}
}
@@ -0,0 +1,34 @@
<?php
namespace Filament\Notifications\Testing;
use Closure;
use Filament\Notifications\Notification;
use Livewire\Component;
use Livewire\Features\SupportTesting\Testable;
/**
* @method Component instance()
*
* @mixin Testable
*/
class TestsNotifications
{
public function assertNotified(): Closure
{
return function (Notification | string | null $notification = null): static {
Notification::assertNotified($notification);
return $this;
};
}
public function assertNotNotified(): Closure
{
return function (Notification | string | null $notification = null): static {
Notification::assertNotNotified($notification);
return $this;
};
}
}
@@ -0,0 +1,15 @@
<?php
namespace Filament\Notifications\Testing;
use Filament\Notifications\Notification;
/**
* @return TestCall | TestCase | mixed
*/
function assertNotified(Notification | string | null $notification = null)
{
Notification::assertNotified($notification);
return test();
}
@@ -0,0 +1,18 @@
<?php
namespace Filament\Notifications\View\Components;
use Filament\Support\View\Components\Contracts\HasColor;
use Filament\Support\View\Components\Contracts\HasDefaultGrayColor;
class NotificationComponent implements HasColor, HasDefaultGrayColor
{
/**
* @param array<int, string> $color
* @return array<string, int>
*/
public function getColorMap(array $color): array
{
return [];
}
}
@@ -0,0 +1,18 @@
<?php
namespace Filament\Notifications\View\Components\NotificationComponent;
use Filament\Support\View\Components\Contracts\HasColor;
use Filament\Support\View\Components\Contracts\HasDefaultGrayColor;
class IconComponent implements HasColor, HasDefaultGrayColor
{
/**
* @param array<int, string> $color
* @return array<string, int>
*/
public function getColorMap(array $color): array
{
return [];
}
}
@@ -0,0 +1,18 @@
<?php
namespace Filament\Notifications\View;
class NotificationsIconAlias
{
const DATABASE_MODAL_EMPTY_STATE = 'notifications::database.modal.empty-state';
const NOTIFICATION_CLOSE_BUTTON = 'notifications::notification.close-button';
const NOTIFICATION_DANGER = 'notifications::notification.danger';
const NOTIFICATION_INFO = 'notifications::notification.info';
const NOTIFICATION_SUCCESS = 'notifications::notification.success';
const NOTIFICATION_WARNING = 'notifications::notification.warning';
}