diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c68765b..4597dfb 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: :vendor_name +github: TappNetwork diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 12ab7c2..32bae48 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,3 +1,3 @@ # Security Policy -If you discover any security related issues, please email author@domain.com instead of using the issue tracker. +If you discover any security related issues, please email steve@tappnetwork.com instead of using the issue tracker. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..15a0cc2 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,3 @@ +# Details + +Details of the feature / fix this PR addresses. diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 3855a08..d9306d6 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -16,7 +16,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.3' coverage: none - name: Install composer dependencies diff --git a/README.md b/README.md index 3eca440..69f5599 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,14 @@ A comprehensive file and document management system for Filament applications, f ## Features - **📁 File & Folder Management** - Upload files, create folders, and organize content -- **🔗 External Links** - Add and manage external links with descriptions +- **🔗 External Links** - Add and manage external links with descriptions (including video embeds) - **👥 Advanced Permissions** - Google Drive-style ownership with Creator, Owner, Editor, and Viewer roles - **🔄 Automatic Inheritance** - Permissions automatically inherit from parent folders -- **🔍 Multiple Views** - Public Library, My Documents, Shared with Me, Created by Me, and Search All +- **🔍 Multiple Views** - Public Library, My Documents, Shared with Me, Created by Me, Favorites, and Search All +- **🏷️ Tags & Favorites** - Organize items with tags and mark favorites for quick access - **⚙️ Configurable Admin Access** - Flexible admin role configuration - **🎨 Filament Integration** - Native Filament UI components and navigation +- **🏢 Multi-Tenancy Support** - Optional team/organization scoping for all library content ## Installation @@ -35,6 +37,9 @@ class User extends Authenticatable } ``` +> [!WARNING] +> If you are using multi-tenancy please see the "Multi-Tenancy Support" instructions below **before** publishing and running migrations. + You can publish and run the migrations with: ```bash @@ -86,16 +91,17 @@ public function boot() ### 3. Navigation -The plugin automatically adds navigation items: +The plugin automatically adds navigation items under "Resource Library": - **Library** - Main library view - **Search All** - Search across all accessible content - **My Documents** - Personal documents and folders - **Shared with Me** - Items shared by other users - **Created by Me** - Items you created +- **Favorites** - Items you've marked as favorites ## Permissions System -The plugin features a sophisticated permissions system inspired by Google Drive. See [Permissions Documentation](docs/permissions.md) for complete details. +The plugin features a sophisticated permissions system inspired by Google Drive. ### Quick Overview @@ -110,29 +116,95 @@ The plugin features a sophisticated permissions system inspired by Google Drive. - **Permission Inheritance** - Child items inherit parent folder permissions - **Admin Override** - Library admins can access all content +## Multi-Tenancy Support + +Filament Library includes built-in support for multi-tenancy, allowing you to scope library items, permissions, and tags to specific tenants (e.g., teams, organizations, workspaces). + +### ⚠️ Important: Enable Tenancy Before Migrations + +**You MUST configure and enable tenancy in the config file BEFORE running the migrations.** The migrations check the tenancy configuration to determine whether to add tenant columns to the database tables. If you enable tenancy after running migrations, you'll need to manually add the tenant columns to your database. + +### Quick Setup + +1. **Configure your Filament panel with tenancy** (see [Filament Tenancy docs](https://filamentphp.com/docs/4.x/users/tenancy)) +2. **Publish the config file**: + ```bash + php artisan vendor:publish --tag="filament-library-config" + ``` +3. **Enable tenancy in `config/filament-library.php`**: + ```php + 'tenancy' => [ + 'enabled' => true, // ⚠️ Set this BEFORE running migrations! + 'model' => \App\Models\Team::class, + ], + ``` +4. **Run migrations**: + ```bash + php artisan migrate + ``` + +For complete setup instructions, troubleshooting, and advanced configuration, see [TENANCY.md](TENANCY.md). + ## Configuration -### Admin Role Configuration +The config file (`config/filament-library.php`) includes the following options: + +### User Model ```php -// config/filament-library.php -return [ - 'admin_role' => 'Admin', // Default admin role - 'admin_callback' => null, // Custom callback function -]; +'user_model' => env('FILAMENT_LIBRARY_USER_MODEL', 'App\\Models\\User'), ``` -### Environment Variables +Specify the user model for the application. -```env -LIBRARY_ADMIN_ROLE=super-admin +### Video Link Support (Optional) + +The library supports video links from various platforms. To customize supported domains, add this to your config: + +```php +'video' => [ + 'supported_domains' => [ + 'youtube.com', + 'youtu.be', + 'vimeo.com', + 'wistia.com', + ], +], +``` + +### Secure File URLs (Optional) + +Configure how long temporary download URLs remain valid: + +```php +'url' => [ + 'temporary_expiration_minutes' => 60, // Default: 60 minutes +], ``` -## Documentation +### Admin Access Configuration (Optional) + +To configure which users can access admin features, add this to your config: + +```php +'admin_role' => 'Admin', // Role name to check +'admin_callback' => null, // Custom callback function +``` + +Or set it programmatically in your `AppServiceProvider`: + +```php +use Tapp\FilamentLibrary\FilamentLibraryPlugin; + +public function boot() +{ + FilamentLibraryPlugin::setLibraryAdminCallback(function ($user) { + return $user->hasRole('super-admin'); + }); +} +``` -- [Permissions System](docs/permissions.md) - Complete permissions guide -- [Customization Guide](docs/customization.md) - Customizing admin access -- [API Reference](docs/api.md) - Developer documentation +**Note:** By default, users have an `isLibraryAdmin()` method that returns `false`. You can override this in your User model for custom logic. ## Testing diff --git a/TENANCY.md b/TENANCY.md new file mode 100644 index 0000000..06d1504 --- /dev/null +++ b/TENANCY.md @@ -0,0 +1,242 @@ +# Multi-Tenancy Support in Filament Library + +This guide provides detailed instructions for setting up multi-tenancy in Filament Library. + +## ⚠️ CRITICAL: Enable Tenancy Before Migrations + +**You MUST configure and enable tenancy in the config file BEFORE running the plugin's migrations.** + +The migrations check the `filament-library.tenancy.enabled` config value to determine whether to add tenant columns (e.g.: `team_id` or your custom column) to the database tables. If you enable tenancy after running migrations, you'll need to manually add the tenant columns to your database. + +## Setup Order + +1. **First**: Configure tenancy in your application's Filament panel +2. **Second**: Publish and configure the Filament Library config file +3. **Third**: Enable tenancy in the Library config +4. **Fourth**: Run the migrations + +## Step-by-Step Setup + +### 1. Configure Tenancy in Your Filament Panel + +First, set up multi-tenancy in your Filament admin panel (in the examples below we are using `Team` as tenant): + +```php +// app/Providers/Filament/AdminPanelProvider.php + +use App\Models\Team; + +public function panel(Panel $panel): Panel +{ + return $panel + ->tenant(Team::class) + // ... other configuration +} +``` + +Make sure your `User` model implements the required contracts: + +```php +use Filament\Models\Contracts\FilamentUser; +use Filament\Models\Contracts\HasTenants; +use Filament\Panel; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; + +class User extends Authenticatable implements FilamentUser, HasTenants +{ + public function canAccessPanel(Panel $panel): bool + { + return true; + } + + public function teams(): BelongsToMany + { + return $this->belongsToMany(Team::class); + } + + public function getTenants(Panel $panel): Collection + { + return $this->teams; + } + + public function canAccessTenant(Model $tenant): bool + { + return $this->teams()->whereKey($tenant)->exists(); + } +} +``` + +Your `Team` model should implement `HasName`: + +```php +use Filament\Models\Contracts\HasName; + +class Team extends Model implements HasName +{ + public function getFilamentName(): string + { + return $this->name; + } + + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class); + } +} +``` + +### 2. Publish the Library Config File + +Publish the Filament Library config file: + +```bash +php artisan vendor:publish --tag="filament-library-config" +``` + +### 3. Enable Tenancy in the Library Config + +Edit `config/filament-library.php` and configure the tenancy settings: + +```php +'tenancy' => [ + /* + | Enable or disable tenancy support + */ + 'enabled' => true, // ⚠️ Set this BEFORE running migrations! + + /* + | The tenant model class (e.g., App\Models\Team::class) + */ + 'model' => \App\Models\Team::class, + + /* + | The name of the relationship to the tenant (optional, defaults to 'tenant') + */ + 'relationship_name' => 'team', // Optional: customize relationship name + + /* + | The name of the tenant foreign key column (optional, defaults to 'team_id') + */ + 'column' => 'team_id', // Optional: customize column name +], +``` + +### 4. Run the Migrations + +Now run the migrations. The tenant columns will be added automatically: + +```bash +php artisan migrate +``` + +## What Gets Added to the Database + +When tenancy is enabled, the following tables get a tenant foreign key column: + +- `library_items` - gets `team_id` (or your custom column name) +- `library_item_permissions` - gets `team_id` +- `library_item_tags` - gets `team_id` + +The `library_item_favorites` pivot table does NOT get a tenant column as it's a simple many-to-many pivot between users and library items. + +## How It Works + +### Automatic Tenant Assignment + +When you create library items, permissions, or tags through Filament, the tenant ID is automatically assigned: + +1. **For resources with dedicated Filament Resources** (like `LibraryItem`): + - Filament's built-in observer automatically sets the tenant ID + - This happens seamlessly when you create items through the admin panel + +2. **For child models** (like `LibraryItemPermission`): + - If the tenant ID is already set, nothing happens (Filament's observer took care of it) + - If not set, the trait automatically inherits the tenant from the parent `LibraryItem` + +3. **For models without dedicated resources** (created via relation managers or custom code): + - The `BelongsToTenant` trait provides a fallback + - It first checks if Filament has already set the tenant + - If not, it tries to inherit from parent relationships + - This ensures tenant ID is always set correctly + +### Parent-Child Relationships + +The tenant is automatically inherited through these relationships: + +- `LibraryItemPermission` → inherits from `LibraryItem` +- Child `LibraryItem` (when `parent_id` is set) → inherits from parent `LibraryItem` +- `LibraryItemTag` → gets tenant from Filament context + +### URL Structure + +With tenancy enabled, your Library URLs will include the tenant: + +``` +/admin/{tenant}/library +/admin/{tenant}/library/my-documents +/admin/{tenant}/library/shared-with-me +``` + +For example: +``` +/admin/acme-corp/library +/admin/acme-corp/library/1/edit-file +``` + +### Scoping + +All Library resources are automatically scoped to the current tenant: + +- Users can only see library items belonging to their current tenant +- Permissions and tags are also scoped to the tenant +- Personal folders are created per tenant (a user has a separate personal folder for each team) + +## Disabling Tenancy + +If you need to disable tenancy after enabling it: + +1. Set `'enabled' => false` in `config/filament-library.php` +2. Note: This will NOT remove the tenant columns from your database +3. The columns will simply be ignored by the application + +## Important Notes + +- **Migration Order**: You MUST enable tenancy in the config BEFORE running migrations +- **Existing Data**: If you have existing library data and enable tenancy later, you'll need to manually add tenant columns and populate them +- **Personal Folders**: Each user gets a separate personal folder per tenant +- **URL Changes**: Enabling tenancy changes your URL structure to include the tenant slug +- **Access Control**: Make sure users are assigned to teams to access the Library + +## Troubleshooting + +### "Field 'team_id' doesn't have a default value" Error + +This means: +1. Tenancy is enabled in the config +2. The migration added the `team_id` column +3. But the model can't determine the tenant automatically + +**Solution**: Make sure you're creating library items through Filament's admin panel with a tenant context, or manually set the tenant when creating items programmatically. + +### Existing Data Migration + +If you need to add tenancy to an existing installation: + +1. Backup your database +2. Manually add the tenant columns: + ```sql + ALTER TABLE library_items ADD COLUMN team_id BIGINT UNSIGNED NOT NULL; + ALTER TABLE library_item_permissions ADD COLUMN team_id BIGINT UNSIGNED NOT NULL; + ALTER TABLE library_item_tags ADD COLUMN team_id BIGINT UNSIGNED NOT NULL; + + -- Add foreign keys + ALTER TABLE library_items ADD FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE; + ALTER TABLE library_item_permissions ADD FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE; + ALTER TABLE library_item_tags ADD FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE; + ``` +3. Update existing records with appropriate tenant IDs +4. Enable tenancy in the config + +## Support + +For issues or questions about multi-tenancy in Filament Library, please open an issue on the GitHub repository. diff --git a/config/filament-library.php b/config/filament-library.php index 2b288b4..cb93f80 100644 --- a/config/filament-library.php +++ b/config/filament-library.php @@ -13,4 +13,74 @@ */ 'user_model' => env('FILAMENT_LIBRARY_USER_MODEL', 'App\\Models\\User'), + /* + |-------------------------------------------------------------------------- + | Video Link Support + |-------------------------------------------------------------------------- + | + | Configure which video platforms are supported for link embeds. + | When a library item is a link to one of these domains, it will be + | treated as a video link and displayed accordingly. + | + */ + 'video' => [ + 'supported_domains' => [ + 'youtube.com', + 'youtu.be', + 'vimeo.com', + 'wistia.com', + ], + ], + + /* + |-------------------------------------------------------------------------- + | URL Configuration + |-------------------------------------------------------------------------- + | + | Configure how library item URLs are generated and secured. + | + */ + 'url' => [ + /* + | Number of minutes that temporary URLs remain valid. + | Used when generating secure download links for files. + */ + 'temporary_expiration_minutes' => 60, + ], + + /* + |-------------------------------------------------------------------------- + | Multi-Tenancy Configuration + |-------------------------------------------------------------------------- + | + | Enable multi-tenancy support for the Library plugin. When enabled, + | library items, permissions, and tags will be scoped to tenants. + | + | IMPORTANT: You must configure and enable tenancy BEFORE running + | the migrations. The migrations check this config to determine + | whether to add tenant columns to the database tables. + | + */ + 'tenancy' => [ + /* + | Enable or disable tenancy support + */ + 'enabled' => false, + + /* + | The tenant model class (e.g., App\Models\Team::class) + */ + 'model' => null, + + /* + | The name of the relationship to the tenant (optional, defaults to 'tenant') + */ + 'relationship_name' => null, + + /* + | The name of the tenant foreign key column (optional, defaults to 'team_id') + */ + 'column' => null, + ], + ]; diff --git a/database/migrations/2024_01_01_000000_create_library_items_table.php b/database/migrations/2024_01_01_000000_create_library_items_table.php index d48b261..3efe7e5 100644 --- a/database/migrations/2024_01_01_000000_create_library_items_table.php +++ b/database/migrations/2024_01_01_000000_create_library_items_table.php @@ -13,6 +13,15 @@ public function up(): void { Schema::create('library_items', function (Blueprint $table) { $table->id(); + + // Add tenant foreign key if tenancy is enabled + if (config('filament-library.tenancy.enabled')) { + $tenantModel = config('filament-library.tenancy.model'); + if ($tenantModel) { + $table->foreignIdFor($tenantModel)->constrained()->cascadeOnDelete(); + } + } + $table->string('name'); $table->string('slug'); $table->enum('type', ['folder', 'file', 'link']); diff --git a/database/migrations/2024_01_01_000001_create_library_item_permissions_table.php b/database/migrations/2024_01_01_000001_create_library_item_permissions_table.php index 20f2fd3..1d254ae 100644 --- a/database/migrations/2024_01_01_000001_create_library_item_permissions_table.php +++ b/database/migrations/2024_01_01_000001_create_library_item_permissions_table.php @@ -13,6 +13,15 @@ public function up(): void { Schema::create('library_item_permissions', function (Blueprint $table) { $table->id(); + + // Add tenant foreign key if tenancy is enabled + if (config('filament-library.tenancy.enabled')) { + $tenantModel = config('filament-library.tenancy.model'); + if ($tenantModel) { + $table->foreignIdFor($tenantModel)->constrained()->cascadeOnDelete(); + } + } + $table->foreignId('library_item_id')->constrained()->cascadeOnDelete(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->enum('role', ['owner', 'editor', 'viewer']); diff --git a/database/migrations/2024_01_01_000002_create_library_item_tags_table.php b/database/migrations/2024_01_01_000002_create_library_item_tags_table.php index ef19a37..914f0bf 100644 --- a/database/migrations/2024_01_01_000002_create_library_item_tags_table.php +++ b/database/migrations/2024_01_01_000002_create_library_item_tags_table.php @@ -13,6 +13,15 @@ public function up(): void { Schema::create('library_item_tags', function (Blueprint $table) { $table->id(); + + // Add tenant foreign key if tenancy is enabled + if (config('filament-library.tenancy.enabled')) { + $tenantModel = config('filament-library.tenancy.model'); + if ($tenantModel) { + $table->foreignIdFor($tenantModel)->constrained()->cascadeOnDelete(); + } + } + $table->string('name'); $table->string('slug')->unique(); $table->string('color')->nullable(); diff --git a/src/FilamentLibraryPlugin.php b/src/FilamentLibraryPlugin.php index 7420fb4..2987d2e 100644 --- a/src/FilamentLibraryPlugin.php +++ b/src/FilamentLibraryPlugin.php @@ -64,41 +64,41 @@ public function register(Panel $panel): void ]) ->navigationItems([ NavigationItem::make('Library') - ->url('/library') + ->url(fn () => \Tapp\FilamentLibrary\Resources\LibraryItemResource::getUrl('index')) ->icon('heroicon-o-building-library') ->group('Resource Library') ->sort(1) - ->isActiveWhen(fn () => request()->is('library')), + ->isActiveWhen(fn () => request()->routeIs('filament.admin.resources.library.index')), NavigationItem::make('Search All') - ->url('/library/search-all') + ->url(fn () => \Tapp\FilamentLibrary\Resources\LibraryItemResource::getUrl('search-all')) ->icon('heroicon-o-magnifying-glass') ->group('Resource Library') ->sort(2) - ->isActiveWhen(fn () => request()->is('library/search-all')), + ->isActiveWhen(fn () => request()->routeIs('filament.admin.resources.library.search-all')), NavigationItem::make('My Documents') - ->url('/library/my-documents') + ->url(fn () => \Tapp\FilamentLibrary\Resources\LibraryItemResource::getUrl('my-documents')) ->icon('heroicon-o-folder') ->group('Resource Library') ->sort(3) - ->isActiveWhen(fn () => request()->is('library/my-documents')), + ->isActiveWhen(fn () => request()->routeIs('filament.admin.resources.library.my-documents')), NavigationItem::make('Shared with Me') - ->url('/library/shared-with-me') + ->url(fn () => \Tapp\FilamentLibrary\Resources\LibraryItemResource::getUrl('shared-with-me')) ->icon('heroicon-o-share') ->group('Resource Library') ->sort(4) - ->isActiveWhen(fn () => request()->is('library/shared-with-me')), + ->isActiveWhen(fn () => request()->routeIs('filament.admin.resources.library.shared-with-me')), NavigationItem::make('Created by Me') - ->url('/library/created-by-me') + ->url(fn () => \Tapp\FilamentLibrary\Resources\LibraryItemResource::getUrl('created-by-me')) ->icon('heroicon-o-user') ->group('Resource Library') ->sort(5) - ->isActiveWhen(fn () => request()->is('library/created-by-me')), + ->isActiveWhen(fn () => request()->routeIs('filament.admin.resources.library.created-by-me')), NavigationItem::make('Favorites') - ->url('/library/favorites') + ->url(fn () => \Tapp\FilamentLibrary\Resources\LibraryItemResource::getUrl('favorites')) ->icon('heroicon-o-star') ->group('Resource Library') ->sort(6) - ->isActiveWhen(fn () => request()->is('library/favorites')), + ->isActiveWhen(fn () => request()->routeIs('filament.admin.resources.library.favorites')), ]); } diff --git a/src/Models/LibraryItem.php b/src/Models/LibraryItem.php index ac86aaf..92f437f 100644 --- a/src/Models/LibraryItem.php +++ b/src/Models/LibraryItem.php @@ -12,9 +12,11 @@ use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\MediaCollections\Models\Media; +use Tapp\FilamentLibrary\Models\Traits\BelongsToTenant; class LibraryItem extends Model implements HasMedia { + use BelongsToTenant; use HasFactory; use InteractsWithMedia; use SoftDeletes; @@ -103,21 +105,17 @@ public function children(): HasMany */ public function creator(): BelongsTo { - return $this->belongsTo(\App\Models\User::class, 'created_by')->withDefault(function () { + return $this->belongsTo(config('filament-library.user_model'), 'created_by')->withDefault(function ($instance) { // Check if 'name' field exists if (\Illuminate\Support\Facades\Schema::hasColumn('users', 'name')) { - return [ - 'name' => 'Unknown User', - 'email' => 'deleted@example.com', - ]; + $instance->name = 'Unknown User'; + $instance->email = 'deleted@example.com'; + } else { + // Fall back to first_name/last_name + $instance->first_name = 'Unknown'; + $instance->last_name = 'User'; + $instance->email = 'deleted@example.com'; } - - // Fall back to first_name/last_name - return [ - 'first_name' => 'Unknown', - 'last_name' => 'User', - 'email' => 'deleted@example.com', - ]; }); } @@ -126,7 +124,7 @@ public function creator(): BelongsTo */ public function updater(): BelongsTo { - return $this->belongsTo(\App\Models\User::class, 'updated_by'); + return $this->belongsTo(config('filament-library.user_model'), 'updated_by'); } /** @@ -221,7 +219,7 @@ public function getEffectiveRole($user): ?string /** * Get the current owner of this item. */ - public function getCurrentOwner(): ?\App\Models\User + public function getCurrentOwner(): ?Model { $ownerPermission = $this->permissions() ->where('role', 'owner') @@ -248,7 +246,7 @@ public function isCreatorOwner(): bool /** * Transfer ownership to another user. */ - public function transferOwnership(\App\Models\User $newOwner): void + public function transferOwnership(Model $newOwner): void { // Remove existing owner permissions $this->permissions()->where('role', 'owner')->delete(); @@ -271,7 +269,7 @@ public function transferOwnership(\App\Models\User $newOwner): void /** * Ensure a user has a personal folder (like Google Drive's "My Drive"). */ - public static function ensurePersonalFolder(\App\Models\User $user): self + public static function ensurePersonalFolder(Model $user): self { // Check if user already has a personal folder via the relationship if ($user->personal_folder_id) { @@ -306,7 +304,7 @@ public static function ensurePersonalFolder(\App\Models\User $user): self /** * Get a user's personal folder. */ - public static function getPersonalFolder(\App\Models\User $user): ?self + public static function getPersonalFolder(Model $user): ?self { if (! $user->personal_folder_id) { return null; @@ -318,7 +316,7 @@ public static function getPersonalFolder(\App\Models\User $user): ?self /** * Generate the personal folder name for a user. */ - public static function getPersonalFolderName(\App\Models\User $user): string + public static function getPersonalFolderName(Model $user): string { // Try to get a display name from various user fields $name = $user->first_name ?? $user->name ?? $user->email ?? 'User'; diff --git a/src/Models/LibraryItemPermission.php b/src/Models/LibraryItemPermission.php index 42f5464..1114c1f 100644 --- a/src/Models/LibraryItemPermission.php +++ b/src/Models/LibraryItemPermission.php @@ -5,9 +5,11 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Tapp\FilamentLibrary\Models\Traits\BelongsToTenant; class LibraryItemPermission extends Model { + use BelongsToTenant; use HasFactory; protected $table = 'library_item_permissions'; @@ -36,9 +38,7 @@ public function libraryItem(): BelongsTo */ public function user(): BelongsTo { - $userModel = config('auth.providers.users.model', 'App\\Models\\User'); - - return $this->belongsTo($userModel); + return $this->belongsTo(config('filament-library.user_model')); } /** diff --git a/src/Models/LibraryItemTag.php b/src/Models/LibraryItemTag.php index c52380e..ed7c922 100644 --- a/src/Models/LibraryItemTag.php +++ b/src/Models/LibraryItemTag.php @@ -4,9 +4,11 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Tapp\FilamentLibrary\Models\Traits\BelongsToTenant; class LibraryItemTag extends Model { + use BelongsToTenant; use HasFactory; protected $fillable = [ diff --git a/src/Models/Traits/BelongsToTenant.php b/src/Models/Traits/BelongsToTenant.php new file mode 100644 index 0000000..a388d63 --- /dev/null +++ b/src/Models/Traits/BelongsToTenant.php @@ -0,0 +1,107 @@ +belongsTo(config('filament-library.tenancy.model'), static::getTenantColumnName()); + } + ); + + // Automatically set tenant_id when creating a new model + static::creating(function ($model) { + $tenantColumnName = static::getTenantColumnName(); + + // Early exit if tenant foreign key is already set (e.g., by Filament's observer) + if (! empty($model->{$tenantColumnName})) { + return; + } + + $tenantRelationshipName = static::getTenantRelationshipName(); + + // Try to get tenant from Filament context (Filament's standard method) + if (class_exists(\Filament\Facades\Filament::class)) { + $tenant = \Filament\Facades\Filament::getTenant(); + + if ($tenant) { + // Use Laravel's associate() method on the BelongsTo relationship + $model->{$tenantRelationshipName}()->associate($tenant); + + return; + } + } + + // If still not set, try to infer from parent relationships + // For LibraryItemPermission, get tenant from its LibraryItem + if (method_exists($model, 'libraryItem') && isset($model->library_item_id)) { + $parentItemId = $model->library_item_id; + $parentItemClass = get_class($model->libraryItem()->getRelated()); + $parentItem = $parentItemClass::find($parentItemId); + + if ($parentItem) { + $parentTenant = $parentItem->{$tenantRelationshipName}; + + if ($parentTenant) { + $model->{$tenantRelationshipName}()->associate($parentTenant); + } + } + } + }); + } + + /** + * Get the tenant relationship. + */ + public function tenant() + { + if (! config('filament-library.tenancy.enabled')) { + return null; + } + + $tenantModel = config('filament-library.tenancy.model'); + + if (! $tenantModel) { + return null; + } + + return $this->belongsTo($tenantModel, static::getTenantColumnName()); + } + + /** + * Get the name of the tenant relationship. + */ + public static function getTenantRelationshipName(): string + { + if (! config('filament-library.tenancy.enabled')) { + return 'tenant'; + } + + return config('filament-library.tenancy.relationship_name') ?? 'tenant'; + } + + /** + * Get the name of the tenant column. + */ + public static function getTenantColumnName(): string + { + if (! config('filament-library.tenancy.enabled')) { + return 'tenant_id'; + } + + return config('filament-library.tenancy.column') ?? 'team_id'; + } +} diff --git a/src/Resources/LibraryItemResource.php b/src/Resources/LibraryItemResource.php index c4032be..750d6bb 100644 --- a/src/Resources/LibraryItemResource.php +++ b/src/Resources/LibraryItemResource.php @@ -26,6 +26,26 @@ class LibraryItemResource extends Resource protected static ?string $slug = 'library'; + /** + * Determine if the resource is scoped to a tenant. + */ + public static function isScopedToTenant(): bool + { + return config('filament-library.tenancy.enabled', false); + } + + /** + * Get the name of the tenant ownership relationship. + */ + public static function getTenantOwnershipRelationshipName(): string + { + if (! config('filament-library.tenancy.enabled')) { + return 'tenant'; + } + + return LibraryItem::getTenantRelationshipName(); + } + public static function getNavigationIcon(): ?string { return 'heroicon-o-folder'; @@ -292,7 +312,7 @@ public static function table(Table $table): Table ->toolbarActions([ BulkActionGroup::make([ DeleteBulkAction::make() - ->visible(fn (): bool => auth()->user() && auth()->user()->can('delete', LibraryItem::class)) + ->visible(fn (): bool => auth()->user() && auth()->user()->can('deleteAny', LibraryItem::class)) ->successRedirectUrl(function () { // For bulk actions, redirect to current folder (maintain current location) $currentParent = request()->get('parent'); @@ -300,9 +320,9 @@ public static function table(Table $table): Table return static::getUrl('index', $currentParent ? ['parent' => $currentParent] : []); }), RestoreBulkAction::make() - ->visible(fn (): bool => auth()->user() && auth()->user()->can('delete', LibraryItem::class)), + ->visible(fn (): bool => auth()->user() && auth()->user()->can('restoreAny', LibraryItem::class)), ForceDeleteBulkAction::make() - ->visible(fn (): bool => auth()->user() && auth()->user()->can('delete', LibraryItem::class)) + ->visible(fn (): bool => auth()->user() && auth()->user()->can('forceDeleteAny', LibraryItem::class)) ->successRedirectUrl(function () { // For bulk actions, redirect to current folder (maintain current location) $currentParent = request()->get('parent'); diff --git a/src/Resources/Pages/ListLibraryItems.php b/src/Resources/Pages/ListLibraryItems.php index 53dc70b..75d5fc2 100644 --- a/src/Resources/Pages/ListLibraryItems.php +++ b/src/Resources/Pages/ListLibraryItems.php @@ -180,9 +180,6 @@ protected function getHeaderActions(): array ->required() ->url() ->placeholder('https://example.com'), - TextInput::make('link_icon') - ->label('Icon (Heroicon name)') - ->placeholder('heroicon-o-link'), Textarea::make('link_description') ->label('Description') ->rows(3) @@ -193,7 +190,6 @@ protected function getHeaderActions(): array 'name' => $data['name'], 'type' => 'link', 'external_url' => $data['external_url'], - 'link_icon' => $data['link_icon'] ?? 'heroicon-o-link', 'link_description' => $data['link_description'] ?? null, 'parent_id' => $this->parentId, 'created_by' => auth()->user()?->id,