From 924ddce383723c0295f3b9d5a82a480ed8fb219c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9ia=20Bohner?= Date: Tue, 14 Jul 2026 22:27:29 -0300 Subject: [PATCH 1/3] Allow images on rich editor --- README.md | 19 ++++++++ config/filament-help.php | 24 ++++++++++- src/Resources/HelpArticleResource.php | 9 ++-- .../Feature/AdminHelpArticleResourceTest.php | 43 ++++++++++++++++--- 4 files changed, 84 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d6f0c18a..3d0db8c2 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,25 @@ return [ 'model' => \Tapp\FilamentHelp\Models\HelpArticle::class, + /* + |-------------------------------------------------------------------------- + | Rich Editor Configuration + |-------------------------------------------------------------------------- + | + | Configure how images are stored when uploaded via the Content rich editor. + | By default, attachments are stored publicly so article HTML can be + | rendered without temporary URL generation. + | + */ + + 'editor' => [ + 'file_attachments' => [ + 'disk' => env('FILAMENT_HELP_FILE_ATTACHMENTS_DISK'), + 'directory' => env('FILAMENT_HELP_FILE_ATTACHMENTS_DIRECTORY', 'help-articles'), + 'visibility' => env('FILAMENT_HELP_FILE_ATTACHMENTS_VISIBILITY', 'public'), + ], + ], + /* |-------------------------------------------------------------------------- | Frontend Configuration diff --git a/config/filament-help.php b/config/filament-help.php index 4f203b2b..c33ae318 100644 --- a/config/filament-help.php +++ b/config/filament-help.php @@ -1,5 +1,7 @@ \Tapp\FilamentHelp\Models\HelpArticle::class, + 'model' => HelpArticle::class, + + /* + |-------------------------------------------------------------------------- + | Rich Editor Configuration + |-------------------------------------------------------------------------- + | + | Configure how images are stored when uploaded via the Content rich editor. + | By default, attachments are stored publicly so article HTML can be + | rendered without temporary URL generation. + | + */ + + 'editor' => [ + 'file_attachments' => [ + 'disk' => env('FILAMENT_HELP_FILE_ATTACHMENTS_DISK'), + 'directory' => env('FILAMENT_HELP_FILE_ATTACHMENTS_DIRECTORY', 'help-articles'), + 'visibility' => env('FILAMENT_HELP_FILE_ATTACHMENTS_VISIBILITY', 'public'), + ], + ], /* |-------------------------------------------------------------------------- @@ -101,4 +122,3 @@ ], ]; - diff --git a/src/Resources/HelpArticleResource.php b/src/Resources/HelpArticleResource.php index 85b293d2..4806b615 100644 --- a/src/Resources/HelpArticleResource.php +++ b/src/Resources/HelpArticleResource.php @@ -102,11 +102,14 @@ public static function form(Schema $schema): Schema 'h3', 'blockquote', 'codeBlock', - ]) - ->columnSpanFull() - ->disableToolbarButtons([ 'attachFiles', ]) + ->fileAttachmentsDisk(config('filament-help.editor.file_attachments.disk')) + ->fileAttachmentsDirectory(config('filament-help.editor.file_attachments.directory', 'help-articles')) + ->fileAttachmentsVisibility(config('filament-help.editor.file_attachments.visibility', 'public')) + ->resizableImages() + ->preventFileAttachmentPathTampering() + ->columnSpanFull() ->extraInputAttributes([ 'style' => 'min-height: 200px;', ]), diff --git a/tests/Feature/AdminHelpArticleResourceTest.php b/tests/Feature/AdminHelpArticleResourceTest.php index 4b83843a..b822ca2f 100644 --- a/tests/Feature/AdminHelpArticleResourceTest.php +++ b/tests/Feature/AdminHelpArticleResourceTest.php @@ -1,6 +1,9 @@ create([ @@ -17,9 +20,9 @@ it('can update help article', function () { $helpArticle = HelpArticle::factory()->create(['name' => 'Original Name']); - + $helpArticle->update(['name' => 'Updated Name', 'is_public' => true]); - + expect($helpArticle->fresh()->name)->toBe('Updated Name'); expect($helpArticle->fresh()->is_public)->toBeTrue(); }); @@ -27,16 +30,44 @@ it('can delete help article', function () { $helpArticle = HelpArticle::factory()->create(); $id = $helpArticle->id; - + $helpArticle->delete(); - + expect(HelpArticle::find($id))->toBeNull(); }); it('can filter help articles by public status', function () { HelpArticle::factory()->public()->count(2)->create(); HelpArticle::factory()->private()->count(3)->create(); - + expect(HelpArticle::public()->count())->toBe(2); expect(HelpArticle::where('is_public', false)->count())->toBe(3); -}); \ No newline at end of file +}); + +it('enables image uploads on the content rich editor', function () { + expect(config('filament-help.editor.file_attachments.directory'))->toBe('help-articles'); + expect(config('filament-help.editor.file_attachments.visibility'))->toBe('public'); + + $schema = HelpArticleResource::form(Schema::make()); + + $components = (new ReflectionProperty($schema, 'components'))->getValue($schema); + + $contentField = collect($components) + ->first(fn ($component) => $component instanceof RichEditor && $component->getName() === 'content'); + + expect($contentField)->not->toBeNull(); + + $toolbarButtons = (new ReflectionProperty($contentField, 'toolbarButtons'))->getValue($contentField); + $fileAttachmentsDirectory = (new ReflectionProperty($contentField, 'fileAttachmentsDirectory'))->getValue($contentField); + $fileAttachmentsVisibility = (new ReflectionProperty($contentField, 'fileAttachmentsVisibility'))->getValue($contentField); + $hasResizableImages = (new ReflectionProperty($contentField, 'hasResizableImages'))->getValue($contentField); + $toolbarModifications = (new ReflectionProperty($contentField, 'toolbarButtonsModifications'))->getValue($contentField); + $shouldPreventTampering = (new ReflectionProperty($contentField, 'shouldPreventFileAttachmentPathTampering'))->getValue($contentField); + + expect($toolbarButtons)->toContain('attachFiles'); + expect($fileAttachmentsDirectory)->toBe('help-articles'); + expect($fileAttachmentsVisibility)->toBe('public'); + expect($hasResizableImages)->toBeTrue(); + expect($shouldPreventTampering)->toBeTrue(); + expect($toolbarModifications)->not->toContain(['type' => 'disable', 'buttons' => ['attachFiles']]); +}); From 86b87dcaa0a65cc62c184edd25062cb16500e038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9ia=20Bohner?= Date: Wed, 15 Jul 2026 18:08:50 -0300 Subject: [PATCH 2/3] Render line breaks correctly --- .../views/components/help-article-content.blade.php | 7 +++++++ .../pages/view-help-article.blade.php | 4 +--- .../pages/view-help-article.blade.php | 4 +--- .../pages/view-help-article.blade.php | 4 +--- src/Models/HelpArticle.php | 7 +++++++ src/Resources/HelpArticleResource.php | 2 ++ .../HelpArticleResource/Pages/ViewHelpArticle.php | 13 ++++++++++++- tests/Feature/AdminHelpArticleResourceTest.php | 12 ++++++++++++ tests/Unit/Models/HelpArticleTest.php | 12 ++++++++++++ 9 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 resources/views/components/help-article-content.blade.php diff --git a/resources/views/components/help-article-content.blade.php b/resources/views/components/help-article-content.blade.php new file mode 100644 index 00000000..7e48a503 --- /dev/null +++ b/resources/views/components/help-article-content.blade.php @@ -0,0 +1,7 @@ +@props(['content']) + +@if (filled($content)) +
class(['fi-prose max-w-none']) }}> + {!! str($content)->sanitizeHtml() !!} +
+@endif diff --git a/resources/views/filament/resources/help-article-resource/pages/view-help-article.blade.php b/resources/views/filament/resources/help-article-resource/pages/view-help-article.blade.php index d30cec16..f7d81498 100644 --- a/resources/views/filament/resources/help-article-resource/pages/view-help-article.blade.php +++ b/resources/views/filament/resources/help-article-resource/pages/view-help-article.blade.php @@ -8,9 +8,7 @@ @endif @if($this->record->content) -
- {!! $this->record->content !!} -
+ @else
diff --git a/resources/views/resources/frontend/help-article-resource/pages/view-help-article.blade.php b/resources/views/resources/frontend/help-article-resource/pages/view-help-article.blade.php index d30cec16..f7d81498 100644 --- a/resources/views/resources/frontend/help-article-resource/pages/view-help-article.blade.php +++ b/resources/views/resources/frontend/help-article-resource/pages/view-help-article.blade.php @@ -8,9 +8,7 @@ @endif @if($this->record->content) -
- {!! $this->record->content !!} -
+ @else
diff --git a/resources/views/resources/guest/help-article-resource/pages/view-help-article.blade.php b/resources/views/resources/guest/help-article-resource/pages/view-help-article.blade.php index 69268543..97b6b230 100644 --- a/resources/views/resources/guest/help-article-resource/pages/view-help-article.blade.php +++ b/resources/views/resources/guest/help-article-resource/pages/view-help-article.blade.php @@ -8,9 +8,7 @@ @endif @if($this->record->content) -
- {!! $this->record->content !!} -
+ @else
diff --git a/src/Models/HelpArticle.php b/src/Models/HelpArticle.php index f6af5438..44c5b93e 100644 --- a/src/Models/HelpArticle.php +++ b/src/Models/HelpArticle.php @@ -3,8 +3,10 @@ namespace Tapp\FilamentHelp\Models; use Filament\Facades\Filament; +use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\HtmlString; use Tapp\FilamentHelp\Support\Tenancy; class HelpArticle extends Model @@ -107,4 +109,9 @@ public function getRouteKeyName() { return 'slug'; } + + public function renderContent(): Htmlable + { + return new HtmlString((string) str($this->content ?? '')->sanitizeHtml()); + } } diff --git a/src/Resources/HelpArticleResource.php b/src/Resources/HelpArticleResource.php index 4806b615..a768f09f 100644 --- a/src/Resources/HelpArticleResource.php +++ b/src/Resources/HelpArticleResource.php @@ -24,6 +24,8 @@ class HelpArticleResource extends Resource { protected static ?string $model = null; + protected static ?string $recordTitleAttribute = 'name'; + public static function getModel(): string { return static::$model ?? config('filament-help.model', HelpArticle::class); diff --git a/src/Resources/HelpArticleResource/Pages/ViewHelpArticle.php b/src/Resources/HelpArticleResource/Pages/ViewHelpArticle.php index 657789c2..3ce17c7f 100644 --- a/src/Resources/HelpArticleResource/Pages/ViewHelpArticle.php +++ b/src/Resources/HelpArticleResource/Pages/ViewHelpArticle.php @@ -6,6 +6,7 @@ use Filament\Actions\Action; use Filament\Facades\Filament; use Filament\Resources\Pages\ViewRecord; +use Tapp\FilamentHelp\Models\HelpArticle; use Tapp\FilamentHelp\Resources\Frontend\HelpArticleResource as FrontendHelpArticleResource; use Tapp\FilamentHelp\Resources\Guest\HelpArticleResource as GuestHelpArticleResource; use Tapp\FilamentHelp\Resources\HelpArticleResource; @@ -16,6 +17,16 @@ class ViewHelpArticle extends ViewRecord protected string $view = 'filament-help::filament.resources.help-article-resource.pages.view-help-article'; + public function getTitle(): string + { + return $this->record->name; + } + + public function getHeading(): string + { + return $this->record->name; + } + protected function getHeaderActions(): array { return [ @@ -39,7 +50,7 @@ protected function getHeaderActions(): array protected function getShareUrl(): string { - /** @var \Tapp\FilamentHelp\Models\HelpArticle $record */ + /** @var HelpArticle $record */ $record = $this->record; if ($record->is_public) { diff --git a/tests/Feature/AdminHelpArticleResourceTest.php b/tests/Feature/AdminHelpArticleResourceTest.php index b822ca2f..469f8acd 100644 --- a/tests/Feature/AdminHelpArticleResourceTest.php +++ b/tests/Feature/AdminHelpArticleResourceTest.php @@ -44,6 +44,18 @@ expect(HelpArticle::where('is_public', false)->count())->toBe(3); }); +it('shows the article name as the view page title', function () { + $helpArticle = HelpArticle::factory()->create([ + 'name' => 'Adding an Evaluation Course', + ]); + + $page = new \Tapp\FilamentHelp\Resources\HelpArticleResource\Pages\ViewHelpArticle; + $page->record = $helpArticle; + + expect($page->getTitle())->toBe('Adding an Evaluation Course'); + expect($page->getHeading())->toBe('Adding an Evaluation Course'); +}); + it('enables image uploads on the content rich editor', function () { expect(config('filament-help.editor.file_attachments.directory'))->toBe('help-articles'); expect(config('filament-help.editor.file_attachments.visibility'))->toBe('public'); diff --git a/tests/Unit/Models/HelpArticleTest.php b/tests/Unit/Models/HelpArticleTest.php index 0f5e9136..bb957ded 100644 --- a/tests/Unit/Models/HelpArticleTest.php +++ b/tests/Unit/Models/HelpArticleTest.php @@ -30,3 +30,15 @@ expect($helpArticle->is_public)->toBeTrue(); expect($helpArticle->is_public)->toBeBool(); }); + +it('renders rich editor content as html', function () { + $helpArticle = HelpArticle::factory()->create([ + 'content' => '

First paragraph

Demo

', + ]); + + $html = $helpArticle->renderContent()->toHtml(); + + expect($html)->toContain('

First paragraph

'); + expect($html)->toContain('src="https://example.com/image.png"'); + expect($html)->toContain('alt="Demo"'); +}); From 648608bc5a1acb762db9f184bc37c5dfcb1cf071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9ia=20Bohner?= Date: Wed, 15 Jul 2026 18:51:17 -0300 Subject: [PATCH 3/3] Update tenant migration --- .../create_help_articles_table.php.stub | 31 ++++++-- tests/Unit/HelpArticlesMigrationTest.php | 70 +++++++++++++++++++ 2 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 tests/Unit/HelpArticlesMigrationTest.php diff --git a/database/migrations/create_help_articles_table.php.stub b/database/migrations/create_help_articles_table.php.stub index a6304057..fb339532 100644 --- a/database/migrations/create_help_articles_table.php.stub +++ b/database/migrations/create_help_articles_table.php.stub @@ -15,13 +15,34 @@ return new class extends Migration // Add tenant column if tenancy is enabled in config if (config('filament-help.tenancy.enabled', false)) { - $tenantColumn = config('filament-help.tenancy.column') ?? 'team_id'; - $onDelete = config('filament-help.tenancy.foreign_key.on_delete', 'cascade'); - $onUpdate = config('filament-help.tenancy.foreign_key.on_update', 'cascade'); + $tenantModel = config('filament-help.tenancy.model'); - $table->foreignId($tenantColumn) + throw_if(! is_string($tenantModel) || $tenantModel === '', InvalidArgumentException::class, 'Tenant model not configured in filament-help.tenancy.model'); + + $tenantColumnConfig = config('filament-help.tenancy.column'); + $tenantColumn = is_string($tenantColumnConfig) && $tenantColumnConfig !== '' + ? $tenantColumnConfig + : str($tenantModel)->classBasename()->snake()->append('_id')->toString(); + + $onDelete = match (config('filament-help.tenancy.foreign_key.on_delete', 'cascade')) { + 'cascade' => 'cascade', + 'no action' => 'no action', + 'restrict' => 'restrict', + 'set null' => 'set null', + default => 'cascade', + }; + + $onUpdate = match (config('filament-help.tenancy.foreign_key.on_update', 'cascade')) { + 'cascade' => 'cascade', + 'no action' => 'no action', + 'restrict' => 'restrict', + 'set null' => 'set null', + default => 'cascade', + }; + + $table->foreignIdFor($tenantModel, $tenantColumn) ->nullable() - ->constrained('teams') + ->constrained() ->onDelete($onDelete) ->onUpdate($onUpdate); } diff --git a/tests/Unit/HelpArticlesMigrationTest.php b/tests/Unit/HelpArticlesMigrationTest.php new file mode 100644 index 00000000..519e386a --- /dev/null +++ b/tests/Unit/HelpArticlesMigrationTest.php @@ -0,0 +1,70 @@ +id(); + $table->string('name'); + $table->timestamps(); + }); + + config([ + 'filament-help.tenancy.enabled' => true, + 'filament-help.tenancy.model' => HelpArticlesMigrationOrganization::class, + 'filament-help.tenancy.column' => 'organization_id', + ]); + + $migration = require __DIR__.'/../../database/migrations/create_help_articles_table.php.stub'; + $migration->up(); +}); + +it('creates the tenant foreign key from filament-help.tenancy.model instead of a hard-coded teams table', function (): void { + expect(Schema::hasColumn('help_articles', 'organization_id'))->toBeTrue() + ->and(Schema::hasColumn('help_articles', 'team_id'))->toBeFalse() + ->and(Schema::hasTable('teams'))->toBeFalse(); + + $organization = HelpArticlesMigrationOrganization::query()->create(['name' => 'Acme']); + + $articleId = DB::table('help_articles')->insertGetId([ + 'organization_id' => $organization->id, + 'name' => 'Tenant Scoped Article', + 'slug' => 'tenant-scoped-article', + 'is_public' => false, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + expect(DB::table('help_articles')->where('id', $articleId)->value('organization_id')) + ->toBe($organization->id); +}); + +it('requires a tenant model when tenancy is enabled', function (): void { + Schema::dropIfExists('help_articles'); + + config([ + 'filament-help.tenancy.enabled' => true, + 'filament-help.tenancy.model' => null, + 'filament-help.tenancy.column' => 'organization_id', + ]); + + $migration = require __DIR__.'/../../database/migrations/create_help_articles_table.php.stub'; + + expect(fn () => $migration->up()) + ->toThrow(InvalidArgumentException::class, 'Tenant model not configured in filament-help.tenancy.model'); +});