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/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
new file mode 100644
index 0000000..aae9e26
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,34 @@
+name: Tests
+
+on:
+ push:
+ pull_request:
+
+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@v6
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite
+ coverage: none
+
+ - name: Install dependencies
+ uses: ramsey/composer-install@v4
+
+ - name: Run tests
+ run: vendor/bin/pest --parallel
+
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/.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/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
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..90ccf70 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,59 @@
# Resource Lock
-
-
-
+[](https://packagist.org/packages/blendbyte/filament-resource-lock)
+[](https://github.com/blendbyte/filament-resource-lock/actions/workflows/tests.yml)
+[](https://github.com/blendbyte/filament-resource-lock/actions/workflows/static-analysis.yml)
+[](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.
-[](https://packagist.org/packages/kenepa/resource-lock)
-[](https://packagist.org/packages/kenepa/resource-lock)
+> **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.
-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.
+## Migrating from kenepa/resource-lock
-
+This fork introduces the following breaking changes:
-## Installation
+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
+ ```
-| 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+ |
+4. **Artisan commands** — command signatures changed from `resource-lock:*` to `filament-resource-lock:*` (e.g. `filament-resource-lock:install`)
-You can install the package via composer:
+5. **Filament version** — this package requires **Filament v5**. The original `kenepa/resource-lock` targets Filament v3/v4.
+
+Quick steps:
```bash
-composer require kenepa/resource-lock
+composer remove kenepa/resource-lock
+composer require blendbyte/filament-resource-lock
+php artisan filament-resource-lock:install
```
-Then run the installation command to publish and run migration(s)
+Then update all `use Kenepa\ResourceLock\...` import statements to `use Blendbyte\FilamentResourceLock\...`.
+
+## Installation
```bash
-php artisan resource-lock:install
+composer require blendbyte/filament-resource-lock
```
-Register plugin with a panel
+Then run the installation command to publish and run the migration:
+
+```bash
+php artisan filament-resource-lock:install
+```
+
+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 +62,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 +93,90 @@ 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)
+### Relation manager locking
-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 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 Kenepa\ResourceLock\ResourceLockPlugin;
-use Filament\Panel;
-
-public function panel(Panel $panel): Panel
+use Blendbyte\FilamentResourceLock\Resources\Pages\Concerns\UsesRelationManagerResourceLock;
+
+class PostCommentsRelationManager extends RelationManager
{
- return $panel
- // ...
- ->plugin(ResourceLockPlugin::make()
- ->usesPollingToDetectPresence()
- ->presencePollingInterval(10)
- ->lockTimeout(15)
- );
+ use UsesRelationManagerResourceLock;
+
+ protected static string $relationship = 'comments';
}
```
-> **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.
+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 Configuration
+## Polling (SPA mode)
-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.
+To support SPA mode, enable polling-based presence detection in the plugin:
-):
+```php
+->plugin(ResourceLockPlugin::make()
+ ->usesPollingToDetectPresence()
+ ->presencePollingInterval(10)
+ ->lockTimeout(15)
+)
+```
-- **`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.
+> **Tip:** Make sure the lock timeout is not lower than the polling interval — otherwise the lock may expire before the next heartbeat is sent.
-## Resource Lock manager
+Additional polling options:
+- **`pollingKeepAlive()`**: Keeps polling alive when the tab is in the background.
+- **`pollingVisible()`**: Only polls when the browser tab is visible.
-
+## Resource Lock Manager
-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
-
-
-
-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
-
-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)
+### Custom lock owner display
-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,80 +187,39 @@ 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.
-
## 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 0e6d53a..e139007 100644
--- a/composer.json
+++ b/composer.json
@@ -1,42 +1,40 @@
{
- "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",
- "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",
+ "larastan/larastan": "^3.0",
+ "orchestra/testbench": "^9.0|^10.0|^11.0",
+ "pestphp/pest": "^3.7",
+ "pestphp/pest-plugin-laravel": "^3.1",
"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": {
- "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": {
@@ -51,7 +49,6 @@
"config": {
"sort-packages": true,
"allow-plugins": {
- "composer/package-versions-deprecated": true,
"pestphp/pest-plugin": true,
"phpstan/extension-installer": true
}
@@ -59,10 +56,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 84%
rename from config/resource-lock.php
rename to config/filament-resource-lock.php
index 5687999..8219b90 100644
--- a/config/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' => \Kenepa\ResourceLock\Resources\LockResource::class,
+ 'class' => LockResource::class,
],
/*
@@ -125,6 +129,22 @@
*/
'actions' => [
- 'get_resource_lock_owner_action' => \Kenepa\ResourceLock\Actions\GetResourceLockOwnerAction::class,
+ '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/database/factories/ResourceLockFactory.php b/database/factories/ResourceLockFactory.php
index 2d4fd2e..f1579ea 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 @@ 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 dbb4460..a2d289f 100644 --- a/src/Console/Commands/ResourceLockClearExpiredCommand.php +++ b/src/Console/Commands/ResourceLockClearExpiredCommand.php @@ -1,14 +1,14 @@ 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/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 @@ +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/Models/Concerns/HasLocks.php b/src/Models/Concerns/HasLocks.php index 6f8308d..ce851e4 100644 --- a/src/Models/Concerns/HasLocks.php +++ b/src/Models/Concerns/HasLocks.php @@ -1,11 +1,15 @@ 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 2c43196..64b7584 100644 --- a/src/Models/ResourceLock.php +++ b/src/Models/ResourceLock.php @@ -1,14 +1,18 @@ 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', GetResourceLockOwnerAction::class); } public function usesPollingToDetectPresence(bool $enable = true): static @@ -356,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/ResourceLockServiceProvider.php b/src/ResourceLockServiceProvider.php index ae08e32..a8cc255 100644 --- a/src/ResourceLockServiceProvider.php +++ b/src/ResourceLockServiceProvider.php @@ -1,16 +1,31 @@ callAfterResolving('livewire.finder', function ($finder) { + $finder->addComponent('filament-resource-lock-observer', class: Http\Livewire\ResourceLockObserver::class); + }); + } + + public function packageBooted(): void + { + parent::packageBooted(); + + Livewire::component('filament-resource-lock-observer', Http\Livewire\ResourceLockObserver::class); + } public function configurePackage(Package $package): void { @@ -23,7 +38,7 @@ public function configurePackage(Package $package): void $command ->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..378e5e6 100644 --- a/src/Resources/LockResource.php +++ b/src/Resources/LockResource.php @@ -1,17 +1,19 @@ columns([ @@ -48,7 +50,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()) { @@ -65,28 +68,48 @@ public static function table(Table $table): Tables\Table return 'heroicon-o-lock-closed'; })->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([ // ]) - ->actions([ + ->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(__('resource-lock::manager.unlocked')) - ->label(__('resource-lock::manager.unlock')), + ->successNotificationTitle(__('filament-resource-lock::manager.unlocked')) + ->label(__('filament-resource-lock::manager.unlock')), ]) - ->bulkActions([ + ->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') - ->successNotificationTitle(__('resource-lock::manager.unlocked_selected')) - ->label(__('resource-lock::manager.unlock')), + ->successNotificationTitle(__('filament-resource-lock::manager.unlocked_selected')) + ->label(__('filament-resource-lock::manager.unlock')), ]); } @@ -100,7 +123,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 +134,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; @@ -121,7 +148,7 @@ public static function getNavigationBadge(): ?string return null; } - return static::getModel()::count(); + return (string) static::getModel()::count(); } public static function getNavigationLabel(): string diff --git a/src/Resources/LockResource/ManageResourceLocks.php b/src/Resources/LockResource/ManageResourceLocks.php index e51015e..f8941d1 100644 --- a/src/Resources/LockResource/ManageResourceLocks.php +++ b/src/Resources/LockResource/ManageResourceLocks.php @@ -1,11 +1,12 @@ 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()) + ->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/src/Resources/Pages/Concerns/UsesLocks.php b/src/Resources/Pages/Concerns/UsesLocks.php index 84f2cee..df678a8 100644 --- a/src/Resources/Pages/Concerns/UsesLocks.php +++ b/src/Resources/Pages/Concerns/UsesLocks.php @@ -1,6 +1,8 @@ dispatch('disablePollingInResourceLockObserver'); } + #[On('resourceLockObserver::renewLock')] public function renewLock() { $record = $this->record ?? $this->resourceRecord; 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 8dcf385..a8e853e 100644 --- a/src/Resources/Pages/Concerns/UsesResourceLock.php +++ b/src/Resources/Pages/Concerns/UsesResourceLock.php @@ -1,8 +1,9 @@ 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,11 +39,15 @@ 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)) { - $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 9c91cac..b9ab2e3 100644 --- a/src/Resources/Pages/Concerns/UsesSimpleResourceLock.php +++ b/src/Resources/Pages/Concerns/UsesSimpleResourceLock.php @@ -1,8 +1,9 @@ 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::unloadSimple')] public function resourceLockObserverUnload() { $this->resourceRecord->unlock(); $this->disablePolling(); } + #[On('resourceLockObserver::unlock')] public function resourceLockObserverUnlock() { if ($this->resourceRecord->unlock(force: true)) { 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: [], -} diff --git a/tests/Feature/ConfigureResourceLockPluginTest.php b/tests/Feature/ConfigureResourceLockPluginTest.php index 0bb2d3e..e1eba4d 100644 --- a/tests/Feature/ConfigureResourceLockPluginTest.php +++ b/tests/Feature/ConfigureResourceLockPluginTest.php @@ -2,14 +2,14 @@ declare(strict_types=1); -use Kenepa\ResourceLock\ResourceLockPlugin; +use Blendbyte\FilamentResourceLock\ResourceLockPlugin; function getResourceLockNavigationItem($panel, string $group = 'Settings', string $label = 'Resource Lock Manager') { $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/ResourceLockCommandTest.php b/tests/Feature/ResourceLockCommandTest.php index fdaa76c..ec463ea 100644 --- a/tests/Feature/ResourceLockCommandTest.php +++ b/tests/Feature/ResourceLockCommandTest.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-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-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-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-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-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-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-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-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-resource-lock:clear-expired --force') ->expectsOutput('No expired resource locks found to clear.') ->assertExitCode(0); 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/Feature/ResourceLockObserverTest.php b/tests/Feature/ResourceLockObserverTest.php index 3fc68dc..e495e57 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 () { @@ -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/Filament/Resources/LockResource/LockResourceTest.php b/tests/Filament/Resources/LockResource/LockResourceTest.php index cb9a720..9122217 100644 --- a/tests/Filament/Resources/LockResource/LockResourceTest.php +++ b/tests/Filament/Resources/LockResource/LockResourceTest.php @@ -2,16 +2,15 @@ declare(strict_types=1); -use Kenepa\ResourceLock\Resources\LockResource\ManageResourceLocks; - -use function Pest\Livewire\livewire; +use Blendbyte\FilamentResourceLock\Resources\LockResource\ManageResourceLocks; +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'); }); diff --git a/tests/Fixtures/AdminPanelProvider.php b/tests/Fixtures/AdminPanelProvider.php index aa86170..cc1f59b 100644 --- a/tests/Fixtures/AdminPanelProvider.php +++ b/tests/Fixtures/AdminPanelProvider.php @@ -1,7 +1,8 @@ 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..2ac652c 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,26 +38,25 @@ 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'; + $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) { $providers = [ ActionsServiceProvider::class, - BladeCaptureDirectiveServiceProvider::class, BladeHeroiconsServiceProvider::class, BladeIconsServiceProvider::class, FilamentServiceProvider::class, diff --git a/tests/Unit/modelHasLockTest.php b/tests/Unit/modelHasLockTest.php index abb717f..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(); + }); +});