diff --git a/app-modules/knowledge-base/database/migrations/2024_12_10_084809_add_parent_id_to_knowledge_base_categories_table.php b/app-modules/knowledge-base/database/migrations/2024_12_10_084809_add_parent_id_to_knowledge_base_categories_table.php new file mode 100644 index 000000000..cb66697cc --- /dev/null +++ b/app-modules/knowledge-base/database/migrations/2024_12_10_084809_add_parent_id_to_knowledge_base_categories_table.php @@ -0,0 +1,55 @@ + + + Copyright © 2016-2024, Canyon GBS LLC. All rights reserved. + + Aiding App™ is licensed under the Elastic License 2.0. For more details, + see + + Notice: + + - You may not provide the software to third parties as a hosted or managed + service, where the service provides users with access to any substantial set of + the features or functionality of the software. + - You may not move, change, disable, or circumvent the license key functionality + in the software, and you may not remove or obscure any functionality in the + software that is protected by the license key. + - You may not alter, remove, or obscure any licensing, copyright, or other notices + of the licensor in the software. Any use of the licensor’s trademarks is subject + to applicable law. + - Canyon GBS LLC respects the intellectual property rights of others and expects the + same in return. Canyon GBS™ and Aiding App™ are registered trademarks of + Canyon GBS LLC, and we are committed to enforcing and protecting our trademarks + vigorously. + - The software solution, including services, infrastructure, and code, is offered as a + Software as a Service (SaaS) by Canyon GBS LLC. + - Use of this software implies agreement to the license terms and conditions as stated + in the Elastic License 2.0. + + For more information or inquiries please visit our website at + or contact us via email at legal@canyongbs.com. + + +*/ + +use Illuminate\Support\Facades\Schema; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; + +return new class () extends Migration { + public function up(): void + { + Schema::table('knowledge_base_categories', function (Blueprint $table) { + $table->foreignUuid('parent_id')->nullable()->constrained('knowledge_base_categories'); + }); + } + + public function down(): void + { + Schema::table('knowledge_base_categories', function (Blueprint $table) { + $table->dropConstrainedForeignId('parent_id'); + }); + } +}; diff --git a/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource.php b/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource.php index 72891feae..33f3592ec 100644 --- a/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource.php +++ b/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource.php @@ -43,6 +43,7 @@ use AidingApp\KnowledgeBase\Filament\Resources\KnowledgeBaseCategoryResource\Pages\ViewKnowledgeBaseCategory; use AidingApp\KnowledgeBase\Filament\Resources\KnowledgeBaseCategoryResource\Pages\CreateKnowledgeBaseCategory; use AidingApp\KnowledgeBase\Filament\Resources\KnowledgeBaseCategoryResource\Pages\ListKnowledgeBaseCategories; +use AidingApp\KnowledgeBase\Filament\Resources\KnowledgeBaseCategoryResource\RelationManagers\SubCategoriesRelationManager; class KnowledgeBaseCategoryResource extends Resource { @@ -65,4 +66,11 @@ public static function getPages(): array 'edit' => EditKnowledgeBaseCategory::route('/{record}/edit'), ]; } + + public static function getRelations(): array + { + return [ + SubCategoriesRelationManager::class, + ]; + } } diff --git a/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/Pages/EditKnowledgeBaseCategory.php b/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/Pages/EditKnowledgeBaseCategory.php index 67d55cc7d..eea78e92e 100644 --- a/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/Pages/EditKnowledgeBaseCategory.php +++ b/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/Pages/EditKnowledgeBaseCategory.php @@ -72,6 +72,11 @@ public function form(Form $form): Form ]); } + public static function getNavigationLabel(): string + { + return 'Edit'; + } + protected function getHeaderActions(): array { return [ diff --git a/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/Pages/ListKnowledgeBaseCategories.php b/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/Pages/ListKnowledgeBaseCategories.php index b22be938c..4a9c898d1 100644 --- a/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/Pages/ListKnowledgeBaseCategories.php +++ b/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/Pages/ListKnowledgeBaseCategories.php @@ -44,6 +44,8 @@ use Filament\Tables\Columns\TextColumn; use App\Filament\Tables\Columns\IdColumn; use Filament\Resources\Pages\ListRecords; +use Illuminate\Database\Eloquent\Builder; +use App\Features\KnowledgeBaseSubcategory; use Filament\Tables\Actions\BulkActionGroup; use Filament\Tables\Actions\DeleteBulkAction; use AidingApp\KnowledgeBase\Filament\Resources\KnowledgeBaseCategoryResource; @@ -55,6 +57,9 @@ class ListKnowledgeBaseCategories extends ListRecords public function table(Table $table): Table { return $table + ->modifyQueryUsing( + fn (Builder $query) => $query->when(KnowledgeBaseSubcategory::active(), fn (Builder $query) => $query->doesntHave('parentCategory')) + ) ->columns([ IdColumn::make(), TextColumn::make('name') diff --git a/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/Pages/ViewKnowledgeBaseCategory.php b/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/Pages/ViewKnowledgeBaseCategory.php index ee5c9f82c..44d10509f 100644 --- a/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/Pages/ViewKnowledgeBaseCategory.php +++ b/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/Pages/ViewKnowledgeBaseCategory.php @@ -70,6 +70,11 @@ public function infolist(Infolist $infolist): Infolist ]); } + public static function getNavigationLabel(): string + { + return 'View'; + } + protected function getHeaderActions(): array { return [ diff --git a/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/RelationManagers/SubCategoriesRelationManager.php b/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/RelationManagers/SubCategoriesRelationManager.php new file mode 100644 index 000000000..f36db0acb --- /dev/null +++ b/app-modules/knowledge-base/src/Filament/Resources/KnowledgeBaseCategoryResource/RelationManagers/SubCategoriesRelationManager.php @@ -0,0 +1,154 @@ + + + Copyright © 2016-2024, Canyon GBS LLC. All rights reserved. + + Aiding App™ is licensed under the Elastic License 2.0. For more details, + see + + Notice: + + - You may not provide the software to third parties as a hosted or managed + service, where the service provides users with access to any substantial set of + the features or functionality of the software. + - You may not move, change, disable, or circumvent the license key functionality + in the software, and you may not remove or obscure any functionality in the + software that is protected by the license key. + - You may not alter, remove, or obscure any licensing, copyright, or other notices + of the licensor in the software. Any use of the licensor’s trademarks is subject + to applicable law. + - Canyon GBS LLC respects the intellectual property rights of others and expects the + same in return. Canyon GBS™ and Aiding App™ are registered trademarks of + Canyon GBS LLC, and we are committed to enforcing and protecting our trademarks + vigorously. + - The software solution, including services, infrastructure, and code, is offered as a + Software as a Service (SaaS) by Canyon GBS LLC. + - Use of this software implies agreement to the license terms and conditions as stated + in the Elastic License 2.0. + + For more information or inquiries please visit our website at + or contact us via email at legal@canyongbs.com. + + +*/ + +namespace AidingApp\KnowledgeBase\Filament\Resources\KnowledgeBaseCategoryResource\RelationManagers; + +use Filament\Forms\Form; +use Filament\Tables\Table; +use Filament\Infolists\Infolist; +use Filament\Forms\Components\Textarea; +use Filament\Tables\Actions\EditAction; +use Filament\Tables\Actions\ViewAction; +use Filament\Tables\Columns\IconColumn; +use Filament\Forms\Components\TextInput; +use Filament\Tables\Actions\ActionGroup; +use Filament\Tables\Actions\CreateAction; +use Filament\Tables\Actions\DeleteAction; +use Illuminate\Database\Eloquent\Builder; +use Filament\Infolists\Components\Section; +use Filament\Infolists\Components\TextEntry; +use Filament\Tables\Actions\AssociateAction; +use Filament\Tables\Actions\BulkActionGroup; +use App\Filament\Forms\Components\IconSelect; +use Filament\Tables\Actions\DeleteBulkAction; +use Filament\Tables\Actions\DissociateAction; +use Filament\Tables\Actions\DissociateBulkAction; +use App\Filament\Tables\Columns\OpenSearch\TextColumn; +use Filament\Resources\RelationManagers\RelationManager; +use AidingApp\KnowledgeBase\Models\KnowledgeBaseCategory; + +class SubCategoriesRelationManager extends RelationManager +{ + protected static string $relationship = 'subCategories'; + + protected static ?string $inverseRelationship = 'parentCategory'; + + public function infolist(Infolist $infolist): Infolist + { + return $infolist + ->schema([ + Section::make() + ->schema([ + TextEntry::make('name') + ->label('Name'), + TextEntry::make('icon') + ->state(fn (KnowledgeBaseCategory $record): string => (string) str($record->icon)->after('heroicon-o-')->headline()) + ->icon(fn (KnowledgeBaseCategory $record): string => $record->icon) + ->hidden(fn (KnowledgeBaseCategory $record): bool => blank($record->icon)), + TextEntry::make('description') + ->label('Description') + ->columnSpanFull(), + TextEntry::make('slug') + ->hidden(fn (KnowledgeBaseCategory $record): bool => blank($record->slug)), + ]) + ->columns(), + ]); + } + + public function form(Form $form): Form + { + return $form + ->schema([ + TextInput::make('name') + ->label('Name') + ->required() + ->string(), + IconSelect::make('icon'), + TextInput::make('slug') + ->regex('/^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$/') + ->unique(ignoreRecord: true) + ->maxLength(255) + ->required() + ->dehydrateStateUsing(fn (string $state): string => strtolower($state)), + Textarea::make('description') + ->label('Description') + ->nullable() + ->string() + ->columnSpanFull(), + ]); + } + + public function table(Table $table): Table + { + return $table + ->recordTitleAttribute('name') + ->heading('Subcategories') + ->columns([ + TextColumn::make('name'), + TextColumn::make('slug'), + IconColumn::make('icon') + ->icon(fn (string $state): string => $state) + ->tooltip(fn (?string $state): ?string => filled($state) ? (string) str($state)->after('heroicon-o-')->headline() : null), + ]) + ->headerActions([ + CreateAction::make() + ->label('New Subcategory') + ->modalHeading('Create knowledge base subcategory'), + AssociateAction::make() + ->modalHeading('Associate knowledge base subcategory') + ->preloadRecordSelect() + ->recordSelectOptionsQuery( + fn (Builder $query) => $query->where('id', '!=', $this->getOwnerRecord()->getKey()) + ->doesntHave('parentCategory') + ->doesntHave('subCategories') + ), + ]) + ->actions([ + ActionGroup::make([ + ViewAction::make(), + EditAction::make(), + DissociateAction::make(), + DeleteAction::make(), + ]), + ]) + ->bulkActions([ + BulkActionGroup::make([ + DissociateBulkAction::make(), + DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app-modules/knowledge-base/src/Models/KnowledgeBaseCategory.php b/app-modules/knowledge-base/src/Models/KnowledgeBaseCategory.php index 63402fd0a..5a580ac1d 100644 --- a/app-modules/knowledge-base/src/Models/KnowledgeBaseCategory.php +++ b/app-modules/knowledge-base/src/Models/KnowledgeBaseCategory.php @@ -42,6 +42,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use AidingApp\Audit\Models\Concerns\Auditable as AuditableTrait; /** @@ -65,6 +66,16 @@ public function knowledgeBaseItems(): HasMany return $this->hasMany(KnowledgeBaseItem::class, 'category_id'); } + public function parentCategory(): BelongsTo + { + return $this->belongsTo(self::class, 'parent_id', 'id'); + } + + public function subCategories(): HasMany + { + return $this->hasMany(self::class, 'parent_id'); + } + protected function serializeDate(DateTimeInterface $date): string { return $date->format(config('project.datetime_format') ?? 'Y-m-d H:i:s'); diff --git a/app-modules/knowledge-base/tests/KnowledgeBaseCategory/EditKnowledgeBaseCategoryTest.php b/app-modules/knowledge-base/tests/KnowledgeBaseCategory/EditKnowledgeBaseCategoryTest.php index aad32b200..6f7a9dfdc 100644 --- a/app-modules/knowledge-base/tests/KnowledgeBaseCategory/EditKnowledgeBaseCategoryTest.php +++ b/app-modules/knowledge-base/tests/KnowledgeBaseCategory/EditKnowledgeBaseCategoryTest.php @@ -39,12 +39,19 @@ use function Pest\Laravel\actingAs; use function Pest\Livewire\livewire; + +use Filament\Forms\Components\Select; +use Filament\Tables\Actions\CreateAction; +use Filament\Tables\Actions\AssociateAction; + use function PHPUnit\Framework\assertEquals; use AidingApp\Authorization\Enums\LicenseType; use AidingApp\KnowledgeBase\Models\KnowledgeBaseCategory; use AidingApp\KnowledgeBase\Filament\Resources\KnowledgeBaseCategoryResource; +use AidingApp\KnowledgeBase\Filament\Resources\KnowledgeBaseCategoryResource\Pages\EditKnowledgeBaseCategory; use AidingApp\KnowledgeBase\Tests\KnowledgeBaseCategory\RequestFactories\EditKnowledgeBaseCategoryRequestFactory; +use AidingApp\KnowledgeBase\Filament\Resources\KnowledgeBaseCategoryResource\RelationManagers\SubCategoriesRelationManager; // TODO: Write EditKnowledgeBaseCategory tests //test('A successful action on the EditKnowledgeBaseCategory page', function () {}); @@ -65,7 +72,7 @@ ]) )->assertForbidden(); - livewire(KnowledgeBaseCategoryResource\Pages\EditKnowledgeBaseCategory::class, [ + livewire(EditKnowledgeBaseCategory::class, [ 'record' => $knowledgeBaseCategory->getRouteKey(), ]) ->assertForbidden(); @@ -82,7 +89,7 @@ $request = collect(EditKnowledgeBaseCategoryRequestFactory::new()->create()); - livewire(KnowledgeBaseCategoryResource\Pages\EditKnowledgeBaseCategory::class, [ + livewire(EditKnowledgeBaseCategory::class, [ 'record' => $knowledgeBaseCategory->getRouteKey(), ]) ->fillForm($request->toArray()) @@ -113,7 +120,7 @@ ]) )->assertForbidden(); - livewire(KnowledgeBaseCategoryResource\Pages\EditKnowledgeBaseCategory::class, [ + livewire(EditKnowledgeBaseCategory::class, [ 'record' => $knowledgeBaseCategory->getRouteKey(), ]) ->assertForbidden(); @@ -131,7 +138,7 @@ $request = collect(EditKnowledgeBaseCategoryRequestFactory::new()->create()); - livewire(KnowledgeBaseCategoryResource\Pages\EditKnowledgeBaseCategory::class, [ + livewire(EditKnowledgeBaseCategory::class, [ 'record' => $knowledgeBaseCategory->getRouteKey(), ]) ->fillForm($request->toArray()) @@ -140,3 +147,99 @@ assertEquals($request['name'], $knowledgeBaseCategory->fresh()->name); }); + +test('can create subcategory', function () { + $user = User::factory()->licensed(LicenseType::cases())->create(); + + $user->givePermissionTo('product_admin.view-any'); + $user->givePermissionTo('product_admin.create'); + $user->givePermissionTo('product_admin.*.update'); + + actingAs($user); + + $knowledgeBaseCategory = KnowledgeBaseCategory::factory()->create(); + + $knowledgeBaseSubCategory = KnowledgeBaseCategory::factory()->state([ + 'parent_id' => $knowledgeBaseCategory->getKey(), + ])->make(); + + livewire(SubCategoriesRelationManager::class, [ + 'ownerRecord' => $knowledgeBaseCategory, + 'pageClass' => EditKnowledgeBaseCategory::class, + ]) + ->callTableAction( + name: CreateAction::class, + data: $knowledgeBaseSubCategory->toArray() + ) + ->assertHasNoTableActionErrors(); + + expect($knowledgeBaseCategory->fresh()->subCategories()->count()) + ->toEqual(1); +}); + +test('exclude already attached subcategories in search', function () { + $user = User::factory()->licensed(LicenseType::cases())->create(); + + $user->givePermissionTo('product_admin.view-any'); + $user->givePermissionTo('product_admin.*.update'); + + actingAs($user); + + $knowledgeBaseCategory = KnowledgeBaseCategory::factory()->create(); + + $knowledgeBaseSubCategory = KnowledgeBaseCategory::factory()->state([ + 'parent_id' => $knowledgeBaseCategory->getKey(), + ])->make(); + + expect($knowledgeBaseCategory->subCategories) + ->toBeEmpty(); + + $newknowledgeBaseCategory = KnowledgeBaseCategory::factory()->create(); + + livewire(SubCategoriesRelationManager::class, [ + 'ownerRecord' => $knowledgeBaseCategory, + 'pageClass' => EditKnowledgeBaseCategory::class, + ]) + ->mountTableAction(AssociateAction::class) + ->assertFormFieldExists('recordId', 'mountedTableActionForm', function (Select $select) use ($knowledgeBaseSubCategory) { + $options = $select->getOptions(); + $searchOptions = $select->getSearchResults($knowledgeBaseSubCategory->name); + + return ! in_array($knowledgeBaseSubCategory->name, $options) && empty($searchOptions); + }) + ->assertFormFieldExists('recordId', 'mountedTableActionForm', function (Select $select) use ($newknowledgeBaseCategory) { + $searchOptions = $select->getSearchResults($newknowledgeBaseCategory->name); + + return ! empty($searchOptions) ? true : false; + }); +}); + +test('can attach subcategories into categories', function () { + $user = User::factory()->licensed(LicenseType::cases())->create(); + + $user->givePermissionTo('product_admin.view-any'); + $user->givePermissionTo('product_admin.*.update'); + + actingAs($user); + + $knowledgeBaseCategory = KnowledgeBaseCategory::factory()->create(); + + $knowledgeBaseSubCategory = KnowledgeBaseCategory::factory()->create(); + + expect($knowledgeBaseCategory->subCategories) + ->toBeEmpty(); + + livewire(SubCategoriesRelationManager::class, [ + 'ownerRecord' => $knowledgeBaseCategory, + 'pageClass' => EditKnowledgeBaseCategory::class, + ]) + ->callTableAction( + AssociateAction::class, + data: ['recordId' => $knowledgeBaseSubCategory->getKey()] + )->assertSuccessful(); + + expect($knowledgeBaseCategory->refresh()) + ->subCategories + ->pluck('id') + ->toContain($knowledgeBaseSubCategory->getKey()); +}); diff --git a/app/Features/KnowledgeBaseSubcategory.php b/app/Features/KnowledgeBaseSubcategory.php new file mode 100644 index 000000000..068020b30 --- /dev/null +++ b/app/Features/KnowledgeBaseSubcategory.php @@ -0,0 +1,47 @@ + + + Copyright © 2016-2024, Canyon GBS LLC. All rights reserved. + + Aiding App™ is licensed under the Elastic License 2.0. For more details, + see + + Notice: + + - You may not provide the software to third parties as a hosted or managed + service, where the service provides users with access to any substantial set of + the features or functionality of the software. + - You may not move, change, disable, or circumvent the license key functionality + in the software, and you may not remove or obscure any functionality in the + software that is protected by the license key. + - You may not alter, remove, or obscure any licensing, copyright, or other notices + of the licensor in the software. Any use of the licensor’s trademarks is subject + to applicable law. + - Canyon GBS LLC respects the intellectual property rights of others and expects the + same in return. Canyon GBS™ and Aiding App™ are registered trademarks of + Canyon GBS LLC, and we are committed to enforcing and protecting our trademarks + vigorously. + - The software solution, including services, infrastructure, and code, is offered as a + Software as a Service (SaaS) by Canyon GBS LLC. + - Use of this software implies agreement to the license terms and conditions as stated + in the Elastic License 2.0. + + For more information or inquiries please visit our website at + or contact us via email at legal@canyongbs.com. + + +*/ + +namespace App\Features; + +use App\Support\AbstractFeatureFlag; + +class KnowledgeBaseSubcategory extends AbstractFeatureFlag +{ + public function resolve(mixed $scope): mixed + { + return false; + } +} diff --git a/database/migrations/2024_12_10_174023_data_activate_knowledge_base_subcategory_feature_flag.php b/database/migrations/2024_12_10_174023_data_activate_knowledge_base_subcategory_feature_flag.php new file mode 100644 index 000000000..e3bc62c45 --- /dev/null +++ b/database/migrations/2024_12_10_174023_data_activate_knowledge_base_subcategory_feature_flag.php @@ -0,0 +1,50 @@ + + + Copyright © 2016-2024, Canyon GBS LLC. All rights reserved. + + Aiding App™ is licensed under the Elastic License 2.0. For more details, + see + + Notice: + + - You may not provide the software to third parties as a hosted or managed + service, where the service provides users with access to any substantial set of + the features or functionality of the software. + - You may not move, change, disable, or circumvent the license key functionality + in the software, and you may not remove or obscure any functionality in the + software that is protected by the license key. + - You may not alter, remove, or obscure any licensing, copyright, or other notices + of the licensor in the software. Any use of the licensor’s trademarks is subject + to applicable law. + - Canyon GBS LLC respects the intellectual property rights of others and expects the + same in return. Canyon GBS™ and Aiding App™ are registered trademarks of + Canyon GBS LLC, and we are committed to enforcing and protecting our trademarks + vigorously. + - The software solution, including services, infrastructure, and code, is offered as a + Software as a Service (SaaS) by Canyon GBS LLC. + - Use of this software implies agreement to the license terms and conditions as stated + in the Elastic License 2.0. + + For more information or inquiries please visit our website at + or contact us via email at legal@canyongbs.com. + + +*/ + +use App\Features\KnowledgeBaseSubcategory; +use Illuminate\Database\Migrations\Migration; + +return new class () extends Migration { + public function up(): void + { + KnowledgeBaseSubcategory::activate(); + } + + public function down(): void + { + KnowledgeBaseSubcategory::deactivate(); + } +};