diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b9c96f..4a9e11d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,15 @@ All notable changes to `:package_name` will be documented in this file. -## v1.3.6 - 2026-07-13 +## v1.3.7 - 2026-07-13 ### What's Changed +* Assign Users / Manage Permissions: add **Select all matching users** toggle to select every user matching the current filters (not just the first 50 search results) +* Replace built-in `user_filters` config with host-owned `assignment.filter_provider` contract for domain-specific assignment filters * Fix `sharing.show_nested_shared_in_library` so it also gates root-level shared items in the main Library view (classic default `false` restores pre-v1.3.4 behavior) -**Full Changelog**: https://github.com/TappNetwork/Filament-Library/compare/v1.3.5...v1.3.6 +**Full Changelog**: https://github.com/TappNetwork/Filament-Library/compare/v1.3.5...v1.3.7 ## v1.3.5 - 2026-07-10 diff --git a/config/filament-library.php b/config/filament-library.php index bf845f2..9dadae1 100644 --- a/config/filament-library.php +++ b/config/filament-library.php @@ -65,30 +65,22 @@ /* |-------------------------------------------------------------------------- - | User assignment filters + | User assignment filters (host-provided) |-------------------------------------------------------------------------- | - | Optional filters shown when filter-based assignment (or the bulk manage - | action) is enabled. Host apps can enable community and role filters by - | pointing these options at their own models. + | When filter-based assignment is enabled, register a class that implements + | Tapp\FilamentLibrary\Contracts\UserAssignmentFilterProvider. The host app + | owns domain-specific filters (e.g. community, role, signup date). + | + | Example (in your app's config/filament-library.php): + | + | 'assignment' => [ + | 'filter_provider' => \App\Library\LibraryUserAssignmentFilterProvider::class, + | ], | */ - 'user_filters' => [ - 'community' => [ - 'enabled' => false, - 'model' => null, - 'title_attribute' => 'name', - 'user_foreign_key' => 'community_id', - ], - 'role' => [ - 'enabled' => false, - 'model' => null, - 'title_attribute' => 'name', - ], - 'signup_date' => [ - 'enabled' => false, - 'column' => 'created_at', - ], + 'assignment' => [ + 'filter_provider' => null, ], /* diff --git a/src/Contracts/UserAssignmentFilterProvider.php b/src/Contracts/UserAssignmentFilterProvider.php new file mode 100644 index 0000000..9b935aa --- /dev/null +++ b/src/Contracts/UserAssignmentFilterProvider.php @@ -0,0 +1,30 @@ + + */ + public function schema(): array; + + /** + * Form field names whose values are passed to {@see apply()}. + * + * @return list + */ + public function filterKeys(): array; + + /** + * Scope the user query using filter form state. + * + * @param array $filters + */ + public function apply(Builder $query, array $filters): Builder; +} diff --git a/src/Resources/RelationManagers/LibraryItemPermissionsRelationManager.php b/src/Resources/RelationManagers/LibraryItemPermissionsRelationManager.php index c96d204..f04d760 100644 --- a/src/Resources/RelationManagers/LibraryItemPermissionsRelationManager.php +++ b/src/Resources/RelationManagers/LibraryItemPermissionsRelationManager.php @@ -213,7 +213,7 @@ protected function filterBasedCreateAction(): CreateAction return CreateAction::make() ->label('Assign Users') ->modalHeading('Assign User Permissions') - ->modalDescription('Filter users by community, user level, and sign-up date, then assign permissions in bulk.') + ->modalDescription('Filter users, then assign permissions in bulk.') ->schema([ ...UserAssignmentFilters::schema('user_ids'), Forms\Components\Select::make('role') diff --git a/src/Support/NullUserAssignmentFilterProvider.php b/src/Support/NullUserAssignmentFilterProvider.php new file mode 100644 index 0000000..842dba8 --- /dev/null +++ b/src/Support/NullUserAssignmentFilterProvider.php @@ -0,0 +1,30 @@ + + */ + public function schema(): array + { + return []; + } + + /** + * @return list + */ + public function filterKeys(): array + { + return []; + } + + public function apply(Builder $query, array $filters): Builder + { + return $query; + } +} diff --git a/src/Support/UserAssignmentFilters.php b/src/Support/UserAssignmentFilters.php index cbd7eea..1427421 100644 --- a/src/Support/UserAssignmentFilters.php +++ b/src/Support/UserAssignmentFilters.php @@ -2,13 +2,14 @@ namespace Tapp\FilamentLibrary\Support; -use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\Field; -use Filament\Forms\Components\Select; +use Filament\Forms\Components\Toggle; use Filament\Schemas\Components\Utilities\Get; +use Filament\Schemas\Components\Utilities\Set; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Schema; +use Tapp\FilamentLibrary\Contracts\UserAssignmentFilterProvider; use Tapp\FilamentLibrary\Forms\Components\UserSearchSelect; class UserAssignmentFilters @@ -19,7 +20,8 @@ class UserAssignmentFilters public static function schema(string $usersField = 'user_ids'): array { return [ - ...static::filterFields(), + ...static::filterFields($usersField), + static::selectAllMatchingToggle($usersField), static::usersSelect($usersField), ]; } @@ -27,43 +29,38 @@ public static function schema(string $usersField = 'user_ids'): array /** * @return array */ - public static function filterFields(): array + public static function filterFields(string $usersField = 'user_ids'): array { - $fields = []; - - if (static::communityFilterEnabled()) { - $fields[] = Select::make('community_id') - ->label('Community') - ->options(fn (): array => static::communityOptions()) - ->searchable() - ->preload() - ->live() - ->placeholder('All communities'); - } + $provider = static::provider(); - if (static::roleFilterEnabled()) { - $fields[] = Select::make('role_name') - ->label('User Level') - ->options(fn (): array => static::roleOptions()) - ->searchable() - ->preload() - ->live() - ->placeholder('All user levels'); + if (! $provider instanceof UserAssignmentFilterProvider) { + return []; } - if (static::signupDateFilterEnabled()) { - $fields[] = DatePicker::make('signed_up_from') - ->label('Signed Up From') - ->live() - ->native(false); + return collect($provider->schema()) + ->map(function (Field $field) use ($usersField): Field { + return $field + ->live() + ->afterStateUpdated(fn (Get $get, Set $set): mixed => static::refreshSelectedUsersIfSelectingAll($get, $set, $usersField)); + }) + ->all(); + } - $fields[] = DatePicker::make('signed_up_until') - ->label('Signed Up Until') - ->live() - ->native(false); - } + public static function selectAllMatchingToggle(string $usersField = 'user_ids'): Toggle + { + return Toggle::make('select_all_matching') + ->label('Select all matching users') + ->helperText('Selects every user matching the filters above, not just the first 50 search results. With no filters, this selects all users.') + ->live() + ->afterStateUpdated(function (Get $get, Set $set, mixed $state) use ($usersField): void { + if ($state) { + $set($usersField, static::filteredUserIds($get)); + + return; + } - return $fields; + $set($usersField, []); + }); } public static function usersSelect(string $name = 'user_ids'): UserSearchSelect @@ -72,7 +69,7 @@ public static function usersSelect(string $name = 'user_ids'): UserSearchSelect ->label('Users') ->placeholder('Search for users by name or email...') ->required() - ->helperText('Select one or more users matching the filters above.') + ->helperText('Select one or more users matching the filters above, or use “Select all matching users”.') ->options(fn (Get $get): array => static::filteredUserOptions($get)) ->getSearchResultsUsing(fn (string $search, Get $get): array => static::filteredUserOptions($get, $search)) ->getOptionLabelsUsing(fn (array $values): array => static::labelsForIds($values)); @@ -98,25 +95,10 @@ public static function applyFilters(Builder $query, array $filters, ?string $sea }); } - if (static::communityFilterEnabled() && filled($filters['community_id'] ?? null)) { - $foreignKey = config('filament-library.user_filters.community.user_foreign_key', 'community_id'); - $query->where($foreignKey, $filters['community_id']); - } + $provider = static::provider(); - if (static::roleFilterEnabled() && filled($filters['role_name'] ?? null)) { - $query->whereHas('roles', function (Builder $roleQuery) use ($filters): void { - $roleQuery->where('name', $filters['role_name']); - }); - } - - $signupColumn = config('filament-library.user_filters.signup_date.column', 'created_at'); - - if (static::signupDateFilterEnabled() && filled($filters['signed_up_from'] ?? null)) { - $query->whereDate($signupColumn, '>=', $filters['signed_up_from']); - } - - if (static::signupDateFilterEnabled() && filled($filters['signed_up_until'] ?? null)) { - $query->whereDate($signupColumn, '<=', $filters['signed_up_until']); + if ($provider instanceof UserAssignmentFilterProvider) { + $query = $provider->apply($query, $filters); } return $query; @@ -127,12 +109,7 @@ public static function applyFilters(Builder $query, array $filters, ?string $sea */ public static function filteredUserOptions(Get $get, ?string $search = null): array { - $filters = [ - 'community_id' => $get('community_id'), - 'role_name' => $get('role_name'), - 'signed_up_from' => $get('signed_up_from'), - 'signed_up_until' => $get('signed_up_until'), - ]; + $filters = static::filtersFromGet($get); $userModel = static::userModel(); @@ -145,6 +122,19 @@ public static function filteredUserOptions(Get $get, ?string $search = null): ar ->all(); } + /** + * @return list + */ + public static function filteredUserIds(Get $get): array + { + $filters = static::filtersFromGet($get); + $userModel = static::userModel(); + + return static::applyFilters($userModel::query(), $filters) + ->pluck((new $userModel)->getKeyName()) + ->all(); + } + /** * @param array $values * @return array @@ -182,50 +172,45 @@ public static function userModel(): string return config('filament-library.user_model', config('auth.providers.users.model', 'App\\Models\\User')); } - public static function communityFilterEnabled(): bool + public static function provider(): ?UserAssignmentFilterProvider { - return (bool) config('filament-library.user_filters.community.enabled', false) - && filled(config('filament-library.user_filters.community.model')); - } + $class = config('filament-library.assignment.filter_provider'); - public static function roleFilterEnabled(): bool - { - return (bool) config('filament-library.user_filters.role.enabled', false) - && filled(config('filament-library.user_filters.role.model')); - } + if (! filled($class)) { + return null; + } - public static function signupDateFilterEnabled(): bool - { - return (bool) config('filament-library.user_filters.signup_date.enabled', true); + $provider = app($class); + + if (! $provider instanceof UserAssignmentFilterProvider) { + return null; + } + + return $provider; } /** - * @return array + * @return array */ - protected static function communityOptions(): array + protected static function filtersFromGet(Get $get): array { - /** @var class-string $model */ - $model = config('filament-library.user_filters.community.model'); - $title = config('filament-library.user_filters.community.title_attribute', 'name'); + $provider = static::provider(); + + if (! $provider instanceof UserAssignmentFilterProvider) { + return []; + } - return $model::query() - ->orderBy($title) - ->pluck($title, (new $model)->getKeyName()) + return collect($provider->filterKeys()) + ->mapWithKeys(fn (string $key): array => [$key => $get($key)]) ->all(); } - /** - * @return array - */ - protected static function roleOptions(): array + protected static function refreshSelectedUsersIfSelectingAll(Get $get, Set $set, string $usersField): void { - /** @var class-string $model */ - $model = config('filament-library.user_filters.role.model'); - $title = config('filament-library.user_filters.role.title_attribute', 'name'); + if (! $get('select_all_matching')) { + return; + } - return $model::query() - ->orderBy($title) - ->pluck($title, $title) - ->all(); + $set($usersField, static::filteredUserIds($get)); } }