diff --git a/readme.md b/readme.md index 4fd6c5d..54b275d 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,4 @@ -
- -![header](./.github/resources/pxlrbt-activity-log.png) - -
+![header](./.github/resources/header.png) # Filament Activity Log @@ -11,15 +7,10 @@ ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/pxlrbt/filament-activity-log/code-style.yml?branch=main&label=Code%20style&style=flat-square) [![Total Downloads](https://img.shields.io/packagist/dt/pxlrbt/filament-activity-log.svg)](https://packagist.org/packages/pxlrbt/filament-activity-log) +This package adds pages to the Filament Admin panel to view the activity log generated by [`spatie/laravel-activitylog`](https://github.com/spatie/laravel-activitylog). -This package adds a page to the Filament Admin panel to view the activity log generated by [`spatie/laravel-activitylog`](https://github.com/spatie/laravel-activitylog). - -
- ![Screenshot](./.github/resources/screenshot.png) -
- ## Installation | Plugin Version | Filament Version | Activitylog | PHP Version | @@ -31,14 +22,14 @@ This package adds a page to the Filament Admin panel to view the activity log ge Install via Composer. -**Requires PHP 8.0 and Filament 2.0** +**Requires PHP 8.1, Filament 4.0 or 5.0, and spatie/laravel-activitylog 4.7 or 5.0** ```bash composer require pxlrbt/filament-activity-log ``` > **Warning** -> This plugin only offers a page to show activities related to your model. You need [`spatie/laravel-activitylog`](https://github.com/spatie/laravel-activitylog) installed and configured for it to work. It is important you are using the `LogsActivity` trait as per [Spatie's docs](https://spatie.be/docs/laravel-activitylog/v5/advanced-usage/logging-model-events) for this work as we use the `->activitiesAsSubject()` method of the trait. +> This plugin offers two pages: one listing activities **on a subject** (use `ListActivitiesBySubject`) and one listing activities **caused by** a record such as a user (use `ListActivitiesByCauser`). You need [`spatie/laravel-activitylog`](https://github.com/spatie/laravel-activitylog) installed and configured for it to work. The subject page uses the `LogsActivity` trait's `activitiesAsSubject()` relation; the causer page uses the `CausesActivity` trait's `activitiesAsCauser()` relation. ## Filament v4 Upgrade Make sure you have a custom theme, add this line and recompile: `@import '../../../../vendor/pxlrbt/filament-activity-log/resources/css/styles.css';` @@ -47,23 +38,47 @@ Make sure you have a custom theme, add this line and recompile: `@import '../../ Make sure you use a **custom theme** and the vendor folder for this plugins is published, so that it includes the Tailwind CSS classes. -### Create a page +## Listing activities for a subject + +![Screenshot](./.github/resources/screenshot.png) + +Use `ListActivitiesBySubject` to show all activities recorded **on** a record (e.g. every change to an order). + +### Setup spatie/laravel-activitylog + +Make sure your resource model uses the `LogsActivity` trait. + +```php + **Note** +> The legacy class `ListActivities` is kept as an abstract alias of `ListActivitiesBySubject`, so existing subclasses continue to work. + ### Register the page Add the page to your resource's `getPages()` method. @@ -80,27 +95,61 @@ public static function getPages(): array } ``` -### Link to your page -Use a Filament action to link to your from your table or page. +## Listing activities caused by a record + +Use `ListActivitiesByCauser` to show all activities a record (typically a user) has caused across every subject. Each row links to the affected subject's resource when one is registered. + +### Setup spatie/laravel-activitylog + +Make sure your user model uses the `CausesActivity` trait. ```php -$table->actions([ - Action::make('activities')->url(fn ($record) => YourResource::getUrl('activities', ['record' => $record])) -]); + Pages\ListUsers::route('/'), + 'create' => Pages\CreateUser::route('/create'), + 'activities' => Pages\ListUserActivities::route('/{record}/activities'), + 'edit' => Pages\EditUser::route('/{record}/edit'), + ]; +} +``` + ## Contributing If you want to contribute to this packages, you may want to test it in a real Filament project: diff --git a/resources/views/pages/list-activities-by-causer.blade.php b/resources/views/pages/list-activities-by-causer.blade.php new file mode 100644 index 0000000..5e841ba --- /dev/null +++ b/resources/views/pages/list-activities-by-causer.blade.php @@ -0,0 +1,107 @@ + +
+ @foreach($this->getActivities() as $activityItem) + + @php + /* @var \Spatie\Activitylog\Models\Activity $activityItem */ + $changes = $activityItem->attribute_changes ?? collect(); + $resource = $this->getSubjectResource($activityItem->subject_type); + @endphp + +
+
+
+
+ + @if ($resource) + {{ $resource::getModelLabel() }}: + @if ($activityItem->subject && $resource::hasRecordTitle()) + @php + $url = $activityItem->subject->exists + ? $resource::getUrl('edit', ['record' => $activityItem->subject]) + : null; + @endphp + @if ($url) + {{ $resource::getRecordTitle($activityItem->subject) }} + @else + {{ $resource::getRecordTitle($activityItem->subject) }} + @endif + @else + {{ $activityItem->subject_id }} + @endif + @else + {{ $activityItem->subject_type }}: {{ $activityItem->subject_id }} + @endif + + + {{ __('filament-activity-log::activities.events.' . $activityItem->event) }} {{ $activityItem->created_at->format(__('filament-activity-log::activities.default_datetime_format')) }} + +
+
+
+ + @if ($changes->isNotEmpty()) + + + + + + + + + @foreach (data_get($changes, 'attributes', []) as $field => $change) + @php + $oldValue = data_get($changes, "old.{$field}", ''); + $newValue = data_get($changes, "attributes.{$field}", ''); + @endphp + $loop->even + ]) + > + + + + + @endforeach + +
+ {{ __('filament-activity-log::activities.table.field') }} + + {{ __('filament-activity-log::activities.table.old') }} + + {{ __('filament-activity-log::activities.table.new') }} +
+ {{ $this->getFieldLabel($resource, $field) }} + + @if(is_array($oldValue)) +
{{ json_encode($oldValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}
+ @elseif (is_bool($oldValue)) + {{ $oldValue ? 'true' : 'false' }} + @else + {{ $oldValue }} + @endif +
+ @if (is_bool($newValue)) + {{ $newValue ? 'true' : 'false' }} + @elseif(is_array($newValue)) +
{{ json_encode($newValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}
+ @else + {{ $newValue }} + @endif +
+ @endif +
+ @endforeach + + +
+
diff --git a/resources/views/pages/list-activities.blade.php b/resources/views/pages/list-activities-by-subject.blade.php similarity index 100% rename from resources/views/pages/list-activities.blade.php rename to resources/views/pages/list-activities-by-subject.blade.php diff --git a/src/Pages/ListActivities.php b/src/Pages/ListActivities.php index d8d9f4b..4f91f31 100644 --- a/src/Pages/ListActivities.php +++ b/src/Pages/ListActivities.php @@ -2,144 +2,7 @@ namespace pxlrbt\FilamentActivityLog\Pages; -use Exception; -use Filament\Forms\Components\Field; -use Filament\Forms\Components\MorphToSelect; -use Filament\Forms\Contracts\HasForms; -use Filament\Notifications\Notification; -use Filament\Pages\Concerns\InteractsWithFormActions; -use Filament\Resources\Pages\Concerns\InteractsWithRecord; -use Filament\Resources\Pages\Page; -use Filament\Schemas\Schema; -use Filament\Tables\Enums\PaginationMode; -use Illuminate\Support\Collection; -use Livewire\WithPagination; -use pxlrbt\FilamentActivityLog\Pages\Concerns\CanPaginate; - -abstract class ListActivities extends Page implements HasForms -{ - use CanPaginate; - use InteractsWithFormActions; - use InteractsWithRecord; - use WithPagination; - - protected string $view = 'filament-activity-log::pages.list-activities'; - - protected static Collection $fieldLabelMap; - - public function mount($record) - { - $this->record = $this->resolveRecord($record); - $this->recordsPerPage = $this->getDefaultRecordsPerPageSelectOption(); - } - - public function getBreadcrumb(): string - { - return static::$breadcrumb ?? __('filament-activity-log::activities.breadcrumb'); - } - - public function getTitle(): string - { - return __('filament-activity-log::activities.title', ['record' => $this->getRecordTitle()]); - } - - public function getActivities() - { - return $this->paginateQuery( - $this->record->activitiesAsSubject()->with('causer')->latest()->getQuery() - ); - } - - public function getPaginationMode(): PaginationMode - { - return PaginationMode::Default; - } - - public function getFieldLabel(string $name): string - { - static::$fieldLabelMap ??= $this->createFieldLabelMap(); - - return static::$fieldLabelMap[$name] ?? $name; - } - - protected function createFieldLabelMap(): Collection - { - $schema = static::getResource()::form(new Schema($this)); - - $components = collect($schema->getComponents()); - $extracted = collect(); - - while (($component = $components->shift()) !== null) { - if ($component instanceof Field || $component instanceof MorphToSelect) { - $extracted->push($component); - - continue; - } - - $children = $component->getChildComponents(); - - if (count($children) > 0) { - $components = $components->merge($children); - - continue; - } - - $extracted->push($component); - } - - return $extracted - ->filter(fn ($field) => $field instanceof Field) - ->mapWithKeys(fn (Field $field) => [ - $field->getName() => $field->getLabel(), - ]); - } - - public function canRestoreActivity(): bool - { - return static::getResource()::canRestore($this->record); - } - - public function restoreActivity(int|string $key) - { - if (! $this->canRestoreActivity()) { - abort(403); - } - - $activity = $this->record->activitiesAsSubject() - ->whereKey($key) - ->first(); - - $oldAttributes = data_get($activity, 'attribute_changes.old'); - - if ($oldAttributes === null) { - $this->sendRestoreFailureNotification(); - - return; - } - - try { - $this->record->update($oldAttributes); - - $this->sendRestoreSuccessNotification(); - } catch (Exception $e) { - $this->sendRestoreFailureNotification($e->getMessage()); - } - } - - protected function sendRestoreSuccessNotification(): Notification - { - return Notification::make() - ->title(__('filament-activity-log::activities.events.restore_successful')) - ->success() - ->send(); - } - - protected function sendRestoreFailureNotification(?string $message = null): Notification - { - return Notification::make() - ->title(__('filament-activity-log::activities.events.restore_failed')) - ->body($message) - ->danger() - ->send(); - } -} +/** + * @deprecated Use ListActivitiesBySubject directly + */ +abstract class ListActivities extends ListActivitiesBySubject {} diff --git a/src/Pages/ListActivitiesByCauser.php b/src/Pages/ListActivitiesByCauser.php new file mode 100644 index 0000000..c38de3e --- /dev/null +++ b/src/Pages/ListActivitiesByCauser.php @@ -0,0 +1,109 @@ +> */ + protected static array $fieldLabelMaps = []; + + public function mount($record) + { + $this->record = $this->resolveRecord($record); + $this->recordsPerPage = $this->getDefaultRecordsPerPageSelectOption(); + } + + public function getBreadcrumb(): string + { + return static::$breadcrumb ?? __('filament-activity-log::activities.breadcrumb'); + } + + public function getTitle(): string + { + return __('filament-activity-log::activities.title', ['record' => $this->getRecordTitle()]); + } + + public function getActivities() + { + return $this->paginateQuery( + $this->record->activitiesAsCauser()->with('subject')->latest()->getQuery() + ); + } + + public function getPaginationMode(): PaginationMode + { + return PaginationMode::Default; + } + + public function getSubjectResource(string $subjectType): ?string + { + $modelClass = Relation::getMorphedModel($subjectType) ?? $subjectType; + + return Filament::getModelResource($modelClass); + } + + public function getFieldLabel(?string $resourceClass, string $name): string + { + if ($resourceClass === null) { + return $name; + } + + static::$fieldLabelMaps[$resourceClass] ??= $this->createFieldLabelMap($resourceClass); + + return static::$fieldLabelMaps[$resourceClass][$name] ?? $name; + } + + /** @param class-string $resourceClass */ + protected function createFieldLabelMap(string $resourceClass): Collection + { + $schema = $resourceClass::form(new Schema($this)); + + $components = collect($schema->getComponents()); + $extracted = collect(); + + while (($component = $components->shift()) !== null) { + if ($component instanceof Field || $component instanceof MorphToSelect) { + $extracted->push($component); + + continue; + } + + $children = $component->getChildComponents(); + + if (count($children) > 0) { + $components = $components->merge($children); + + continue; + } + + $extracted->push($component); + } + + return $extracted + ->filter(fn ($field) => $field instanceof Field) + ->mapWithKeys(fn (Field $field) => [ + $field->getName() => $field->getLabel(), + ]); + } +} diff --git a/src/Pages/ListActivitiesBySubject.php b/src/Pages/ListActivitiesBySubject.php new file mode 100644 index 0000000..c7301c4 --- /dev/null +++ b/src/Pages/ListActivitiesBySubject.php @@ -0,0 +1,145 @@ +record = $this->resolveRecord($record); + $this->recordsPerPage = $this->getDefaultRecordsPerPageSelectOption(); + } + + public function getBreadcrumb(): string + { + return static::$breadcrumb ?? __('filament-activity-log::activities.breadcrumb'); + } + + public function getTitle(): string + { + return __('filament-activity-log::activities.title', ['record' => $this->getRecordTitle()]); + } + + public function getActivities() + { + return $this->paginateQuery( + $this->record->activitiesAsSubject()->with('causer')->latest()->getQuery() + ); + } + + public function getPaginationMode(): PaginationMode + { + return PaginationMode::Default; + } + + public function getFieldLabel(string $name): string + { + static::$fieldLabelMap ??= $this->createFieldLabelMap(); + + return static::$fieldLabelMap[$name] ?? $name; + } + + protected function createFieldLabelMap(): Collection + { + $schema = static::getResource()::form(new Schema($this)); + + $components = collect($schema->getComponents()); + $extracted = collect(); + + while (($component = $components->shift()) !== null) { + if ($component instanceof Field || $component instanceof MorphToSelect) { + $extracted->push($component); + + continue; + } + + $children = $component->getChildComponents(); + + if (count($children) > 0) { + $components = $components->merge($children); + + continue; + } + + $extracted->push($component); + } + + return $extracted + ->filter(fn ($field) => $field instanceof Field) + ->mapWithKeys(fn (Field $field) => [ + $field->getName() => $field->getLabel(), + ]); + } + + public function canRestoreActivity(): bool + { + return static::getResource()::canRestore($this->record); + } + + public function restoreActivity(int|string $key) + { + if (! $this->canRestoreActivity()) { + abort(403); + } + + $activity = $this->record->activitiesAsSubject() + ->whereKey($key) + ->first(); + + $oldAttributes = data_get($activity, 'attribute_changes.old'); + + if ($oldAttributes === null) { + $this->sendRestoreFailureNotification(); + + return; + } + + try { + $this->record->update($oldAttributes); + + $this->sendRestoreSuccessNotification(); + } catch (Exception $e) { + $this->sendRestoreFailureNotification($e->getMessage()); + } + } + + protected function sendRestoreSuccessNotification(): Notification + { + return Notification::make() + ->title(__('filament-activity-log::activities.events.restore_successful')) + ->success() + ->send(); + } + + protected function sendRestoreFailureNotification(?string $message = null): Notification + { + return Notification::make() + ->title(__('filament-activity-log::activities.events.restore_failed')) + ->body($message) + ->danger() + ->send(); + } +}