From 27912cfe3edc184f81a771489943c51229647b99 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 24 Mar 2026 09:55:19 +0800 Subject: [PATCH 01/17] Rebrand from Kenepa to Blendbyte, rename package to filament-resource-lock - Replace all Kenepa/kenepa references with Blendbyte/blendbyte - Rename PHP namespace from Blendbyte\ResourceLock to Blendbyte\FilamentResourceLock - Rename composer package to blendbyte/filament-resource-lock - Rename config file to filament-resource-lock.php - Update artisan command signatures to filament-resource-lock:* - Simplify and clean up README, remove upgrade guides and version table --- LICENSE.md | 2 +- README.md | 279 ++++-------------- composer.json | 18 +- ...ce-lock.php => filament-resource-lock.php} | 4 +- database/factories/ResourceLockFactory.php | 4 +- database/factories/UserFactory.php | 4 +- .../resource-lock-observer.blade.php | 8 +- src/Actions/GetResourceLockOwnerAction.php | 2 +- .../Commands/ResourceLockClearCommand.php | 6 +- .../ResourceLockClearExpiredCommand.php | 6 +- src/Http/Livewire/ResourceLockObserver.php | 6 +- src/Models/Concerns/HasLocks.php | 6 +- src/Models/ResourceLock.php | 4 +- src/ResourceLockPlugin.php | 48 +-- src/ResourceLockServiceProvider.php | 10 +- src/Resources/LockResource.php | 18 +- .../LockResource/ManageResourceLocks.php | 10 +- src/Resources/Pages/Concerns/UsesLocks.php | 2 +- .../Pages/Concerns/UsesResourceLock.php | 4 +- .../Pages/Concerns/UsesSimpleResourceLock.php | 4 +- .../ConfigureResourceLockPluginTest.php | 2 +- tests/Feature/ResourceLockCommandTest.php | 22 +- tests/Feature/ResourceLockObserverTest.php | 4 +- .../LockResource/LockResourceTest.php | 2 +- tests/Fixtures/AdminPanelProvider.php | 4 +- tests/Pest.php | 12 +- tests/Resources/Models/Post.php | 4 +- tests/Resources/Models/User.php | 2 +- tests/TestCase.php | 10 +- tests/Unit/modelHasLockTest.php | 2 +- 30 files changed, 172 insertions(+), 337 deletions(-) rename config/{resource-lock.php => filament-resource-lock.php} (95%) diff --git a/LICENSE.md b/LICENSE.md index b196e73..e7eb87d 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) Kenepa +Copyright (c) Blendbyte Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 94aeb4e..b065a57 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,25 @@ # Resource Lock - -filament-resource-lock-art - - - -[![Latest Version on Packagist](https://img.shields.io/packagist/v/kenepa/resource-lock.svg?style=flat-square)](https://packagist.org/packages/kenepa/resource-lock) -[![Total Downloads](https://img.shields.io/packagist/dt/kenepa/resource-lock.svg?style=flat-square)](https://packagist.org/packages/kenepa/resource-lock) - -Filament Resource Lock is a Filament plugin that adds resource locking functionality to your site. When a -user begins editing a resource, Filament Resource Lock automatically locks the resource to prevent other users from -editing it at the same time. The resource will be automatically unlocked after a set period of time, or when the user -saves or discards their changes. - -filament-resource-lock-art +Filament Resource Lock is a Filament plugin that adds resource locking functionality to your site. When a user begins editing a resource, it is automatically locked to prevent other users from editing it at the same time. The resource will be automatically unlocked after a set period of time, or when the user saves or discards their changes. ## Installation -| Plugin Version | Filament Version | PHP Version | -|----------------|------------------|-------------| -| 1.x | 2.x | 8.0+ | -| 2.x | 3.x | 8.1+ | -| 3.x | 3.x | 8.1+ | -| 4.x | 4.x | 8.2+ | - -You can install the package via composer: - ```bash -composer require kenepa/resource-lock +composer require blendbyte/filament-resource-lock ``` -Then run the installation command to publish and run migration(s) +Then run the installation command to publish and run the migration: ```bash -php artisan resource-lock:install +php artisan filament-resource-lock:install ``` -Register plugin with a panel +Register the plugin with a panel: + ```php -use Kenepa\ResourceLock\ResourceLockPlugin; +use Blendbyte\FilamentResourceLock\ResourceLockPlugin; use Filament\Panel; - + public function panel(Panel $panel): Panel { return $panel @@ -49,63 +28,28 @@ public function panel(Panel $panel): Panel } ``` -# Upgrade guides - -## Upgrade to 2.x - -> Notice - Upgrading to Version 2.1.x : -> In case you have published the config, make sure to update the following in your config: -> ```php -> 'resource' => [ -> 'class' => \Kenepa\ResourceLock\Resources\LockResource::class, -> ], -> ``` - -## Upgrade from 2.x to 3.x - -### Breaking Changes - -- **Plugin-based Customization**: Icons, labels, model classes, and access gates now configured via the plugin. -- **Timeout unit changed**: Now uses seconds instead of minutes - ## Usage -The Filament Resource Lock package enables you to lock a resource and prevent other users from editing it at the same -time. Currently, this package only locks -the [EditRecord](https://filamentphp.com/docs/2.x/admin/resources/editing-records) page and the edit modal when editing -a [simple modal resource.](https://filamentphp.com/docs/2.x/admin/resources/getting-started#simple-modal-resources) -Follow the steps below to add locks to your resources. - ### Add Locks to your model -The first step is to add the HasLocks trait to the model of your resource. The HasLocks trait enables the locking -functionality on your model. +Add the `HasLocks` trait to the model you want to lock: ```php -// Post.php - -use Kenepa\ResourceLock\Models\Concerns\HasLocks; +use Blendbyte\FilamentResourceLock\Models\Concerns\HasLocks; class Post extends Model { use HasFactory; use HasLocks; - - protected $table = 'posts'; - - protected $guarded = []; } ``` -### Add Locks to your EditRecord Page +### Add Locks to your EditRecord page -The second step is to add the UsesResourceLock trait to your EditRecord page. The UsesResourceLock trait enables the -locking function on your edit page. +Add the `UsesResourceLock` trait to your `EditRecord` page: ```php -// EditPost.php - -use Kenepa\ResourceLock\Resources\Pages\Concerns\UsesResourceLock; +use Blendbyte\FilamentResourceLock\Resources\Pages\Concerns\UsesResourceLock; class EditPost extends EditRecord { @@ -115,145 +59,73 @@ class EditPost extends EditRecord } ``` -#### Simple modal Resource +### Simple modal resource -If your resource is -a [simple modal](https://filamentphp.com/docs/2.x/admin/resources/getting-started#simple-modal-resources) resource, -you'll need to use the UsesSimpleResourceLock trait instead. +For simple modal resources, use `UsesSimpleResourceLock` instead: ```php -// ManagePosts.php - -use Kenepa\ResourceLock\Resources\Pages\Concerns\UsesSimpleResourceLock; +use Blendbyte\FilamentResourceLock\Resources\Pages\Concerns\UsesSimpleResourceLock; class ManagePosts extends ManageRecords { use UsesSimpleResourceLock; protected static string $resource = PostResource::class; - } ``` -And that's it! Your resource is now able to be locked. Refer to the documentation below for more information on how to -configure the locking functionality. - -## Use polling to detect presence (SPA mode) +## Polling (SPA mode) -To make the resource locking feature possible for SPA we polling based detection to check if a user is still editing the resource. -This is disabled by default but you can enable it in the config: +To support SPA mode, enable polling-based presence detection in the plugin: ```php -use Kenepa\ResourceLock\ResourceLockPlugin; -use Filament\Panel; - -public function panel(Panel $panel): Panel -{ - return $panel - // ... - ->plugin(ResourceLockPlugin::make() - ->usesPollingToDetectPresence() - ->presencePollingInterval(10) - ->lockTimeout(15) - ); -} +->plugin(ResourceLockPlugin::make() + ->usesPollingToDetectPresence() + ->presencePollingInterval(10) + ->lockTimeout(15) +) ``` -> **Tip:** -> Make sure the lock timeout is **not** set to a value lower than the presence‑polling interval. If it is, the lock may time out before a new heartbeat is sent, allowing another user to acquire the lock while the current user is still editing the page. - -### Polling Configuration - -When using polling to detect presence, you can configure additional options (by default Livewire will reduce the number of polling requests by 95% until the user revisits the tab. +> **Tip:** Make sure the lock timeout is not lower than the polling interval — otherwise the lock may expire before the next heartbeat is sent. -): +Additional polling options: +- **`pollingKeepAlive()`**: Keeps polling alive when the tab is in the background. +- **`pollingVisible()`**: Only polls when the browser tab is visible. -- **`pollingKeepAlive()`**: Keeps the polling connection alive even when the user has the tab in the background. -- **`pollingVisible()`**: Only polls when the browser tab is visible to the user. This helps reduce server load by pausing polling when users switch to other tabs. +## Resource Lock Manager -## Resource Lock manager - -filament-resource-lock-art - -The package also provides a simple way to manage and view all your active and expired locks within your app. And it also -provides a way to quickly unlock all resources or specific locks. +The package includes a UI to view and manage all active and expired locks, and to unlock resources individually or in bulk. ## Configuration -### Access - -filament-resource-lock-art - -You can restrict the access to the **Unlock** button or to the resource manager by adjusting the access variable. -Enabling the "limited" key and -setting it to true allows you to specify either a Laravel Gate class or a permission name from -the [Spatie Permissions package](https://github.com/spatie/laravel-permission). - -```php -use Kenepa\ResourceLock\ResourceLockPlugin; -use Filament\Panel; - -public function panel(Panel $panel): Panel -{ - return $panel - // ... - ->limitedAccessToResourceLockManager() - ->gate('unlock') -} -``` +### Access control -Example +Restrict access to the Unlock button or resource manager using a gate or Spatie permission: ```php - -// Example using gates -// More info about gates: https://laravel.com/docs/authorization#writing-gates -Gate::define('unlock', function (User $user, Post $post) { - return $user->email === 'admin@mail.com'; -}); - -// Example using spatie permission package -Permission::create(['name' => 'unlock']); +->plugin(ResourceLockPlugin::make() + ->limitedAccessToResourceLockManager() + ->gate('unlock') +) ``` -### Using custom models - -Sometimes, you may have a customized implementation for the User model in your application, or you may want to use a -custom class for the ResourceLock functionality. In such cases, you can update the configuration file to specify the new -class you want to use. This will ensure that the ResourceLock functionality works as expected with the new -implementation. +### Custom models ```php -use Kenepa\ResourceLock\ResourceLockPlugin; -use Filament\Panel; - -public function panel(Panel $panel): Panel -{ - return $panel - // ... - ->userModel(\App\Models\CustomUser::class) - ->resourceLockModel(\App\Models\CustomResourceLock::class); -} +->plugin(ResourceLockPlugin::make() + ->userModel(\App\Models\CustomUser::class) + ->resourceLockModel(\App\Models\CustomResourceLock::class) +) ``` -### Displaying the user who has locked the resource +### Custom lock owner display -This package uses actions which allows you to implement your own custom logic. An action class is nothing more than a -simple class with a method that executes some -logic. [Learn more about actions](https://freek.dev/2442-strategies-for-making-laravel-packages-customizable) - -To create a custom action, first create a file within your project and name -it ```CustomGetResourceLockOwnerAction.php```, for -example. In this file, create a new class that extends the ```GetResourceLockOwnerAction``` class and override the -execute -method to return the desired identifier. For example: +Create a custom action class extending `GetResourceLockOwnerAction`: ```php -// CustomGetResourceLockOwnerAction.php - namespace App\Actions; -use Kenepa\ResourceLock\Actions\GetResourceLockOwnerAction; +use Blendbyte\FilamentResourceLock\Actions\GetResourceLockOwnerAction; class CustomResourceLockOwnerAction extends GetResourceLockOwnerAction { @@ -264,73 +136,36 @@ class CustomResourceLockOwnerAction extends GetResourceLockOwnerAction } ``` -Next, register your custom action within the your plugin configuration: +Register it in the plugin: ```php -use Kenepa\ResourceLock\ResourceLockPlugin; -use Filament\Panel; - -public function panel(Panel $panel): Panel -{ - return $panel - // ... - ->plugin( - ResourceLockPlugin::make() - ->resourceLockOwnerAction(\Kenepa\ResourceLock\Actions\CustomGetResourceLockOwnerAction::class) - ); -} +->plugin(ResourceLockPlugin::make() + ->resourceLockOwnerAction(\App\Actions\CustomResourceLockOwnerAction::class) +) ``` ### Overriding default functionality -If you need some custom functionality beyond what the traits provide, you can override the functions that they use. For -example, if you want to change the URL that the "Return" button redirects to, you can override the -resourceLockReturnUrl() function. By default, this button takes you to the index page of the resource, but you can -change it to whatever URL you want by adding your custom implementation in the resourceLockReturnUrl() function. - -For instance, if you want the "Return" button to redirect to https://laracasts.com, you can override the function as -follows: +Override `resourceLockReturnUrl()` to change where the Return button redirects: ```php - public function resourceLockReturnUrl(): string - { - return 'https://laracasts.com'; - } -``` - -Now the return url will redirect to laracasts.com - -This will change the behavior of the "Return" button to redirect to the provided URL. - -## Publishing migrations, configuration and view - -```bash -php artisan vendor:publish --tag="resource-lock-migrations" -php artisan migrate +public function resourceLockReturnUrl(): string +{ + return route('dashboard'); +} ``` -You can publish and run the migrations with: +## Publishing assets ```bash -php artisan vendor:publish --tag="resource-lock-migrations" +# Migrations +php artisan vendor:publish --tag="filament-resource-lock-migrations" php artisan migrate -``` - -Optionally, you can publish the views using -> Note: Publishing Blade views can introduce breaking changes into your app. If you're interested in how to stay -> safe, [see this article by Dan Harrin](https://filamentphp.com/blog/publishing-views-in-laravel). - -```bash -php artisan vendor:publish --tag="resource-lock-views" +# Views +php artisan vendor:publish --tag="filament-resource-lock-views" ``` -## Coming soon - -- Locked status indicator for table rows -- Polling -- Optimistic Locking - ## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. diff --git a/composer.json b/composer.json index 0e6d53a..a0e9906 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,12 @@ { - "name": "kenepa/resource-lock", + "name": "blendbyte/filament-resource-lock", "description": "Filament Resource Lock is a Filament plugin that adds resource locking functionality to your site.", "keywords": [ - "Kenepa", + "Blendbyte", "laravel", - "resource-lock" + "filament-resource-lock" ], - "homepage": "https://github.com/kenepa/resource-lock", + "homepage": "https://github.com/blendbyte/filament-resource-lock", "license": "MIT", "require": { "php": "^8.2", @@ -30,13 +30,13 @@ }, "autoload": { "psr-4": { - "Kenepa\\ResourceLock\\": "src", - "Kenepa\\ResourceLock\\Database\\Factories\\": "database/factories" + "Blendbyte\\FilamentResourceLock\\": "src", + "Blendbyte\\FilamentResourceLock\\Database\\Factories\\": "database/factories" } }, "autoload-dev": { "psr-4": { - "Kenepa\\ResourceLock\\Tests\\": "tests" + "Blendbyte\\FilamentResourceLock\\Tests\\": "tests" } }, "scripts": { @@ -59,10 +59,10 @@ "extra": { "laravel": { "providers": [ - "Kenepa\\ResourceLock\\ResourceLockServiceProvider" + "Blendbyte\\FilamentResourceLock\\ResourceLockServiceProvider" ], "aliases": { - "ResourceLock": "Kenepa\\ResourceLock\\Facades\\ResourceLock" + "ResourceLock": "Blendbyte\\FilamentResourceLock\\Facades\\ResourceLock" } } }, diff --git a/config/resource-lock.php b/config/filament-resource-lock.php similarity index 95% rename from config/resource-lock.php rename to config/filament-resource-lock.php index 5687999..bfe1863 100644 --- a/config/resource-lock.php +++ b/config/filament-resource-lock.php @@ -28,7 +28,7 @@ | */ 'resource' => [ - 'class' => \Kenepa\ResourceLock\Resources\LockResource::class, + 'class' => \Blendbyte\FilamentResourceLock\Resources\LockResource::class, ], /* @@ -125,6 +125,6 @@ */ 'actions' => [ - 'get_resource_lock_owner_action' => \Kenepa\ResourceLock\Actions\GetResourceLockOwnerAction::class, + 'get_resource_lock_owner_action' => \Blendbyte\FilamentResourceLock\Actions\GetResourceLockOwnerAction::class, ], ]; diff --git a/database/factories/ResourceLockFactory.php b/database/factories/ResourceLockFactory.php index 2d4fd2e..81b54bf 100644 --- a/database/factories/ResourceLockFactory.php +++ b/database/factories/ResourceLockFactory.php @@ -1,9 +1,9 @@

