diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 8b2b714..fd7fff4 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -17,16 +17,14 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - php: [8.4, 8.3, 8.2] - laravel: [12.*, 11.*, 10.*] + php: [8.4, 8.3] + laravel: [12.*, 11.*] stability: [prefer-lowest, prefer-stable] include: - laravel: 12.* testbench: 10.* - laravel: 11.* testbench: 9.* - - laravel: 10.* - testbench: 8.* name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} diff --git a/composer.json b/composer.json index 9ff92fe..7568340 100644 --- a/composer.json +++ b/composer.json @@ -20,9 +20,9 @@ } ], "require": { - "php": "^8.2", + "php": "^8.3", "filament/filament": "^4.0", - "illuminate/contracts": "^10.0||^11.0||^12.0", + "illuminate/contracts": "^11.0||^12.0", "maatwebsite/excel": "^3.1", "spatie/eloquent-sortable": "^4.3", "spatie/laravel-medialibrary": "^11.12", @@ -30,15 +30,15 @@ }, "require-dev": { "laravel/pint": "^1.14", - "nunomaduro/collision": "^8.1.1||^7.10.0", - "larastan/larastan": "^2.9||^3.0", - "orchestra/testbench": "^10.0.0||^9.0.0||^8.22.0", - "pestphp/pest": "^3.0||^2.34", - "pestphp/pest-plugin-arch": "^3.0||^2.7", - "pestphp/pest-plugin-laravel": "^3.0||^2.3", - "phpstan/extension-installer": "^1.3||^2.0", - "phpstan/phpstan-deprecation-rules": "^1.1||^2.0", - "phpstan/phpstan-phpunit": "^1.3||^2.0", + "larastan/larastan": "^3.0", + "nunomaduro/collision": "^8.1.1", + "orchestra/testbench": "^10.0.0||^9.0.0", + "pestphp/pest": "^3.0", + "pestphp/pest-plugin-arch": "^3.0", + "pestphp/pest-plugin-laravel": "^3.0", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", "spatie/laravel-ray": "^1.35" }, "autoload": { diff --git a/config/filament-form-builder.php b/config/filament-form-builder.php index 2ea07cc..09c67b1 100644 --- a/config/filament-form-builder.php +++ b/config/filament-form-builder.php @@ -32,4 +32,20 @@ 'admin-panel-filament-form-field-name-plural' => 'Fields', 'preview-route' => 'filament-form-builder.show', + + /* + |-------------------------------------------------------------------------- + | Notification Emails Configuration + |-------------------------------------------------------------------------- + | + | Configure the notification emails field in the form builder. + | + | 'user_model': The User model class to use for the select field. + | Set to null to use TagsInput for manual email entry. + | Default: 'App\Models\User' + | + | Example: 'user_model' => null, // Use TagsInput instead + | + */ + 'user_model' => 'App\Models\User', ]; diff --git a/database/migrations/add_notification_emails_to_filament_forms_table.php.stub b/database/migrations/add_notification_emails_to_filament_forms_table.php.stub new file mode 100644 index 0000000..f3dd9e7 --- /dev/null +++ b/database/migrations/add_notification_emails_to_filament_forms_table.php.stub @@ -0,0 +1,17 @@ +json('notification_emails')->nullable()->after('description'); + }); + } +}; diff --git a/resources/views/mail/submission-notification.blade.php b/resources/views/mail/submission-notification.blade.php new file mode 100644 index 0000000..60c0382 --- /dev/null +++ b/resources/views/mail/submission-notification.blade.php @@ -0,0 +1,20 @@ + +# New Form Submission + +A new submission has been received for **{{ $form->name }}**. + +**Submitted by:** {{ $entry->user?->name ?? 'Guest' }} + +**Submitted at:** {{ $entry->created_at->format('F j, Y g:i A') }} + + +View Form & Entries + + +Thanks,
+{{ config('app.name') }} +
+ + + + diff --git a/src/Filament/Resources/FilamentFormResource.php b/src/Filament/Resources/FilamentFormResource.php index a6bc76a..a5731df 100644 --- a/src/Filament/Resources/FilamentFormResource.php +++ b/src/Filament/Resources/FilamentFormResource.php @@ -6,11 +6,15 @@ use Filament\Actions\BulkActionGroup; use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; +use Filament\Forms\Components\Select; +use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Notifications\Notification; use Filament\Resources\Resource; +use Filament\Schemas\Components\Component; +use Filament\Schemas\Components\Section; use Filament\Schemas\Schema; use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; @@ -68,6 +72,13 @@ public static function form(Schema $schema): Schema ->hint('(optional) complete this field to provide a custom redirect url on form completion. Use a fully qualified URL including "https://" to redirect to an external link, otherwise url will be relative to this sites domain'), Textarea::make('description') ->columnSpanFull(), + Section::make('Notifications') + ->description('Configure email notifications for form submissions') + ->schema([ + static::getNotificationEmailsField(), + ]) + ->collapsible() + ->collapsed(), ]); } @@ -116,6 +127,7 @@ public static function table(Table $table): Table 'permit_guest_entries' => $record->permit_guest_entries, 'redirect_url' => $record->redirect_url, 'description' => $record->description, + 'notification_emails' => $record->notification_emails, ]); $record->filamentFormFields->each(function ($field) use ($formCopy) { @@ -164,4 +176,40 @@ public static function getPages(): array 'edit' => EditFilamentForm::route('/{record}/edit'), ]; } + + protected static function getNotificationEmailsField(): Component + { + $userModel = config('filament-form-builder.user_model'); + + // If user model is configured, use Select with user search + if ($userModel && class_exists($userModel)) { + return Select::make('notification_emails') + ->label('Notification Recipients') + ->helperText('Select users who should receive notifications when this form is submitted.') + ->multiple() + ->searchable() + ->getSearchResultsUsing(function ($search) use ($userModel) { + return $userModel::query() + ->where(function ($query) use ($search) { + $query->where('name', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%"); + }) + ->limit(50) + ->get() + ->mapWithKeys(fn ($user) => [$user->email => $user->name.' ('.$user->email.')']); + }) + ->getOptionLabelsUsing(function (array $values) use ($userModel): array { + return $userModel::whereIn('email', $values) + ->get() + ->mapWithKeys(fn ($user) => [$user->email => $user->name.' ('.$user->email.')']) + ->toArray(); + }); + } + + // Default: TagsInput for manual email entry + return TagsInput::make('notification_emails') + ->label('Notification Email Addresses') + ->helperText('Enter email addresses that should receive notifications when this form is submitted. Press Enter after each email.') + ->placeholder('email@example.com'); + } } diff --git a/src/FilamentFormBuilderServiceProvider.php b/src/FilamentFormBuilderServiceProvider.php index 18ccf22..ccafbef 100644 --- a/src/FilamentFormBuilderServiceProvider.php +++ b/src/FilamentFormBuilderServiceProvider.php @@ -9,6 +9,8 @@ use Tapp\FilamentFormBuilder\Livewire\FilamentForm\Show as FilamentFormShow; use Tapp\FilamentFormBuilder\Livewire\FilamentFormUser\Entry as FilamentFormUserEntry; use Tapp\FilamentFormBuilder\Livewire\FilamentFormUser\Show as FilamentFormUserShow; +use Tapp\FilamentFormBuilder\Models\FilamentFormUser; +use Tapp\FilamentFormBuilder\Observers\FilamentFormUserObserver; class FilamentFormBuilderServiceProvider extends PackageServiceProvider { @@ -23,6 +25,7 @@ public function configurePackage(Package $package): void $package->name('filament-form-builder') ->hasMigration('create_dynamic_filament_form_tables') ->hasMigration('add_schema_to_filament_form_fields') + ->hasMigration('add_notification_emails_to_filament_forms_table') ->hasConfigFile('filament-form-builder') ->hasRoute('routes') ->hasViews('filament-form-builder'); @@ -39,6 +42,9 @@ public function boot() // Register the new layout components Livewire::component('tapp.filament-form-builder.livewire.filament-form.form', FilamentForm::class); Livewire::component('tapp.filament-form-builder.livewire.filament-form-user.entry', FilamentFormUserEntry::class); + + // Register observer for form submission notifications + FilamentFormUser::observe(FilamentFormUserObserver::class); } public function packageBooted(): void diff --git a/src/Mail/FormSubmissionNotification.php b/src/Mail/FormSubmissionNotification.php new file mode 100644 index 0000000..9743195 --- /dev/null +++ b/src/Mail/FormSubmissionNotification.php @@ -0,0 +1,49 @@ +form->name, + ); + } + + public function content(): Content + { + return new Content( + markdown: 'filament-form-builder::mail.submission-notification', + ); + } + + /** + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/src/Models/FilamentForm.php b/src/Models/FilamentForm.php index f726e24..ecbb124 100644 --- a/src/Models/FilamentForm.php +++ b/src/Models/FilamentForm.php @@ -10,8 +10,11 @@ /** * @property int $id + * @property string $name + * @property string|null $description * @property string|null $redirect_url * @property bool $permit_guest_entries + * @property array|null $notification_emails * @property-read string $form_link */ class FilamentForm extends Model @@ -22,6 +25,7 @@ class FilamentForm extends Model protected $casts = [ 'permit_guest_entries' => 'boolean', + 'notification_emails' => 'array', ]; public function users(): BelongsToMany diff --git a/src/Models/FilamentFormUser.php b/src/Models/FilamentFormUser.php index 9045af8..6684224 100644 --- a/src/Models/FilamentFormUser.php +++ b/src/Models/FilamentFormUser.php @@ -13,6 +13,7 @@ * @property array $entry * @property array|null $firstEntry * @property-read array $key_value_entry + * @property-read FilamentForm $filamentForm */ class FilamentFormUser extends Model implements HasMedia { diff --git a/src/Observers/FilamentFormUserObserver.php b/src/Observers/FilamentFormUserObserver.php new file mode 100644 index 0000000..ac153ea --- /dev/null +++ b/src/Observers/FilamentFormUserObserver.php @@ -0,0 +1,55 @@ +sendNotifications($filamentFormUser); + } + + public function updated(FilamentFormUser $filamentFormUser): void + { + // Log that the updated event fired + \Log::info('FilamentFormUserObserver::updated() fired for entry ID: '.$filamentFormUser->id); + + $this->sendNotifications($filamentFormUser); + } + + protected function sendNotifications(FilamentFormUser $filamentFormUser): void + { + /** @var \Tapp\FilamentFormBuilder\Models\FilamentForm|null $form */ + $form = $filamentFormUser->filamentForm; + + // Check if notification emails are configured for this form + if (! $form || ! $form->notification_emails) { + \Log::info('No notification emails configured for form ID: '.($form->id ?? 'null')); + + return; + } + + // Filter out empty email addresses and send notifications + $emails = array_filter($form->notification_emails); + + if (empty($emails)) { + \Log::info('Notification emails array is empty after filtering'); + + return; + } + + \Log::info('Sending notifications for entry ID: '.$filamentFormUser->id.' to: '.json_encode($emails)); + + foreach ($emails as $email) { + Mail::to($email)->queue(new FormSubmissionNotification($form, $filamentFormUser)); + } + + \Log::info('Queued '.count($emails).' notification emails for entry ID: '.$filamentFormUser->id); + } +}