From 7bd3e2fbad5d467d87d4d65e7bfb3a0d91ae647a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Azad=20Furkan=20=C5=9EAKAR?= Date: Thu, 8 Aug 2024 14:00:12 +0300 Subject: [PATCH] wip --- .phpunit.cache/test-results | 1 + composer.json | 19 +- ...form-maker.php => filament-form-maker.php} | 8 +- .../create_form_maker_table.php.stub | 19 +- resources/css/form.css | 270 ++++++++++++++++++ .../views/livewire/form-builder.blade.php | 19 ++ src/Enums/FieldTypes.php | 36 +++ src/Enums/FormStatus.php | 25 ++ src/Fields/Classes/Checkbox.php | 76 +++++ src/Fields/Classes/DatePicker.php | 48 ++++ src/Fields/Classes/FileInput.php | 50 ++++ src/Fields/Classes/PhoneInput.php | 47 +++ src/Fields/Classes/Radio.php | 76 +++++ src/Fields/Classes/Select.php | 72 +++++ src/Fields/Classes/TextInput.php | 47 +++ src/Fields/Classes/Textarea.php | 45 +++ src/Fields/Contract/Field.php | 10 + src/Fields/FieldBuilder.php | 104 +++++++ src/Filament/Forms/FormPicker.php | 27 ++ .../FormBuilderCollectionResource.php | 123 ++++++++ .../Pages/CreateFormBuilderCollection.php | 11 + .../Pages/EditFormBuilderCollection.php | 19 ++ .../Pages/ListFormBuilderCollections.php | 19 ++ .../Resources/FormBuilderDataResource.php | 209 ++++++++++++++ .../Pages/CreateFormBuilderData.php | 11 + .../Pages/EditFormBuilderData.php | 19 ++ .../Pages/ListFormBuilderData.php | 19 ++ .../Pages/ViewFormBuilderData.php | 19 ++ .../Resources/FormBuilderResource.php | 251 ++++++++++++++++ .../Pages/CreateFormBuilder.php | 11 + .../Pages/EditFormBuilder.php | 24 ++ .../Pages/ListFormBuilders.php | 19 ++ src/FormMaker.php | 8 +- src/FormMakerPlugin.php | 10 +- src/FormMakerServiceProvider.php | 14 +- src/Helpers/FormBuilderHelper.php | 60 ++++ src/Livewire/FormBuilder.php | 75 +++++ src/Models/FormBuilder.php | 34 +++ src/Models/FormBuilderCollection.php | 27 ++ src/Models/FormBuilderData.php | 62 ++++ src/Models/FormBuilderField.php | 52 ++++ src/Models/FormBuilderSection.php | 58 ++++ src/Models/Scopes/OrderScope.php | 25 ++ src/Models/Traits/HasHiddenOptions.php | 37 +++ src/Models/Traits/HasOptions.php | 263 +++++++++++++++++ .../InteractsWithFormBuilderCollection.php | 10 + src/Models/Traits/InteractsWithFormMaker.php | 16 ++ src/Models/Traits/SubmitAction.php | 106 +++++++ src/Notifications/MessageNotification.php | 177 ++++++++++++ 49 files changed, 2756 insertions(+), 31 deletions(-) create mode 100644 .phpunit.cache/test-results rename config/{form-maker.php => filament-form-maker.php} (66%) create mode 100644 resources/css/form.css create mode 100644 resources/views/livewire/form-builder.blade.php create mode 100644 src/Enums/FieldTypes.php create mode 100644 src/Enums/FormStatus.php create mode 100644 src/Fields/Classes/Checkbox.php create mode 100644 src/Fields/Classes/DatePicker.php create mode 100644 src/Fields/Classes/FileInput.php create mode 100644 src/Fields/Classes/PhoneInput.php create mode 100644 src/Fields/Classes/Radio.php create mode 100644 src/Fields/Classes/Select.php create mode 100644 src/Fields/Classes/TextInput.php create mode 100644 src/Fields/Classes/Textarea.php create mode 100644 src/Fields/Contract/Field.php create mode 100644 src/Fields/FieldBuilder.php create mode 100644 src/Filament/Forms/FormPicker.php create mode 100644 src/Filament/Resources/FormBuilderCollectionResource.php create mode 100644 src/Filament/Resources/FormBuilderCollectionResource/Pages/CreateFormBuilderCollection.php create mode 100644 src/Filament/Resources/FormBuilderCollectionResource/Pages/EditFormBuilderCollection.php create mode 100644 src/Filament/Resources/FormBuilderCollectionResource/Pages/ListFormBuilderCollections.php create mode 100644 src/Filament/Resources/FormBuilderDataResource.php create mode 100644 src/Filament/Resources/FormBuilderDataResource/Pages/CreateFormBuilderData.php create mode 100644 src/Filament/Resources/FormBuilderDataResource/Pages/EditFormBuilderData.php create mode 100644 src/Filament/Resources/FormBuilderDataResource/Pages/ListFormBuilderData.php create mode 100644 src/Filament/Resources/FormBuilderDataResource/Pages/ViewFormBuilderData.php create mode 100644 src/Filament/Resources/FormBuilderResource.php create mode 100644 src/Filament/Resources/FormBuilderResource/Pages/CreateFormBuilder.php create mode 100644 src/Filament/Resources/FormBuilderResource/Pages/EditFormBuilder.php create mode 100644 src/Filament/Resources/FormBuilderResource/Pages/ListFormBuilders.php create mode 100644 src/Helpers/FormBuilderHelper.php create mode 100644 src/Livewire/FormBuilder.php create mode 100644 src/Models/FormBuilder.php create mode 100644 src/Models/FormBuilderCollection.php create mode 100644 src/Models/FormBuilderData.php create mode 100644 src/Models/FormBuilderField.php create mode 100644 src/Models/FormBuilderSection.php create mode 100644 src/Models/Scopes/OrderScope.php create mode 100644 src/Models/Traits/HasHiddenOptions.php create mode 100644 src/Models/Traits/HasOptions.php create mode 100644 src/Models/Traits/InteractsWithFormBuilderCollection.php create mode 100644 src/Models/Traits/InteractsWithFormMaker.php create mode 100644 src/Models/Traits/SubmitAction.php create mode 100644 src/Notifications/MessageNotification.php diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results new file mode 100644 index 0000000..a360541 --- /dev/null +++ b/.phpunit.cache/test-results @@ -0,0 +1 @@ +{"version":"pest_2.35.0","defects":[],"times":{"P\\Tests\\ExampleTest::__pest_evaluable_it_can_test":0.01,"P\\Tests\\ArchTest::__pest_evaluable_it_will_not_use_debugging_functions":0.318}} \ No newline at end of file diff --git a/composer.json b/composer.json index 4e37283..b712645 100644 --- a/composer.json +++ b/composer.json @@ -22,12 +22,16 @@ "require": { "php": "^8.1", "filament/filament": "^3.0", - "spatie/laravel-package-tools": "^1.15.0" + "filament/spatie-laravel-media-library-plugin": "^3.4", + "ryangjchandler/blade-tabler-icons": "^2.3", + "spatie/eloquent-sortable": "^4.0", + "spatie/laravel-package-tools": "^1.15.0", + "ysfkaya/filament-phone-input": "^2.0" }, "require-dev": { "laravel/pint": "^1.0", "nunomaduro/collision": "^7.9", - "nunomaduro/larastan": "^2.0.1", + "larastan/larastan": "^2.9", "orchestra/testbench": "^8.0", "pestphp/pest": "^2.1", "pestphp/pest-plugin-arch": "^2.0", @@ -49,10 +53,15 @@ }, "scripts": { "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", - "analyse": "vendor/bin/phpstan analyse", + "phpstan": "vendor/bin/phpstan analyse --error-format=github --memory-limit=1G", "test": "vendor/bin/pest", "test-coverage": "vendor/bin/pest --coverage", - "format": "vendor/bin/pint" + "lint": "vendor/bin/pint -v", + "checks": [ + "@lint", + "@phpstan", + "@test" + ] }, "config": { "sort-packages": true, @@ -73,4 +82,4 @@ }, "minimum-stability": "dev", "prefer-stable": true -} \ No newline at end of file +} diff --git a/config/form-maker.php b/config/filament-form-maker.php similarity index 66% rename from config/form-maker.php rename to config/filament-form-maker.php index 864a728..bc7e743 100644 --- a/config/form-maker.php +++ b/config/filament-form-maker.php @@ -1,13 +1,7 @@ 'form_maker', - - 'table_prefix' => 'form_maker_', - - 'models' => [ - // - ], + 'user' => App\Models\User::class, // @phpstan-ignore-line 'extra_collections' => [ // App\Models\User::class => 'User List', diff --git a/database/migrations/create_form_maker_table.php.stub b/database/migrations/create_form_maker_table.php.stub index 3d37eb3..8f8198a 100644 --- a/database/migrations/create_form_maker_table.php.stub +++ b/database/migrations/create_form_maker_table.php.stub @@ -8,7 +8,7 @@ return new class extends Migration { public function up() { - Schema::create(config('form-maker.main_table'), function (Blueprint $table) { + Schema::create('form_builders', function (Blueprint $table) { $table->id(); $table->string('name')->unique(); $table->string('slug')->unique(); @@ -16,28 +16,27 @@ return new class extends Migration $table->timestamps(); }); - Schema::create(config('form-maker.table_prefix') . 'sections', function (Blueprint $table) { + Schema::create('form_builder_sections', function (Blueprint $table) { $table->id(); - $table->foreignId(config('form-maker.table_prefix') . 'id')->constrained()->onDelete('cascade'); - $table->json('title')->nullable(); + $table->foreignId('form_builder_id')->constrained()->onDelete('cascade'); + $table->string('title')->nullable(); $table->integer('columns')->default(1); $table->json('options')->nullable(); - $table->json('optional')->nullable(); $table->integer('order_column')->index()->nullable(); $table->timestamps(); }); - Schema::create(config('form-maker.table_prefix') . 'fields', function (Blueprint $table) { + Schema::create('form_builder_fields', function (Blueprint $table) { $table->id(); - $table->foreignId(config('form-maker.table_prefix') . 'section_id')->constrained()->onDelete('cascade'); - $table->json('name'); + $table->foreignId('form_builder_section_id')->constrained()->onDelete('cascade'); + $table->string('name'); $table->string('type'); $table->json('options')->nullable(); $table->integer('order_column')->index()->nullable(); $table->timestamps(); }); - Schema::create(config('form-maker.table_prefix') . 'collections', function (Blueprint $table) { + Schema::create('form_builder_collections', function (Blueprint $table) { $table->id(); $table->string('name')->unique(); $table->string('type')->default('list'); @@ -46,7 +45,7 @@ return new class extends Migration $table->timestamps(); }); - Schema::create(config('form-maker.table_prefix') . 'data', function (Blueprint $table) { + Schema::create('form_builder_data', function (Blueprint $table) { $table->id(); $table->string('name'); $table->json('fields'); diff --git a/resources/css/form.css b/resources/css/form.css new file mode 100644 index 0000000..4fe3004 --- /dev/null +++ b/resources/css/form.css @@ -0,0 +1,270 @@ +.choices__list.choices__list--multiple { + padding-right: 15px!important; +} + +.choices__list.choices__list--multiple > .choices__item { + background: #13293e !important; + color: #fff !important; +} + +.choices__list.choices__list--dropdown > div > .choices__item.choices__item--selectable.is-highlighted { + background: #13293e !important; + color: #fff !important; +} + +.choices__input.choices__input--cloned { + font-size: 18px !important; +} + +.choices__placeholder { + font-size: 18px !important; + padding-left: 10px!important; +} + +.choices__inner { + min-height: 50px !important; + background-color: transparent !important; + display: grid; + align-items: center; +} + +.choices__item.choices__item--selectable { + font-size: 18px !important; +} + +.filepond--label-action { + color: rgb(203 21 23) !important; +} + +.filepond--hopper.filepond--root { + border: 1px #8B8B8B !important; + background-color: transparent !important; +} + +.choices__list.choices__list--multiple > .choices__item > button.choices__button { + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJtMi41OTIuMDQ0IDE4LjM2NCAxOC4zNjQtMi41NDggMi41NDhMLjA0NCAyLjU5MnoiLz48cGF0aCBkPSJNMCAxOC4zNjQgMTguMzY0IDBsMi41NDggMi41NDhMMi41NDggMjAuOTEyeiIvPjwvZz48L3N2Zz4=) !important; +} + + +@media (min-width: 640px) { + .choices__inner { + min-height: 45px !important; + } +} + +@media (min-width: 1024px) { + .choices__inner { + min-height: 60px !important; + } +} + + +.fi-input-wrp { + background-color: transparent !important; + } + .fi-input { + display: block; + height: 60px; + line-height: 60px; + background-color: transparent; + outline: none; + font-size: 18px; + line-height: normal; + color: #000; + transition: all 350ms; + placeholder: #8B8B8B; + border-radius: 8px; + group-hover: !border-primary; + focus: !border-primary; + padding: 0 20px; + width: 100%; + } + + .fi-fo-date-time-picker-display-text-input{ + display: block; + height: 60px; + line-height: 60px; + background-color: transparent; + outline: none; + font-size: 18px; + line-height: normal; + color: #000; + transition: all 350ms; + placeholder: #8B8B8B; + border-radius: 8px; + group-hover: !border-primary; + focus: !border-primary; + padding: 0 20px; + width: 100%; + } + + .fi-select-input { + display: block; + height: auto; + background-color: transparent; + outline: none; + font-size: 18px; + line-height: normal; + color: #000; + transition: all 350ms; + placeholder: #8B8B8B; + border-radius: 8px; + group-hover: !border-primary; + focus: !border-primary; + width: 100%; + } + + .fi-fo-select { + display: block; + height: auto; + background-color: transparent; + outline: none; + font-size: 18px; + line-height: normal; + color: #000; + transition: all 350ms; + placeholder: #8B8B8B; + border-radius: 8px; + group-hover: !border-primary; + focus: !border-primary; + width: 100%; + } + + .fi-fo-textarea { + display: block; + min-height: 170px; + padding: 20px; + width: 100%; + height: 60px; + background-color: transparent; + outline: none; + font-size: 18px; + line-height: normal; + color: #000; + transition: all 350ms; + placeholder: #8B8B8B; + border-radius: 8px; + group-hover: !border-primary; + focus: !border-primary; + scrollbar: scrollbar; + scrollbar-width: 5px; + scrollbar-height: 5px; + scrollbar-track: transparent; + scrollbar-thumb: primary; + scrollbar-thumb-radius: full; + } + + .fi-fo-checkbox-list, .fi-fo-radio { + gap: 15px; + padding: 15px 0; + } + + .fi-checkbox-input, .fi-radio-input { + width: 20px; + height: 20px; + background-color: transparent; + outline: none; + border-radius: 8px; + group-hover: !border-primary; + } + + .fi-fo-field-wrp > div > div { + margin-bottom: 0; + } + + .fi-fo-phone-input { + position: relative; + z-index: 1; + } + + .iti--allow-dropdown .fi-input { + padding-left: 50px!important; + } + + .fi-no.pointer-events-none { + pointer-events: none; + z-index: 1000; + } + + .fi-fo-component-ctn { + gap: 30px; + padding: 15px 0; + } + + input.choices__input.choices__input--cloned { + display: block; + height: 50px; + background-color: transparent; + outline: none; + font-size: 18px; + line-height: normal; + color: #000; + transition: all 350ms; + placeholder: #8B8B8B; + border-radius: 8px; + width: 100%; + } + + .choices__list.choices__list--dropdown { + z-index: 999999 !important; + } + +.choices__list.choices__list--multiple { + padding-right: 15px!important; +} + +.choices__list.choices__list--multiple > .choices__item { + background: #13293e !important; + color: #fff !important; +} + +.choices__list.choices__list--dropdown > div > .choices__item.choices__item--selectable.is-highlighted { + background: #13293e !important; + color: #fff !important; +} + +.choices__input.choices__input--cloned { + font-size: 18px !important; +} + +.choices__placeholder { + font-size: 18px !important; + padding-left: 10px!important; +} + +.choices__inner { + min-height: 50px !important; + background-color: transparent !important; + display: grid; + align-items: center; +} + +.choices__item.choices__item--selectable { + font-size: 18px !important; +} + +.filepond--label-action { + color: rgb(203 21 23) !important; +} + +.filepond--hopper.filepond--root { + border: 1px #8B8B8B !important; + background-color: transparent !important; +} + +.choices__list.choices__list--multiple > .choices__item > button.choices__button { + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJtMi41OTIuMDQ0IDE4LjM2NCAxOC4zNjQtMi41NDggMi41NDhMLjA0NCAyLjU5MnoiLz48cGF0aCBkPSJNMCAxOC4zNjQgMTguMzY0IDBsMi41NDggMi41NDhMMi41NDggMjAuOTEyeiIvPjwvZz48L3N2Zz4=) !important; +} + + +@media (min-width: 640px) { + .choices__inner { + min-height: 45px !important; + } +} + +@media (min-width: 1024px) { + .choices__inner { + min-height: 60px !important; + } +} diff --git a/resources/views/livewire/form-builder.blade.php b/resources/views/livewire/form-builder.blade.php new file mode 100644 index 0000000..c67dbb5 --- /dev/null +++ b/resources/views/livewire/form-builder.blade.php @@ -0,0 +1,19 @@ +
+
+
+ + {{ $this->form }} + +
+ + Submit + +
+
+ + +
+
diff --git a/src/Enums/FieldTypes.php b/src/Enums/FieldTypes.php new file mode 100644 index 0000000..26b63fe --- /dev/null +++ b/src/Enums/FieldTypes.php @@ -0,0 +1,36 @@ + 'Metin', + self::PHONE => 'Telefon', + self::TEXTAREA => 'Uzun Metin', + self::SELECT => 'Seçim Kutusu', + self::FILE => 'Dosya', + self::DATE => 'Tarih', + self::CHECKBOX => 'Onay Kutusu', + self::RADIO => 'Seçim Düğmesi', + }; + } + + public static function options(): array + { + return collect(self::cases()) + ->mapWithKeys(fn (self $type) => [$type->value => $type->label()]) + ->toArray(); + } +} diff --git a/src/Enums/FormStatus.php b/src/Enums/FormStatus.php new file mode 100644 index 0000000..af9e69b --- /dev/null +++ b/src/Enums/FormStatus.php @@ -0,0 +1,25 @@ + 'Kapatıldı', + self::OPEN => 'Açık', + }; + } + + public function color(): string + { + return match ($this) { + self::CLOSED => 'success', + self::OPEN => 'danger', + }; + } +} diff --git a/src/Fields/Classes/Checkbox.php b/src/Fields/Classes/Checkbox.php new file mode 100644 index 0000000..c091691 --- /dev/null +++ b/src/Fields/Classes/Checkbox.php @@ -0,0 +1,76 @@ +label(data_get($field, 'name')) + ->columnSpan([ + 'xs' => 1, + 'sm' => 1, + 'md' => data_get($field, 'options.column_span', 1), + 'lg' => data_get($field, 'options.column_span', 1), + 'xl' => data_get($field, 'options.column_span', 1), + 'default' => data_get($field, 'options.column_span', 1), + ]) + ->columns([ + 'xs' => 1, + 'sm' => 1, + 'md' => data_get($field, 'options.columns', 2), + 'lg' => data_get($field, 'options.columns', 2), + 'xl' => data_get($field, 'options.columns', 2), + 'default' => data_get($field, 'options.columns', 2), + ]) + ->gridDirection('row') + ->id(data_get($field, 'options.htmlId')) + ->options(self::getOptions($field)) + ->visible(function ($get) use ($field) { + $visibilty = data_get($field, 'options.visibility'); + + if (! data_get($visibilty, 'active')) { + return true; + } + + $fieldId = data_get($visibilty, 'fieldId'); + + $value = data_get($visibilty, 'values'); + + if (! $value) { + return $get($fieldId); + } + + return $value === $get($fieldId); + }) + ->required(data_get($field, 'options.is_required', false)); + } + + protected static function getOptions($field): array + { + $type = data_get($field, 'options.data_source'); + + $collection = FormBuilderCollection::find($type); + + $options = []; + + if ($collection?->type === 'list') { // @phpstan-ignore-line + collect($collection->values)->each(function ($value) use (&$options) { // @phpstan-ignore-line + $options[$value['value']] = $value['label']; + }); + } else { + $model = $collection?->model; // @phpstan-ignore-line + $label = $model::getLabelColumn(); + + $options = $model::all()->pluck($label, $label)->toArray(); + } + + return $options; + } +} diff --git a/src/Fields/Classes/DatePicker.php b/src/Fields/Classes/DatePicker.php new file mode 100644 index 0000000..f3f238e --- /dev/null +++ b/src/Fields/Classes/DatePicker.php @@ -0,0 +1,48 @@ +hiddenLabel() + ->placeholder(data_get($field, 'options.is_required', false) ? data_get($field, 'name') . ' *' : data_get($field, 'name')) + ->columnSpan([ + 'xs' => 1, + 'sm' => 1, + 'md' => data_get($field, 'options.column_span', 1), + 'lg' => data_get($field, 'options.column_span', 1), + 'xl' => data_get($field, 'options.column_span', 1), + 'default' => data_get($field, 'options.column_span', 1), + ]) + ->id(data_get($field, 'options.htmlId')) + ->closeOnDateSelection() + ->format('d F Y') + ->displayFormat('d F Y') + ->native(false) + ->visible(function ($get) use ($field) { + $visibilty = data_get($field, 'options.visibility'); + + if (! data_get($visibilty, 'active')) { + return true; + } + + $fieldId = data_get($visibilty, 'fieldId'); + + $value = data_get($visibilty, 'values'); + + if (! $value) { + return $get($fieldId); + } + + return $value === $get($fieldId); + }) + ->required(data_get($field, 'options.is_required', false)); + } +} diff --git a/src/Fields/Classes/FileInput.php b/src/Fields/Classes/FileInput.php new file mode 100644 index 0000000..f970fff --- /dev/null +++ b/src/Fields/Classes/FileInput.php @@ -0,0 +1,50 @@ +label(data_get($field, 'name')) + ->columnSpan([ + 'xs' => 1, + 'sm' => 1, + 'md' => data_get($field, 'options.column_span', 1), + 'lg' => data_get($field, 'options.column_span', 1), + 'xl' => data_get($field, 'options.column_span', 1), + 'default' => data_get($field, 'options.column_span', 1), + ]) + ->id(data_get($field, 'options.htmlId')) + ->acceptedFileTypes(data_get($field, 'options.accepted_file_types') ?? ['application/pdf']) + ->maxSize(data_get($field, 'options.max_size', 5120)) + ->directory('form') + ->helperText(trans('validation.max.file', ['attribute' => data_get($field, 'name'), 'max' => data_get($field, 'options.max_size', 5120)])) + ->storeFiles(false) + ->visible(function ($get) use ($field) { + $visibilty = data_get($field, 'options.visibility'); + + if (! data_get($visibilty, 'active')) { + return true; + } + + $fieldId = data_get($visibilty, 'fieldId'); + + $value = data_get($visibilty, 'values'); + + if (! $value) { + return $get($fieldId); + } + + return $value === $get($fieldId); + }) + ->required(data_get($field, 'options.is_required', false)); + } +} diff --git a/src/Fields/Classes/PhoneInput.php b/src/Fields/Classes/PhoneInput.php new file mode 100644 index 0000000..fe6e9e6 --- /dev/null +++ b/src/Fields/Classes/PhoneInput.php @@ -0,0 +1,47 @@ +hiddenLabel() + ->placeholder(data_get($field, 'options.is_required', false) ? data_get($field, 'name') . ' *' : data_get($field, 'name')) + ->columnSpan([ + 'xs' => 1, + 'sm' => 1, + 'md' => data_get($field, 'options.column_span', 1), + 'lg' => data_get($field, 'options.column_span', 1), + 'xl' => data_get($field, 'options.column_span', 1), + 'default' => data_get($field, 'options.column_span', 1), + ]) + ->id(data_get($field, 'options.htmlId')) + ->visible(function ($get) use ($field) { + $visibilty = data_get($field, 'options.visibility'); + + if (! data_get($visibilty, 'active')) { + return true; + } + + $fieldId = data_get($visibilty, 'fieldId'); + + $value = data_get($visibilty, 'values'); + + if (! $value) { + return $get($fieldId); + } + + return $value === $get($fieldId); + }) + ->required(data_get($field, 'options.is_required', false)); + } +} diff --git a/src/Fields/Classes/Radio.php b/src/Fields/Classes/Radio.php new file mode 100644 index 0000000..f4b66c7 --- /dev/null +++ b/src/Fields/Classes/Radio.php @@ -0,0 +1,76 @@ +label(data_get($field, 'name')) + ->columnSpan([ + 'xs' => 1, + 'sm' => 1, + 'md' => data_get($field, 'options.column_span', 1), + 'lg' => data_get($field, 'options.column_span', 1), + 'xl' => data_get($field, 'options.column_span', 1), + 'default' => data_get($field, 'options.column_span', 1), + ]) + ->columns([ + 'xs' => 1, + 'sm' => 1, + 'md' => data_get($field, 'options.columns', 2), + 'lg' => data_get($field, 'options.columns', 2), + 'xl' => data_get($field, 'options.columns', 2), + 'default' => data_get($field, 'options.columns', 2), + ]) + ->gridDirection('row') + ->id(data_get($field, 'options.htmlId')) + ->options(self::getOptions($field)) + ->visible(function ($get) use ($field) { + $visibilty = data_get($field, 'options.visibility'); + + if (! data_get($visibilty, 'active')) { + return true; + } + + $fieldId = data_get($visibilty, 'fieldId'); + + $value = data_get($visibilty, 'values'); + + if (! $value) { + return $get($fieldId); + } + + return $value === $get($fieldId); + }) + ->required(data_get($field, 'options.is_required', false)); + } + + protected static function getOptions($field): array + { + $type = data_get($field, 'options.data_source'); + + $collection = FormBuilderCollection::find($type); + + $options = []; + + if ($collection?->type === 'list') { // @phpstan-ignore-line + collect($collection->values)->each(function ($value) use (&$options) { // @phpstan-ignore-line + $options[$value['value']] = $value['label']; + }); + } else { + $model = $collection?->model; // @phpstan-ignore-line + $label = $model::getLabelColumn(); + + $options = $model::all()->pluck($label, $label)->toArray(); + } + + return $options; + } +} diff --git a/src/Fields/Classes/Select.php b/src/Fields/Classes/Select.php new file mode 100644 index 0000000..9622001 --- /dev/null +++ b/src/Fields/Classes/Select.php @@ -0,0 +1,72 @@ +hiddenLabel() + ->placeholder(data_get($field, 'options.is_required', false) ? data_get($field, 'name') . ' *' : data_get($field, 'name')) + ->columnSpan([ + 'xs' => 1, + 'sm' => 1, + 'md' => data_get($field, 'options.column_span', 1), + 'lg' => data_get($field, 'options.column_span', 1), + 'xl' => data_get($field, 'options.column_span', 1), + 'default' => data_get($field, 'options.column_span', 1), + ]) + ->id(data_get($field, 'options.htmlId')) + ->reactive() + ->searchable(data_get($field, 'options.is_multiple', false) || data_get($field, 'options.is_searchable', false)) + ->multiple(data_get($field, 'options.is_multiple', false)) + ->preload(data_get($field, 'options.is_multiple', false)) + ->options(self::getOptions($field)) + ->visible(function ($get) use ($field) { + $visibilty = data_get($field, 'options.visibility'); + + if (! data_get($visibilty, 'active')) { + return true; + } + + $fieldId = data_get($visibilty, 'fieldId'); + + $value = data_get($visibilty, 'values'); + + if (! $value) { + return $get($fieldId); + } + + return $value === $get($fieldId); + }) + ->required(data_get($field, 'options.is_required', false)); + } + + protected static function getOptions($field): array + { + $type = data_get($field, 'options.data_source'); + + $collection = FormBuilderCollection::find($type); + + $options = []; + + if ($collection?->type === 'list') { // @phpstan-ignore-line + collect($collection->values)->each(function ($value) use (&$options) { // @phpstan-ignore-line + $options[$value['value']] = $value['label']; + }); + } else { + $model = $collection?->model; // @phpstan-ignore-line + $label = $model::getLabelColumn(); + + $options = $model::all()->pluck($label, $label)->toArray(); + } + + return $options; + } +} diff --git a/src/Fields/Classes/TextInput.php b/src/Fields/Classes/TextInput.php new file mode 100644 index 0000000..da6b689 --- /dev/null +++ b/src/Fields/Classes/TextInput.php @@ -0,0 +1,47 @@ +hiddenLabel() + ->type(data_get($field, 'options.field_type', 'text')) + ->placeholder(data_get($field, 'options.is_required', false) ? data_get($field, 'name') . ' *' : data_get($field, 'name')) + ->columnSpan([ + 'xs' => 1, + 'sm' => 1, + 'md' => data_get($field, 'options.column_span', 1), + 'lg' => data_get($field, 'options.column_span', 1), + 'xl' => data_get($field, 'options.column_span', 1), + 'default' => data_get($field, 'options.column_span', 1), + ]) + ->id(data_get($field, 'options.htmlId')) + ->maxValue(data_get($field, 'options.max_value', null)) + ->minValue(data_get($field, 'options.min_value', null)) + ->visible(function ($get) use ($field) { + $visibilty = data_get($field, 'options.visibility'); + + if (! data_get($visibilty, 'active')) { + return true; + } + + $fieldId = data_get($visibilty, 'fieldId'); + + $value = data_get($visibilty, 'values'); + + if (! $value) { + return $get($fieldId); + } + + return $value === $get($fieldId); + }) + ->required(data_get($field, 'options.is_required', false)); + } +} diff --git a/src/Fields/Classes/Textarea.php b/src/Fields/Classes/Textarea.php new file mode 100644 index 0000000..cdd6782 --- /dev/null +++ b/src/Fields/Classes/Textarea.php @@ -0,0 +1,45 @@ +hiddenLabel() + ->placeholder(data_get($field, 'options.is_required', false) ? data_get($field, 'name') . ' *' : data_get($field, 'name')) + ->columnSpan([ + 'xs' => 1, + 'sm' => 1, + 'md' => data_get($field, 'options.column_span', 1), + 'lg' => data_get($field, 'options.column_span', 1), + 'xl' => data_get($field, 'options.column_span', 1), + 'default' => data_get($field, 'options.column_span', 1), + ]) + ->autosize() + ->id(data_get($field, 'options.htmlId')) + ->visible(function ($get) use ($field) { + $visibilty = data_get($field, 'options.visibility'); + + if (! data_get($visibilty, 'active')) { + return true; + } + + $fieldId = data_get($visibilty, 'fieldId'); + + $value = data_get($visibilty, 'values'); + + if (! $value) { + return $get($fieldId); + } + + return $value === $get($fieldId); + }) + ->required(data_get($field, 'options.is_required', false)); + } +} diff --git a/src/Fields/Contract/Field.php b/src/Fields/Contract/Field.php new file mode 100644 index 0000000..cb6c773 --- /dev/null +++ b/src/Fields/Contract/Field.php @@ -0,0 +1,10 @@ + $builder]); + + return self::makeSections($builder); + } + + protected static function makeSections($builder): Forms\Components\Group + { + $sections = $builder->sections; + + $newSections = []; + $fields = []; + + $grid = 2; + + foreach ($sections as $section) { + + foreach ($section->fields as $field) { + $fields[$section->id][] = self::getField($field); + } + + $newSections[] = + Forms\Components\Grid::make() + ->columns([ + 'xs' => 1, + 'sm' => 1, + 'md' => $section->columns ?? $grid, + 'lg' => $section->columns ?? $grid, + 'xl' => $section->columns ?? $grid, + 'default' => $section->columns ?? $grid, + ]) + ->schema([ + self::placeholder($section->title), + ...$fields[$section->id], + ]); + } + + return Forms\Components\Group::make() + ->schema($newSections); + } + + protected static function placeholder(?string $title): Forms\Components\Placeholder + { + return Forms\Components\Placeholder::make('title') + ->label(new HtmlString('
+

' . $title . '

+
')) + ->columnSpanFull() + ->hidden(! $title) + ->dehydrated(false); + } + + // TODO: Add extra fields feature + protected static function getExtraFields($field): array + { + $extraFields = config('filament-form-maker.extra_fields'); + + $fields = []; + + foreach ($extraFields as $key => $extraField) { + if ($field->type->value === $key) { + $fields[] = $extraField::make($field); + } + } + + return $fields; + } + + protected static function getField($field): Forms\Components\Field + { + return match ($field->type->value) { + 'text' => TextInput::make($field), + 'phone' => PhoneInput::make($field), + 'select' => Select::make($field), + 'textarea' => Textarea::make($field), + 'file' => FileInput::make($field), + 'date' => DatePicker::make($field), + 'checkbox' => Checkbox::make($field), + 'radio' => Radio::make($field), + default => Forms\Components\Hidden::make('null')->dehydrated(false), + }; + } +} diff --git a/src/Filament/Forms/FormPicker.php b/src/Filament/Forms/FormPicker.php new file mode 100644 index 0000000..f26141e --- /dev/null +++ b/src/Filament/Forms/FormPicker.php @@ -0,0 +1,27 @@ +schema([ + Forms\Components\Select::make('form_builder_id') + ->label('Form Seç') + ->native(false) + ->searchable() + ->preload() + ->options(fn () => FormBuilder::pluck('name', 'id')->toArray()) + ->required(), + ]); + } +} diff --git a/src/Filament/Resources/FormBuilderCollectionResource.php b/src/Filament/Resources/FormBuilderCollectionResource.php new file mode 100644 index 0000000..1316656 --- /dev/null +++ b/src/Filament/Resources/FormBuilderCollectionResource.php @@ -0,0 +1,123 @@ +schema([ + Forms\Components\Section::make() + ->schema([ + Forms\Components\TextInput::make('name') + ->label('Adı') + ->required(), + Forms\Components\Select::make('type') + ->label('Tipi') + ->live() + ->options([ + 'list' => 'Liste', + 'model' => 'Model', + ]) + ->default('list'), + Forms\Components\Select::make('model') + ->label('Model') + ->visible(fn ($get) => $get('type') === 'model') + ->live() + ->options(function () { + $collections = FormBuilderHelper::getAllResources(); + + return [ + config('filament-form-maker.extra_collections'), + ...$collections, + ]; + }) + ->required(), + Forms\Components\Repeater::make('values') + ->label('Değerler') + ->visible(fn ($get) => $get('type') === 'list') + ->addActionLabel('Değer Ekle') + ->grid(3) + ->schema([ + Forms\Components\TextInput::make('value') + ->label('Değer') + ->required(), + Forms\Components\TextInput::make('label') + ->label('Etiket') + ->required(), + ]), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->searchable() + ->label('Adı'), + Tables\Columns\TextColumn::make('type') + ->label('Tipi') + ->badge() + ->color('info') + ->formatStateUsing(function ($state, $record) { + return match ($state) { + default => 'Liste', + 'model' => (new $record->model)->getClassName(), + }; + }), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListFormBuilderCollections::route('/'), + 'create' => Pages\CreateFormBuilderCollection::route('/create'), + 'edit' => Pages\EditFormBuilderCollection::route('/{record}/edit'), + ]; + } +} diff --git a/src/Filament/Resources/FormBuilderCollectionResource/Pages/CreateFormBuilderCollection.php b/src/Filament/Resources/FormBuilderCollectionResource/Pages/CreateFormBuilderCollection.php new file mode 100644 index 0000000..cfc8974 --- /dev/null +++ b/src/Filament/Resources/FormBuilderCollectionResource/Pages/CreateFormBuilderCollection.php @@ -0,0 +1,11 @@ +where('status', 'open')->count(); + } + + public static function getNavigationBadgeColor(): ?string + { + return 'danger'; + } + + public static function canCreate(): bool + { + return false; + } + + public static function canEdit(Model $record): bool + { + return false; + } + + public static function infolist(Infolist $infolist): Infolist + { + return $infolist + ->columns(1) + ->schema([ + Section::make() + ->columns(2) + ->schema([ + TextEntry::make('name') + ->label('Form Adı'), + + TextEntry::make('status') + ->label('Durum') + ->badge() + ->formatStateUsing(fn ($state) => $state?->label()) + ->color(fn ($state) => $state?->color()), + + TextEntry::make('ip') + ->label('IP Adresi'), + + TextEntry::make('url') + ->label('URL'), + + TextEntry::make('user_agent') + ->columnSpanFull() + ->label('Tarayıcı Bilgisi'), + + TextEntry::make('file') + ->label('Dosya') + ->columnSpanFull() + ->visible(fn ($record) => $record->getFirstMedia('file')) + ->default(function ($record) { + $cv = $record->getFirstMedia('file') ? $record->getFirstMediaUrl('file') : null; + + return new HtmlString("Dosyayı İndir"); + }), + ]), + + Tabs::make() + ->schema([ + Tabs\Tab::make('Genel Bilgiler') + ->columns(2) + ->statePath('fields') + ->schema( + fn ($state) => collect($state) + ->reject(fn ($value, $key) => strtolower((string) $key) === 'locale') + ->sortBy(fn ($value, $key) => $key) + ->map(fn ($value, $key) => TextEntry::make($key) + ->label($key) + ->getStateUsing(fn () => $value))->values()->all() + ), + ]), + ]); + } + + public static function form(Form $form): Form + { + return $form; + } + + public static function table(Table $table): Table + { + return $table + ->defaultSort('created_at', 'desc') + ->columns([ + Tables\Columns\TextColumn::make('id') + ->label('ID') + ->searchable() + ->sortable(), + + Tables\Columns\TextColumn::make('name') + ->badge() + ->label('Form Adı') + ->searchable() + ->sortable(), + + Tables\Columns\TextColumn::make('status') + ->badge() + ->formatStateUsing(fn ($state) => $state?->label()) + ->color(fn ($state) => $state?->color()) + ->label('Durum') + ->sortable(), + + Tables\Columns\TextColumn::make('created_at') + ->dateTime() + ->label('Oluşturulma Tarihi') + ->sortable(), + + Tables\Columns\TextColumn::make('updated_at') + ->dateTime() + ->label('Değiştirme Tarihi') + ->sortable(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\ViewAction::make() + ->modalHeading('Görüntüle'), + Tables\Actions\Action::make('close') + ->label('Kapat') + ->color('success') + ->icon('heroicon-o-x-circle') + ->requiresConfirmation() + ->modalHeading('Formu Kapat') + ->modalDescription('Bu formu kapatmak istediğinize emin misiniz?') + ->successNotificationTitle('Form başarıyla kapatıldı.') + ->action(function ($record, $action) { + $record->update(['status' => 'closed']); + + return $action->success(); + })->visible(fn ($record) => $record->status === FormStatus::OPEN), + Tables\Actions\Action::make('open') + ->color('danger') + ->label('Yeniden Aç') + ->icon('heroicon-o-check-circle') + ->requiresConfirmation() + ->modalHeading('Formu Yeniden Aç') + ->modalDescription('Bu formu yeniden açmak istediğinize emin misiniz?') + ->successNotificationTitle('Form başarıyla yeniden açıldı.') + ->action(function ($record, $action) { + $record->update(['status' => 'open']); + + return $action->success(); + })->visible(fn ($record) => $record->status === FormStatus::CLOSED), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListFormBuilderData::route('/'), + 'create' => Pages\CreateFormBuilderData::route('/create'), + 'view' => Pages\ViewFormBuilderData::route('/{record}'), + 'edit' => Pages\EditFormBuilderData::route('/{record}/edit'), + ]; + } +} diff --git a/src/Filament/Resources/FormBuilderDataResource/Pages/CreateFormBuilderData.php b/src/Filament/Resources/FormBuilderDataResource/Pages/CreateFormBuilderData.php new file mode 100644 index 0000000..fdd4f3f --- /dev/null +++ b/src/Filament/Resources/FormBuilderDataResource/Pages/CreateFormBuilderData.php @@ -0,0 +1,11 @@ +columns(1) + ->schema([ + Forms\Components\Section::make('Form Bilgileri') + ->headerActions([ + Forms\Components\Actions\Action::make('Form Ayarları') + ->slideOver() + ->tooltip('Form Ayarları') + ->icon('heroicon-m-cog') + ->modalIcon('heroicon-m-cog') + ->modalDescription('Daha Fazla Form Ayarları') + ->fillForm(fn ( + $state, + array $arguments, + $component + ) => $component->getState()) + ->form(function ($get, array $arguments, $component, $state) { + $arguments = $component->getState(); + + return [ + static::staticFormBuilderOptions(), + ]; + }) + ->action(function (array $data, array $arguments, $component): void { + $state = $component->getState(); + $state = array_merge($state, $data); + $component->state($state); + }), + ]) + ->schema([ + Forms\Components\Grid::make() + ->schema([ + Forms\Components\TextInput::make('name') + ->label('Adı') + ->required() + ->live(true) + ->unique('form_builders', 'name', ignoreRecord: true) + ->afterStateUpdated(function ($state, $set, $context) { + if ($context === 'edit') { + return; + } + $set('slug', str($state)->slug()); + }), + Forms\Components\TextInput::make('slug') + ->label('Kısa Adı') + ->readOnly() + ->hintIcon('heroicon-s-information-circle', 'Kısa Ad sadece alan oluşturulduğunda otomatik olarak oluşturulur. Kullanıcı için herhangi bir etkisi yoktur.') + ->unique('form_builders', 'slug', ignoreRecord: true) + ->required(), + self::hiddenFormBuilderLabels(), + ]), + ]), + Forms\Components\Repeater::make('sections') + ->label('Bölümler') + ->addActionLabel('Bölüm Ekle') + ->itemLabel(fn ($state) => $state['title'] ?? 'Yeni Bölüm') + ->collapsed(fn (string $operation) => $operation === 'edit') + ->cloneable() + ->relationship('sections') + ->reorderable() + ->orderColumn('order_column') + ->deleteAction( + fn ($action) => $action->requiresConfirmation(), + ) + ->schema([ + Forms\Components\TextInput::make('title') + ->label('Başlık') + ->lazy() + ->nullable(), + Forms\Components\Select::make('columns') + ->options(fn (): array => array_combine(range(1, 3), range(1, 3))) + ->label('Sütun Sayısı') + ->required() + ->default(1) + ->hint('Bölümün sütun sayısını belirler.'), + + Forms\Components\Repeater::make('fields') + ->label('Alanlar') + ->addActionLabel('Alan Ekle') + ->minItems(1) + ->grid(3) + ->itemLabel(fn ($state) => $state['name'] ?? 'Yeni Alan') + ->collapsed(fn (string $operation) => $operation === 'edit') + ->cloneable() + ->deleteAction( + fn ($action) => $action->requiresConfirmation(), + ) + ->extraItemActions([ + Forms\Components\Actions\Action::make('Alan Ayarları') + ->slideOver() + ->tooltip('Alan Ayarları') + ->icon('heroicon-m-cog') + ->modalIcon('heroicon-m-cog') + ->modalDescription('Daha Fazla Alan Ayarları') + ->fillForm(fn ( + $state, + array $arguments, + Forms\Components\Repeater $component + ) => $component->getItemState($arguments['item'])) + ->form(function ($get, array $arguments, Forms\Components\Repeater $component, $state) { + $arguments = $component->getState()[$arguments['item']]; + + $type = $arguments['type'] ?? null; + + $collections = match ($type) { + 'select' => static::selectFieldOptions(), + 'checkbox', 'radio' => static::checkboxRadioFieldOptions(), + 'text' => static::textFieldOptions(), + 'file' => static::getFileFieldOptions(), + default => [], + }; + + $parentComponent = $component->getParentRepeater(); + + $fields = $parentComponent?->getState(); + + return [ + static::staticFieldOptions(), + static::getConditionalFieldOptions($fields), + ...$collections, + ]; + }) + ->action(function (array $data, array $arguments, Forms\Components\Repeater $component): void { + $state = $component->getState(); + $state[$arguments['item']] = array_merge($state[$arguments['item']], $data); + $component->state($state); + }), + ]) + ->cloneAction(fn ($action) => $action->action(function (array $arguments, Forms\Components\Repeater $component) { + $items = $component->getState(); + $originalItem = $component->getState()[$arguments['item']]; + + $options = collect($originalItem['options'])->except(['htmlId', 'fieldId'])->toArray(); + + $clonedItem = array_merge($originalItem, [ + 'name' => $originalItem['name'] . ' new', + 'options' => [ + 'htmlId' => $originalItem['options']['htmlId'] . Str::random(2), + 'fieldId' => $originalItem['options']['fieldId'] . Str::random(2), + ...$options, + ], + ]); + + $items[] = $clonedItem; + $component->state($items); + + return $items; + })) + ->relationship('fields') + ->reorderable() + ->orderColumn('order_column') + ->schema([ + Forms\Components\TextInput::make('name') + ->label('Alan Adı') + ->lazy() + ->afterStateUpdated(function ($state, $set, $context) { + if ($context === 'edit') { + return; + } + $set('slug', str($state)->slug('_')); + }) + ->required(), + Forms\Components\Select::make('type') + ->label('Alan Tipi') + ->options(FieldTypes::options()) + ->native(false) + ->searchable() + ->live() + ->required(), + self::hiddenFieldLabels(), + ]), + ]), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->searchable() + ->label('Adı'), + Tables\Columns\TextColumn::make('slug') + ->label('Kısa Adı'), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListFormBuilders::route('/'), + 'create' => Pages\CreateFormBuilder::route('/create'), + 'edit' => Pages\EditFormBuilder::route('/{record}/edit'), + ]; + } +} diff --git a/src/Filament/Resources/FormBuilderResource/Pages/CreateFormBuilder.php b/src/Filament/Resources/FormBuilderResource/Pages/CreateFormBuilder.php new file mode 100644 index 0000000..770c3d4 --- /dev/null +++ b/src/Filament/Resources/FormBuilderResource/Pages/CreateFormBuilder.php @@ -0,0 +1,11 @@ + $this->getRecord()]); + } +} diff --git a/src/Filament/Resources/FormBuilderResource/Pages/ListFormBuilders.php b/src/Filament/Resources/FormBuilderResource/Pages/ListFormBuilders.php new file mode 100644 index 0000000..328eb7f --- /dev/null +++ b/src/Filament/Resources/FormBuilderResource/Pages/ListFormBuilders.php @@ -0,0 +1,19 @@ +resources([ + FormBuilderResource::class, + FormBuilderCollectionResource::class, + FormBuilderDataResource::class, + ]); } public function boot(Panel $panel): void diff --git a/src/FormMakerServiceProvider.php b/src/FormMakerServiceProvider.php index e8ede24..860b97b 100644 --- a/src/FormMakerServiceProvider.php +++ b/src/FormMakerServiceProvider.php @@ -12,6 +12,7 @@ use Filament\Support\Facades\FilamentIcon; use Illuminate\Filesystem\Filesystem; use Livewire\Features\SupportTesting\Testable; +use Livewire\Livewire; use Spatie\LaravelPackageTools\Commands\InstallCommand; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -30,6 +31,7 @@ public function configurePackage(Package $package): void * More info: https://github.com/spatie/laravel-package-tools */ $package->name(static::$name) + ->hasViews('filament-form-maker') ->hasCommands($this->getCommands()) ->hasInstallCommand(function (InstallCommand $command) { $command @@ -52,10 +54,6 @@ public function configurePackage(Package $package): void if (file_exists($package->basePath('/../resources/lang'))) { $package->hasTranslations(); } - - if (file_exists($package->basePath('/../resources/views'))) { - $package->hasViews(static::$viewNamespace); - } } public function packageRegistered(): void {} @@ -87,6 +85,8 @@ public function packageBooted(): void // Testing Testable::mixin(new TestsFormMaker); + + Livewire::component('afsakar.form-builder', \Afsakar\FormMaker\Livewire\FormBuilder::class); } protected function getAssetPackageName(): ?string @@ -101,8 +101,8 @@ protected function getAssets(): array { return [ // AlpineComponent::make('filament-form-maker', __DIR__ . '/../resources/dist/components/filament-form-maker.js'), - Css::make('filament-form-maker-styles', __DIR__ . '/../resources/dist/filament-form-maker.css'), - Js::make('filament-form-maker-scripts', __DIR__ . '/../resources/dist/filament-form-maker.js'), + Css::make('filament-form-maker-styles', __DIR__ . '/../resources/css/form.css'), + // Js::make('filament-form-maker-scripts', __DIR__ . '/../resources/dist/filament-form-maker.js'), ]; } @@ -146,7 +146,7 @@ protected function getScriptData(): array protected function getMigrations(): array { return [ - 'create_filament-form-maker_table', + 'create_form_maker_table', ]; } } diff --git a/src/Helpers/FormBuilderHelper.php b/src/Helpers/FormBuilderHelper.php new file mode 100644 index 0000000..32b55bd --- /dev/null +++ b/src/Helpers/FormBuilderHelper.php @@ -0,0 +1,60 @@ +isDir() || $file->getExtension() !== 'php') { + continue; + } + + $path = $file->getRealPath(); + $relativePath = Str::after($path, realpath(app_path('Models')) . DIRECTORY_SEPARATOR); + $class = 'App\\Models\\' . str_replace(['/', '.php'], ['\\', ''], $relativePath); + + if (class_exists($class)) { + $models[] = $class; + } + } + + return $models; + } + + public static function hasTrait($class, $trait): bool + { + $reflector = new ReflectionClass($class); + + return in_array($trait, $reflector->getTraitNames(), true); + } + + public static function getAllResources(): array + { + $modelsDirectory = app_path('Models'); + $allModels = self::getModels($modelsDirectory); + $modelsWithTrait = []; + + foreach ($allModels as $model) { + if (self::hasTrait($model, InteractsWithFormBuilderCollection::class)) { + $modelLabel = new ($model); + + $className = $modelLabel::getClassName(); + + $modelsWithTrait[$model] = $className; + } + } + + return $modelsWithTrait; + } +} diff --git a/src/Livewire/FormBuilder.php b/src/Livewire/FormBuilder.php new file mode 100644 index 0000000..5ffa19b --- /dev/null +++ b/src/Livewire/FormBuilder.php @@ -0,0 +1,75 @@ +builderModel = FormBuilderModel::find($this->formBuilderId); + + $this->form->fill(); // @phpstan-ignore-line + + $this->url = url()->current(); + + $this->options = data_get($this->builderModel, 'options'); + } + + public function form(Forms\Form $form): Forms\Form + { + return $form + ->schema([ + FieldBuilder::make($this->builderModel), + Forms\Components\Hidden::make('terms') + ->default(false) + ->rule(['accepted']) + ->dehydrated(false), + ]) + ->statePath('fields'); + } + + public function save() + { + $staticName = data_get($this->options, 'static_name') ?? $this->builderModel?->name; // @phpstan-ignore-line + + $data = [ + 'fields' => $this->form->getState(), // @phpstan-ignore-line + 'form_name' => $staticName, + 'url' => $this->url, + 'admin_ids' => data_get($this->options, 'admin_ids') ?? [], + ]; + + $this->submit($data); + + $this->form->fill(); // @phpstan-ignore-line + } + + public function render(): View + { + return view('filament-form-maker::livewire.form-builder'); + } +} diff --git a/src/Models/FormBuilder.php b/src/Models/FormBuilder.php new file mode 100644 index 0000000..26ad3ea --- /dev/null +++ b/src/Models/FormBuilder.php @@ -0,0 +1,34 @@ + 'array', + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'name', + 'slug', + 'options', + ]; + + public function sections(): HasMany + { + return $this->hasMany(FormBuilderSection::class, 'form_builder_id'); + } +} diff --git a/src/Models/FormBuilderCollection.php b/src/Models/FormBuilderCollection.php new file mode 100644 index 0000000..4ae2271 --- /dev/null +++ b/src/Models/FormBuilderCollection.php @@ -0,0 +1,27 @@ + 'collection', + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'name', + 'types', + 'values', + 'model', + ]; +} diff --git a/src/Models/FormBuilderData.php b/src/Models/FormBuilderData.php new file mode 100644 index 0000000..3274242 --- /dev/null +++ b/src/Models/FormBuilderData.php @@ -0,0 +1,62 @@ + + */ + protected $fillable = [ + 'name', + 'locale', + 'fields', + 'status', + 'ip', + 'user_agent', + 'url', + ]; + + protected static function booted() + { + self::creating(function ($model) { + $model->ip = request()->ip(); + $model->user_agent = request()->userAgent(); + }); + } + + public function scopeOpen(Builder $builder) + { + return $builder->whereStatus(FormStatus::OPEN); // @phpstan-ignore-line + } + + public function scopeClosed(Builder $builder) + { + return $builder->whereStatus(FormStatus::CLOSED); // @phpstan-ignore-line + } + + /** + * The attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'status' => FormStatus::class, + 'fields' => 'array', + ]; + } +} diff --git a/src/Models/FormBuilderField.php b/src/Models/FormBuilderField.php new file mode 100644 index 0000000..1a0ab0f --- /dev/null +++ b/src/Models/FormBuilderField.php @@ -0,0 +1,52 @@ + + */ + protected $fillable = [ + 'form_builder_section_id', + 'name', + 'type', + 'options', + 'order_column', + ]; + + protected $casts = [ + 'options' => 'array', + 'type' => FieldTypes::class, + ]; + + protected static function boot(): void + { + parent::boot(); + static::addGlobalScope(new OrderScope('order_column', 'asc')); + } + + public function builder(): BelongsTo + { + return $this->belongsTo(FormBuilder::class, 'form_builder_id'); + } + + public function buildSortQuery(): Builder + { + return static::query()->where('form_builder_section_id', $this->form_builder_section_id); // @phpstan-ignore-line + } +} diff --git a/src/Models/FormBuilderSection.php b/src/Models/FormBuilderSection.php new file mode 100644 index 0000000..368180c --- /dev/null +++ b/src/Models/FormBuilderSection.php @@ -0,0 +1,58 @@ + 'array', + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'form_builder_id', + 'title', + 'columns', + 'options', + 'order_column', + ]; + + protected static function boot(): void + { + parent::boot(); + static::addGlobalScope(new OrderScope('order_column', 'asc')); + } + + public function builder(): BelongsTo + { + return $this->belongsTo(FormBuilder::class, 'form_builder_id'); + } + + public function fields(): HasMany + { + return $this->hasMany(FormBuilderField::class, 'form_builder_section_id'); + } + + public function buildSortQuery(): Builder + { + return static::query()->where('form_builder_id', $this->form_builder_id); // @phpstan-ignore-line + } +} diff --git a/src/Models/Scopes/OrderScope.php b/src/Models/Scopes/OrderScope.php new file mode 100644 index 0000000..4b24233 --- /dev/null +++ b/src/Models/Scopes/OrderScope.php @@ -0,0 +1,25 @@ +column = $column; + $this->direction = $direction; + } + + public function apply(Builder $builder, Model $model) + { + $builder->orderBy($this->column, $this->direction); + } +} diff --git a/src/Models/Traits/HasHiddenOptions.php b/src/Models/Traits/HasHiddenOptions.php new file mode 100644 index 0000000..991e4b9 --- /dev/null +++ b/src/Models/Traits/HasHiddenOptions.php @@ -0,0 +1,37 @@ +statePath('options') + ->schema([ + Forms\Components\Hidden::make('static_name')->default(null), + Forms\Components\Hidden::make('background_color')->default(null), + Forms\Components\Hidden::make('admin_ids')->default(null), + ]) + ->columns(1); + } + + protected static function hiddenFieldLabels(): Forms\Components\Group + { + return Forms\Components\Group::make() + ->statePath('options') + ->schema([ + Forms\Components\Hidden::make('is_required')->default(true), + Forms\Components\Hidden::make('is_multiple')->default(false), + Forms\Components\Hidden::make('is_searchable')->default(false), + Forms\Components\Hidden::make('htmlId')->default(str()->random(6)), + Forms\Components\Hidden::make('fieldId')->default(str()->random(6)), + Forms\Components\Hidden::make('field_type')->default('text'), + Forms\Components\Hidden::make('column_span')->default(1), + Forms\Components\Hidden::make('data_source'), + ]) + ->columns(1); + } +} diff --git a/src/Models/Traits/HasOptions.php b/src/Models/Traits/HasOptions.php new file mode 100644 index 0000000..7dba1fe --- /dev/null +++ b/src/Models/Traits/HasOptions.php @@ -0,0 +1,263 @@ +statePath('options') + ->columns(2) + ->schema([ + Forms\Components\TextInput::make('static_name') + ->label('Bildirim Adı') + ->nullable(), + Forms\Components\Toggle::make('is_required') + ->label('Zorunlu Alan') + ->default(true), + Forms\Components\TextInput::make('fieldId') + ->required() + ->default(str()->random(6)) + ->readOnly() + ->label(__('Alan ID')), + Forms\Components\TextInput::make('htmlId') + ->required() + ->default(str()->random(6)) + ->readOnly() + ->label(__('HTML ID')), + Forms\Components\ToggleButtons::make('column_span') + ->label('Sütun Genişliği') + ->options([ + '1' => '1/3', + '2' => '2/3', + 'full' => 'Tam Genişlik', + ]) + ->inline() + ->default('1'), + ]) + ->columns(1); + } + + protected static function staticFormBuilderOptions(): Forms\Components\Group + { + return Forms\Components\Group::make() + ->statePath('options') + ->schema([ + Forms\Components\TextInput::make('static_name') + ->label('Bildirim Adı') + ->nullable(), + Forms\Components\ColorPicker::make('background_color') + ->label('Arka Plan Rengi') + ->default('transparent'), + Forms\Components\Section::make('Mail Bildirimleri') + ->schema([ + Forms\Components\Select::make('admin_ids') + ->label('Kullanıcı E-postaları') + ->hintIcon('tabler-info-circle', 'Forum\'un doldurulduktan sonra iletilmesini istediğiniz yöneticileri seçiniz.') + ->native(false) + ->searchable() + ->options(config('filament-form-maker.user')::pluck('name', 'id')->toArray()) + ->multiple() + ->nullable(), + ]), + ]); + } + + protected static function selectFieldOptions(): array + { + return [ + Forms\Components\Section::make('Seçim Kutusu Ayarları') + ->collapsed() + ->statePath('options') + ->schema([ + Forms\Components\Toggle::make('is_multiple') + ->label('Çoklu Seçim') + ->default(false), + Forms\Components\Toggle::make('is_searchable') + ->label('Arama Yapılabilir') + ->default(false), + Forms\Components\Select::make('data_source') + ->label('Veri Kaynağı') + ->native(false) + ->searchable() + ->preload() + ->options(FormBuilderCollection::pluck('name', 'id')->toArray()) + ->required(), + ]), + ]; + } + + protected static function checkboxRadioFieldOptions(): array + { + return [ + Forms\Components\Section::make('Onay Kutusu / Seçim Düğmesi Ayarları') + ->collapsed() + ->statePath('options') + ->schema([ + Forms\Components\Select::make('data_source') + ->label('Veri Kaynağı') + ->native(false) + ->searchable() + ->preload() + ->options(FormBuilderCollection::pluck('name', 'id')->toArray()) + ->required(), + Forms\Components\ToggleButtons::make('columns') + ->label('Sütun Sayısı') + ->inline() + ->options([ + 1 => 'Tek Sütun', + 2 => 'İki Sütun', + 3 => 'Üç Sütun', + 4 => 'Dört Sütun', + ]) + ->default(2), + ]), + ]; + } + + protected static function textFieldOptions(): array + { + return [ + Forms\Components\Section::make('Metin Alanı Ayarları') + ->collapsed() + ->statePath('options') + ->schema([ + Forms\Components\Select::make('field_type') + ->label('Metin Alanı Tipi') + ->native(false) + ->searchable() + ->preload() + ->live() + ->options([ + 'text' => 'Metin', + 'email' => 'E-posta', + 'url' => 'URL', + 'number' => 'Sayı', + ]) + ->default('text') + ->required(), + Forms\Components\Grid::make() + ->visible(fn ($get) => $get('field_type') === 'number') + ->schema([ + Forms\Components\TextInput::make('max_value') + ->label('Maksimum Değer') + ->type('number') + ->nullable(), + Forms\Components\TextInput::make('min_value') + ->label('Minimum Değer') + ->type('number') + ->nullable(), + ]), + ]), + ]; + } + + protected static function getFileFieldOptions(): array + { + return [ + Forms\Components\Section::make('Dosya Alanı Ayarları') + ->statePath('options') + ->collapsed() + ->schema([ + Forms\Components\TextInput::make('max_size') + ->label('Maksimum Dosya Boyutu') + ->type('number') + ->default(5120) + ->hint('KB cinsinden') + ->suffix('KB') + ->nullable(), + Forms\Components\Select::make('accepted_file_types') + ->label('Kabul Edilen Dosya Türleri') + ->options([ + 'application/pdf' => 'PDF', + 'application/msword' => 'Word', + 'application/vnd.ms-excel' => 'Excel', + 'application/vnd.ms-powerpoint' => 'PowerPoint', + 'application/zip' => 'Zip', + 'image/*' => 'Resim', + ]) + ->native(false) + ->searchable() + ->multiple() + ->required(), + Forms\Components\TextInput::make('helper_text') + ->label('Yardımcı Metin') + ->nullable(), + ]), + ]; + } + + protected static function getConditionalFieldOptions($getFields): Forms\Components\Component + { + if (filled($getFields)) { + $getFields = collect($getFields) + ->pluck('fields') + ->mapWithKeys(function (array $item) { + return $item; + }); + } + + return Forms\Components\Section::make('Koşullu Görünürlük') + ->statePath('options.visibility') + ->collapsed() + ->schema([ + Forms\Components\Toggle::make('active') + ->live() + ->label('Koşullu Görünürlük Aktif'), + + Forms\Components\Select::make('fieldId') + ->label('Koşul Bağlanacak Alan:') + ->live() + ->searchable(false) + ->visible(fn ($get): bool => ! empty($get('active'))) + ->required(fn ($get): bool => ! empty($get('active'))) + ->native(false) + ->searchable() + ->options(optional($getFields)->where('type', 'select')->pluck('name', 'options.fieldId')->toArray()) + ->afterStateUpdated(fn ($get, $set) => $set('values', null)), + + Forms\Components\Select::make('values') + ->label('Görünürlük Değeri:') + ->live() + ->searchable(false) + ->visible(fn ($get): bool => ! empty($get('active'))) + ->helperText('Eğer herhangi bir değer seçildiği takdirde görünür olmasını istiyorsanız bu alanı boş bırakın.') + ->native(false) + ->searchable() + ->options(function ($get) use ($getFields) { + $getRelated = $getFields->where('options.fieldId', $get('fieldId'))->where('type', 'select')->first(); + + if ($get('fieldId') === null) { + return []; + } + + if (! isset($getRelated['options']['data_source'])) { + return []; + } + + $collection = FormBuilderCollection::find($getRelated['options']['data_source']); + + $options = []; + + if ($collection?->type === 'list') { // @phpstan-ignore-line + collect($collection->values)->each(function ($value) use (&$options) { // @phpstan-ignore-line + $options[$value['value']] = $value['label']; + }); + } else { + $model = $collection?->model; // @phpstan-ignore-line + $label = $model::getLabelColumn(); + + $options = $model::all()->pluck($label, $label)->toArray(); + } + + return $options; + }), + ]); + } +} diff --git a/src/Models/Traits/InteractsWithFormBuilderCollection.php b/src/Models/Traits/InteractsWithFormBuilderCollection.php new file mode 100644 index 0000000..c46867f --- /dev/null +++ b/src/Models/Traits/InteractsWithFormBuilderCollection.php @@ -0,0 +1,10 @@ +get(); + + return $users->each->notify($notification); + } +} diff --git a/src/Models/Traits/SubmitAction.php b/src/Models/Traits/SubmitAction.php new file mode 100644 index 0000000..1a00cad --- /dev/null +++ b/src/Models/Traits/SubmitAction.php @@ -0,0 +1,106 @@ +map(function ($field, $key) use (&$expects) { + if ($field instanceof TemporaryUploadedFile) { + $expects[] = $key; + } + }); + + $form = FormBuilderData::create( + [ + 'name' => $form_name, + 'url' => $data['url'] ?? request()->url(), + 'locale' => app()->getLocale(), + 'fields' => collect($fields)->except($expects)->toArray(), + ] + ); + + collect($fields)->map(function ($field) use ($form) { + if ($field instanceof TemporaryUploadedFile) { + $this->attachFile($field, $form); + } + }); + + $lines = collect($fields)->except($expects)->map(function ($value, $key) { + if (is_array($value)) { + $value = implode(', ', $value); + } + + return "{$key}: {$value}"; + })->toArray(); + + $messageNotification = MessageNotification::make( + (new MailMessage) + ->subject('Yeni Bildirim - ' . $form_name) + ->greeting('Merhaba!') + ->lines($lines) + ->action('Görüntüle', FormBuilderDataResource::getUrl('view', ['record' => $form])), + Notification::make() + ->title('Yeni Bildirim - ' . $form_name) + ->actions([ + Action::make('view') + ->label('Görüntüle') + ->url(FormBuilderDataResource::getUrl('view', ['record' => $form])), + ]) + ); + + $userModel = Filament::auth()->getProvider()->getModel(); // @phpstan-ignore-line + + $userModel::notification($messageNotification, data_get($data, 'admin_ids', [])); + + Notification::make() + ->title('Form Submitted') + ->body('Your message has been sent. We will contact you as soon as possible.') + ->send(); + } + + protected function attachFile($file, $form, $collection = 'file'): void + { + $file = is_array($file) ? head($file) : $file; + + if ($file instanceof TemporaryUploadedFile) { + $form->addMedia($file)->toMediaCollection($collection); + } + } + + protected static function manipulateFields($data): array + { + $keys = collect($data)->keys(); + + $fields = FormBuilderField::whereIn('options->fieldId', $keys)->get(); + + $fields = $fields->mapWithKeys(function ($field) { + $staticName = data_get($field->options, 'static_name') ?? $field->name; // @phpstan-ignore-line + + return [$field->options['fieldId'] => $staticName]; // @phpstan-ignore-line + }); + + return collect($data)->mapWithKeys(function ($value, $key) use ($fields) { + return [ + $fields[$key] => $value, + ]; + })->toArray(); + } +} diff --git a/src/Notifications/MessageNotification.php b/src/Notifications/MessageNotification.php new file mode 100644 index 0000000..f868d18 --- /dev/null +++ b/src/Notifications/MessageNotification.php @@ -0,0 +1,177 @@ +message->setToMailUsing($toMailUsing); + + return $this; + } + + public function setToArrayUsing(callable $toArrayUsing) + { + $this->message->setToArrayUsing($toArrayUsing); + + return $this; + } + + public function setVia(array $via) + { + $this->message->setVia($via); + + return $this; + } + + /** + * Get the notification's delivery channels. + * + * @return array + */ + public function via(mixed $notifiable) + { + return $this->message->via($notifiable); + } + + /** + * Get the mail representation of the notification. + * + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail(mixed $notifiable) + { + return $this->message->toMail($notifiable); + } + + /** + * Get the array representation of the notification. + * + * @return array + */ + public function toArray(mixed $notifiable) + { + return $this->message->toArray($notifiable); + } + + public static function make(?MailMessage $mailMessage = null, array | FilamentNotification | null $data = null): self + { + if (is_null($data)) { + $data = []; + } elseif ($data instanceof FilamentNotification) { + $data = $data->getDatabaseMessage(); + } + + $message = new Message($mailMessage, $data); + + return new self($message); + } +} + +class Message +{ + public ?Closure $toMailUsing = null; + + public ?Closure $toArrayUsing = null; + + public ?array $via = null; + + public function __construct(public ?MailMessage $mailMessage = null, public array $data = []) + { + // + } + + public function setToMailUsing(callable $toMailUsing) + { + $this->toMailUsing = $toMailUsing; + + return $this; + } + + public function setToArrayUsing(callable $toArrayUsing) + { + $this->toArrayUsing = $toArrayUsing; + + return $this; + } + + public function setVia(array $via) + { + $this->via = $via; + + return $this; + } + + public function via($notifiable) + { + if (filled($this->via)) { + return $this->via; + } + + $via = []; + + if (filled($this->mailMessage)) { + $via[] = 'mail'; + } + + if (filled($this->data)) { + $via[] = 'database'; + } + + return $via; + } + + public function toMail($notifiable) + { + if ($this->toMailUsing instanceof \Closure) { + return call_user_func($this->toMailUsing, $this->mailMessage, $notifiable); + } + + $mailMessage = $this->mailMessage; + + if ($mailMessage->subject !== '' && $mailMessage->subject !== '0') { + $mailMessage->subject(Str::finish($mailMessage->subject, ' - ' . config('app.name'))); + } + + return $mailMessage; + } + + public function toArray($notifiable) + { + if ($this->toArrayUsing instanceof \Closure) { + return call_user_func($this->toArrayUsing, $this->data, $notifiable); + } + + return $this->data; + } + + public function retryUntil() + { + return now()->addMinutes(2); + } +}