- {{ __('resource-lock::modal.locked_notice_user') }} + {{ __('filament-resource-lock::modal.locked_notice_user') }}

- {{ __('resource-lock::modal.locked_notice') }} + {{ __('filament-resource-lock::modal.locked_notice') }}

@@ -89,7 +89,7 @@ function startObserving() { @if ($isAllowedToUnlock) @endif @@ -97,7 +97,7 @@ function startObserving() { class="fi-btn fi-size-md" :href="url" tabindex="-1"> - {{ __('resource-lock::modal.return_button') }} + {{ __('filament-resource-lock::modal.return_button') }} diff --git a/src/Actions/GetResourceLockOwnerAction.php b/src/Actions/GetResourceLockOwnerAction.php index 2a5c784..eb84beb 100644 --- a/src/Actions/GetResourceLockOwnerAction.php +++ b/src/Actions/GetResourceLockOwnerAction.php @@ -1,6 +1,6 @@ Blade::render('@livewire(\'resource-lock-observer\')'), + fn (): string => Blade::render('@livewire(\'filament-resource-lock-observer\')'), ); } @@ -102,7 +102,7 @@ public function displayResourceLockOwner(bool $display = true): static public function shouldDisplayResourceLockOwner(): bool { - return $this->displayResourceLockOwner ?? config('resource-lock.lock_notice.display_resource_lock_owner', true); + return $this->displayResourceLockOwner ?? config('filament-resource-lock.lock_notice.display_resource_lock_owner', true); } public function navigationBadge(bool $show = true): static @@ -114,7 +114,7 @@ public function navigationBadge(bool $show = true): static public function shouldShowNavigationBadge(): bool { - return $this->navigationBadge ?? config('resource-lock.manager.navigation_badge', false); + return $this->navigationBadge ?? config('filament-resource-lock.manager.navigation_badge', false); } public function navigationIcon(?string $icon): static @@ -126,7 +126,7 @@ public function navigationIcon(?string $icon): static public function getNavigationIcon(): ?string { - return $this->navigationIcon ?? config('resource-lock.manager.navigation_icon', 'heroicon-o-lock-closed'); + return $this->navigationIcon ?? config('filament-resource-lock.manager.navigation_icon', 'heroicon-o-lock-closed'); } public function navigationLabel(?string $label): static @@ -138,7 +138,7 @@ public function navigationLabel(?string $label): static public function getNavigationLabel(): string { - return __($this->navigationLabel ?? config('resource-lock.manager.navigation_label', 'Resource Lock Manager')); + return __($this->navigationLabel ?? config('filament-resource-lock.manager.navigation_label', 'Resource Lock Manager')); } public function pluralLabel(?string $label): static @@ -150,7 +150,7 @@ public function pluralLabel(?string $label): static public function getPluralLabel(): string { - return __($this->pluralLabel ?? config('resource-lock.manager.plural_label', 'Resource Locks')); + return __($this->pluralLabel ?? config('filament-resource-lock.manager.plural_label', 'Resource Locks')); } public function navigationGroup(?string $group): static @@ -162,7 +162,7 @@ public function navigationGroup(?string $group): static public function getNavigationGroup(): ?string { - return $this->navigationGroup ?? config('resource-lock.manager.navigation_group'); + return $this->navigationGroup ?? config('filament-resource-lock.manager.navigation_group'); } public function navigationSort(?int $sort): static @@ -174,7 +174,7 @@ public function navigationSort(?int $sort): static public function getNavigationSort(): ?int { - return $this->navigationSort ?? config('resource-lock.manager.navigation_sort'); + return $this->navigationSort ?? config('filament-resource-lock.manager.navigation_sort'); } public function limitedAccessToResourceLockManager(bool $limited = true): static @@ -186,7 +186,7 @@ public function limitedAccessToResourceLockManager(bool $limited = true): static public function shouldLimitAccessToResourceLockManager(): bool { - return $this->limitedAccessToResourceLockManager ?? config('resource-lock.manager.limited_access', false); + return $this->limitedAccessToResourceLockManager ?? config('filament-resource-lock.manager.limited_access', false); } public function gate(?string $gate): static @@ -198,7 +198,7 @@ public function gate(?string $gate): static public function getGate(): ?string { - return $this->gate ?? config('resource-lock.manager.gate', null); + return $this->gate ?? config('filament-resource-lock.manager.gate', null); } public function registerNavigation(bool $register = true): static @@ -210,7 +210,7 @@ public function registerNavigation(bool $register = true): static public function shouldRegisterNavigation(): bool { - return $this->shouldRegisterNavigation ?? config('resource-lock.manager.should_register_navigation', true); + return $this->shouldRegisterNavigation ?? config('filament-resource-lock.manager.should_register_navigation', true); } public function unlockerLimitedAccess(bool $limited = true): static @@ -222,7 +222,7 @@ public function unlockerLimitedAccess(bool $limited = true): static public function shouldLimitUnlockerAccess(): bool { - return $this->unlockerLimitedAccess ?? config('resource-lock.unlocker.limited_access', false); + return $this->unlockerLimitedAccess ?? config('filament-resource-lock.unlocker.limited_access', false); } public function unlockerGate(?string $gate): static @@ -234,7 +234,7 @@ public function unlockerGate(?string $gate): static public function getUnlockerGate(): ?string { - return $this->unlockerGate ?? config('resource-lock.unlocker.gate', null); + return $this->unlockerGate ?? config('filament-resource-lock.unlocker.gate', null); } public function resourceClass(?string $class): static @@ -246,7 +246,7 @@ public function resourceClass(?string $class): static public function getResourceClass(): string { - return $this->resourceClass ?? config('resource-lock.resource.class', LockResource::class); + return $this->resourceClass ?? config('filament-resource-lock.resource.class', LockResource::class); } public function userModel(?string $model): static @@ -258,7 +258,7 @@ public function userModel(?string $model): static public function getUserModel(): string { - return $this->userModel ?? config('resource-lock.models.User', 'App\\Models\\User'); + return $this->userModel ?? config('filament-resource-lock.models.User', 'App\\Models\\User'); } public function resourceLockModel(?string $model): static @@ -270,7 +270,7 @@ public function resourceLockModel(?string $model): static public function getResourceLockModel(): string { - return $this->resourceLockModel ?? config('resource-lock.models.ResourceLock', ResourceLock::class); + return $this->resourceLockModel ?? config('filament-resource-lock.models.ResourceLock', ResourceLock::class); } public function lockTimeout(?int $seconds): static @@ -282,7 +282,7 @@ public function lockTimeout(?int $seconds): static public function getLockTimeout(): int { - return $this->lockTimeout ?? config('resource-lock.lock_timeout', 600); + return $this->lockTimeout ?? config('filament-resource-lock.lock_timeout', 600); } public function checkLocksBeforeSaving(bool $check = true): static @@ -294,7 +294,7 @@ public function checkLocksBeforeSaving(bool $check = true): static public function shouldCheckLocksBeforeSaving(): bool { - return $this->checkLocksBeforeSaving ?? config('resource-lock.check_locks_before_saving', true); + return $this->checkLocksBeforeSaving ?? config('filament-resource-lock.check_locks_before_saving', true); } public function resourceLockOwnerAction(?string $action): static @@ -306,7 +306,7 @@ public function resourceLockOwnerAction(?string $action): static public function getResourceLockOwnerAction(): string { - return $this->resourceLockOwnerAction ?? config('resource-lock.actions.get_resource_lock_owner_action', \Kenepa\ResourceLock\Actions\GetResourceLockOwnerAction::class); + return $this->resourceLockOwnerAction ?? config('filament-resource-lock.actions.get_resource_lock_owner_action', \Blendbyte\FilamentResourceLock\Actions\GetResourceLockOwnerAction::class); } public function usesPollingToDetectPresence(bool $enable = true): static diff --git a/src/ResourceLockServiceProvider.php b/src/ResourceLockServiceProvider.php index ae08e32..54f9b80 100644 --- a/src/ResourceLockServiceProvider.php +++ b/src/ResourceLockServiceProvider.php @@ -1,16 +1,16 @@ publishMigrations() ->askToRunMigrations() - ->askToStarRepoOnGitHub('kenepa/resource-lock'); + ->askToStarRepoOnGitHub('blendbyte/filament-resource-lock'); }) ->hasCommand(ResourceLockClearCommand::class) ->hasCommand(ResourceLockClearExpiredCommand::class); diff --git a/src/Resources/LockResource.php b/src/Resources/LockResource.php index e1a1b7c..e8bb1b4 100644 --- a/src/Resources/LockResource.php +++ b/src/Resources/LockResource.php @@ -1,6 +1,6 @@ formatStateUsing(static function ($record) { if ($record->isExpired()) { - return __('resource-lock::manager.expired'); + return __('filament-resource-lock::manager.expired'); } - return __('resource-lock::manager.active'); + return __('filament-resource-lock::manager.active'); }), ]) ->filters([ @@ -77,16 +77,16 @@ public static function table(Table $table): Tables\Table ->actions([ DeleteAction::make() ->icon('heroicon-o-lock-open') - ->successNotificationTitle(__('resource-lock::manager.unlocked')) - ->label(__('resource-lock::manager.unlock')), + ->successNotificationTitle(__('filament-resource-lock::manager.unlocked')) + ->label(__('filament-resource-lock::manager.unlock')), ]) ->bulkActions([ DeleteBulkAction::make() ->deselectRecordsAfterCompletion() ->requiresConfirmation() ->icon('heroicon-o-lock-open') - ->successNotificationTitle(__('resource-lock::manager.unlocked_selected')) - ->label(__('resource-lock::manager.unlock')), + ->successNotificationTitle(__('filament-resource-lock::manager.unlocked_selected')) + ->label(__('filament-resource-lock::manager.unlock')), ]); } diff --git a/src/Resources/LockResource/ManageResourceLocks.php b/src/Resources/LockResource/ManageResourceLocks.php index e51015e..d09fc29 100644 --- a/src/Resources/LockResource/ManageResourceLocks.php +++ b/src/Resources/LockResource/ManageResourceLocks.php @@ -1,11 +1,11 @@ label(__('resource-lock::manager.unlock_all')) + Action::make(__('filament-resource-lock::manager.unlock_all')) + ->label(__('filament-resource-lock::manager.unlock_all')) ->icon('heroicon-o-lock-open') ->action(fn () => ResourceLockPlugin::get()->getResourceLockModel()::truncate()) ->requiresConfirmation(), diff --git a/src/Resources/Pages/Concerns/UsesLocks.php b/src/Resources/Pages/Concerns/UsesLocks.php index 84f2cee..4a37e61 100644 --- a/src/Resources/Pages/Concerns/UsesLocks.php +++ b/src/Resources/Pages/Concerns/UsesLocks.php @@ -1,6 +1,6 @@ expectsOutput('Removing 2 resource lock(s)...') ->expectsOutput('All resource locks successfully removed.') ->assertExitCode(0); @@ -37,7 +37,7 @@ assertDatabaseCount(ResourceLock::class, 1); // Act - user confirms - artisan('resource-lock:clear') + artisan('filament-filament-resource-lock:clear') ->expectsConfirmation('Are you sure you want to clear all resource locks? This action cannot be undone.', 'yes') ->expectsOutput('Removing 1 resource lock(s)...') ->expectsOutput('All resource locks successfully removed.') @@ -56,7 +56,7 @@ assertDatabaseCount(ResourceLock::class, 1); // Act - user declines - artisan('resource-lock:clear') + artisan('filament-filament-resource-lock:clear') ->expectsConfirmation('Are you sure you want to clear all resource locks? This action cannot be undone.', 'no') ->expectsOutput('Operation cancelled.') ->assertExitCode(0); @@ -70,7 +70,7 @@ assertDatabaseCount(ResourceLock::class, 0); // Act - artisan('resource-lock:clear --force') + artisan('filament-filament-resource-lock:clear --force') ->expectsOutput('No resource locks found to clear.') ->assertExitCode(0); @@ -97,7 +97,7 @@ assertDatabaseCount(ResourceLock::class, 3); // Act - artisan('resource-lock:clear-expired --force') + artisan('filament-filament-resource-lock:clear-expired --force') ->expectsOutput('Removing 2 expired resource lock(s)...') ->expectsOutput('All expired resource locks successfully removed.') ->assertExitCode(0); @@ -120,7 +120,7 @@ assertDatabaseCount(ResourceLock::class, 1); // Act - user confirms - artisan('resource-lock:clear-expired') + artisan('filament-filament-resource-lock:clear-expired') ->expectsConfirmation('Are you sure you want to clear all expired resource locks? This action cannot be undone.', 'yes') ->expectsOutput('Removing 1 expired resource lock(s)...') ->expectsOutput('All expired resource locks successfully removed.') @@ -139,7 +139,7 @@ assertDatabaseCount(ResourceLock::class, 1); // Act - user declines - artisan('resource-lock:clear-expired') + artisan('filament-filament-resource-lock:clear-expired') ->expectsConfirmation('Are you sure you want to clear all expired resource locks? This action cannot be undone.', 'no') ->expectsOutput('Operation cancelled.') ->assertExitCode(0); @@ -157,7 +157,7 @@ assertDatabaseCount(ResourceLock::class, 1); // Act - artisan('resource-lock:clear-expired --force') + artisan('filament-filament-resource-lock:clear-expired --force') ->expectsOutput('No expired resource locks found to clear.') ->assertExitCode(0); @@ -183,7 +183,7 @@ assertDatabaseCount(ResourceLock::class, 4); // Act - artisan('resource-lock:clear-expired --force') + artisan('filament-filament-resource-lock:clear-expired --force') ->expectsOutput('Removing 2 expired resource lock(s)...') ->expectsOutput('All expired resource locks successfully removed.') ->assertExitCode(0); @@ -204,7 +204,7 @@ assertDatabaseCount(ResourceLock::class, 0); // Act - artisan('resource-lock:clear-expired --force') + artisan('filament-filament-resource-lock:clear-expired --force') ->expectsOutput('No expired resource locks found to clear.') ->assertExitCode(0); diff --git a/tests/Feature/ResourceLockObserverTest.php b/tests/Feature/ResourceLockObserverTest.php index 3fc68dc..96d5027 100644 --- a/tests/Feature/ResourceLockObserverTest.php +++ b/tests/Feature/ResourceLockObserverTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use Kenepa\ResourceLock\Http\Livewire\ResourceLockObserver; -use Kenepa\ResourceLock\ResourceLockPlugin; +use Blendbyte\FilamentResourceLock\Http\Livewire\ResourceLockObserver; +use Blendbyte\FilamentResourceLock\ResourceLockPlugin; use Livewire\Livewire; describe('ResourceLockObserver Keep-Alive Configuration', function () { diff --git a/tests/Filament/Resources/LockResource/LockResourceTest.php b/tests/Filament/Resources/LockResource/LockResourceTest.php index cb9a720..75f929a 100644 --- a/tests/Filament/Resources/LockResource/LockResourceTest.php +++ b/tests/Filament/Resources/LockResource/LockResourceTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use Kenepa\ResourceLock\Resources\LockResource\ManageResourceLocks; +use Blendbyte\FilamentResourceLock\Resources\LockResource\ManageResourceLocks; use function Pest\Livewire\livewire; diff --git a/tests/Fixtures/AdminPanelProvider.php b/tests/Fixtures/AdminPanelProvider.php index aa86170..12a072d 100644 --- a/tests/Fixtures/AdminPanelProvider.php +++ b/tests/Fixtures/AdminPanelProvider.php @@ -1,6 +1,6 @@ in(__DIR__); @@ -37,7 +37,7 @@ function createExpiredResourceLock(User $user, Post $post): ResourceLock $resourceLock = (new ResourceLock)->forceFill([ 'updated_at' => Carbon::now()->subMinutes(30), 'user_id' => $user->id, - 'lockable_type' => 'Kenepa\ResourceLock\Tests\Resources\Models\Post', + 'lockable_type' => 'Blendbyte\FilamentResourceLock\Tests\Resources\Models\Post', 'lockable_id' => $post->id, ]); @@ -51,7 +51,7 @@ function createActiveResourceLock($user, $post): ResourceLock $resourceLock = (new ResourceLock)->forceFill([ 'updated_at' => Carbon::now(), 'user_id' => $user->id, - 'lockable_type' => 'Kenepa\ResourceLock\Tests\Resources\Models\Post', + 'lockable_type' => 'Blendbyte\FilamentResourceLock\Tests\Resources\Models\Post', 'lockable_id' => $post->id, ]); diff --git a/tests/Resources/Models/Post.php b/tests/Resources/Models/Post.php index 2292173..8dd63c8 100644 --- a/tests/Resources/Models/Post.php +++ b/tests/Resources/Models/Post.php @@ -1,10 +1,10 @@ 'Kenepa\\ResourceLock\\Database\\Factories\\' . class_basename($modelName) . 'Factory' + fn (string $modelName) => 'Blendbyte\\FilamentResourceLock\\Database\\Factories\\' . class_basename($modelName) . 'Factory' ); } @@ -39,7 +39,7 @@ public function getEnvironmentSetUp($app) 'database' => ':memory:', 'prefix' => '', ]); - config()->set('resource-lock.models.User', '\Kenepa\ResourceLock\Tests\Resources\Models\User'); + config()->set('filament-resource-lock.models.User', '\Blendbyte\FilamentResourceLock\Tests\Resources\Models\User'); config()->set('app.key', '6rE9Nz59bGRbeMATftriyQjrpF7DcOQm'); $migration = include __DIR__ . '/../database/migrations/create_resource_lock_table.php.stub'; diff --git a/tests/Unit/modelHasLockTest.php b/tests/Unit/modelHasLockTest.php index abb717f..b26928b 100644 --- a/tests/Unit/modelHasLockTest.php +++ b/tests/Unit/modelHasLockTest.php @@ -1,7 +1,7 @@ Date: Tue, 24 Mar 2026 10:11:48 +0800 Subject: [PATCH 02/17] Upgrade dependencies to Filament 5, Laravel 12/13, Livewire 4 - Bump filament/filament to ^5.0, illuminate/contracts to ^11.28|^12.0|^13.0 - Bump nunomaduro/larastan to ^3.0, orchestra/testbench to ^9.0|^10.0|^11.0 - Bump pestphp/pest to ^3.7 and related plugins, tightenco/duster to ^3.1 - Drop spatie/laravel-ray and composer/package-versions-deprecated - Replace $this->listeners boot methods with #[On] attributes for Livewire 4 - Remove BladeCaptureDirectiveServiceProvider dropped by Filament 5 - Clean up phpunit.xml.dist for PHPUnit 11 (removed deprecated attributes) --- composer.json | 26 +++++++++---------- phpunit.xml.dist | 12 ++------- src/Resources/Pages/Concerns/UsesLocks.php | 3 +++ .../Pages/Concerns/UsesResourceLock.php | 24 +++-------------- .../Pages/Concerns/UsesSimpleResourceLock.php | 13 +++------- tests/TestCase.php | 2 -- 6 files changed, 24 insertions(+), 56 deletions(-) diff --git a/composer.json b/composer.json index a0e9906..88f4329 100644 --- a/composer.json +++ b/composer.json @@ -10,23 +10,22 @@ "license": "MIT", "require": { "php": "^8.2", - "filament/filament": "^4.0", - "spatie/laravel-package-tools": "^1.15.0", - "illuminate/contracts": "^9.0|^10.0|^11.0|^12.0" + "filament/filament": "^5.0", + "spatie/laravel-package-tools": "^1.16.0", + "illuminate/contracts": "^11.28|^12.0|^13.0" }, "require-dev": { "laravel/pint": "^1.0", - "nunomaduro/collision": "^7.0|^8.1", - "nunomaduro/larastan": "^2.0.1", - "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", - "pestphp/pest": "^2.0|^3.7", - "pestphp/pest-plugin-laravel": "^2.0|^3.1", - "pestphp/pest-plugin-livewire": "^2.0|^3.0", + "nunomaduro/collision": "^8.1", + "nunomaduro/larastan": "^3.0", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^3.7", + "pestphp/pest-plugin-laravel": "^3.1", + "pestphp/pest-plugin-livewire": "^3.0", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", - "phpstan/phpstan-phpunit": "^1.0|^2.0", - "spatie/laravel-ray": "^1.26", - "tightenco/duster": "^1.1|^3.1" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "tightenco/duster": "^3.1" }, "autoload": { "psr-4": { @@ -51,7 +50,6 @@ "config": { "sort-packages": true, "allow-plugins": { - "composer/package-versions-deprecated": true, "pestphp/pest-plugin": true, "phpstan/extension-installer": true } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 92b89cc..67de6f6 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,24 +2,16 @@ - + tests @@ -36,4 +28,4 @@ - \ No newline at end of file + diff --git a/src/Resources/Pages/Concerns/UsesLocks.php b/src/Resources/Pages/Concerns/UsesLocks.php index 4a37e61..df678a8 100644 --- a/src/Resources/Pages/Concerns/UsesLocks.php +++ b/src/Resources/Pages/Concerns/UsesLocks.php @@ -2,6 +2,8 @@ namespace Blendbyte\FilamentResourceLock\Resources\Pages\Concerns; +use Livewire\Attributes\On; + /** * This trait provides common methods used by both UsesResourceLock and * UsesSimpleResourceLock traits, offering core functionality for managing @@ -81,6 +83,7 @@ public function disablePolling() $this->dispatch('disablePollingInResourceLockObserver'); } + #[On('resourceLockObserver::renewLock')] public function renewLock() { $record = $this->record ?? $this->resourceRecord; diff --git a/src/Resources/Pages/Concerns/UsesResourceLock.php b/src/Resources/Pages/Concerns/UsesResourceLock.php index 313311b..f421811 100644 --- a/src/Resources/Pages/Concerns/UsesResourceLock.php +++ b/src/Resources/Pages/Concerns/UsesResourceLock.php @@ -3,6 +3,7 @@ namespace Blendbyte\FilamentResourceLock\Resources\Pages\Concerns; use Blendbyte\FilamentResourceLock\ResourceLockPlugin; +use Livewire\Attributes\On; /* * The Resource Lock Trait provides several functions to an Edit Resource page to lock & unlock resources. @@ -19,26 +20,7 @@ trait UsesResourceLock private bool $isLockable = true; - /* - * Initializes livewire event listeners on boot. This function uses livewire lifecycle hooks - * to hook into lifecycle events of the livewire component that uses this trait - * learn more: https://laravel-livewire.com/docs/2.x/traits - */ - public function bootUsesResourceLock(): void - { - $this->listeners = array_merge($this->listeners, [ - 'resourceLockObserver::init' => 'resourceLockObserverInit', - 'resourceLockObserver::unload' => 'resourceLockObserverUnload', - 'resourceLockObserver::unlock' => 'resourceLockObserverUnlock', - 'resourceLockObserver::renewLock' => 'renewLock', - ]); - } - - /* - * This function is triggered when the resource lock observer component has been loaded. - * The resource lock observer is a livewire component that triggers function based - * on certain states and events that are happening on the page. - */ + #[On('resourceLockObserver::init')] public function resourceLockObserverInit() { $this->returnUrl = $this->getResource()::getUrl('index'); @@ -46,6 +28,7 @@ public function resourceLockObserverInit() $this->setupPolling(); } + #[On('resourceLockObserver::unload')] public function resourceLockObserverUnload() { $this->record->unlock(); @@ -56,6 +39,7 @@ public function resourceLockObserverUnload() * presented to the users. This action is seen as forced unlock and will replace any lock * That is currently in place for that specific resource. Hone this power with care. */ + #[On('resourceLockObserver::unlock')] public function resourceLockObserverUnlock() { if ($this->record->unlock(force: true)) { diff --git a/src/Resources/Pages/Concerns/UsesSimpleResourceLock.php b/src/Resources/Pages/Concerns/UsesSimpleResourceLock.php index 848f110..c42c7a8 100644 --- a/src/Resources/Pages/Concerns/UsesSimpleResourceLock.php +++ b/src/Resources/Pages/Concerns/UsesSimpleResourceLock.php @@ -3,6 +3,7 @@ namespace Blendbyte\FilamentResourceLock\Resources\Pages\Concerns; use Blendbyte\FilamentResourceLock\ResourceLockPlugin; +use Livewire\Attributes\On; trait UsesSimpleResourceLock { @@ -16,16 +17,6 @@ trait UsesSimpleResourceLock private bool $isLockable = true; - public function bootUsesSimpleResourceLock(): void - { - $this->listeners = array_merge($this->listeners, [ - 'resourceLockObserver::init' => 'resourceLockObserverInit', - 'resourceLockObserver::unload' => 'resourceLockObserverUnload', - 'resourceLockObserver::unlock' => 'resourceLockObserverUnlock', - 'resourceLockObserver::renewLock' => 'renewLock', - ]); - } - public function mountTableAction(string $name, ?string $record = null, array $arguments = []): mixed { parent::mountTableAction($name, $record); @@ -54,12 +45,14 @@ public function callMountedTableAction(array $arguments = []): mixed return null; } + #[On('resourceLockObserver::unload')] public function resourceLockObserverUnload() { $this->resourceRecord->unlock(); $this->disablePolling(); } + #[On('resourceLockObserver::unlock')] public function resourceLockObserverUnlock() { if ($this->resourceRecord->unlock(force: true)) { diff --git a/tests/TestCase.php b/tests/TestCase.php index 1bc4dbb..f2a051c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -18,7 +18,6 @@ use Blendbyte\FilamentResourceLock\Tests\Fixtures\AdminPanelProvider; use Livewire\LivewireServiceProvider; use Orchestra\Testbench\TestCase as Orchestra; -use RyanChandler\BladeCaptureDirective\BladeCaptureDirectiveServiceProvider; class TestCase extends Orchestra { @@ -58,7 +57,6 @@ protected function getPackageProviders($app) { $providers = [ ActionsServiceProvider::class, - BladeCaptureDirectiveServiceProvider::class, BladeHeroiconsServiceProvider::class, BladeIconsServiceProvider::class, FilamentServiceProvider::class, From 4e7f190d7980deea321e95822fba180bda001fae Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 24 Mar 2026 10:12:46 +0800 Subject: [PATCH 03/17] wip --- .github/CONTRIBUTING.MD | 55 -------------------------- .github/workflows/run-duster.yml | 14 ------- .github/workflows/run-tests.yml | 24 ----------- .github/workflows/update-changelog.yml | 28 ------------- CHANGELOG.md | 7 ---- 5 files changed, 128 deletions(-) delete mode 100644 .github/CONTRIBUTING.MD delete mode 100644 .github/workflows/run-duster.yml delete mode 100644 .github/workflows/run-tests.yml delete mode 100644 .github/workflows/update-changelog.yml delete mode 100644 CHANGELOG.md diff --git a/.github/CONTRIBUTING.MD b/.github/CONTRIBUTING.MD deleted file mode 100644 index 937d029..0000000 --- a/.github/CONTRIBUTING.MD +++ /dev/null @@ -1,55 +0,0 @@ -# Contributing - -Contributions are **welcome** and will be fully **credited**. - -Please read and understand the contribution guide before creating an issue or pull request. - -## Etiquette - -This project is open source, and as such, the maintainers give their free time to build and maintain the source code -held within. They make the code freely available in the hope that it will be of use to other developers. It would be -extremely unfair for them to suffer abuse or anger for their hard work. - -Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the -world that developers are civilized and selfless people. - -It's the duty of the maintainer to ensure that all submissions to the project are of sufficient -quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. - -## Viability - -When requesting or submitting new features, first consider whether it might be useful to others. Open -source projects are used by many developers, who may have entirely different needs to your own. Think about -whether or not your feature is likely to be used by other users of the project. - -## Procedure - -Before filing an issue: - -- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. -- Check to make sure your feature suggestion isn't already present within the project. -- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. -- Check the pull requests tab to ensure that the feature isn't already in progress. - -Before submitting a pull request: - -- Check the codebase to ensure that your feature doesn't already exist. -- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. - -## Requirements - -If the project maintainer has any additional requirements, you will find them listed here. - -- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). - -- **Add tests!** - Your patch won't be accepted if it doesn't have tests. - -- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. - -- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. - -- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. - -- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. - -**Happy coding**! \ No newline at end of file diff --git a/.github/workflows/run-duster.yml b/.github/workflows/run-duster.yml deleted file mode 100644 index 5f50234..0000000 --- a/.github/workflows/run-duster.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Run Duster - -on: - push: - branches: [ main ] - pull_request: - -jobs: - duster: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: "Duster" - uses: tighten/duster-action@v1 \ No newline at end of file diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml deleted file mode 100644 index 78bbd55..0000000 --- a/.github/workflows/run-tests.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Tests - -on: ['push', 'pull_request'] - -jobs: - ci: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 8.2 - tools: composer:v2 - coverage: xdebug - - - name: Install Dependencies - run: composer install --no-interaction --prefer-dist --optimize-autoloader - - - name: Tests - run: ./vendor/bin/pest \ No newline at end of file diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml deleted file mode 100644 index 7787ca1..0000000 --- a/.github/workflows/update-changelog.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: "Update Changelog" - -on: - release: - types: [released] - -jobs: - update: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - ref: main - - - name: Update Changelog - uses: stefanzweifel/changelog-updater-action@v1 - with: - latest-version: ${{ github.event.release.name }} - release-notes: ${{ github.event.release.body }} - - - name: Commit updated CHANGELOG - uses: stefanzweifel/git-auto-commit-action@v4 - with: - branch: main - commit_message: Update CHANGELOG - file_pattern: CHANGELOG.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e5cd3ef..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# Changelog - -All notable changes to `ResourceLock` will be documented in this file. - -## 1.0.0 - 202X-XX-XX - -- initial release From 52f4b3abc9ad733f56c056b0bd967ea8d15be163 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 24 Mar 2026 10:17:06 +0800 Subject: [PATCH 04/17] Remove unused JS build toolchain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No custom CSS or JS to compile — the plugin uses only inline styles/scripts and Filament's own components. Filament 5 handles its own Tailwind compilation. --- .prettierrc | 5 ----- package.json | 22 ---------------------- postcss.config.js | 6 ------ resources/dist/.gitkeep | 0 tailwind.config.js | 21 --------------------- 5 files changed, 54 deletions(-) delete mode 100644 .prettierrc delete mode 100644 package.json delete mode 100644 postcss.config.js delete mode 100644 resources/dist/.gitkeep delete mode 100644 tailwind.config.js diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 98406c6..0000000 --- a/.prettierrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "semi": false, - "singleQuote": true, - "trailingComma": "all" -} diff --git a/package.json b/package.json deleted file mode 100644 index f8ebddd..0000000 --- a/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "private": true, - "scripts": { - "dev:styles": "npx tailwindcss -i resources/css/plugin.css -o resources/dist/resourcelock.css --postcss --watch", - "dev:scripts": "esbuild resources/js/plugin.js --bundle --sourcemap=inline --outfile=resources/dist/resourcelock.js --watch", - "build:styles": "npx tailwindcss -i resources/css/plugin.css -o resources/dist/resourcelock.css --postcss --minify && npm run purge", - "build:scripts": "esbuild resources/js/plugin.js --bundle --minify --outfile=resources/dist/resourcelock.js", - "purge": "filament-purge -i resources/dist/resourcelock.css -o resources/dist/resourcelock.css", - "dev": "npm-run-all --parallel dev:*", - "build": "npm-run-all build:*" - }, - "devDependencies": { - "@awcodes/filament-plugin-purge": "^1.0.2", - "autoprefixer": "^10.4.7", - "esbuild": "^0.8.57", - "npm-run-all": "^4.1.5", - "postcss": "^8.4.14", - "prettier": "^2.7.1", - "prettier-plugin-tailwindcss": "^0.1.13", - "tailwindcss": "^3.1.6" - } -} diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index fef1b22..0000000 --- a/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/resources/dist/.gitkeep b/resources/dist/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index 86a17ee..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,21 +0,0 @@ -const colors = require('tailwindcss/colors') - -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: ['./resources/views/**/*.blade.php', './src/**/*.php'], - darkMode: 'class', - theme: { - extend: { - colors: { - danger: colors.rose, - primary: colors.amber, - success: colors.green, - warning: colors.amber, - }, - }, - }, - corePlugins: { - preflight: false, - }, - plugins: [], -} From 55dd4f8620a2842f091b073b0a1c9c69a96919c5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 24 Mar 2026 10:26:24 +0800 Subject: [PATCH 05/17] Fix test suite for Livewire 4 and clean up config - Fix doubled command name in ResourceLockCommandTest - Replace Pest\Livewire\livewire() with Livewire::test() for Livewire 4 - Drop pestphp/pest-plugin-livewire (requires Livewire 3) - Migrate phpunit.xml.dist to current PHPUnit schema - Remove dead CHANGELOG.md link from README --- README.md | 6 +-- composer.json | 1 - phpunit.xml.dist | 50 ++++++++----------- tests/Feature/ResourceLockCommandTest.php | 20 ++++---- .../LockResource/LockResourceTest.php | 7 ++- 5 files changed, 35 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index b065a57..6f8cbc2 100644 --- a/README.md +++ b/README.md @@ -166,13 +166,9 @@ php artisan migrate php artisan vendor:publish --tag="filament-resource-lock-views" ``` -## Changelog - -Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. - ## Contributing -Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. +Please see [GitHub releases](https://github.com/blendbyte/filament-resource-lock/releases) for changelog information. ## License diff --git a/composer.json b/composer.json index 88f4329..3c134e8 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,6 @@ "orchestra/testbench": "^9.0|^10.0|^11.0", "pestphp/pest": "^3.7", "pestphp/pest-plugin-laravel": "^3.1", - "pestphp/pest-plugin-livewire": "^3.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-phpunit": "^2.0", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 67de6f6..eff3c3b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,31 +1,23 @@ - - - - tests - - - - - ./src - - - - - - - - - - + + + + tests + + + + + + + + + + + + + + + ./src + + diff --git a/tests/Feature/ResourceLockCommandTest.php b/tests/Feature/ResourceLockCommandTest.php index c4d2648..ec463ea 100644 --- a/tests/Feature/ResourceLockCommandTest.php +++ b/tests/Feature/ResourceLockCommandTest.php @@ -19,7 +19,7 @@ assertDatabaseCount(ResourceLock::class, 2); // Act - artisan('filament-filament-resource-lock:clear --force') + artisan('filament-resource-lock:clear --force') ->expectsOutput('Removing 2 resource lock(s)...') ->expectsOutput('All resource locks successfully removed.') ->assertExitCode(0); @@ -37,7 +37,7 @@ assertDatabaseCount(ResourceLock::class, 1); // Act - user confirms - artisan('filament-filament-resource-lock:clear') + artisan('filament-resource-lock:clear') ->expectsConfirmation('Are you sure you want to clear all resource locks? This action cannot be undone.', 'yes') ->expectsOutput('Removing 1 resource lock(s)...') ->expectsOutput('All resource locks successfully removed.') @@ -56,7 +56,7 @@ assertDatabaseCount(ResourceLock::class, 1); // Act - user declines - artisan('filament-filament-resource-lock:clear') + artisan('filament-resource-lock:clear') ->expectsConfirmation('Are you sure you want to clear all resource locks? This action cannot be undone.', 'no') ->expectsOutput('Operation cancelled.') ->assertExitCode(0); @@ -70,7 +70,7 @@ assertDatabaseCount(ResourceLock::class, 0); // Act - artisan('filament-filament-resource-lock:clear --force') + artisan('filament-resource-lock:clear --force') ->expectsOutput('No resource locks found to clear.') ->assertExitCode(0); @@ -97,7 +97,7 @@ assertDatabaseCount(ResourceLock::class, 3); // Act - artisan('filament-filament-resource-lock:clear-expired --force') + artisan('filament-resource-lock:clear-expired --force') ->expectsOutput('Removing 2 expired resource lock(s)...') ->expectsOutput('All expired resource locks successfully removed.') ->assertExitCode(0); @@ -120,7 +120,7 @@ assertDatabaseCount(ResourceLock::class, 1); // Act - user confirms - artisan('filament-filament-resource-lock:clear-expired') + artisan('filament-resource-lock:clear-expired') ->expectsConfirmation('Are you sure you want to clear all expired resource locks? This action cannot be undone.', 'yes') ->expectsOutput('Removing 1 expired resource lock(s)...') ->expectsOutput('All expired resource locks successfully removed.') @@ -139,7 +139,7 @@ assertDatabaseCount(ResourceLock::class, 1); // Act - user declines - artisan('filament-filament-resource-lock:clear-expired') + artisan('filament-resource-lock:clear-expired') ->expectsConfirmation('Are you sure you want to clear all expired resource locks? This action cannot be undone.', 'no') ->expectsOutput('Operation cancelled.') ->assertExitCode(0); @@ -157,7 +157,7 @@ assertDatabaseCount(ResourceLock::class, 1); // Act - artisan('filament-filament-resource-lock:clear-expired --force') + artisan('filament-resource-lock:clear-expired --force') ->expectsOutput('No expired resource locks found to clear.') ->assertExitCode(0); @@ -183,7 +183,7 @@ assertDatabaseCount(ResourceLock::class, 4); // Act - artisan('filament-filament-resource-lock:clear-expired --force') + artisan('filament-resource-lock:clear-expired --force') ->expectsOutput('Removing 2 expired resource lock(s)...') ->expectsOutput('All expired resource locks successfully removed.') ->assertExitCode(0); @@ -204,7 +204,7 @@ assertDatabaseCount(ResourceLock::class, 0); // Act - artisan('filament-filament-resource-lock:clear-expired --force') + artisan('filament-resource-lock:clear-expired --force') ->expectsOutput('No expired resource locks found to clear.') ->assertExitCode(0); diff --git a/tests/Filament/Resources/LockResource/LockResourceTest.php b/tests/Filament/Resources/LockResource/LockResourceTest.php index 75f929a..9122217 100644 --- a/tests/Filament/Resources/LockResource/LockResourceTest.php +++ b/tests/Filament/Resources/LockResource/LockResourceTest.php @@ -3,15 +3,14 @@ declare(strict_types=1); use Blendbyte\FilamentResourceLock\Resources\LockResource\ManageResourceLocks; - -use function Pest\Livewire\livewire; +use Livewire\Livewire; it('can render lock resource index page', function () { - livewire(ManageResourceLocks::class) + Livewire::test(ManageResourceLocks::class) ->assertSuccessful(); }); it('can render the unlock all resources button', function () { - livewire(ManageResourceLocks::class) + Livewire::test(ManageResourceLocks::class) ->assertSee('Unlock all resources'); }); From af2c3eb708f66a0bfe8da38c601c499de8eaa665 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 24 Mar 2026 10:29:53 +0800 Subject: [PATCH 06/17] Add Brazilian Portuguese (pt_BR) translations --- resources/lang/pt_BR/manager.php | 10 ++++++++++ resources/lang/pt_BR/modal.php | 8 ++++++++ 2 files changed, 18 insertions(+) create mode 100644 resources/lang/pt_BR/manager.php create mode 100644 resources/lang/pt_BR/modal.php diff --git a/resources/lang/pt_BR/manager.php b/resources/lang/pt_BR/manager.php new file mode 100644 index 0000000..5be2d2e --- /dev/null +++ b/resources/lang/pt_BR/manager.php @@ -0,0 +1,10 @@ + 'Ativo', + 'expired' => 'Expirado', + 'unlock' => 'Desbloquear', + 'unlocked' => 'Recurso desbloqueado', + 'unlocked_selected' => 'Recursos selecionados desbloqueados', + 'unlock_all' => 'Desbloquear todos os recursos', +]; diff --git a/resources/lang/pt_BR/modal.php b/resources/lang/pt_BR/modal.php new file mode 100644 index 0000000..8db5336 --- /dev/null +++ b/resources/lang/pt_BR/modal.php @@ -0,0 +1,8 @@ + 'Esta página foi bloqueada porque outro usuário está editando-a no momento.', + 'locked_notice_user' => 'está editando esta página no momento, portanto ela está bloqueada para edição.', + 'unlock_button' => 'Desbloquear página', + 'return_button' => 'Voltar', +]; From 41f89ab7472b5df1695a0e71551a70384a84b975 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 24 Mar 2026 10:38:39 +0800 Subject: [PATCH 07/17] Add relation manager locking support (PR #52) and fix Livewire 4 event dispatch --- README.md | 17 ++++ .../resource-lock-observer.blade.php | 4 +- .../UsesRelationManagerResourceLock.php | 84 +++++++++++++++++++ .../Pages/Concerns/UsesResourceLock.php | 9 +- .../Pages/Concerns/UsesSimpleResourceLock.php | 2 +- 5 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 src/Resources/Pages/Concerns/UsesRelationManagerResourceLock.php diff --git a/README.md b/README.md index 6f8cbc2..280abaf 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,23 @@ class ManagePosts extends ManageRecords } ``` +### Relation manager locking + +To lock related records when editing them via a relation manager, add `UsesRelationManagerResourceLock` to your relation manager class. The related model also needs the `HasLocks` trait. + +```php +use Blendbyte\FilamentResourceLock\Resources\Pages\Concerns\UsesRelationManagerResourceLock; + +class PostCommentsRelationManager extends RelationManager +{ + use UsesRelationManagerResourceLock; + + protected static string $relationship = 'comments'; +} +``` + +When a user opens the edit modal for a related record, it is locked for the duration of the edit session and released when the modal is closed. + ## Polling (SPA mode) To support SPA mode, enable polling-based presence detection in the plugin: diff --git a/resources/views/components/resource-lock-observer.blade.php b/resources/views/components/resource-lock-observer.blade.php index c5455b7..34b8b94 100644 --- a/resources/views/components/resource-lock-observer.blade.php +++ b/resources/views/components/resource-lock-observer.blade.php @@ -12,7 +12,7 @@ function trackModalContainers() { ['modal-closed'].forEach(eventType => { container.addEventListener(eventType, event => { if (event.detail.id.endsWith('-table-action')) { - Livewire.dispatch('resourceLockObserver::unload') + Livewire.dispatch('resourceLockObserver::unloadSimple') } }); }); @@ -43,7 +43,7 @@ function startObserving() { window.addEventListener('close-modal', event => { if (event.detail.id.endsWith('-table-action')) { - Livewire.dispatch('resourceLockObserver::unload') + Livewire.dispatch('resourceLockObserver::unloadSimple') } }); diff --git a/src/Resources/Pages/Concerns/UsesRelationManagerResourceLock.php b/src/Resources/Pages/Concerns/UsesRelationManagerResourceLock.php new file mode 100644 index 0000000..cc972c3 --- /dev/null +++ b/src/Resources/Pages/Concerns/UsesRelationManagerResourceLock.php @@ -0,0 +1,84 @@ +parentClass = get_class($this->getRelationship()->getParent()); + $this->relatedClass = get_class($this->getRelationship()->getRelated()); + } + + #[On('resourceLockObserver::unlock')] + public function resourceLockObserverUnlock() + { + if ($this->relatedRecord) { + if ($this->relatedRecord->unlock(force: true)) { + $this->closeLockedResourceModal(); + $this->relatedRecord->refresh(); + $this->relatedRecord->lock(); + } + } else { + $ownerRecord = $this->getOwnerRecord(); + if ($ownerRecord->unlock(force: true)) { + $this->closeLockedResourceModal(); + $ownerRecord->refresh(); + $ownerRecord->lock(); + } + } + } + + public function mountTableAction(string $name, ?string $record = null, array $arguments = []): mixed + { + parent::mountTableAction($name, $record); + + if ($name === 'edit') { + $this->relatedRecord = $this->relatedClass::find($record); + $this->checkIfResourceLockHasExpired($this->relatedRecord); + $this->lockResource($this->relatedRecord); + } + + return null; + } + + public function unmountTableAction(bool $shouldCancelParentActions = true): void + { + if ($this->mountedTableActionRecord) { + $this->relatedRecord = $this->relatedClass::find($this->mountedTableActionRecord); + $this->relatedRecord->unlock(); + } + + parent::unmountTableAction($shouldCancelParentActions); + } + + public function resourceLockReturnUrl() + { + $parentResource = filament()->getCurrentPanel()->getModelResource( + $this->getRelationship()->getParent() + ); + + return $parentResource::getUrl('edit', ['record' => $this->getOwnerRecord()->id]); + } + + public function getResourceLockOwner(): void + { + if ($this->relatedRecord?->resourceLock && ResourceLockPlugin::get()->shouldDisplayResourceLockOwner()) { + $getResourceLockOwnerActionClass = ResourceLockPlugin::get()->getResourceLockOwnerAction(); + $getResourceLockOwnerAction = app($getResourceLockOwnerActionClass); + + $this->resourceLockOwner = $getResourceLockOwnerAction->execute($this->relatedRecord->resourceLock->user); + } + } +} diff --git a/src/Resources/Pages/Concerns/UsesResourceLock.php b/src/Resources/Pages/Concerns/UsesResourceLock.php index f421811..a8e853e 100644 --- a/src/Resources/Pages/Concerns/UsesResourceLock.php +++ b/src/Resources/Pages/Concerns/UsesResourceLock.php @@ -42,9 +42,12 @@ public function resourceLockObserverUnload() #[On('resourceLockObserver::unlock')] public function resourceLockObserverUnlock() { - if ($this->record->unlock(force: true)) { - $this->closeLockedResourceModal(); - $this->record->lock(); + if (is_null($this->activeRelationManager ?? null)) { + if ($this->record->unlock(force: true)) { + $this->closeLockedResourceModal(); + $this->record->refresh(); + $this->record->lock(); + } } } diff --git a/src/Resources/Pages/Concerns/UsesSimpleResourceLock.php b/src/Resources/Pages/Concerns/UsesSimpleResourceLock.php index c42c7a8..b9ab2e3 100644 --- a/src/Resources/Pages/Concerns/UsesSimpleResourceLock.php +++ b/src/Resources/Pages/Concerns/UsesSimpleResourceLock.php @@ -45,7 +45,7 @@ public function callMountedTableAction(array $arguments = []): mixed return null; } - #[On('resourceLockObserver::unload')] + #[On('resourceLockObserver::unloadSimple')] public function resourceLockObserverUnload() { $this->resourceRecord->unlock(); From 9c0b2bb0f19a47386c67dc1d5bf7eb93a254f144 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 24 Mar 2026 10:40:22 +0800 Subject: [PATCH 08/17] Update larastan package name from nunomaduro/larastan to larastan/larastan --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3c134e8..e139007 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "require-dev": { "laravel/pint": "^1.0", "nunomaduro/collision": "^8.1", - "nunomaduro/larastan": "^3.0", + "larastan/larastan": "^3.0", "orchestra/testbench": "^9.0|^10.0|^11.0", "pestphp/pest": "^3.7", "pestphp/pest-plugin-laravel": "^3.1", From f02fb53429ef0bfebb32d643390bea67a39330f4 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 24 Mar 2026 10:54:22 +0800 Subject: [PATCH 09/17] Fix type errors and null safety bugs - Fix ?object property types for userModel and resourceLockModel (should be ?string) - Guard Gate::allows() calls against null gate in LockResource and ResourceLockObserver - Fix duplicate updated_at column key in LockResource table; use virtual lock_status column for Expired badge --- src/Http/Livewire/ResourceLockObserver.php | 7 +++++-- src/ResourceLockPlugin.php | 4 ++-- src/Resources/LockResource.php | 11 ++++++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Http/Livewire/ResourceLockObserver.php b/src/Http/Livewire/ResourceLockObserver.php index a39f045..c48bfb0 100644 --- a/src/Http/Livewire/ResourceLockObserver.php +++ b/src/Http/Livewire/ResourceLockObserver.php @@ -28,8 +28,11 @@ public function mount() { if (! ResourceLockPlugin::get()->shouldLimitUnlockerAccess()) { $this->isAllowedToUnlock = true; - } elseif (ResourceLockPlugin::get()->shouldLimitUnlockerAccess() && Gate::allows(ResourceLockPlugin::get()->getUnlockerGate())) { - $this->isAllowedToUnlock = true; + } else { + $gate = ResourceLockPlugin::get()->getUnlockerGate(); + if ($gate !== null && Gate::allows($gate)) { + $this->isAllowedToUnlock = true; + } } } diff --git a/src/ResourceLockPlugin.php b/src/ResourceLockPlugin.php index 85069aa..53c47f2 100644 --- a/src/ResourceLockPlugin.php +++ b/src/ResourceLockPlugin.php @@ -39,9 +39,9 @@ class ResourceLockPlugin implements Plugin protected ?string $resourceClass = null; - protected ?object $userModel = null; + protected ?string $userModel = null; - protected ?object $resourceLockModel = null; + protected ?string $resourceLockModel = null; protected ?int $lockTimeout = null; diff --git a/src/Resources/LockResource.php b/src/Resources/LockResource.php index e8bb1b4..8af123b 100644 --- a/src/Resources/LockResource.php +++ b/src/Resources/LockResource.php @@ -48,7 +48,8 @@ public static function table(Table $table): Tables\Table TextColumn::make('lockable_type')->label(__('Lockable type')), TextColumn::make('created_at')->label(__('Created at')), TextColumn::make('updated_at')->label(__('Updated at')), - TextColumn::make('updated_at')->label(__('Expired')) + TextColumn::make('lock_status')->label(__('Expired')) + ->state(fn ($record) => $record->isExpired()) ->badge() ->color(static function ($record): string { if ($record->isExpired()) { @@ -100,7 +101,9 @@ public static function getPages(): array public static function canViewAny(): bool { if (ResourceLockPlugin::get()->shouldLimitAccessToResourceLockManager()) { - return Gate::allows(ResourceLockPlugin::get()->getGate()); + $gate = ResourceLockPlugin::get()->getGate(); + + return $gate !== null && Gate::allows($gate); } return true; @@ -109,7 +112,9 @@ public static function canViewAny(): bool public static function canDeleteAny(): bool { if (ResourceLockPlugin::get()->shouldLimitAccessToResourceLockManager()) { - return Gate::allows(ResourceLockPlugin::get()->getGate()); + $gate = ResourceLockPlugin::get()->getGate(); + + return $gate !== null && Gate::allows($gate); } return true; From b3327b16a9fbf7d886eb1485288bab4612a44258 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 24 Mar 2026 10:58:10 +0800 Subject: [PATCH 10/17] Add GitHub Actions workflow to run Pest tests on push and PR --- .github/workflows/tests.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..df836c9 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,26 @@ +name: Tests + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite + coverage: none + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: Run tests + run: vendor/bin/pest --parallel From 529504aed256fdcb906e66133697f300046f6d12 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 24 Mar 2026 11:07:48 +0800 Subject: [PATCH 11/17] Fix Livewire ComponentNotFoundException by registering component in service provider Move Livewire::component() registration from the Filament plugin's boot() to ResourceLockServiceProvider::packageBooted() so the component alias is always registered, including during Livewire AJAX requests where the panel may not boot. --- src/ResourceLockPlugin.php | 3 --- src/ResourceLockServiceProvider.php | 8 ++++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ResourceLockPlugin.php b/src/ResourceLockPlugin.php index 53c47f2..7583836 100644 --- a/src/ResourceLockPlugin.php +++ b/src/ResourceLockPlugin.php @@ -9,7 +9,6 @@ use Illuminate\Support\Facades\Blade; use Blendbyte\FilamentResourceLock\Models\ResourceLock; use Blendbyte\FilamentResourceLock\Resources\LockResource; -use Livewire\Livewire; class ResourceLockPlugin implements Plugin { @@ -85,8 +84,6 @@ public function register(Panel $panel): void public function boot(Panel $panel): void { - Livewire::component('filament-resource-lock-observer', Http\Livewire\ResourceLockObserver::class); - FilamentView::registerRenderHook( PanelsRenderHook::PAGE_START, fn (): string => Blade::render('@livewire(\'filament-resource-lock-observer\')'), diff --git a/src/ResourceLockServiceProvider.php b/src/ResourceLockServiceProvider.php index 54f9b80..49f58ea 100644 --- a/src/ResourceLockServiceProvider.php +++ b/src/ResourceLockServiceProvider.php @@ -4,6 +4,7 @@ use Blendbyte\FilamentResourceLock\Console\Commands\ResourceLockClearCommand; use Blendbyte\FilamentResourceLock\Console\Commands\ResourceLockClearExpiredCommand; +use Livewire\Livewire; use Spatie\LaravelPackageTools\Commands\InstallCommand; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -12,6 +13,13 @@ class ResourceLockServiceProvider extends PackageServiceProvider { public static string $name = 'filament-resource-lock'; + public function packageBooted(): void + { + parent::packageBooted(); + + Livewire::component('filament-resource-lock-observer', Http\Livewire\ResourceLockObserver::class); + } + public function configurePackage(Package $package): void { $package->name(static::$name) From 20328a6cebec8f0ab823ec6ae773dcba8cec96fe Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 25 Mar 2026 07:33:10 +0800 Subject: [PATCH 12/17] Add callAfterResolving as belt-and-suspenders for Livewire component registration Registers the filament-resource-lock-observer component via callAfterResolving in packageRegistered() in addition to packageBooted(). This ensures the component is always registered even if the boot chain fails to complete, fixing the intermittent ComponentNotFoundException on Livewire AJAX update requests. --- src/ResourceLockServiceProvider.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ResourceLockServiceProvider.php b/src/ResourceLockServiceProvider.php index 49f58ea..a8cc255 100644 --- a/src/ResourceLockServiceProvider.php +++ b/src/ResourceLockServiceProvider.php @@ -13,6 +13,13 @@ class ResourceLockServiceProvider extends PackageServiceProvider { public static string $name = 'filament-resource-lock'; + public function packageRegistered(): void + { + $this->callAfterResolving('livewire.finder', function ($finder) { + $finder->addComponent('filament-resource-lock-observer', class: Http\Livewire\ResourceLockObserver::class); + }); + } + public function packageBooted(): void { parent::packageBooted(); From d8593bdfe8280d3a2153466734a288cc2249140c Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 25 Mar 2026 07:35:07 +0800 Subject: [PATCH 13/17] Add PHPStan/Larastan with CI job and fix reported errors - Add phpstan.neon.dist (level 5, src/ scope) and phpstan-baseline.neon for unfixable library-pattern errors (unused public API traits, vendor view-string) - Add phpstan CI job to GitHub Actions workflow with 512M memory limit - Fix getNavigationBadge() return type (cast int to string) - Replace deprecated Filament actions()/bulkActions() with recordActions()/toolbarActions() - Add @property annotation for $updated_at on ResourceLock model --- .github/workflows/tests.yml | 20 ++++++++++++++++++ phpstan-baseline.neon | 37 ++++++++++++++++++++++++++++++++++ phpstan.neon.dist | 7 +++++++ src/Models/ResourceLock.php | 3 +++ src/Resources/LockResource.php | 6 +++--- 5 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon.dist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index df836c9..8265ae4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,3 +24,23 @@ jobs: - name: Run tests run: vendor/bin/pest --parallel + + phpstan: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite + coverage: none + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: Run PHPStan + run: vendor/bin/phpstan analyse --no-progress --memory-limit=512M -c phpstan.neon.dist diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..8bb4033 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,37 @@ +parameters: + ignoreErrors: + - + message: '#^Parameter \#1 \$view of function view expects view\-string\|null, string given\.$#' + identifier: argument.type + count: 1 + path: src/Http/Livewire/ResourceLockObserver.php + + - + message: '#^Trait Blendbyte\\FilamentResourceLock\\Models\\Concerns\\HasLocks is used zero times and is not analysed\.$#' + identifier: trait.unused + count: 1 + path: src/Models/Concerns/HasLocks.php + + - + message: '#^Trait Blendbyte\\FilamentResourceLock\\Resources\\Pages\\Concerns\\UsesLocks is used zero times and is not analysed\.$#' + identifier: trait.unused + count: 1 + path: src/Resources/Pages/Concerns/UsesLocks.php + + - + message: '#^Trait Blendbyte\\FilamentResourceLock\\Resources\\Pages\\Concerns\\UsesRelationManagerResourceLock is used zero times and is not analysed\.$#' + identifier: trait.unused + count: 1 + path: src/Resources/Pages/Concerns/UsesRelationManagerResourceLock.php + + - + message: '#^Trait Blendbyte\\FilamentResourceLock\\Resources\\Pages\\Concerns\\UsesResourceLock is used zero times and is not analysed\.$#' + identifier: trait.unused + count: 1 + path: src/Resources/Pages/Concerns/UsesResourceLock.php + + - + message: '#^Trait Blendbyte\\FilamentResourceLock\\Resources\\Pages\\Concerns\\UsesSimpleResourceLock is used zero times and is not analysed\.$#' + identifier: trait.unused + count: 1 + path: src/Resources/Pages/Concerns/UsesSimpleResourceLock.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..bc0f2c3 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 5 + paths: + - src diff --git a/src/Models/ResourceLock.php b/src/Models/ResourceLock.php index 8309e12..309748b 100644 --- a/src/Models/ResourceLock.php +++ b/src/Models/ResourceLock.php @@ -9,6 +9,9 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; use Blendbyte\FilamentResourceLock\ResourceLockPlugin; +/** + * @property \Carbon\Carbon|null $updated_at + */ class ResourceLock extends Model { use HasFactory; diff --git a/src/Resources/LockResource.php b/src/Resources/LockResource.php index 8af123b..79d8f8c 100644 --- a/src/Resources/LockResource.php +++ b/src/Resources/LockResource.php @@ -75,13 +75,13 @@ public static function table(Table $table): Tables\Table ->filters([ // ]) - ->actions([ + ->recordActions([ DeleteAction::make() ->icon('heroicon-o-lock-open') ->successNotificationTitle(__('filament-resource-lock::manager.unlocked')) ->label(__('filament-resource-lock::manager.unlock')), ]) - ->bulkActions([ + ->toolbarActions([ DeleteBulkAction::make() ->deselectRecordsAfterCompletion() ->requiresConfirmation() @@ -126,7 +126,7 @@ public static function getNavigationBadge(): ?string return null; } - return static::getModel()::count(); + return (string) static::getModel()::count(); } public static function getNavigationLabel(): string From 01a1017eeeeafd0895c3e9c7fe855a5ca6ab4105 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 25 Mar 2026 07:37:03 +0800 Subject: [PATCH 14/17] Apply Laravel Pint formatting across codebase Fixes ordered_imports, concat_space, not_operator_with_successor_space, fully_qualified_strict_types, no_unused_imports, and whitespace issues across src/, tests/, config/, and database/factories/. --- config/filament-resource-lock.php | 10 +++++-- database/factories/ResourceLockFactory.php | 2 +- database/factories/UserFactory.php | 2 +- .../Commands/ResourceLockClearCommand.php | 4 +-- .../ResourceLockClearExpiredCommand.php | 4 +-- src/Http/Livewire/ResourceLockObserver.php | 2 +- src/Models/Concerns/HasLocks.php | 4 +-- src/Models/ResourceLock.php | 4 +-- src/ResourceLockPlugin.php | 7 +++-- src/Resources/LockResource.php | 11 ++++--- .../LockResource/ManageResourceLocks.php | 4 +-- .../ConfigureResourceLockPluginTest.php | 3 +- tests/Feature/ResourceLockObserverTest.php | 29 +++++++++---------- tests/Fixtures/AdminPanelProvider.php | 2 +- tests/Pest.php | 2 +- tests/Resources/Models/Post.php | 2 +- tests/TestCase.php | 14 ++++----- tests/Unit/modelHasLockTest.php | 2 +- 18 files changed, 55 insertions(+), 53 deletions(-) diff --git a/config/filament-resource-lock.php b/config/filament-resource-lock.php index bfe1863..90cfdfd 100644 --- a/config/filament-resource-lock.php +++ b/config/filament-resource-lock.php @@ -1,5 +1,9 @@ [ - 'User' => \App\Models\User::class, + 'User' => User::class, // 'ResourceLock' => null, ], @@ -28,7 +32,7 @@ | */ 'resource' => [ - 'class' => \Blendbyte\FilamentResourceLock\Resources\LockResource::class, + 'class' => LockResource::class, ], /* @@ -125,6 +129,6 @@ */ 'actions' => [ - 'get_resource_lock_owner_action' => \Blendbyte\FilamentResourceLock\Actions\GetResourceLockOwnerAction::class, + 'get_resource_lock_owner_action' => GetResourceLockOwnerAction::class, ], ]; diff --git a/database/factories/ResourceLockFactory.php b/database/factories/ResourceLockFactory.php index 81b54bf..f1579ea 100644 --- a/database/factories/ResourceLockFactory.php +++ b/database/factories/ResourceLockFactory.php @@ -2,8 +2,8 @@ namespace Blendbyte\FilamentResourceLock\Database\Factories; -use Illuminate\Database\Eloquent\Factories\Factory; use Blendbyte\FilamentResourceLock\Models\ResourceLock; +use Illuminate\Database\Eloquent\Factories\Factory; class ResourceLockFactory extends Factory { diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 2900b7e..5b92ee3 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -2,8 +2,8 @@ namespace Blendbyte\FilamentResourceLock\Database\Factories; -use Illuminate\Database\Eloquent\Factories\Factory; use Blendbyte\FilamentResourceLock\Tests\Resources\Models\User; +use Illuminate\Database\Eloquent\Factories\Factory; class UserFactory extends Factory { diff --git a/src/Console/Commands/ResourceLockClearCommand.php b/src/Console/Commands/ResourceLockClearCommand.php index 7b7c0b1..8e71e9b 100644 --- a/src/Console/Commands/ResourceLockClearCommand.php +++ b/src/Console/Commands/ResourceLockClearCommand.php @@ -2,9 +2,9 @@ namespace Blendbyte\FilamentResourceLock\Console\Commands; +use Blendbyte\FilamentResourceLock\Models\ResourceLock; use Exception; use Illuminate\Console\Command; -use Blendbyte\FilamentResourceLock\Models\ResourceLock; class ResourceLockClearCommand extends Command { @@ -33,7 +33,7 @@ public function handle(): void ResourceLock::truncate(); $this->info('All resource locks successfully removed.'); } catch (Exception $e) { - $this->error('Failed to clear resource locks: ' . $e->getMessage()); + $this->error('Failed to clear resource locks: '.$e->getMessage()); return; } diff --git a/src/Console/Commands/ResourceLockClearExpiredCommand.php b/src/Console/Commands/ResourceLockClearExpiredCommand.php index 237418f..a2d289f 100644 --- a/src/Console/Commands/ResourceLockClearExpiredCommand.php +++ b/src/Console/Commands/ResourceLockClearExpiredCommand.php @@ -2,9 +2,9 @@ namespace Blendbyte\FilamentResourceLock\Console\Commands; +use Blendbyte\FilamentResourceLock\Models\ResourceLock; use Exception; use Illuminate\Console\Command; -use Blendbyte\FilamentResourceLock\Models\ResourceLock; class ResourceLockClearExpiredCommand extends Command { @@ -41,7 +41,7 @@ public function handle(): void $this->info('All expired resource locks successfully removed.'); } catch (Exception $e) { - $this->error('Failed to clear expired resource locks: ' . $e->getMessage()); + $this->error('Failed to clear expired resource locks: '.$e->getMessage()); return; } diff --git a/src/Http/Livewire/ResourceLockObserver.php b/src/Http/Livewire/ResourceLockObserver.php index c48bfb0..f99ebf4 100644 --- a/src/Http/Livewire/ResourceLockObserver.php +++ b/src/Http/Livewire/ResourceLockObserver.php @@ -2,8 +2,8 @@ namespace Blendbyte\FilamentResourceLock\Http\Livewire; -use Illuminate\Support\Facades\Gate; use Blendbyte\FilamentResourceLock\ResourceLockPlugin; +use Illuminate\Support\Facades\Gate; use Livewire\Attributes\On; use Livewire\Component; diff --git a/src/Models/Concerns/HasLocks.php b/src/Models/Concerns/HasLocks.php index ebd0529..6ba0205 100644 --- a/src/Models/Concerns/HasLocks.php +++ b/src/Models/Concerns/HasLocks.php @@ -2,10 +2,10 @@ namespace Blendbyte\FilamentResourceLock\Models\Concerns; -use Filament\Facades\Filament; -use Illuminate\Database\Eloquent\Relations\MorphOne; use Blendbyte\FilamentResourceLock\Models\ResourceLock; use Blendbyte\FilamentResourceLock\ResourceLockPlugin; +use Filament\Facades\Filament; +use Illuminate\Database\Eloquent\Relations\MorphOne; /** * The HasLocks trait provides several functions to models to handle locking and unlocking of records. diff --git a/src/Models/ResourceLock.php b/src/Models/ResourceLock.php index 309748b..e200676 100644 --- a/src/Models/ResourceLock.php +++ b/src/Models/ResourceLock.php @@ -2,15 +2,15 @@ namespace Blendbyte\FilamentResourceLock\Models; +use Blendbyte\FilamentResourceLock\ResourceLockPlugin; use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; -use Blendbyte\FilamentResourceLock\ResourceLockPlugin; /** - * @property \Carbon\Carbon|null $updated_at + * @property Carbon|null $updated_at */ class ResourceLock extends Model { diff --git a/src/ResourceLockPlugin.php b/src/ResourceLockPlugin.php index 7583836..9e41624 100644 --- a/src/ResourceLockPlugin.php +++ b/src/ResourceLockPlugin.php @@ -2,13 +2,14 @@ namespace Blendbyte\FilamentResourceLock; +use Blendbyte\FilamentResourceLock\Actions\GetResourceLockOwnerAction; +use Blendbyte\FilamentResourceLock\Models\ResourceLock; +use Blendbyte\FilamentResourceLock\Resources\LockResource; use Filament\Contracts\Plugin; use Filament\Panel; use Filament\Support\Facades\FilamentView; use Filament\View\PanelsRenderHook; use Illuminate\Support\Facades\Blade; -use Blendbyte\FilamentResourceLock\Models\ResourceLock; -use Blendbyte\FilamentResourceLock\Resources\LockResource; class ResourceLockPlugin implements Plugin { @@ -303,7 +304,7 @@ public function resourceLockOwnerAction(?string $action): static public function getResourceLockOwnerAction(): string { - return $this->resourceLockOwnerAction ?? config('filament-resource-lock.actions.get_resource_lock_owner_action', \Blendbyte\FilamentResourceLock\Actions\GetResourceLockOwnerAction::class); + return $this->resourceLockOwnerAction ?? config('filament-resource-lock.actions.get_resource_lock_owner_action', GetResourceLockOwnerAction::class); } public function usesPollingToDetectPresence(bool $enable = true): static diff --git a/src/Resources/LockResource.php b/src/Resources/LockResource.php index 79d8f8c..9b520b2 100644 --- a/src/Resources/LockResource.php +++ b/src/Resources/LockResource.php @@ -2,16 +2,15 @@ namespace Blendbyte\FilamentResourceLock\Resources; -use Filament\Schemas\Schema; -use Filament\Resources\Resource; -use Filament\Tables; +use Blendbyte\FilamentResourceLock\ResourceLockPlugin; +use Blendbyte\FilamentResourceLock\Resources\LockResource\ManageResourceLocks; use Filament\Actions\DeleteAction; use Filament\Actions\DeleteBulkAction; +use Filament\Resources\Resource; +use Filament\Schemas\Schema; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Illuminate\Support\Facades\Gate; -use Blendbyte\FilamentResourceLock\ResourceLockPlugin; -use Blendbyte\FilamentResourceLock\Resources\LockResource\ManageResourceLocks; class LockResource extends Resource { @@ -38,7 +37,7 @@ public static function form(Schema $schema): Schema ]); } - public static function table(Table $table): Tables\Table + public static function table(Table $table): Table { return $table ->columns([ diff --git a/src/Resources/LockResource/ManageResourceLocks.php b/src/Resources/LockResource/ManageResourceLocks.php index d09fc29..6b34c97 100644 --- a/src/Resources/LockResource/ManageResourceLocks.php +++ b/src/Resources/LockResource/ManageResourceLocks.php @@ -2,10 +2,10 @@ namespace Blendbyte\FilamentResourceLock\Resources\LockResource; -use Filament\Actions\Action; -use Filament\Resources\Pages\ManageRecords; use Blendbyte\FilamentResourceLock\ResourceLockPlugin; use Blendbyte\FilamentResourceLock\Resources\LockResource; +use Filament\Actions\Action; +use Filament\Resources\Pages\ManageRecords; class ManageResourceLocks extends ManageRecords { diff --git a/tests/Feature/ConfigureResourceLockPluginTest.php b/tests/Feature/ConfigureResourceLockPluginTest.php index f72b6cd..e1eba4d 100644 --- a/tests/Feature/ConfigureResourceLockPluginTest.php +++ b/tests/Feature/ConfigureResourceLockPluginTest.php @@ -9,7 +9,7 @@ function getResourceLockNavigationItem($panel, string $group = 'Settings', strin $navigationItems = $panel->getNavigation(); $navigationItem = array_find($navigationItems, function ($value, $key) use ($group) { - return str_contains($key, $group); + return str_contains($key, $group); }); return $navigationItem->getItems()[$label]; @@ -97,7 +97,6 @@ function getResourceLockNavigationItem($panel, string $group = 'Settings', strin expect($plugin->shouldUsePollingKeepAlive())->toBeTrue(); }); - it('uses default visible value when not configured', function () { $plugin = ResourceLockPlugin::make(); diff --git a/tests/Feature/ResourceLockObserverTest.php b/tests/Feature/ResourceLockObserverTest.php index 96d5027..e495e57 100644 --- a/tests/Feature/ResourceLockObserverTest.php +++ b/tests/Feature/ResourceLockObserverTest.php @@ -63,8 +63,8 @@ $html = $component->viewData('usesPollingToDetectPresence') ? $component->viewData('pollingKeepAlive') - ? 'wire:poll.keep-alive.' . $component->viewData('presencePollingInterval') . 's' - : 'wire:poll.' . $component->viewData('presencePollingInterval') . 's' + ? 'wire:poll.keep-alive.'.$component->viewData('presencePollingInterval').'s' + : 'wire:poll.'.$component->viewData('presencePollingInterval').'s' : null; expect($html)->toBe('wire:poll.keep-alive.10s'); @@ -82,8 +82,8 @@ $html = $component->viewData('usesPollingToDetectPresence') ? $component->viewData('pollingKeepAlive') - ? 'wire:poll.keep-alive.' . $component->viewData('presencePollingInterval') . 's' - : 'wire:poll.' . $component->viewData('presencePollingInterval') . 's' + ? 'wire:poll.keep-alive.'.$component->viewData('presencePollingInterval').'s' + : 'wire:poll.'.$component->viewData('presencePollingInterval').'s' : null; expect($html)->toBe('wire:poll.15s'); @@ -117,7 +117,6 @@ expect($component->get('usesPollingToDetectPresence'))->toBeTrue(); }); - it('resets visible when disabling polling', function () { $panel = filament()->getDefaultPanel(); $panel->plugin(ResourceLockPlugin::make() @@ -144,11 +143,11 @@ $component = Livewire::test(ResourceLockObserver::class); $component->call('enablePolling'); - $html = $component->viewData('usesPollingToDetectPresence') - ? 'wire:poll' . - ($component->viewData('pollingKeepAlive') ? '.keep-alive' : '') . - ($component->viewData('pollingVisible') ? '.visible' : '') . - '.' . $component->viewData('presencePollingInterval') . 's' + $html = $component->viewData('usesPollingToDetectPresence') + ? 'wire:poll'. + ($component->viewData('pollingKeepAlive') ? '.keep-alive' : ''). + ($component->viewData('pollingVisible') ? '.visible' : ''). + '.'.$component->viewData('presencePollingInterval').'s' : null; expect($html)->toBe('wire:poll.visible.20s'); @@ -165,11 +164,11 @@ $component = Livewire::test(ResourceLockObserver::class); $component->call('enablePolling'); - $html = $component->viewData('usesPollingToDetectPresence') - ? 'wire:poll' . - ($component->viewData('pollingKeepAlive') ? '.keep-alive' : '') . - ($component->viewData('pollingVisible') ? '.visible' : '') . - '.' . $component->viewData('presencePollingInterval') . 's' + $html = $component->viewData('usesPollingToDetectPresence') + ? 'wire:poll'. + ($component->viewData('pollingKeepAlive') ? '.keep-alive' : ''). + ($component->viewData('pollingVisible') ? '.visible' : ''). + '.'.$component->viewData('presencePollingInterval').'s' : null; expect($html)->toBe('wire:poll.keep-alive.visible.30s'); diff --git a/tests/Fixtures/AdminPanelProvider.php b/tests/Fixtures/AdminPanelProvider.php index 12a072d..cc1f59b 100644 --- a/tests/Fixtures/AdminPanelProvider.php +++ b/tests/Fixtures/AdminPanelProvider.php @@ -2,6 +2,7 @@ namespace Blendbyte\FilamentResourceLock\Tests\Fixtures; +use Blendbyte\FilamentResourceLock\ResourceLockPlugin; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DispatchServingFilamentEvent; @@ -14,7 +15,6 @@ use Illuminate\Session\Middleware\AuthenticateSession; use Illuminate\Session\Middleware\StartSession; use Illuminate\View\Middleware\ShareErrorsFromSession; -use Blendbyte\FilamentResourceLock\ResourceLockPlugin; class AdminPanelProvider extends PanelProvider { diff --git a/tests/Pest.php b/tests/Pest.php index 84c8554..5af52d5 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,10 +1,10 @@ in(__DIR__); diff --git a/tests/Resources/Models/Post.php b/tests/Resources/Models/Post.php index 8dd63c8..2ac652c 100644 --- a/tests/Resources/Models/Post.php +++ b/tests/Resources/Models/Post.php @@ -2,9 +2,9 @@ namespace Blendbyte\FilamentResourceLock\Tests\Resources\Models; +use Blendbyte\FilamentResourceLock\Models\Concerns\HasLocks; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Blendbyte\FilamentResourceLock\Models\Concerns\HasLocks; class Post extends Model { diff --git a/tests/TestCase.php b/tests/TestCase.php index f2a051c..84fbfba 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,6 +4,8 @@ use BladeUI\Heroicons\BladeHeroiconsServiceProvider; use BladeUI\Icons\BladeIconsServiceProvider; +use Blendbyte\FilamentResourceLock\ResourceLockServiceProvider; +use Blendbyte\FilamentResourceLock\Tests\Fixtures\AdminPanelProvider; use Filament\Actions\ActionsServiceProvider; use Filament\FilamentServiceProvider; use Filament\Forms\FormsServiceProvider; @@ -14,8 +16,6 @@ use Filament\Tables\TablesServiceProvider; use Filament\Widgets\WidgetsServiceProvider; use Illuminate\Database\Eloquent\Factories\Factory; -use Blendbyte\FilamentResourceLock\ResourceLockServiceProvider; -use Blendbyte\FilamentResourceLock\Tests\Fixtures\AdminPanelProvider; use Livewire\LivewireServiceProvider; use Orchestra\Testbench\TestCase as Orchestra; @@ -26,7 +26,7 @@ protected function setUp(): void parent::setUp(); Factory::guessFactoryNamesUsing( - fn (string $modelName) => 'Blendbyte\\FilamentResourceLock\\Database\\Factories\\' . class_basename($modelName) . 'Factory' + fn (string $modelName) => 'Blendbyte\\FilamentResourceLock\\Database\\Factories\\'.class_basename($modelName).'Factory' ); } @@ -41,16 +41,16 @@ public function getEnvironmentSetUp($app) config()->set('filament-resource-lock.models.User', '\Blendbyte\FilamentResourceLock\Tests\Resources\Models\User'); config()->set('app.key', '6rE9Nz59bGRbeMATftriyQjrpF7DcOQm'); - $migration = include __DIR__ . '/../database/migrations/create_resource_lock_table.php.stub'; + $migration = include __DIR__.'/../database/migrations/create_resource_lock_table.php.stub'; $migration->up(); - $migration = include __DIR__ . '/Migrations/post_migration.php'; + $migration = include __DIR__.'/Migrations/post_migration.php'; $migration->up(); - $migration = include __DIR__ . '/Migrations/user_migration.php'; + $migration = include __DIR__.'/Migrations/user_migration.php'; $migration->up(); - view()->addLocation(__DIR__ . '/Fixtures/views'); + view()->addLocation(__DIR__.'/Fixtures/views'); } protected function getPackageProviders($app) diff --git a/tests/Unit/modelHasLockTest.php b/tests/Unit/modelHasLockTest.php index b26928b..f32107b 100644 --- a/tests/Unit/modelHasLockTest.php +++ b/tests/Unit/modelHasLockTest.php @@ -1,7 +1,7 @@ Date: Wed, 25 Mar 2026 07:43:51 +0800 Subject: [PATCH 15/17] Update GitHub Actions versions and expand PHP matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - actions/checkout v4 → v6 - ramsey/composer-install v3 → v4 - PHP matrix: 8.2, 8.3, 8.4, 8.5 (was single 8.2) - PHPStan runs on PHP 8.5 with progress output enabled --- .github/workflows/tests.yml | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8265ae4..067ce3a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,19 +8,26 @@ jobs: test: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3', '8.4', '8.5'] + + name: PHP ${{ matrix.php }} + steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite coverage: none - name: Install dependencies - uses: ramsey/composer-install@v3 + uses: ramsey/composer-install@v4 - name: Run tests run: vendor/bin/pest --parallel @@ -30,17 +37,17 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: '8.5' extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite coverage: none - name: Install dependencies - uses: ramsey/composer-install@v3 + uses: ramsey/composer-install@v4 - name: Run PHPStan - run: vendor/bin/phpstan analyse --no-progress --memory-limit=512M -c phpstan.neon.dist + run: vendor/bin/phpstan analyse --memory-limit=512M -c phpstan.neon.dist From d88bea4d8a613797fcf8905be858991bee3f4517 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 25 Mar 2026 09:56:50 +0800 Subject: [PATCH 16/17] Add fork note, migration guide, badges, and extract PHPStan workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add fork note and migration guide to README (kenepa/resource-lock → blendbyte/filament-resource-lock) - Add Packagist, Tests, Static Analysis, and License badges - Extract PHPStan job from tests.yml into dedicated static-analysis.yml workflow --- .github/workflows/static-analysis.yml | 26 ++++++++++++++++++++ .github/workflows/tests.yml | 19 --------------- README.md | 34 +++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/static-analysis.yml diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..8ff63ab --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,26 @@ +name: Static Analysis + +on: + push: + pull_request: + +jobs: + phpstan: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite + coverage: none + + - name: Install dependencies + uses: ramsey/composer-install@v4 + + - name: Run PHPStan + run: vendor/bin/phpstan analyse --memory-limit=512M -c phpstan.neon.dist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 067ce3a..aae9e26 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,22 +32,3 @@ jobs: - name: Run tests run: vendor/bin/pest --parallel - phpstan: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.5' - extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite - coverage: none - - - name: Install dependencies - uses: ramsey/composer-install@v4 - - - name: Run PHPStan - run: vendor/bin/phpstan analyse --memory-limit=512M -c phpstan.neon.dist diff --git a/README.md b/README.md index 280abaf..90ccf70 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,41 @@ # Resource Lock +[![Latest Version on Packagist](https://img.shields.io/packagist/v/blendbyte/filament-resource-lock.svg?style=flat-square)](https://packagist.org/packages/blendbyte/filament-resource-lock) +[![Tests](https://github.com/blendbyte/filament-resource-lock/actions/workflows/tests.yml/badge.svg)](https://github.com/blendbyte/filament-resource-lock/actions/workflows/tests.yml) +[![Static Analysis](https://github.com/blendbyte/filament-resource-lock/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/blendbyte/filament-resource-lock/actions/workflows/static-analysis.yml) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) + Filament Resource Lock is a Filament plugin that adds resource locking functionality to your site. When a user begins editing a resource, it is automatically locked to prevent other users from editing it at the same time. The resource will be automatically unlocked after a set period of time, or when the user saves or discards their changes. +> **Note:** This package is a fork of [kenepa/resource-lock](https://github.com/kenepa/resource-lock), updated for **Filament v5** compatibility. If you are currently using `kenepa/resource-lock`, see the [migration guide](#migrating-from-keneparesource-lock) below. + +## Migrating from kenepa/resource-lock + +This fork introduces the following breaking changes: + +1. **Composer package** — replace `kenepa/resource-lock` with `blendbyte/filament-resource-lock` + +2. **PHP namespace** — find and replace `Kenepa\ResourceLock` with `Blendbyte\FilamentResourceLock` across your application + +3. **Config file** — the config was renamed from `resource-lock.php` to `filament-resource-lock.php`. Re-publish if you have a customised config: + ```bash + php artisan vendor:publish --tag="filament-resource-lock-config" --force + ``` + +4. **Artisan commands** — command signatures changed from `resource-lock:*` to `filament-resource-lock:*` (e.g. `filament-resource-lock:install`) + +5. **Filament version** — this package requires **Filament v5**. The original `kenepa/resource-lock` targets Filament v3/v4. + +Quick steps: + +```bash +composer remove kenepa/resource-lock +composer require blendbyte/filament-resource-lock +php artisan filament-resource-lock:install +``` + +Then update all `use Kenepa\ResourceLock\...` import statements to `use Blendbyte\FilamentResourceLock\...`. + ## Installation ```bash From 08b8d94b9309687122f52af43929e3f5873576e8 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 3 Apr 2026 21:46:43 +0800 Subject: [PATCH 17/17] Add first-class Laravel event dispatching for lock lifecycle transitions Dispatches ResourceLocked, ResourceUnlocked, ResourceLockExpired, and ResourceLockForceUnlocked events from all lock/unlock surfaces including the HasLocks model trait, Lock Manager table actions, and the Unlock All header action. Events are opt-out via config (events.enabled) and a new ResourceLockPlugin::enableEvents() fluent method. Also cleans up orphaned expired lock records when a new lock overwrites them in HasLocks::lock(), and adds @property int|string $user_id to the ResourceLock model for PHPStan correctness. --- config/filament-resource-lock.php | 16 +++ src/Events/ResourceLockExpired.php | 17 +++ src/Events/ResourceLockForceUnlocked.php | 18 +++ src/Events/ResourceLocked.php | 17 +++ src/Events/ResourceUnlocked.php | 17 +++ src/Models/Concerns/HasLocks.php | 28 ++++ src/Models/ResourceLock.php | 1 + src/ResourceLockPlugin.php | 14 ++ src/Resources/LockResource.php | 23 ++++ .../LockResource/ManageResourceLocks.php | 17 ++- tests/Feature/ResourceLockEventsTest.php | 103 ++++++++++++++ tests/Unit/modelHasLockTest.php | 130 ++++++++++++++++++ 12 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 src/Events/ResourceLockExpired.php create mode 100644 src/Events/ResourceLockForceUnlocked.php create mode 100644 src/Events/ResourceLocked.php create mode 100644 src/Events/ResourceUnlocked.php create mode 100644 tests/Feature/ResourceLockEventsTest.php diff --git a/config/filament-resource-lock.php b/config/filament-resource-lock.php index 90cfdfd..8219b90 100644 --- a/config/filament-resource-lock.php +++ b/config/filament-resource-lock.php @@ -131,4 +131,20 @@ 'actions' => [ 'get_resource_lock_owner_action' => GetResourceLockOwnerAction::class, ], + + /* + |-------------------------------------------------------------------------- + | Events + |-------------------------------------------------------------------------- + | + | When enabled, the package dispatches Laravel events for every lock + | lifecycle transition: ResourceLocked, ResourceUnlocked, + | ResourceLockExpired, and ResourceLockForceUnlocked. + | Set to false to disable all event dispatching globally. + | + */ + + 'events' => [ + 'enabled' => true, + ], ]; diff --git a/src/Events/ResourceLockExpired.php b/src/Events/ResourceLockExpired.php new file mode 100644 index 0000000..ec283c5 --- /dev/null +++ b/src/Events/ResourceLockExpired.php @@ -0,0 +1,17 @@ +isUnlocked()) { + // An expired record exists — dispatch expiry event, clean it up + if ($this->resourceLock !== null && $this->resourceLock->isExpired()) { + $expiredUserId = $this->resourceLock->user_id; + $this->resourceLock()->delete(); + $this->unsetRelation('resourceLock'); + + if (config('filament-resource-lock.events.enabled', true)) { + ResourceLockExpired::dispatch($this, $expiredUserId); + } + } + $resourceLockModel = ResourceLockPlugin::get()->getResourceLockModel(); $guard = $this->getCurrentAuthGuardName(); $resourceLock = new $resourceLockModel; $resourceLock->user_id = auth()->guard($guard)->user()->id; $this->resourceLock()->save($resourceLock); + if (config('filament-resource-lock.events.enabled', true)) { + ResourceLocked::dispatch($this, $resourceLock->user_id); + } + return true; } @@ -112,8 +131,17 @@ public function unlock(bool $force = false): bool { if ($this->isLocked()) { if ($force || $this->lockCreatedByCurrentUser() || $this->hasExpiredLock()) { + $originalUserId = $this->resourceLock->user_id; $this->resourceLock()->delete(); + if (config('filament-resource-lock.events.enabled', true)) { + if ($force) { + ResourceLockForceUnlocked::dispatch($this, $originalUserId, auth()->id()); + } else { + ResourceUnlocked::dispatch($this, $originalUserId); + } + } + return true; } } diff --git a/src/Models/ResourceLock.php b/src/Models/ResourceLock.php index e200676..64b7584 100644 --- a/src/Models/ResourceLock.php +++ b/src/Models/ResourceLock.php @@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; /** + * @property int|string $user_id * @property Carbon|null $updated_at */ class ResourceLock extends Model diff --git a/src/ResourceLockPlugin.php b/src/ResourceLockPlugin.php index 9e41624..82106bc 100644 --- a/src/ResourceLockPlugin.php +++ b/src/ResourceLockPlugin.php @@ -57,6 +57,8 @@ class ResourceLockPlugin implements Plugin protected bool $pollingVisible = false; + protected ?bool $eventsEnabled = null; + public static function make(): static { return app(static::class); @@ -354,4 +356,16 @@ public function shouldUsePollingVisible(): bool { return $this->pollingVisible ?? false; } + + public function enableEvents(bool $enable = true): static + { + $this->eventsEnabled = $enable; + + return $this; + } + + public function shouldDispatchEvents(): bool + { + return $this->eventsEnabled ?? config('filament-resource-lock.events.enabled', true); + } } diff --git a/src/Resources/LockResource.php b/src/Resources/LockResource.php index 9b520b2..378e5e6 100644 --- a/src/Resources/LockResource.php +++ b/src/Resources/LockResource.php @@ -2,10 +2,13 @@ namespace Blendbyte\FilamentResourceLock\Resources; +use Blendbyte\FilamentResourceLock\Events\ResourceLockForceUnlocked; +use Blendbyte\FilamentResourceLock\Models\ResourceLock; use Blendbyte\FilamentResourceLock\ResourceLockPlugin; use Blendbyte\FilamentResourceLock\Resources\LockResource\ManageResourceLocks; use Filament\Actions\DeleteAction; use Filament\Actions\DeleteBulkAction; +use Illuminate\Support\Collection; use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Tables\Columns\TextColumn; @@ -76,12 +79,32 @@ public static function table(Table $table): Table ]) ->recordActions([ DeleteAction::make() + ->before(function (ResourceLock $record) { + if (config('filament-resource-lock.events.enabled', true)) { + ResourceLockForceUnlocked::dispatch( + $record->lockable, + $record->user_id, + auth()->id() + ); + } + }) ->icon('heroicon-o-lock-open') ->successNotificationTitle(__('filament-resource-lock::manager.unlocked')) ->label(__('filament-resource-lock::manager.unlock')), ]) ->toolbarActions([ DeleteBulkAction::make() + ->before(function (Collection $records) { + if (config('filament-resource-lock.events.enabled', true)) { + $records->each(function (ResourceLock $record) { + ResourceLockForceUnlocked::dispatch( + $record->lockable, + $record->user_id, + auth()->id() + ); + }); + } + }) ->deselectRecordsAfterCompletion() ->requiresConfirmation() ->icon('heroicon-o-lock-open') diff --git a/src/Resources/LockResource/ManageResourceLocks.php b/src/Resources/LockResource/ManageResourceLocks.php index 6b34c97..f8941d1 100644 --- a/src/Resources/LockResource/ManageResourceLocks.php +++ b/src/Resources/LockResource/ManageResourceLocks.php @@ -2,6 +2,7 @@ namespace Blendbyte\FilamentResourceLock\Resources\LockResource; +use Blendbyte\FilamentResourceLock\Events\ResourceLockForceUnlocked; use Blendbyte\FilamentResourceLock\ResourceLockPlugin; use Blendbyte\FilamentResourceLock\Resources\LockResource; use Filament\Actions\Action; @@ -17,7 +18,21 @@ protected function getHeaderActions(): array Action::make(__('filament-resource-lock::manager.unlock_all')) ->label(__('filament-resource-lock::manager.unlock_all')) ->icon('heroicon-o-lock-open') - ->action(fn () => ResourceLockPlugin::get()->getResourceLockModel()::truncate()) + ->action(function () { + $lockModel = ResourceLockPlugin::get()->getResourceLockModel(); + + if (config('filament-resource-lock.events.enabled', true)) { + $lockModel::with('lockable')->get()->each(function ($lock) { + ResourceLockForceUnlocked::dispatch( + $lock->lockable, + $lock->user_id, + auth()->id() + ); + }); + } + + $lockModel::truncate(); + }) ->requiresConfirmation(), ]; } diff --git a/tests/Feature/ResourceLockEventsTest.php b/tests/Feature/ResourceLockEventsTest.php new file mode 100644 index 0000000..7295ea4 --- /dev/null +++ b/tests/Feature/ResourceLockEventsTest.php @@ -0,0 +1,103 @@ +callAction('Unlock all resources'); + + Event::assertDispatchedTimes(ResourceLockForceUnlocked::class, 2); +}); + +it('dispatches no events on Unlock All when events are disabled', function () { + Event::fake([ResourceLockForceUnlocked::class]); + config(['filament-resource-lock.events.enabled' => false]); + + $user = createUser(); + actingAs($user); + + $post = createPost(); + createActiveResourceLock($user, $post); + + Livewire::test(ManageResourceLocks::class) + ->callAction('Unlock all resources'); + + Event::assertNothingDispatched(); +}); + +it('dispatches ResourceLockForceUnlocked with correct lock owner on Unlock All', function () { + Event::fake([ResourceLockForceUnlocked::class]); + + $lockOwner = createUser(); + $actor = createUser(); + actingAs($actor); + + $post = createPost(); + createActiveResourceLock($lockOwner, $post); + + Livewire::test(ManageResourceLocks::class) + ->callAction('Unlock all resources'); + + Event::assertDispatched(ResourceLockForceUnlocked::class, function ($event) use ($post, $lockOwner, $actor) { + return $event->lockable->id === $post->id + && $event->originalUserId === $lockOwner->id + && $event->actorUserId === $actor->id; + }); +}); + +it('dispatches ResourceLockForceUnlocked when a row is deleted in the Lock Manager', function () { + Event::fake([ResourceLockForceUnlocked::class]); + + $lockOwner = createUser(); + $actor = createUser(); + actingAs($actor); + + $post = createPost(); + $lock = createActiveResourceLock($lockOwner, $post); + + Livewire::test(ManageResourceLocks::class) + ->callTableAction('delete', $lock); + + Event::assertDispatched(ResourceLockForceUnlocked::class, function ($event) use ($post, $lockOwner, $actor) { + return $event->lockable->id === $post->id + && $event->originalUserId === $lockOwner->id + && $event->actorUserId === $actor->id; + }); +}); + +it('dispatches ResourceLockForceUnlocked for each row on bulk delete in the Lock Manager', function () { + Event::fake([ResourceLockForceUnlocked::class]); + + $lockOwner = createUser(); + $actor = createUser(); + actingAs($actor); + + $post1 = createPost(); + $post2 = createPost(); + $lock1 = createActiveResourceLock($lockOwner, $post1); + $lock2 = createActiveResourceLock($lockOwner, $post2); + + Livewire::test(ManageResourceLocks::class) + ->callTableBulkAction('delete', [$lock1, $lock2]); + + Event::assertDispatchedTimes(ResourceLockForceUnlocked::class, 2); +}); diff --git a/tests/Unit/modelHasLockTest.php b/tests/Unit/modelHasLockTest.php index f32107b..2414f96 100644 --- a/tests/Unit/modelHasLockTest.php +++ b/tests/Unit/modelHasLockTest.php @@ -1,7 +1,12 @@ and($post->lock()) ->toBeFalse(); }); + +describe('Lock Events', function () { + it('dispatches ResourceLocked when a new lock is acquired', function () { + Event::fake(); + + $user = createUser(); + actingAs($user); + $post = createPost(); + + $post->lock(); + + Event::assertDispatched(ResourceLocked::class, function ($event) use ($post, $user) { + return $event->lockable->id === $post->id + && $event->userId === $user->id; + }); + }); + + it('does not dispatch ResourceLocked on keepalive touch', function () { + Event::fake(); + + $user = createUser(); + actingAs($user); + $post = createPost(); + + $post->lock(); + $post->refresh(); + sleep(1); + $post->lock(); // keepalive + + Event::assertDispatchedTimes(ResourceLocked::class, 1); + }); + + it('dispatches ResourceLockExpired then ResourceLocked when a new lock overwrites an expired one', function () { + Event::fake(); + + $user1 = createUser(); + $user2 = createUser(); + $post = createPost(); + createExpiredResourceLock($user1, $post); + + actingAs($user2); + $post->refresh(); + $post->lock(); + + Event::assertDispatched(ResourceLockExpired::class, function ($event) use ($post, $user1) { + return $event->lockable->id === $post->id + && $event->originalUserId === $user1->id; + }); + + Event::assertDispatched(ResourceLocked::class, function ($event) use ($post, $user2) { + return $event->lockable->id === $post->id + && $event->userId === $user2->id; + }); + }); + + it('dispatches ResourceUnlocked on natural unlock by owner', function () { + Event::fake(); + + $user = createUser(); + actingAs($user); + $post = createPost(); + $post->lock(); + $post->refresh(); + + $post->unlock(); + + Event::assertDispatched(ResourceUnlocked::class, function ($event) use ($post, $user) { + return $event->lockable->id === $post->id + && $event->userId === $user->id; + }); + Event::assertNotDispatched(ResourceLockForceUnlocked::class); + }); + + it('does not dispatch ResourceUnlocked when unlock is rejected', function () { + Event::fake(); + + $user1 = createUser(); + actingAs($user1); + $post = createPost(); + $post->lock(); + + $user2 = createUser(); + actingAs($user2); + $post->refresh(); + $post->unlock(force: false); + + Event::assertNotDispatched(ResourceUnlocked::class); + Event::assertNotDispatched(ResourceLockForceUnlocked::class); + }); + + it('dispatches ResourceLockForceUnlocked with correct user IDs on force unlock', function () { + Event::fake(); + + $user1 = createUser(); + actingAs($user1); + $post = createPost(); + $post->lock(); + + $user2 = createUser(); + actingAs($user2); + $post->refresh(); + $post->unlock(force: true); + + Event::assertDispatched(ResourceLockForceUnlocked::class, function ($event) use ($post, $user1, $user2) { + return $event->lockable->id === $post->id + && $event->originalUserId === $user1->id + && $event->actorUserId === $user2->id; + }); + Event::assertNotDispatched(ResourceUnlocked::class); + }); + + it('dispatches no events when events are disabled via config', function () { + Event::fake([ResourceLocked::class, ResourceUnlocked::class, ResourceLockExpired::class, ResourceLockForceUnlocked::class]); + config(['filament-resource-lock.events.enabled' => false]); + + $user = createUser(); + actingAs($user); + $post = createPost(); + $post->lock(); + $post->refresh(); + $post->unlock(); + + Event::assertNothingDispatched(); + }); +});