diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 3147413..9b807a4 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -13,7 +13,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest, windows-latest] - php: [8.2, 8.1] + php: [8.3, 8.2, 8.1] laravel: [10.*] stability: [prefer-stable] include: diff --git a/README.md b/README.md index 2da29f3..6733d83 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,10 @@ -
- -
- [![Latest Version on Packagist](https://img.shields.io/packagist/v/rickdbcn/filament-email.svg?style=flat-square)](https://packagist.org/packages/rickdbcn/filament-email) [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/rickdbcn/filament-email/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/rickdbcn/filament-email/actions?query=workflow%3Arun-tests+branch%3Amain) [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/rickdbcn/filament-email/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/rickdbcn/filament-email/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) [![Total Downloads](https://img.shields.io/packagist/dt/rickdbcn/filament-email.svg?style=flat-square)](https://packagist.org/packages/rickdbcn/filament-email) Log all outgoing emails in your Laravel project within your Filament panel. You can also resend emails with 1-click in case your recipient hasn't received your email. + ## Installation You can install the package via composer: @@ -31,8 +28,7 @@ php artisan vendor:publish --tag="filament-email-config" Register the plugin through your panel service provider: ```php -// add this within return $panel: -->plugin(new \RickDBCN\FilamentEmail\FilamentEmail()) +->plugin(new \RickDBCN\FilamentEmail\FilamentEmail::make()) ``` @@ -41,13 +37,30 @@ Register the plugin through your panel service provider: ```bash composer test ``` + ## Screenshots -![](https://raw.githubusercontent.com/RickDBCN/filament-email/main/screenshots/tableview.png) + +### E-mail list + +![](https://raw.githubusercontent.com/RickDBCN/filament-email/main/screenshots/table.png) + +### Advanced filters + +![](https://raw.githubusercontent.com/RickDBCN/filament-email/main/screenshots/filters.png) + +### Resend e-mail + +![](https://raw.githubusercontent.com/RickDBCN/filament-email/main/screenshots/resend.png) + +### Update addresses and resend e-mail + +![](https://raw.githubusercontent.com/RickDBCN/filament-email/main/screenshots/update-and-resend.png) ## Credits - [Rick de Boer](https://github.com/RickDBCN) - [Ramnzys](https://github.com/ramnzys/filament-email-log) +- [Marco Germani](https://github.com/marcogermani87) - [All Contributors](../../contributors) ## License diff --git a/composer.json b/composer.json index 237264e..9b31ba8 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "keywords": [ "RickDBCN", "laravel", - "filament-email" + "filament-email", + "marcogermani87" ], "homepage": "https://github.com/rickdbcn/filament-email", "license": "MIT", diff --git a/config/filament-email.php b/config/filament-email.php index 1dcadf8..5e7e521 100644 --- a/config/filament-email.php +++ b/config/filament-email.php @@ -1,6 +1,5 @@ null, 'default_sort_column' => 'created_at', 'default_sort_direction' => 'desc', + 'datetime_format' => 'Y-m-d H:i:s', + 'filter_date_format' => 'Y-m-d', ], 'keep_email_for_days' => 60, 'label' => null, + + 'can_access' => [ + 'role' => [], + ], ]; diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 0000000..e6eac8b --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,19 @@ + $this->faker->name, + 'email' => $this->faker->unique()->safeEmail, + ]; + } +} diff --git a/resources/lang/en/filament-email.php b/resources/lang/en/filament-email.php new file mode 100644 index 0000000..bd3f66e --- /dev/null +++ b/resources/lang/en/filament-email.php @@ -0,0 +1,27 @@ + 'Created at', + 'sent_at' => 'Sent at', + 'html' => 'HTML', + 'text' => 'Text', + 'raw' => 'Raw', + 'debug_info' => 'Debug info', + 'cc' => 'CC', + 'to' => 'To', + 'from' => 'From', + 'header' => 'Headers', + 'resend_email_heading' => 'Resend e-mail', + 'resend_email_description' => 'Are you sure you want to resend this e-mail?', + 'resend_email_success' => 'The e-mail was sent with success', + 'resend_email_error' => 'Unable to send e-mail, internal error', + 'email_log' => 'E-mail Log', + 'subject' => 'Subject', + 'from_filter' => 'From', + 'to_filter' => 'To', + 'navigation_label' => 'E-mail Log', + 'navigation_group' => 'Admin', + 'bcc' => 'BCC', + 'insert_multiple_email_placelholder' => 'Insert one or more e-mail', + 'update_and_resend_email_heading' => 'Update and resend e-mail', +]; diff --git a/resources/lang/it/filament-email.php b/resources/lang/it/filament-email.php new file mode 100644 index 0000000..322eaf9 --- /dev/null +++ b/resources/lang/it/filament-email.php @@ -0,0 +1,27 @@ + 'Creato il', + 'sent_at' => 'Data di invio', + 'html' => 'HTML', + 'text' => 'Testo', + 'raw' => 'Raw', + 'debug_info' => 'Info di debug', + 'cc' => 'CC', + 'to' => 'A', + 'from' => 'Da', + 'header' => 'Intestazione', + 'resend_email_heading' => 'Re-invia e-mail', + 'resend_email_description' => 'Sei sicuro di voler re-inviare questa e-mail?', + 'resend_email_success' => "L'e-mail è stata inviata con successo", + 'resend_email_error' => "Impossibile inviare l'email, errore interno", + 'email_log' => 'E-mail Log', + 'subject' => 'Oggetto', + 'from_filter' => 'Dal', + 'to_filter' => 'Al', + 'navigation_label' => 'E-mail Log', + 'navigation_group' => 'Admin', + 'bcc' => 'BCC', + 'insert_multiple_email_placelholder' => 'Inserisci una o più e-mail', + 'update_and_resend_email_heading' => 'Modifica and re-invia e-mail', +]; diff --git a/screenshots/filters.png b/screenshots/filters.png new file mode 100644 index 0000000..b53c3c4 Binary files /dev/null and b/screenshots/filters.png differ diff --git a/screenshots/resend.png b/screenshots/resend.png new file mode 100644 index 0000000..5375bf5 Binary files /dev/null and b/screenshots/resend.png differ diff --git a/screenshots/table.png b/screenshots/table.png new file mode 100644 index 0000000..efbdcfe Binary files /dev/null and b/screenshots/table.png differ diff --git a/screenshots/tableview.jpg b/screenshots/tableview.jpg deleted file mode 100644 index a1264aa..0000000 Binary files a/screenshots/tableview.jpg and /dev/null differ diff --git a/screenshots/tableview.png b/screenshots/tableview.png deleted file mode 100644 index a640694..0000000 Binary files a/screenshots/tableview.png and /dev/null differ diff --git a/screenshots/update-and-resend.png b/screenshots/update-and-resend.png new file mode 100644 index 0000000..fd0476a Binary files /dev/null and b/screenshots/update-and-resend.png differ diff --git a/src/Filament/Resources/EmailResource.php b/src/Filament/Resources/EmailResource.php index 6d6af45..589540a 100644 --- a/src/Filament/Resources/EmailResource.php +++ b/src/Filament/Resources/EmailResource.php @@ -2,19 +2,26 @@ namespace RickDBCN\FilamentEmail\Filament\Resources; +use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\Fieldset; use Filament\Forms\Components\Tabs; +use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\ViewField; use Filament\Forms\Form; use Filament\Notifications\Notification; use Filament\Resources\Resource; +use Filament\Support\Enums\IconSize; use Filament\Tables\Actions\Action; use Filament\Tables\Actions\DeleteBulkAction; use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Filters\Filter; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; use RickDBCN\FilamentEmail\Filament\Resources\EmailResource\Pages\ListEmails; use RickDBCN\FilamentEmail\Filament\Resources\EmailResource\Pages\ViewEmail; @@ -27,14 +34,19 @@ class EmailResource extends Resource protected static ?string $slug = 'emails'; + public static function getBreadcrumb(): string + { + return __('filament-email::filament-email.email_log'); + } + public static function getNavigationLabel(): string { - return Config::get('filament-email.label') ?? __('Email log'); + return Config::get('filament-email.label') ?? __('filament-email::filament-email.navigation_label'); } public static function getNavigationGroup(): ?string { - return Config::get('filament-email.resource.group' ?? parent::getNavigationGroup()); + return Config::get('filament-email.resource.group') ?? __('filament-email::filament-email.navigation_group'); } public static function getNavigationSort(): ?int @@ -54,36 +66,47 @@ public static function form(Form $form): Form Fieldset::make('Envelope') ->label('') ->schema([ - TextInput::make('created_at') - ->label(__('Created at')), TextInput::make('from') - ->label(__('From')), + ->label(__('filament-email::filament-email.from')) + ->columnSpan(2), Textinput::make('to') - ->label(__('To')), + ->label(__('filament-email::filament-email.to')) + ->columnSpan(2), TextInput::make('cc') - ->label(__('CC')), - TextInput::make('subject') - ->label(__('Subject')) + ->label(__('filament-email::filament-email.cc')) ->columnSpan(2), - ])->columns(3), + TextInput::make('bcc') + ->label(__('filament-email::filament-email.bcc')) + ->columnSpan(2), + TextInput::make('subject') + ->label(__('filament-email::filament-email.subject')) + ->columnSpan(3), + DateTimePicker::make('created_at') + ->format(config('filament-email.resource.datetime_format')) + ->label(__('filament-email::filament-email.created_at')), + ])->columns(4), Tabs::make('Content')->tabs([ - Tabs\Tab::make('HTML') + Tabs\Tab::make(__('filament-email::filament-email.html')) ->schema([ ViewField::make('html_body') + ->label('') ->view('filament-email::filament-email.emails.html') ->view('filament-email::HtmlEmailView'), ]), - Tabs\Tab::make('Text') + Tabs\Tab::make(__('filament-email::filament-email.text')) ->schema([ - Textarea::make('text_body'), + Textarea::make('text_body') + ->label(''), ]), - Tabs\Tab::make('Raw') + Tabs\Tab::make(__('filament-email::filament-email.raw')) ->schema([ - Textarea::make('raw_body'), + Textarea::make('raw_body') + ->label(''), ]), - Tabs\Tab::make('Debug info') + Tabs\Tab::make(__('filament-email::filament-email.debug_info')) ->schema([ - Textarea::make('sent_debug_info'), + Textarea::make('sent_debug_info') + ->label(''), ]), ])->columnSpan(2), ]); @@ -95,9 +118,9 @@ public static function table(Table $table): Table ->defaultSort(config('filament-email.resource.default_sort_column'), config('filament-email.resource.default_sort_direction')) ->actions([ Action::make('preview') - ->label(__('Preview')) - ->icon('heroicon-m-eye') - ->extraAttributes(['style' => 'h-41']) + ->label(false) + ->icon('heroicon-o-eye') + ->iconSize(IconSize::Medium) ->modalFooterActions( fn ($action): array => [ $action->getModalCancelAction(), @@ -110,12 +133,20 @@ public static function table(Table $table): Table ]; }) ->form([ - ViewField::make('html_body')->hiddenLabel() - ->view('filament-email::filament-email.emails.html')->view('filament-email::HtmlEmailView'), + ViewField::make('html_body') + ->hiddenLabel() + ->view('filament-email::filament-email.emails.html') + ->view('filament-email::HtmlEmailView'), ]), Action::make('resend') - ->label(__('Send again')) - ->icon('heroicon-o-envelope') + ->label(false) + ->icon('heroicon-o-arrow-path') + ->iconSize(IconSize::Medium) + ->tooltip(__('filament-email::filament-email.resend_email_heading')) + ->requiresConfirmation() + ->modalHeading(__('filament-email::filament-email.resend_email_heading')) + ->modalDescription(__('filament-email::filament-email.resend_email_description')) + ->modalIconColor('warning') ->action(function ($record) { try { Mail::to($record->to) @@ -123,42 +154,135 @@ public static function table(Table $table): Table ->bcc($record->bcc) ->send(new ResendMail($record)); Notification::make() - ->title(__('E-mail has been successfully sent')) + ->title(__('filament-email::filament-email.resend_email_success')) ->success() ->duration(5000) ->send(); - } catch (\Exception) { + } catch (\Exception $e) { + Log::error($e->getMessage()); Notification::make() - ->title(__('Something went wrong')) + ->title(__('filament-email::filament-email.resend_email_error')) ->danger() ->duration(5000) ->send(); } }), + Action::make('resend-mod') + ->label(false) + ->icon('heroicon-o-envelope-open') + ->iconSize(IconSize::Medium) + ->tooltip(__('filament-email::filament-email.update_and_resend_email_heading')) + ->modalHeading(__('filament-email::filament-email.update_and_resend_email_heading')) + ->form([ + TextInput::make('to') + ->label(__('filament-email::filament-email.to')) + ->default(fn ($record): string => $record->to) + ->email() + ->required(), + TagsInput::make('cc') + ->label(__('filament-email::filament-email.cc')) + ->placeholder(__('filament-email::filament-email.insert_multiple_email_placelholder')) + ->nestedRecursiveRules([ + 'email', + ]) + ->default(fn ($record): array => ! empty($record->cc) ? explode(',', $record->cc) : []), + TagsInput::make('bcc') + ->label(__('filament-email::filament-email.bcc')) + ->placeholder(__('filament-email::filament-email.insert_multiple_email_placelholder')) + ->nestedRecursiveRules([ + 'email', + ]) + ->default(fn ($record): array => ! empty($record->bcc) ? explode(',', $record->bcc) : []), + ]) + ->action(function (Email $record, array $data) { + try { + Mail::to($data['to']) + ->cc($data['cc']) + ->bcc($data['bcc']) + ->send(new ResendMail($record)); + Notification::make() + ->title(__('filament-email::filament-email.resend_email_success')) + ->success() + ->duration(5000) + ->send(); + } catch (\Exception $e) { + Log::error($e->getMessage()); + Notification::make() + ->title(__('filament-email::filament-email.resend_email_error')) + ->danger() + ->duration(5000) + ->send(); + } + }) + ->modalWidth('2xl'), ]) ->columns([ - TextColumn::make('created_at') - ->label(__('Date and time sent')) - ->dateTime() - ->icon('heroicon-m-calendar') - ->sortable(), TextColumn::make('from') - ->label(__('From')) - ->icon('heroicon-m-envelope') - ->searchable(), - TextColumn::make('to') - ->label(__('To')) - ->icon('heroicon-m-envelope') + ->prefix(__('filament-email::filament-email.from').': ') + ->label(__('filament-email::filament-email.header')) + ->description(fn (Email $record): string => __('filament-email::filament-email.to').': '.$record->to) ->searchable(), TextColumn::make('subject') - ->label(__('Subject')) - ->icon('heroicon-m-chat-bubble-bottom-center') + ->label(__('filament-email::filament-email.subject')) ->limit(50), + TextColumn::make('created_at') + ->label(__('filament-email::filament-email.sent_at')) + ->dateTime(config('filament-email.resource.datetime_format')) + ->sortable(), ]) ->groupedBulkActions([ DeleteBulkAction::make() ->requiresConfirmation(), + ]) + ->persistFiltersInSession() + ->filters([ + Filter::make('created_at') + ->form(function () { + return [ + DateTimePicker::make('created_from') + ->label(__('filament-email::filament-email.from_filter')) + ->native(false) + ->firstDayOfWeek(1) + ->displayFormat(config('filament-email.resource.filter_date_format')) + ->time(false), + DateTimePicker::make('created_until') + ->label(__('filament-email::filament-email.to_filter')) + ->native(false) + ->firstDayOfWeek(1) + ->displayFormat(config('filament-email.resource.filter_date_format')) + ->time(false), + ]; + }) + ->indicateUsing(function (array $data): ?string { + if (! $data['created_from'] && ! $data['created_until']) { + return null; + } + $filter = ''; + $format = config('filament-email.resource.filter_date_format'); + if (! empty($data['created_from'])) { + $from = Carbon::parse($data['created_from'])->format($format); + $filter = __('filament-email::filament-email.from_filter')." $from"; + } + if (! empty($data['created_until'])) { + $to = Carbon::parse($data['created_until'])->format($format); + $toText = __('filament-email::filament-email.to_filter'); + $filter .= (! empty($filter) ? ' '.strtolower($toText).' ' : $toText)."$to"; + } + + return $filter; + }) + ->query(function (Builder $query, array $data): Builder { + return $query + ->when( + $data['created_from'], + fn (Builder $query, $date): Builder => $query->where('created_at', '>=', $date), + ) + ->when( + $data['created_until'], + fn (Builder $query, $date): Builder => $query->where('created_at', '<=', $date), + ); + }), ]); } @@ -169,4 +293,15 @@ public static function getPages(): array 'view' => ViewEmail::route('/{record}'), ]; } + + public static function canAccess(): bool + { + $roles = config('filament-email.can_access.role') ?? []; + + if (method_exists(auth()->user(), 'hasRole') && ! empty($roles)) { + return auth()->user()->hasRole($roles); + } + + return true; + } } diff --git a/src/Filament/Resources/EmailResource/Pages/ListEmails.php b/src/Filament/Resources/EmailResource/Pages/ListEmails.php index 82a3886..d8e7815 100644 --- a/src/Filament/Resources/EmailResource/Pages/ListEmails.php +++ b/src/Filament/Resources/EmailResource/Pages/ListEmails.php @@ -3,6 +3,7 @@ namespace RickDBCN\FilamentEmail\Filament\Resources\EmailResource\Pages; use Filament\Resources\Pages\ListRecords; +use Illuminate\Database\Eloquent\Builder; use RickDBCN\FilamentEmail\Filament\Resources\EmailResource; class ListEmails extends ListRecords @@ -11,4 +12,13 @@ public static function getResource(): string { return config('filament-email.resource.class', EmailResource::class); } + + protected function applySearchToTableQuery(Builder $query): Builder + { + if (filled($searchQuery = $this->getTableSearch())) { + return $query->filter(['search' => $searchQuery]); + } + + return $query; + } } diff --git a/src/FilamentEmail.php b/src/FilamentEmail.php index aaacb56..73db0a1 100755 --- a/src/FilamentEmail.php +++ b/src/FilamentEmail.php @@ -24,4 +24,9 @@ public function boot(Panel $panel): void { } + + public static function make(): static + { + return app(static::class); + } } diff --git a/src/FilamentEmailServiceProvider.php b/src/FilamentEmailServiceProvider.php index 2900729..fbd07cb 100644 --- a/src/FilamentEmailServiceProvider.php +++ b/src/FilamentEmailServiceProvider.php @@ -20,6 +20,7 @@ public function configurePackage(Package $package): void $package ->name('filament-email') ->hasConfigFile('filament-email') + ->hasTranslations() ->hasViews() ->hasMigration('create_filament_email_table'); diff --git a/src/Models/Email.php b/src/Models/Email.php index 481a5e3..2db690d 100644 --- a/src/Models/Email.php +++ b/src/Models/Email.php @@ -35,4 +35,14 @@ public function prunable() { return static::where('created_at', '<=', now()->subDays(Config::get('filament-email-log.keep_email_for_days'))); } + + public function scopeFilter($query, array $filters) + { + $query->when($filters['search'] ?? null, function ($query, $search) { + $query->where('to', 'like', "%$search%") + ->orWhere('from', 'like', "%$search%") + ->orWhere('subject', 'like', "%$search%"); + + }); + } } diff --git a/tests/EmailModelTest.php b/tests/EmailModelTest.php index cfa666b..52f3159 100644 --- a/tests/EmailModelTest.php +++ b/tests/EmailModelTest.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Mail; use RickDBCN\FilamentEmail\Filament\Resources\EmailResource\Pages\ListEmails; use RickDBCN\FilamentEmail\Models\Email; +use RickDBCN\FilamentEmail\Tests\Models\User; use function Pest\Laravel\assertDatabaseCount; use function Pest\Laravel\assertModelExists; @@ -13,6 +14,7 @@ beforeEach(function () { $this->model = Config::get('filament-email.resource.model') ?? Email::class; + $this->actingAs(User::factory()->create()); }); it('can create an Email model', function () { diff --git a/tests/Models/User.php b/tests/Models/User.php index 11439be..f690ced 100644 --- a/tests/Models/User.php +++ b/tests/Models/User.php @@ -7,6 +7,7 @@ use Illuminate\Auth\Authenticatable; use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Auth\Access\Authorizable; @@ -14,8 +15,13 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac { use Authenticatable; use Authorizable; + use HasFactory; + + protected $fillable = [ + 'name', + 'email', + ]; - //protected $fillable = ['email', 'name']; protected $guarded = []; public $timestamps = false; diff --git a/tests/Panels/TestPanelProvider.php b/tests/Panels/TestPanelProvider.php index 50f84e4..0c874b2 100644 --- a/tests/Panels/TestPanelProvider.php +++ b/tests/Panels/TestPanelProvider.php @@ -22,7 +22,7 @@ public function panel(Panel $panel): Panel return $panel ->id('test-panel') ->default() - ->plugin(new FilamentEmail) + ->plugin(FilamentEmail::make()) ->middleware([ EncryptCookies::class, AddQueuedCookiesToResponse::class,