diff --git a/resources/js/components/blueprints/Builder.vue b/resources/js/components/blueprints/Builder.vue index 7a1d7b0598b..b3b8adb3762 100644 --- a/resources/js/components/blueprints/Builder.vue +++ b/resources/js/components/blueprints/Builder.vue @@ -56,7 +56,6 @@ export default { initialBlueprint: Object, showTitle: Boolean, useTabs: { type: Boolean, default: true }, - isFormBlueprint: { type: Boolean, default: false }, }, data() { @@ -84,15 +83,9 @@ export default { this.$events.$on('root-form-save', () => { this.$nextTick(() => this.save()); }); - - if (this.isFormBlueprint) { - Statamic.$config.set('isFormBlueprint', true); - } }, beforeUnmount() { - Statamic.$config.set('isFormBlueprint', false); - this.$events.$off('root-form-save'); this.saveKeyBinding.destroy(); diff --git a/resources/js/components/fields/FieldtypeSelector.vue b/resources/js/components/fields/FieldtypeSelector.vue index 461d40aa72a..fd655eb449f 100644 --- a/resources/js/components/fields/FieldtypeSelector.vue +++ b/resources/js/components/fields/FieldtypeSelector.vue @@ -225,11 +225,7 @@ export default { created() { if (this.fieldtypesLoaded) return; - let url = cp_url('fields/fieldtypes?selectable=true'); - - if (this.$config.get('isFormBlueprint')) url += '&forms=true'; - - this.$axios.get(url) + this.$axios.get(cp_url('fields/fieldtypes?selectable=true')) .then((response) => (loadedFieldtypes.value = response.data)) .catch((e) => { this.$toast.error(e.response?.data?.message || __('Something went wrong')); diff --git a/resources/js/pages/blueprints/Edit.vue b/resources/js/pages/blueprints/Edit.vue index 090e5002e79..46e855ab778 100644 --- a/resources/js/pages/blueprints/Edit.vue +++ b/resources/js/pages/blueprints/Edit.vue @@ -10,7 +10,6 @@ defineProps({ canDefineLocalizable: { type: Boolean, default: undefined }, resetRoute: String, isResettable: Boolean, - isFormBlueprint: Boolean, }); @@ -24,7 +23,6 @@ defineProps({ :initial-blueprint="blueprint" :use-tabs="useTabs" :can-define-localizable="canDefineLocalizable" - :is-form-blueprint="isFormBlueprint" > - - diff --git a/resources/js/pages/forms/Connect.vue b/resources/js/pages/forms/Connect.vue index 24af846472f..a4c7a3c3a97 100644 --- a/resources/js/pages/forms/Connect.vue +++ b/resources/js/pages/forms/Connect.vue @@ -8,6 +8,7 @@ defineOptions({ layout: [Layout, FormsLayout] }); const props = defineProps({ form: Object, + fieldtypes: Array, }); const formTitle = computed(() => props.form?.title || __('Untitled Form')); @@ -33,4 +34,13 @@ const formTitle = computed(() => props.form?.title || __('Untitled Form')); + +
+ +
    +
  • + {{ fieldtype.title }} +
  • +
+
diff --git a/resources/js/pages/forms/Index.vue b/resources/js/pages/forms/Index.vue index af85ee15de7..8b1843af8fd 100644 --- a/resources/js/pages/forms/Index.vue +++ b/resources/js/pages/forms/Index.vue @@ -74,12 +74,6 @@ const reloadPage = () => router.reload(); diff --git a/resources/js/pages/forms/Show.vue b/resources/js/pages/forms/Show.vue index 8c0daaccaef..57fa23dcbb8 100644 --- a/resources/js/pages/forms/Show.vue +++ b/resources/js/pages/forms/Show.vue @@ -123,12 +123,6 @@ function exportSubmissions() { - - - name('asset-containers.edit'); Route::patch('asset-containers/{asset_container}', [AssetContainerBlueprintController::class, 'update'])->name('asset-containers.update'); - Route::get('forms/{form}/edit', [FormBlueprintController::class, 'edit'])->name('forms.edit'); - Route::patch('forms/{form}', [FormBlueprintController::class, 'update'])->name('forms.update'); + Route::get('forms/{form}/edit', FormBlueprintController::class)->name('forms.edit'); Route::get('globals/{global_set}/edit', [GlobalsBlueprintController::class, 'edit'])->name('globals.edit'); Route::patch('globals/{global_set}', [GlobalsBlueprintController::class, 'update'])->name('globals.update'); diff --git a/src/CommandPalette/Palette.php b/src/CommandPalette/Palette.php index 99751cd3cdd..dd363fe44fd 100644 --- a/src/CommandPalette/Palette.php +++ b/src/CommandPalette/Palette.php @@ -114,10 +114,6 @@ protected function buildFields(): self ->map(fn ($set) => $set->blueprintCommandPaletteLink()) ->each(fn (Link $link) => $this->addCommand($link)); - Facades\Form::all() - ->map(fn ($form) => $form->blueprintCommandPaletteLink()) - ->each(fn (Link $link) => $this->addCommand($link)); - $this->addCommand(Facades\User::blueprintCommandPaletteLink()); $this->addCommand(Facades\UserGroup::blueprintCommandPaletteLink()); diff --git a/src/Exceptions/FormFieldtypeNotFoundException.php b/src/Exceptions/FormFieldtypeNotFoundException.php new file mode 100644 index 00000000000..4605473f935 --- /dev/null +++ b/src/Exceptions/FormFieldtypeNotFoundException.php @@ -0,0 +1,13 @@ +selectable; } + /** + * @deprecated Use FormFieldtype::isSelectable() instead. + */ public function selectableInForms(): bool { if (FieldtypeRepository::selectableInFormIsOverriden($this->handle())) { @@ -110,11 +116,17 @@ public function selectableInForms(): bool return $this->selectableInForms; } + /** + * @deprecated Use FormFieldtype::makeSelectable() instead. + */ public static function makeSelectableInForms() { FieldtypeRepository::makeSelectableInForms(self::handle()); } + /** + * @deprecated Use FormFieldtype::makeUnselectable() instead. + */ public static function makeUnselectableInForms() { FieldtypeRepository::makeUnselectableInForms(self::handle()); diff --git a/src/Fields/FieldtypeRepository.php b/src/Fields/FieldtypeRepository.php index 0fe7eefa277..5b3d68829dc 100644 --- a/src/Fields/FieldtypeRepository.php +++ b/src/Fields/FieldtypeRepository.php @@ -39,21 +39,33 @@ public function handles() }); } + /** + * @deprecated Use FormFieldtype::makeSelectable() instead. + */ public function makeSelectableInForms($handle) { $this->selectableInForms[$handle] = true; } + /** + * @deprecated Use FormFieldtype::makeUnselectable() instead. + */ public function makeUnselectableInForms($handle) { $this->selectableInForms[$handle] = false; } + /** + * @deprecated Use FormFieldtype::isSelectable() instead. + */ public function hasBeenMadeSelectableInForms($handle) { return $this->selectableInForms[$handle] ?? false; } + /** + * @deprecated Use FormFieldtype::isSelectable() instead. + */ public function selectableInFormIsOverriden($handle) { return array_key_exists($handle, $this->selectableInForms); diff --git a/src/Forms/Exceptions/BlueprintUndefinedException.php b/src/Forms/Exceptions/BlueprintUndefinedException.php deleted file mode 100644 index 15a687e99ba..00000000000 --- a/src/Forms/Exceptions/BlueprintUndefinedException.php +++ /dev/null @@ -1,36 +0,0 @@ -handle()}] does not have a blueprint"))->setForm($form); - } - - public function setForm(Form $form) - { - $this->form = $form; - - return $this; - } - - public function getSolution(): Solution - { - return BaseSolution::create("The {$this->form->handle()} form does not have a blueprint defined.") - ->setSolutionDescription("A blueprint defines the form's available fields and their behaviors.\n\nYou can add `blueprint: handle` to a form's YAML file.") - ->setDocumentationLinks([ - 'Read the forms guide' => Statamic::docsUrl('forms'), - ]); - } -} diff --git a/src/Forms/Fields/Email.php b/src/Forms/Fields/Email.php new file mode 100644 index 00000000000..148c95a5345 --- /dev/null +++ b/src/Forms/Fields/Email.php @@ -0,0 +1,34 @@ + [ + 'display' => __('Placeholder'), + 'instructions' => __('statamic::fieldtypes.text.config.placeholder'), + 'type' => 'text', + ], + ]; + } + + public function toFieldArray(): array + { + return [ + 'type' => 'text', + 'input_type' => 'email', + 'placeholder' => $this->config('placeholder'), + 'validate' => array_values(array_unique([...((array) $this->config('validate', [])), 'email'])), + ...Arr::except($this->config(), ['type', 'input_type', 'placeholder', 'validate']), + ]; + } +} diff --git a/src/Forms/Fields/Fallback.php b/src/Forms/Fields/Fallback.php new file mode 100644 index 00000000000..51f00c80e83 --- /dev/null +++ b/src/Forms/Fields/Fallback.php @@ -0,0 +1,43 @@ +wrappedFieldtype = $fieldtype; + + return $this; + } + + public function toFieldArray(): array + { + return $this->config(); + } + + public function title(): string + { + return $this->wrappedFieldtype?->title() ?? parent::title(); + } + + public function toArray(): array + { + if (! $this->wrappedFieldtype) { + return parent::toArray(); + } + + return [ + 'handle' => $this->wrappedFieldtype->handle(), + 'title' => $this->wrappedFieldtype->title(), + 'categories' => $this->wrappedFieldtype->categories(), + 'keywords' => $this->wrappedFieldtype->keywords(), + 'icon' => $this->wrappedFieldtype->icon(), + 'config' => $this->wrappedFieldtype->configFields()->toPublishArray(), + ]; + } +} diff --git a/src/Forms/Fields/FormField.php b/src/Forms/Fields/FormField.php new file mode 100644 index 00000000000..9e408cd33d3 --- /dev/null +++ b/src/Forms/Fields/FormField.php @@ -0,0 +1,56 @@ +handle; + } + + public function config(): array + { + return $this->config; + } + + public function type(): string + { + return Arr::get($this->config, 'type', 'short_answer'); + } + + public function fieldtype() + { + try { + return FormFieldtypeRepository::find($this->type())->setField($this); + } catch (FormFieldtypeNotFoundException $e) { + return (new Fallback)->setField($this); + } + } + + public function display() + { + return Arr::get($this->config, 'display', __(Str::slugToTitle($this->handle))); + } + + public function instructions() + { + return Arr::get($this->config, 'instructions'); + } + + public function toFieldArray(): array + { + return $this->fieldtype()->toFieldArray(); + } +} diff --git a/src/Forms/Fields/FormFields.php b/src/Forms/Fields/FormFields.php new file mode 100644 index 00000000000..1c3e35a3593 --- /dev/null +++ b/src/Forms/Fields/FormFields.php @@ -0,0 +1,141 @@ +contents; + } + + public function items(): Collection + { + return collect($this->contents['sections'] ?? [])->flatMap(fn (array $section): array => $section['fields'] ?? []); + } + + public function fields(): Collection + { + return $this->items()->flatMap(function (array $config): array { + if (isset($config['import'])) { + return $this->getImportedFields($config); + } + + if (is_string($config['field'])) { + return [$config['handle'] => $this->getReferencedField($config)]; + } + + return [$config['handle'] => new FormField($config['handle'], $config['field'])]; + })->filter(); + } + + public function field(string $handle): ?FormField + { + return $this->fields()->get($handle); + } + + public function toBlueprint(): Blueprint + { + $contents = collect($this->contents['sections'] ?? []) + ->map(function (array $section): array { + return [ + ...$section, + 'fields' => collect($section['fields'] ?? []) + ->map(function (array $config): array { + if (isset($config['import']) || is_string($config['field'])) { + return $config; + } + + $formField = $this->field($config['handle']); + + return [ + 'handle' => $config['handle'], + 'field' => Arr::removeNullValues($formField->toFieldArray()), + ]; + }) + ->all(), + ]; + }) + ->all(); + + return Facades\Blueprint::make()->setContents([ + 'tabs' => [ + 'main' => [ + 'sections' => $contents, + ], + ], + ]); + } + + /** + * Borrowed from \Statamic\Fields\Fields. + */ + private function getReferencedField(array $config): FormField + { + if (! $field = FieldRepository::find($config['field'])) { + throw new \Exception("Field {$config['field']} not found."); + } + + if ($overrides = Arr::get($config, 'config')) { + $field->setConfig(array_merge($field->config(), $overrides)); + } + + return new FormField($field->handle(), $field->config()); + } + + /** + * Borrowed from \Statamic\Fields\Fields. + */ + private function getImportedFields(array $config): array + { + $recursion = tap(app(FieldsetRecursionStack::class))->push($config['import']); + + $blink = 'form-fields-imported-fields-'.md5(json_encode($config)); + + $imported = Blink::once($blink, function () use ($config) { + if (! $fieldset = FieldsetRepository::find($config['import'])) { + throw new FieldsetNotFoundException($config['import']); + } + + $fields = $fieldset->fields()->all(); + + if ($overrides = $config['config'] ?? null) { + $fields = $fields->map(function ($field, $handle) use ($overrides) { + return $field->setConfig(array_merge($field->config(), $overrides[$handle] ?? [])); + }); + } + + if ($prefix = Arr::get($config, 'prefix')) { + $fields = $fields->mapWithKeys(function ($field) use ($prefix) { + $field = clone $field; + $handle = $prefix.$field->handle(); + $prefix = $prefix.$field->prefix(); + + return [$handle => $field->setHandle($handle)->setPrefix($prefix)]; + }); + } + + return $fields; + })->map(function ($field) { + return new FormField($field->handle(), $field->config()); + })->all(); + + $recursion->pop(); + + return $imported; + } +} diff --git a/src/Forms/Fields/FormFieldtype.php b/src/Forms/Fields/FormFieldtype.php new file mode 100644 index 00000000000..11c5b87fa15 --- /dev/null +++ b/src/Forms/Fields/FormFieldtype.php @@ -0,0 +1,162 @@ +field = clone $field; + + return $this; + } + + public function field(): ?FormField + { + return $this->field; + } + + public static function handle(): string + { + return Str::removeRight(static::traitHandle(), '_form_field'); + } + + public static function fieldtype(): ?string + { + return static::$fieldtype; + } + + public function categories(): array + { + return $this->categories; + } + + public function keywords(): array + { + return $this->keywords; + } + + public function icon(): string + { + return $this->icon ?? "form-field-{$this->handle()}"; + } + + public function config(?string $key = null, $fallback = null) + { + if (! $this->field) { + return $fallback; + } + + $config = $this->configFields()->all() + ->map->defaultValue() + ->merge($this->field->config()); + + return $key + ? ($config->get($key) ?? $fallback) + : $config->all(); + } + + public function configFields(): Fields + { + if ($cached = Blink::get($blink = 'form-config-fields-'.$this->handle())) { + return $cached; + } + + $fields = collect($this->configFieldItems()); + + $fields = $fields + ->map(function ($field, $handle) { + return compact('handle', 'field'); + }); + + $fields = new ConfigFields($fields); + + Blink::put($blink, $fields); + + return $fields; + } + + protected function configFieldItems(): array + { + return $this->configFields; + } + + public function toField(): Field + { + return new Field($this->handle(), $this->toFieldArray()); + } + + abstract public function toFieldArray(): array; + + public function isSelectable(): bool + { + if (FormFieldtypeRepository::selectableIsOverriden($this->handle())) { + return FormFieldtypeRepository::hasBeenMadeSelectable($this->handle()); + } + + return $this->selectable; + } + + public static function makeSelectable(): void + { + FormFieldtypeRepository::makeSelectable(static::handle()); + } + + public static function makeUnselectable(): void + { + FormFieldtypeRepository::makeUnselectable(static::handle()); + } + + public function toArray(): array + { + return [ + 'handle' => $this->handle(), + 'title' => $this->title(), + 'categories' => $this->categories(), + 'keywords' => $this->keywords(), + 'icon' => $this->icon(), + 'config' => $this->configFields()->toPublishArray(), + ]; + } +} diff --git a/src/Forms/Fields/FormFieldtypeRepository.php b/src/Forms/Fields/FormFieldtypeRepository.php new file mode 100644 index 00000000000..ece5c80fa19 --- /dev/null +++ b/src/Forms/Fields/FormFieldtypeRepository.php @@ -0,0 +1,57 @@ +formFieldtypes[$handle])) { + return clone $this->formFieldtypes[$handle]; + } + + if (! ($formFields = $this->classes())->has($handle)) { + throw new FormFieldtypeNotFoundException($handle); + } + + return $this->formFieldtypes[$handle] = app($formFields->get($handle)); + } + + public function classes(): Collection + { + return app('statamic.form-fieldtypes'); + } + + public function handles(): Collection + { + return $this->classes()->map(function ($class) { + return $class::handle(); + }); + } + + public function makeSelectable(string $handle): void + { + $this->selectable[$handle] = true; + } + + public function makeUnselectable(string $handle): void + { + $this->selectable[$handle] = false; + } + + public function hasBeenMadeSelectable(string $handle): bool + { + return $this->selectable[$handle] ?? false; + } + + public function selectableIsOverriden(string $handle): bool + { + return array_key_exists($handle, $this->selectable); + } +} diff --git a/src/Forms/Fields/LongAnswer.php b/src/Forms/Fields/LongAnswer.php new file mode 100644 index 00000000000..6c5d888e437 --- /dev/null +++ b/src/Forms/Fields/LongAnswer.php @@ -0,0 +1,36 @@ + [ + 'display' => __('Placeholder'), + 'instructions' => __('statamic::fieldtypes.text.config.placeholder'), + 'type' => 'text', + ], + 'character_limit' => [ + 'display' => __('Character Limit'), + 'instructions' => __('statamic::fieldtypes.text.config.character_limit_instructions'), + 'type' => 'integer', + ], + ]; + } + + public function toFieldArray(): array + { + return [ + 'type' => 'textarea', + 'placeholder' => $this->config('placeholder'), + 'character_limit' => $this->config('character_limit'), + ...Arr::except($this->config(), ['type', 'placeholder', 'character_limit']), + ]; + } +} diff --git a/src/Forms/Fields/ShortAnswer.php b/src/Forms/Fields/ShortAnswer.php new file mode 100644 index 00000000000..57d819d71b0 --- /dev/null +++ b/src/Forms/Fields/ShortAnswer.php @@ -0,0 +1,36 @@ + [ + 'display' => __('Placeholder'), + 'instructions' => __('statamic::fieldtypes.text.config.placeholder'), + 'type' => 'text', + ], + 'character_limit' => [ + 'display' => __('Character Limit'), + 'instructions' => __('statamic::fieldtypes.text.config.character_limit_instructions'), + 'type' => 'integer', + ], + ]; + } + + public function toFieldArray(): array + { + return [ + 'type' => 'text', + 'placeholder' => $this->config('placeholder'), + 'character_limit' => $this->config('character_limit'), + ...Arr::except($this->config(), ['type', 'placeholder', 'character_limit']), + ]; + } +} diff --git a/src/Forms/Form.php b/src/Forms/Form.php index 1f91f392276..efdfc202cf3 100644 --- a/src/Forms/Form.php +++ b/src/Forms/Form.php @@ -18,13 +18,15 @@ use Statamic\Events\FormDeleting; use Statamic\Events\FormSaved; use Statamic\Events\FormSaving; -use Statamic\Facades\Blueprint; +use Statamic\Facades; +use Statamic\Facades\Blink; use Statamic\Facades\File; use Statamic\Facades\Form as FormFacade; use Statamic\Facades\FormSubmission; use Statamic\Facades\YAML; -use Statamic\Forms\Exceptions\BlueprintUndefinedException; +use Statamic\Fields\Blueprint; use Statamic\Forms\Exporters\Exporter; +use Statamic\Forms\Fields\FormFields; use Statamic\Statamic; use Statamic\Support\Arr; use Statamic\Support\Str; @@ -36,7 +38,7 @@ class Form implements Arrayable, Augmentable, ContainsQueryableValues, FormContr protected $handle; protected $title; - protected $blueprint; + protected $fields; protected $honeypot; protected $store; protected $email; @@ -78,6 +80,83 @@ public function title($title = null) return $this->fluentlyGetOrSet('title')->args(func_get_args()); } + public function formFields($fields = null) + { + return $this + ->fluentlyGetOrSet('fields') + ->getter(function ($fields) { + if (empty($fields) && $blueprint = Facades\Blueprint::find("forms.{$this->handle()}")) { + $fields = $this->convertFieldsFromBlueprint($blueprint); + } + + return new FormFields($fields ?? []); + }) + ->setter(function ($fields) { + Blink::forget('form-blueprint-'.$this->handle()); + + if (isset($fields['tabs'])) { + $fields = [ + 'sections' => collect($fields['tabs'])->flatMap(fn ($tab) => $tab['sections'])->all(), + ]; + } + + if (isset($fields['fields'])) { + $fields = [ + 'sections' => [ + ['fields' => $fields['fields']], + ], + ]; + } + + return $fields; + }) + ->args(func_get_args()); + } + + private function convertFieldsFromBlueprint(Blueprint $blueprint): array + { + $sections = collect($blueprint->contents()['tabs'] ?? [])->flatMap(function (array $tab): array { + return collect($tab['sections'] ?? [])->map(function (array $section): array { + return [ + ...$section, + 'fields' => collect($section['fields'] ?? [])->map(function (array $field): array { + $validate = Arr::get($field, 'field.validate'); + $validateRules = is_string($validate) ? explode('|', $validate) : ($validate ?? []); + + $isEmailRule = fn ($rule) => is_string($rule) && ($rule === 'email' || str_starts_with($rule, 'email:')); + + if (Arr::get($field, 'field.type') === 'text' && collect($validateRules)->contains($isEmailRule)) { + Arr::set($field, 'field.type', 'email'); + Arr::pull($field, 'field.input_type'); + + $remainingValidationRules = collect($validateRules) + ->reject($isEmailRule) + ->values(); + + if ($remainingValidationRules->isEmpty()) { + unset($field['field']['validate']); + } else { + $field['field']['validate'] = $remainingValidationRules->all(); + } + } + + if (Arr::get($field, 'field.type') === 'text') { + Arr::set($field, 'field.type', 'short_answer'); + } + + if (Arr::get($field, 'field.type') === 'textarea') { + Arr::set($field, 'field.type', 'long_answer'); + } + + return $field; + })->all(), + ]; + })->all(); + })->all(); + + return ['sections' => $sections]; + } + /** * Get the blueprint. * @@ -85,22 +164,19 @@ public function title($title = null) */ public function blueprint() { - $blueprint = Blueprint::find('forms.'.$this->handle()) - ?? Blueprint::makeFromFields([])->setHandle($this->handle())->setNamespace('forms'); + if (Blink::has($blink = 'form-blueprint-'.$this->handle())) { + return Blink::get($blink); + } + + $blueprint = $this->formFields()->toBlueprint(); + + Blink::put($blink, $blueprint); FormBlueprintFound::dispatch($blueprint, $this); return $blueprint; } - public function blueprintCommandPaletteLink() - { - return $this->blueprint()?->commandPaletteLink( - type: 'Forms', - url: $this->editBlueprintUrl(), - ); - } - /** * Get or set the honeypot field. * @@ -155,11 +231,7 @@ public function email($emails = null) */ public function fields() { - if (! $blueprint = $this->blueprint()) { - throw BlueprintUndefinedException::create($this); - } - - return $blueprint->fields()->all(); + return $this->blueprint()->fields()->all(); } /** @@ -211,6 +283,7 @@ public function save() $data = $this->data->merge(collect([ 'title' => $this->title, + 'fields' => $this->formFields()->contents(), 'honeypot' => $this->honeypot, 'email' => collect(isset($this->email['to']) ? [$this->email] : $this->email)->map(function ($email) { $email['markdown'] = Arr::get($email, 'markdown') === true ? true : null; @@ -231,6 +304,10 @@ public function save() File::put($this->path(), YAML::dump($data)); + if ($blueprint = Facades\Blueprint::find("forms.{$this->handle()}")) { + $blueprint->delete(); + } + foreach ($afterSaveCallbacks as $callback) { $callback($this); } @@ -290,7 +367,7 @@ public function hydrate() 'email', ]; - $this->merge(collect($contents)->except($methods)); + $this->merge(collect($contents)->except([...$methods, 'fields'])); collect($contents) ->filter(function ($value, $property) use ($methods) { @@ -300,6 +377,10 @@ public function hydrate() $this->{$property}($value); }); + if (isset($contents['fields'])) { + $this->formFields($contents['fields']); + } + return $this; } @@ -404,9 +485,10 @@ public function deleteUrl() return cp_route('forms.destroy', $this->handle()); } + /** @deprecated */ public function editBlueprintUrl() { - return cp_route('blueprints.forms.edit', $this->handle()); + return cp_route('forms.fields.index', $this->handle()); } public function hasFiles() diff --git a/src/Http/Controllers/CP/Fields/BlueprintController.php b/src/Http/Controllers/CP/Fields/BlueprintController.php index 1b5b148f45c..6fee4335ee9 100644 --- a/src/Http/Controllers/CP/Fields/BlueprintController.php +++ b/src/Http/Controllers/CP/Fields/BlueprintController.php @@ -6,11 +6,9 @@ use Statamic\Facades\AssetContainer; use Statamic\Facades\Blueprint; use Statamic\Facades\Collection; -use Statamic\Facades\Form; use Statamic\Facades\GlobalSet; use Statamic\Facades\Nav; use Statamic\Facades\Taxonomy; -use Statamic\Facades\User; use Statamic\Http\Controllers\CP\CpController; class BlueprintController extends CpController @@ -32,7 +30,6 @@ public function index() 'navs' => $this->navs(), 'assetContainers' => $this->assets(), 'globals' => $this->globals(), - 'forms' => $this->forms(), 'userBlueprint' => [ 'edit_url' => cp_route('blueprints.users.edit'), ], @@ -99,15 +96,4 @@ public function globals() 'edit_url' => cp_route('blueprints.globals.edit', $set->handle()), ])->values()->all(); } - - public function forms(): array - { - return User::current()->can('configure form fields') - ? Form::all()->map(fn ($form) => [ - 'title' => $form->title(), - 'handle' => $form->handle(), - 'edit_url' => cp_route('blueprints.forms.edit', $form->handle()), - ])->values()->all() - : []; - } } diff --git a/src/Http/Controllers/CP/Fields/FieldtypesController.php b/src/Http/Controllers/CP/Fields/FieldtypesController.php index 892cccbd9f0..9408688e089 100644 --- a/src/Http/Controllers/CP/Fields/FieldtypesController.php +++ b/src/Http/Controllers/CP/Fields/FieldtypesController.php @@ -16,8 +16,7 @@ public function index(Request $request) }); if ($request->selectable) { - $selectableMethod = $request->forms ? 'selectableInForms' : 'selectable'; - $fieldtypes = $fieldtypes->filter->$selectableMethod(); + $fieldtypes = $fieldtypes->filter->selectable(); } return $fieldtypes->sortBy->handle()->values(); diff --git a/src/Http/Controllers/CP/Forms/FormBlueprintController.php b/src/Http/Controllers/CP/Forms/FormBlueprintController.php index 013e974dc0d..5dda9c993dd 100644 --- a/src/Http/Controllers/CP/Forms/FormBlueprintController.php +++ b/src/Http/Controllers/CP/Forms/FormBlueprintController.php @@ -2,58 +2,13 @@ namespace Statamic\Http\Controllers\CP\Forms; -use Illuminate\Http\Request; -use Statamic\CP\Breadcrumbs\Breadcrumb; -use Statamic\CP\Breadcrumbs\Breadcrumbs; -use Statamic\Facades\Form; use Statamic\Http\Controllers\CP\CpController; -use Statamic\Http\Controllers\CP\Fields\ManagesBlueprints; +/** @deprecated */ class FormBlueprintController extends CpController { - use ManagesBlueprints; - - public function __construct() - { - $this->middleware(\Illuminate\Auth\Middleware\Authorize::class.':configure form fields'); - } - - public function edit($form) + public function __invoke($form) { - $blueprint = $form->blueprint(); - - Breadcrumbs::push(new Breadcrumb( - text: 'Forms', - )); - - Breadcrumbs::push(new Breadcrumb( - text: $form->title(), - url: request()->url(), - icon: 'forms', - links: Form::all() - ->reject(fn ($f) => $f->handle() === $form->handle()) - ->map(fn ($f) => [ - 'text' => $f->title(), - 'icon' => 'forms', - 'url' => cp_route('blueprints.forms.edit', $f->handle()), - ]) - ->values() - ->all(), - )); - - return $this->renderEditPage([ - 'blueprint' => $this->toVueObject($blueprint), - 'action' => cp_route('blueprints.forms.update', $form->handle()), - 'isFormBlueprint' => true, - 'canDefineLocalizable' => false, - 'useTabs' => false, - ]); - } - - public function update(Request $request, $form) - { - $request->validate(['tabs' => 'array']); - - $this->updateBlueprint($request, $form->blueprint()); + return redirect(cp_route('forms.fields.index', $form->handle())); } } diff --git a/src/Http/Controllers/CP/Forms/FormConnectController.php b/src/Http/Controllers/CP/Forms/FormConnectController.php index 0c6ef889db9..41b62a767a0 100644 --- a/src/Http/Controllers/CP/Forms/FormConnectController.php +++ b/src/Http/Controllers/CP/Forms/FormConnectController.php @@ -2,15 +2,41 @@ namespace Statamic\Http\Controllers\CP\Forms; +use Facades\Statamic\Fields\FieldtypeRepository; use Inertia\Inertia; +use Statamic\Forms\Fields\Fallback; +use Statamic\Forms\Fields\FormFieldtype; use Statamic\Http\Controllers\CP\CpController; class FormConnectController extends CpController { public function __invoke($form) { + $this->authorize('edit', $form); + + // TODO: Remove from this controller when wiring up the form builder. + $formFieldtypes = app('statamic.form-fieldtypes') + ->unique() + ->map(fn ($class) => app($class)) + ->filter->isSelectable() + ->values(); + + $fieldtypesPortedToFormFieldtypes = $formFieldtypes + ->map(fn (FormFieldtype $fieldtype) => $fieldtype::fieldtype()) + ->filter() + ->unique() + ->values(); + + $legacySelectableFieldtypes = FieldtypeRepository::classes() + ->map(fn ($class) => app($class)) + ->filter->selectableInForms() + ->reject(fn ($fieldtype) => $fieldtypesPortedToFormFieldtypes->contains($fieldtype->handle())) + ->map(fn ($fieldtype) => (new Fallback)->wrapping($fieldtype)) + ->values(); + return Inertia::render('forms/Connect', [ 'form' => $form, + 'fieldtypes' => $formFieldtypes->merge($legacySelectableFieldtypes)->sortBy->title()->values(), ]); } } diff --git a/src/Http/Controllers/CP/Forms/FormsController.php b/src/Http/Controllers/CP/Forms/FormsController.php index ed5a9da71e7..7aaf4e57525 100644 --- a/src/Http/Controllers/CP/Forms/FormsController.php +++ b/src/Http/Controllers/CP/Forms/FormsController.php @@ -39,7 +39,6 @@ public function index(Request $request) 'show_url' => $form->showUrl(), 'edit_url' => $form->editUrl(), 'fields_url' => cp_route('forms.fields.index', $form->handle()), - 'blueprint_url' => cp_route('blueprints.forms.edit', $form->handle()), 'can_edit' => User::current()->can('edit', $form), 'can_edit_blueprint' => User::current()->can('configure form fields', $form), ]; @@ -74,7 +73,6 @@ public function show($form) 'handle' => $form->handle(), 'editUrl' => $form->editUrl(), 'deleteUrl' => $form->deleteUrl(), - 'blueprintUrl' => cp_route('blueprints.forms.edit', $form->handle()), 'canEdit' => User::current()->can('edit', $form), 'canDelete' => User::current()->can('delete', $form), 'canConfigureFields' => User::current()->can('configure form fields'), @@ -236,18 +234,6 @@ protected function editFormBlueprint($form) 'fields' => [ 'display' => __('Fields'), 'fields' => [ - 'blueprint' => [ - 'display' => __('Blueprint'), - 'instructions' => __('statamic::messages.form_configure_blueprint_instructions'), - 'type' => 'blueprints', - 'options' => [ - [ - 'handle' => 'default', - 'title' => __('Edit Blueprint'), - 'edit_url' => cp_route('blueprints.forms.edit', $form->handle()), - ], - ], - ], 'honeypot' => [ 'type' => 'text', 'instructions' => __('statamic::messages.form_configure_honeypot_instructions'), diff --git a/src/Providers/AddonServiceProvider.php b/src/Providers/AddonServiceProvider.php index aa827598bd0..3393f3f1bb6 100644 --- a/src/Providers/AddonServiceProvider.php +++ b/src/Providers/AddonServiceProvider.php @@ -21,6 +21,7 @@ use Statamic\Facades\Path; use Statamic\Facades\YAML; use Statamic\Fields\Fieldtype; +use Statamic\Forms\Fields\FormFieldtype; use Statamic\Forms\JsDrivers\JsDriver; use Statamic\Modifiers\Modifier; use Statamic\Query\Scopes\Scope; @@ -70,6 +71,11 @@ abstract class AddonServiceProvider extends ServiceProvider */ protected $fieldtypes = []; + /** + * @var list> + */ + protected $formFieldtypes = []; + /** * @var list> */ @@ -203,6 +209,7 @@ public function boot() ->bootActions() ->bootDictionaries() ->bootFieldtypes() + ->bootFormFieldtypes() ->bootModifiers() ->bootWidgets() ->bootFormJsDrivers() @@ -361,6 +368,19 @@ protected function bootFieldtypes() return $this; } + protected function bootFormFieldtypes() + { + $formFieldtypes = collect($this->formFieldtypes) + ->merge($this->autoloadFilesFromFolder('FormFieldtypes', FormFieldtype::class)) + ->unique(); + + foreach ($formFieldtypes as $class) { + $class::register(); + } + + return $this; + } + protected function bootModifiers() { $modifiers = collect($this->modifiers) diff --git a/src/Providers/ExtensionServiceProvider.php b/src/Providers/ExtensionServiceProvider.php index 551d23d7d92..522f46d3cdc 100644 --- a/src/Providers/ExtensionServiceProvider.php +++ b/src/Providers/ExtensionServiceProvider.php @@ -12,6 +12,8 @@ use Statamic\Dictionaries\Dictionary; use Statamic\Fields\Fieldtype; use Statamic\Fieldtypes; +use Statamic\Forms; +use Statamic\Forms\Fields\FormFieldtype; use Statamic\Forms\JsDrivers; use Statamic\Modifiers\CoreModifiers; use Statamic\Modifiers\Modifier; @@ -129,6 +131,12 @@ class ExtensionServiceProvider extends ServiceProvider \Statamic\Forms\Fieldtype::class, ]; + protected $formFieldtypes = [ + Forms\Fields\Email::class, + Forms\Fields\LongAnswer::class, + Forms\Fields\ShortAnswer::class, + ]; + protected $modifierAliases = [ '+' => 'add', '-' => 'subtract', @@ -305,6 +313,11 @@ protected function registerExtensions() 'directory' => 'Fieldtypes', 'extensions' => $this->fieldtypes, ], + 'form-fieldtypes' => [ + 'class' => FormFieldtype::class, + 'directory' => 'FormFieldtypes', + 'extensions' => $this->formFieldtypes, + ], 'modifiers' => [ 'class' => Modifier::class, 'directory' => 'Modifiers', diff --git a/tests/Feature/Forms/EditFormTest.php b/tests/Feature/Forms/EditFormTest.php index 7e8a97aae24..e5d4cd2c7cc 100644 --- a/tests/Feature/Forms/EditFormTest.php +++ b/tests/Feature/Forms/EditFormTest.php @@ -72,7 +72,6 @@ public function fields_can_be_added() ->assertSuccessful() ->assertSeeInOrder([ 'Title', - 'Blueprint', 'Honeypot', 'First injected into fields section', 'Second injected into fields section', diff --git a/tests/Feature/GraphQL/FormTest.php b/tests/Feature/GraphQL/FormTest.php index d3ce69622d4..7ab49259800 100644 --- a/tests/Feature/GraphQL/FormTest.php +++ b/tests/Feature/GraphQL/FormTest.php @@ -3,11 +3,9 @@ namespace Tests\Feature\GraphQL; use Facades\Statamic\API\ResourceAuthorizer; -use Facades\Statamic\Fields\BlueprintRepository; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use Statamic\Contracts\GraphQL\CastableToValidationString; -use Statamic\Facades\Blueprint; use Statamic\Facades\Form; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -24,8 +22,6 @@ public function setUp(): void { parent::setUp(); - BlueprintRepository::partialMock(); - Form::all()->each->delete(); } @@ -111,22 +107,24 @@ public function it_cannot_query_against_non_allowed_sub_resource() #[Test] public function it_queries_the_fields() { - Form::make('contact')->title('Contact Us')->save(); - - $blueprint = Blueprint::makeFromFields([ - 'name' => [ - 'type' => 'text', - 'display' => 'Your Name', - 'instructions' => 'Enter your name', - 'placeholder' => 'Type here...', - 'invalid' => 'This isnt in the fieldtypes config fields so it shouldnt be output', - 'width' => 50, + Form::make('contact')->title('Contact Us')->formFields([ + 'sections' => [ + [ + 'fields' => [ + ['handle' => 'name', 'field' => [ + 'type' => 'short_answer', + 'display' => 'Your Name', + 'instructions' => 'Enter your name', + 'placeholder' => 'Type here...', + 'invalid' => 'This isnt in the fieldtypes config fields so it shouldnt be output', + 'width' => 50, + ]], + ['handle' => 'subject', 'field' => ['type' => 'select', 'options' => ['disco' => 'Disco', 'house' => 'House'], 'if' => ['name' => 'not empty']]], + ['handle' => 'message', 'field' => ['type' => 'long_answer', 'width' => 33, 'unless' => ['subject' => 'equals spam']]], + ], + ], ], - 'subject' => ['type' => 'select', 'options' => ['disco' => 'Disco', 'house' => 'House'], 'if' => ['name' => 'not empty']], - 'message' => ['type' => 'textarea', 'width' => 33, 'unless' => ['subject' => 'equals spam']], - ]); - - BlueprintRepository::shouldReceive('find')->with('forms.contact')->andReturn($blueprint); + ])->save(); $query = <<<'GQL' { @@ -194,15 +192,17 @@ public function it_queries_the_fields() #[Test] public function it_queries_the_validation_rules() { - Form::make('contact')->title('Contact Us')->save(); - - $blueprint = Blueprint::makeFromFields([ - 'name' => ['type' => 'text', 'validate' => ['required']], - 'subject' => ['type' => 'select', 'options' => ['disco' => 'Disco', 'house' => 'House']], - 'message' => ['type' => 'textarea', 'validate' => ['required_if:select_field,disco']], - ]); - - BlueprintRepository::shouldReceive('find')->with('forms.contact')->andReturn($blueprint); + Form::make('contact')->title('Contact Us')->formFields([ + 'sections' => [ + [ + 'fields' => [ + ['handle' => 'name', 'field' => ['type' => 'short_answer', 'validate' => ['required']]], + ['handle' => 'subject', 'field' => ['type' => 'select', 'options' => ['disco' => 'Disco', 'house' => 'House']]], + ['handle' => 'message', 'field' => ['type' => 'long_answer', 'validate' => ['required_if:select_field,disco']]], + ], + ], + ], + ])->save(); $query = <<<'GQL' { @@ -230,29 +230,26 @@ public function it_queries_the_validation_rules() #[Test] public function it_queries_the_sections() { - Form::make('contact')->title('Contact Us')->save(); - - $blueprint = Blueprint::makeFromFields([ - 'name' => [ - 'type' => 'text', - 'display' => 'Your Name', - 'instructions' => 'Enter your name', - 'placeholder' => 'Type here...', - 'invalid' => 'This isnt in the fieldtypes config fields so it shouldnt be output', - 'width' => 50, + Form::make('contact')->title('Contact Us')->formFields([ + 'sections' => [ + [ + 'display' => 'My Section', + 'instructions' => 'The section instructions', + 'fields' => [ + ['handle' => 'name', 'field' => [ + 'type' => 'short_answer', + 'display' => 'Your Name', + 'instructions' => 'Enter your name', + 'placeholder' => 'Type here...', + 'invalid' => 'This isnt in the fieldtypes config fields so it shouldnt be output', + 'width' => 50, + ]], + ['handle' => 'subject', 'field' => ['type' => 'select', 'options' => ['disco' => 'Disco', 'house' => 'House']]], + ['handle' => 'message', 'field' => ['type' => 'long_answer', 'width' => 33]], + ], + ], ], - 'subject' => ['type' => 'select', 'options' => ['disco' => 'Disco', 'house' => 'House']], - 'message' => ['type' => 'textarea', 'width' => 33], - ]); - - // Set section display and instructions. You wouldn't really do this for a form blueprint, - // but this is just to test the section type which doesn't get tested anywhere else. - $contents = $blueprint->contents(); - $contents['tabs']['main']['sections'][0]['display'] = 'My Section'; - $contents['tabs']['main']['sections'][0]['instructions'] = 'The section instructions'; - $blueprint->setContents($contents); - - BlueprintRepository::shouldReceive('find')->with('forms.contact')->andReturn($blueprint); + ])->save(); $query = <<<'GQL' { @@ -322,25 +319,27 @@ public function it_queries_the_sections() #[Test] public function it_returns_string_based_validation_rules_for_mimes_mimetypes_dimension_size_and_image() { - Form::make('contact')->title('Contact Us')->save(); - - $blueprint = Blueprint::makeFromFields([ - 'name' => [ - 'type' => 'assets', - 'display' => 'Asset', - 'validate' => [ - 'mimes:image/jpeg,image/png', - 'mimetypes:image/jpeg,image/png', - 'dimensions:1024', - 'size:1000', - 'image:jpeg', - 'new Tests\Feature\GraphQL\TestValidationRuleWithToString', - 'new Tests\Feature\GraphQL\TestValidationRuleWithoutToString', + Form::make('contact')->title('Contact Us')->formFields([ + 'sections' => [ + [ + 'fields' => [ + ['handle' => 'name', 'field' => [ + 'type' => 'assets', + 'display' => 'Asset', + 'validate' => [ + 'mimes:image/jpeg,image/png', + 'mimetypes:image/jpeg,image/png', + 'dimensions:1024', + 'size:1000', + 'image:jpeg', + 'new Tests\Feature\GraphQL\TestValidationRuleWithToString', + 'new Tests\Feature\GraphQL\TestValidationRuleWithoutToString', + ], + ]], + ], ], ], - ]); - - BlueprintRepository::shouldReceive('find')->with('forms.contact')->andReturn($blueprint); + ])->save(); $query = <<<'GQL' { diff --git a/tests/Forms/EmailTest.php b/tests/Forms/EmailTest.php index 4f166be1d94..52cae6a8c76 100644 --- a/tests/Forms/EmailTest.php +++ b/tests/Forms/EmailTest.php @@ -121,20 +121,23 @@ public function it_adds_subject_from_the_config() #[Test] public function it_adds_data_to_the_view() { - $social = Blueprint::makeFromFields(['twitter' => ['type' => 'text']])->setHandle('social')->setNamespace('globals'); - $company = Blueprint::makeFromFields(['company_name' => ['type' => 'text']])->setHandle('company')->setNamespace('globals'); - $formBlueprint = Blueprint::makeFromFields(['foo' => ['type' => 'text']]); + $socialBlueprint = Blueprint::makeFromFields(['twitter' => ['type' => 'text']])->setHandle('social')->setNamespace('globals'); + $companyBlueprint = Blueprint::makeFromFields(['company_name' => ['type' => 'text']])->setHandle('company')->setNamespace('globals'); - BlueprintRepository::shouldReceive('find')->with('globals.social')->andReturn($social); - BlueprintRepository::shouldReceive('find')->with('globals.company')->andReturn($company); - BlueprintRepository::shouldReceive('find')->with('forms.test')->andReturn($formBlueprint); + BlueprintRepository::partialMock(); + BlueprintRepository::shouldReceive('find')->with('globals.social')->andReturn($socialBlueprint); + BlueprintRepository::shouldReceive('find')->with('globals.company')->andReturn($companyBlueprint); $social = tap(GlobalSet::make('social'))->save(); $social->inDefaultSite()->data(['twitter' => '@statamic'])->save(); $company = tap(GlobalSet::make('company'))->save(); $company->inDefaultSite()->data(['company_name' => 'Statamic'])->save(); - $form = tap(Form::make('test'))->save(); + $form = tap(Form::make('test')->formFields([ + 'fields' => [ + ['handle' => 'foo', 'field' => ['type' => 'short_answer']], + ], + ]))->save(); $submission = $form->makeSubmission()->data(['foo' => 'bar']); $email = $this->makeEmailWithSubmission($submission); @@ -193,20 +196,20 @@ private function makeEmailWithConfig(array $config) 'email' => 'info@example.com', ])->save(); - $formBlueprint = Blueprint::makeFromFields([ - 'name' => ['type' => 'text'], - 'email' => ['type' => 'text'], - ]); - $companyInformationBlueprint = Blueprint::makeFromFields([ 'name' => ['type' => 'text'], 'email' => ['type' => 'text'], ]); - BlueprintRepository::shouldReceive('find')->with('forms.test')->andReturn($formBlueprint); + BlueprintRepository::partialMock(); BlueprintRepository::shouldReceive('find')->with('globals.company_information')->andReturn($companyInformationBlueprint); - $form = tap(Form::make('test'))->save(); + $form = tap(Form::make('test')->formFields([ + 'fields' => [ + ['handle' => 'name', 'field' => ['type' => 'short_answer']], + ['handle' => 'email', 'field' => ['type' => 'email']], + ], + ]))->save(); $submission = $form->makeSubmission()->data([ 'name' => 'Foo Bar', diff --git a/tests/Forms/Fields/ConvertFieldsFromBlueprintTest.php b/tests/Forms/Fields/ConvertFieldsFromBlueprintTest.php new file mode 100644 index 00000000000..97f6fa8cf89 --- /dev/null +++ b/tests/Forms/Fields/ConvertFieldsFromBlueprintTest.php @@ -0,0 +1,210 @@ +each->delete(); + } + + public function tearDown(): void + { + if ($blueprint = Blueprint::find('forms.contact_us')) { + Blueprint::delete($blueprint); + } + + parent::tearDown(); + } + + #[Test] + #[DataProvider('fieldConversionProvider')] + public function it_converts_fields(array $originalBlueprintField, array $expectedFormField) + { + $this->makeBlueprint([ + 'tabs' => [ + 'main' => [ + 'sections' => [ + [ + 'fields' => [ + [ + 'handle' => 'field', + 'field' => $originalBlueprintField, + ], + ], + ], + ], + ], + ], + ]); + + $form = Form::make('contact_us'); + + $this->assertEquals([ + 'handle' => 'field', + 'field' => $expectedFormField, + ], $form->formFields()->items()->first()); + } + + public static function fieldConversionProvider(): array + { + return [ + 'email' => [ + ['type' => 'text', 'display' => 'Email', 'input_type' => 'email', 'validate' => ['email']], + ['type' => 'email', 'display' => 'Email'], + ], + 'email, with validation rules' => [ + ['type' => 'text', 'display' => 'Email', 'input_type' => 'email', 'validate' => ['required', 'email']], + ['type' => 'email', 'display' => 'Email', 'validate' => ['required']], + ], + 'email, with parameterized email rule' => [ + ['type' => 'text', 'display' => 'Email', 'input_type' => 'email', 'validate' => ['required', 'email:rfc,strict']], + ['type' => 'email', 'display' => 'Email', 'validate' => ['required']], + ], + 'email, pipe-string parameterized email rule' => [ + ['type' => 'text', 'display' => 'Email', 'input_type' => 'email', 'validate' => 'required|email:rfc'], + ['type' => 'email', 'display' => 'Email', 'validate' => ['required']], + ], + 'short_answer' => [ + ['type' => 'text', 'display' => 'Name'], + ['type' => 'short_answer', 'display' => 'Name'], + ], + 'long_answer' => [ + ['type' => 'textarea', 'display' => 'Message'], + ['type' => 'long_answer', 'display' => 'Message'], + ], + 'non-form field should be preserved' => [ + ['type' => 'video', 'display' => 'Video', 'default' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'], + ['type' => 'video', 'display' => 'Video', 'default' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'], + ], + ]; + } + + #[Test] + public function it_preserves_fieldsets() + { + $fieldset = (new Fieldset)->setContents([ + 'fields' => [ + ['handle' => 'imported_field', 'field' => ['type' => 'text']], + ], + ]); + + Facades\Fieldset::shouldReceive('find') + ->with('test') + ->andReturn($fieldset); + + $this->makeBlueprint([ + 'tabs' => [ + 'main' => [ + 'sections' => [ + [ + 'fields' => [ + ['handle' => 'normal_field', 'field' => ['type' => 'text']], + ['import' => 'test'], + ['import' => 'test', 'prefix' => 'prefixed_'], + ['handle' => 'renamed_imported_field', 'field' => 'test.imported_field', 'config' => ['display' => 'Renamed Imported Field']], + ], + ], + ], + ], + ], + ]); + + $form = Form::make('contact_us'); + + $this->assertEquals([ + 'sections' => [ + [ + 'fields' => [ + ['handle' => 'normal_field', 'field' => ['type' => 'short_answer']], + ['import' => 'test'], + ['import' => 'test', 'prefix' => 'prefixed_'], + ['handle' => 'renamed_imported_field', 'field' => 'test.imported_field', 'config' => ['display' => 'Renamed Imported Field']], + ], + ], + ], + ], $form->formFields()->contents()); + } + + #[Test] + public function it_flattens_tabs_into_sections() + { + $this->makeBlueprint([ + 'tabs' => [ + 'one' => [ + 'sections' => [ + [ + 'display' => 'Section One', + 'fields' => [ + ['handle' => 'foo', 'field' => ['type' => 'text']], + ], + ], + [ + 'display' => 'Section Two', + 'fields' => [ + ['handle' => 'bar', 'field' => ['type' => 'text']], + ], + ], + ], + ], + 'two' => [ + 'sections' => [ + [ + 'display' => 'Section Three', + 'fields' => [ + ['handle' => 'baz', 'field' => ['type' => 'text']], + ], + ], + ], + ], + ], + ]); + + $form = Form::make('contact_us'); + + $contents = $form->formFields()->contents(); + + $this->assertArrayHasKey('sections', $contents); + $this->assertArrayNotHasKey('tabs', $contents); + $this->assertCount(3, $contents['sections']); + } + + #[Test] + public function it_handles_legacy_format_with_no_tabs() + { + $this->makeBlueprint([ + 'sections' => [ + 'main' => [ + 'display' => 'Section One', + 'fields' => [ + ['handle' => 'foo', 'field' => ['type' => 'text']], + ], + ], + ], + ]); + + $form = Form::make('contact_us'); + + $contents = $form->formFields()->contents(); + + $this->assertArrayHasKey('sections', $contents); + $this->assertArrayNotHasKey('tabs', $contents); + $this->assertCount(1, $contents['sections']); + } + + private function makeBlueprint(array $contents): void + { + Blueprint::make()->setHandle('contact_us')->setNamespace('forms')->setContents($contents)->save(); + } +} diff --git a/tests/Forms/Fields/EmailTest.php b/tests/Forms/Fields/EmailTest.php new file mode 100644 index 00000000000..88b2b0bc1a2 --- /dev/null +++ b/tests/Forms/Fields/EmailTest.php @@ -0,0 +1,73 @@ +setField(new FormField('email', ['type' => 'email'])); + + $this->assertEquals([ + 'type' => 'text', + 'input_type' => 'email', + 'placeholder' => null, + 'validate' => ['email'], + ], $fieldtype->toFieldArray()); + } + + #[Test] + public function it_preserves_existing_validation_rules() + { + $fieldtype = (new Email)->setField(new FormField('email', [ + 'type' => 'email', + 'validate' => ['required', 'max:255'], + ])); + + $this->assertEquals([ + 'type' => 'text', + 'input_type' => 'email', + 'placeholder' => null, + 'validate' => ['required', 'max:255', 'email'], + ], $fieldtype->toFieldArray()); + } + + #[Test] + public function it_does_not_duplicate_the_email_validation_rule() + { + $fieldtype = (new Email)->setField(new FormField('email', [ + 'type' => 'email', + 'validate' => ['required', 'email'], + ])); + + $this->assertEquals([ + 'type' => 'text', + 'input_type' => 'email', + 'placeholder' => null, + 'validate' => ['required', 'email'], + ], $fieldtype->toFieldArray()); + } + + #[Test] + public function it_passes_through_extra_config() + { + $fieldtype = (new Email)->setField(new FormField('email', [ + 'type' => 'email', + 'append' => '@example.com', + ])); + + $this->assertEquals([ + 'type' => 'text', + 'input_type' => 'email', + 'placeholder' => null, + 'validate' => ['email'], + 'append' => '@example.com', + ], $fieldtype->toFieldArray()); + } +} diff --git a/tests/Forms/Fields/FormFieldTest.php b/tests/Forms/Fields/FormFieldTest.php new file mode 100644 index 00000000000..dd1dcd636df --- /dev/null +++ b/tests/Forms/Fields/FormFieldTest.php @@ -0,0 +1,99 @@ +assertEquals( + 'Test Display Value', + (new FormField('test', ['display' => 'Test Display Value']))->display() + ); + + $this->assertEquals( + 'Test', + (new Field('test', []))->display() + ); + + $this->assertEquals( + 'Test Multi Word Handle And No Explicit Display', + (new FormField('test_multi_word_handle_and_no_explicit_display', []))->display() + ); + } + + #[Test] + public function it_gets_instructions() + { + $this->assertEquals( + 'The instructions', + (new FormField('test', ['instructions' => 'The instructions']))->instructions() + ); + + $this->assertNull((new FormField('test', []))->instructions()); + } + + #[Test] + public function it_gets_the_fieldtype() + { + $fieldtype = new class extends FormFieldtype + { + public function toFieldArray(): array + { + // TODO: Implement toFieldArray() method. + } + }; + + FormFieldtypeRepository::shouldReceive('find') + ->with('the_fieldtype') + ->andReturnUsing(fn () => clone $fieldtype); + $field = new FormField('test', ['type' => 'the_fieldtype', 'foo' => 'bar']); + + // The fieldtype from the repository should not have the field attached. + $this->assertNull($fieldtype->field()); + + // The fieldtype from the field should be an instance of that + // fieldtype class, and should have the field attached. + $this->assertInstanceOf(get_class($fieldtype), $field->fieldtype()); + $this->assertEquals($field->config(), $field->fieldtype()->field()->config()); + + // Double check that the fieldtype from the repository still doesn't somehow have the field attached. + $this->assertNull(FormFieldtypeRepository::find('the_fieldtype')->field()); + } + + #[Test] + public function it_falls_back_to_fallback_fieldtype_for_unknown_form_fieldtypes() + { + $field = new FormField('test', ['type' => 'list', 'display' => 'Shopping List']); + + $this->assertInstanceOf(Fallback::class, $field->fieldtype()); + + $this->assertEquals([ + 'type' => 'list', + 'display' => 'Shopping List', + ], $field->toFieldArray()); + } + + #[Test] + public function it_converts_to_field_array() + { + $field = new FormField('email', ['type' => 'email', 'display' => 'Email Address', 'validate' => ['required']]); + + $this->assertEquals([ + 'type' => 'text', + 'validate' => ['required', 'email'], + 'input_type' => 'email', + 'display' => 'Email Address', + 'placeholder' => null, + ], $field->toFieldArray()); + } +} diff --git a/tests/Forms/Fields/FormFieldsTest.php b/tests/Forms/Fields/FormFieldsTest.php new file mode 100644 index 00000000000..a8ff6db271c --- /dev/null +++ b/tests/Forms/Fields/FormFieldsTest.php @@ -0,0 +1,294 @@ + [ + [ + 'display' => 'Contact Info', + 'fields' => [ + ['handle' => 'name', 'field' => ['type' => 'short_answer', 'display' => 'Name']], + ['handle' => 'email', 'field' => ['type' => 'email']], + ['import' => 'test'], + ], + ], + ], + ]; + + $formFields = new FormFields($contents); + + $this->assertSame($contents, $formFields->contents()); + } + + #[Test] + public function it_returns_items() + { + $formFields = new FormFields([ + 'sections' => [ + [ + 'display' => 'Section One', + 'fields' => [ + ['handle' => 'email', 'field' => ['type' => 'email']], + ], + ], + [ + 'display' => 'Section Two', + 'fields' => [ + ['handle' => 'name', 'field' => ['type' => 'short_answer']], + ['handle' => 'message', 'field' => ['type' => 'long_answer']], + ], + ], + ], + ]); + + $this->assertEquals([ + ['handle' => 'email', 'field' => ['type' => 'email']], + ['handle' => 'name', 'field' => ['type' => 'short_answer']], + ['handle' => 'message', 'field' => ['type' => 'long_answer']], + ], $formFields->items()->all()); + } + + #[Test] + public function it_returns_items_from_fieldsets() + { + $fieldset = (new Fieldset)->setContents([ + 'fields' => [ + ['handle' => 'imported_field', 'field' => ['type' => 'text']], + ], + ]); + + Facades\Fieldset::shouldReceive('find') + ->with('test') + ->andReturn($fieldset); + + $formFields = new FormFields([ + 'sections' => [ + [ + 'fields' => [ + ['import' => 'test'], + ['import' => 'test', 'prefix' => 'prefixed_'], + ['handle' => 'renamed_imported_field', 'field' => 'test.imported_field', 'config' => ['display' => 'Renamed Imported Field']], + ], + ], + ], + ]); + + $this->assertEquals([ + ['import' => 'test'], + ['import' => 'test', 'prefix' => 'prefixed_'], + ['handle' => 'renamed_imported_field', 'field' => 'test.imported_field', 'config' => ['display' => 'Renamed Imported Field']], + ], $formFields->items()->all()); + } + + #[Test] + public function it_returns_fields() + { + $formFields = new FormFields([ + 'sections' => [ + [ + 'display' => 'Section One', + 'fields' => [ + ['handle' => 'email', 'field' => ['type' => 'email']], + ], + ], + [ + 'display' => 'Section Two', + 'fields' => [ + ['handle' => 'name', 'field' => ['type' => 'short_answer']], + ['handle' => 'message', 'field' => ['type' => 'long_answer']], + ], + ], + ], + ]); + + $fields = $formFields->fields(); + + $this->assertEveryItemIsInstanceOf(FormField::class, $fields->all()); + $this->assertEquals('email', $fields->get('email')->type()); + $this->assertEquals('short_answer', $fields->get('name')->type()); + $this->assertEquals('long_answer', $fields->get('message')->type()); + } + + #[Test] + public function it_returns_fields_from_fieldsets() + { + $fieldset = (new Fieldset)->setContents([ + 'fields' => [ + ['handle' => 'imported_field', 'field' => ['type' => 'text']], + ], + ]); + + Facades\Fieldset::shouldReceive('find') + ->with('test') + ->andReturn($fieldset); + + $formFields = new FormFields([ + 'sections' => [ + [ + 'fields' => [ + ['import' => 'test'], + ['import' => 'test', 'prefix' => 'prefixed_'], + ['handle' => 'renamed_imported_field', 'field' => 'test.imported_field', 'config' => ['display' => 'Renamed Imported Field']], + ], + ], + ], + ]); + + $fields = $formFields->fields(); + + $this->assertEveryItemIsInstanceOf(FormField::class, $fields->all()); + $this->assertEquals(['imported_field', 'prefixed_imported_field', 'renamed_imported_field'], $fields->keys()->all()); + $this->assertEveryItem($fields->map->type()->values()->all(), fn (string $type) => $type === 'text'); + } + + #[Test] + public function it_returns_a_single_field() + { + $formFields = new FormFields([ + 'sections' => [ + [ + 'fields' => [ + ['handle' => 'name', 'field' => ['type' => 'short_answer']], + ['handle' => 'email', 'field' => ['type' => 'email']], + ], + ], + ], + ]); + + $field = $formFields->field('name'); + + $this->assertInstanceOf(FormField::class, $field); + $this->assertEquals('name', $field->handle()); + $this->assertEquals('short_answer', $field->type()); + } + + #[Test] + public function it_returns_null_for_nonexistent_field() + { + $formFields = new FormFields([ + 'sections' => [ + ['fields' => [['handle' => 'email', 'field' => ['type' => 'email']]]], + ], + ]); + + $this->assertNull($formFields->field('nonexistent')); + } + + #[Test] + public function it_converts_to_blueprint() + { + $formFields = new FormFields([ + 'sections' => [ + [ + 'display' => 'Contact Info', + 'fields' => [ + // Should be converted to their "normal fieldtype" equivalent. + ['handle' => 'name', 'field' => ['type' => 'short_answer', 'display' => 'Name']], + ['handle' => 'email', 'field' => ['type' => 'email', 'display' => 'Email Address']], + ], + ], + [ + 'display' => 'Additional Info', + 'fields' => [ + // List isn't a form fieldtype, so its config shouldn't be touched. + ['handle' => 'shopping_list', 'field' => ['type' => 'list', 'display' => 'Shopping List']], + ], + ], + ], + ]); + + $blueprint = $formFields->toBlueprint(); + + $this->assertEquals([ + 'tabs' => [ + 'main' => [ + 'sections' => [ + [ + 'display' => 'Contact Info', + 'fields' => [ + [ + 'handle' => 'name', + 'field' => [ + 'type' => 'text', + 'display' => 'Name', + ], + ], + [ + 'handle' => 'email', + 'field' => [ + 'type' => 'text', + 'validate' => ['email'], + 'input_type' => 'email', + 'display' => 'Email Address', + ], + ], + ], + ], + [ + 'display' => 'Additional Info', + 'fields' => [ + ['handle' => 'shopping_list', 'field' => ['type' => 'list', 'display' => 'Shopping List']], + ], + ], + ], + ], + ], + ], $blueprint->contents()); + } + + #[Test] + public function it_converts_to_a_blueprint_with_fieldsets() + { + $fieldset = (new Fieldset)->setContents([ + 'fields' => [ + ['handle' => 'imported_field', 'field' => ['type' => 'text']], + ], + ]); + + Facades\Fieldset::shouldReceive('find') + ->with('test') + ->andReturn($fieldset); + + $formFields = new FormFields([ + 'sections' => [ + [ + 'fields' => [ + ['import' => 'test'], + ['import' => 'test', 'prefix' => 'prefixed_'], + ['handle' => 'renamed_imported_field', 'field' => 'test.imported_field', 'config' => ['display' => 'Renamed Imported Field']], + ], + ], + ], + ]); + + $blueprint = $formFields->toBlueprint(); + + $this->assertEquals([ + 'tabs' => [ + 'main' => [ + 'sections' => [ + [ + 'fields' => [ + ['import' => 'test'], + ['import' => 'test', 'prefix' => 'prefixed_'], + ['handle' => 'renamed_imported_field', 'field' => 'test.imported_field', 'config' => ['display' => 'Renamed Imported Field']], + ], + ], + ], + ], + ], + ], $blueprint->contents()); + } +} diff --git a/tests/Forms/Fields/FormFieldtypeRepositoryTest.php b/tests/Forms/Fields/FormFieldtypeRepositoryTest.php new file mode 100644 index 00000000000..6cc3a50188e --- /dev/null +++ b/tests/Forms/Fields/FormFieldtypeRepositoryTest.php @@ -0,0 +1,106 @@ +repo = new FormFieldtypeRepository(); + } + + #[Test] + public function it_gets_a_form_fieldtype() + { + FooFormFieldtype::register(); + + $found = $this->repo->find('test'); + $this->assertInstanceOf(FooFormFieldtype::class, $found); + + // Find it again and assert that it's a different instance each time. + $second = $this->repo->find('test'); + $this->assertInstanceOf(FooFormFieldtype::class, $second); + $this->assertNotSame($found, $second); + } + + #[Test] + public function it_caches_and_clones_existing_instances() + { + FooFormFieldtype::register(); + + $found = $this->repo->find('test'); + $this->assertInstanceOf(FooFormFieldtype::class, $found); + + // Re-register another fieldtype that uses the same handle. + // In reality this wouldn't happen, but we do it for this test to ensure the caching works. + BarFormFieldtype::register(); + + // Assert that it was registered. If you were to manually resolve it + // out of the container you'd get the overridden fieldtype. + $this->assertEquals(BarFormFieldtype::class, app('statamic.form-fieldtypes')->get('test')); + + // Find it again through the repo to assert that it's a different instance each time. + $second = $this->repo->find('test'); + $this->assertInstanceOf(FooFormFieldtype::class, $second); + $this->assertNotSame($found, $second); + } + + #[Test] + public function it_throw_exception_when_finding_invalid_form_fieldtype() + { + $this->expectException(FormFieldtypeNotFoundException::class); + $this->expectExceptionMessage('Form Fieldtype [test] not found'); + $this->repo->find('test'); + } + + #[Test] + public function it_makes_fields_selectable_in_forms() + { + $this->assertFalse($this->repo->hasBeenMadeSelectable('test-selectable')); + + $this->repo->makeSelectable('test-selectable'); + $this->assertTrue($this->repo->hasBeenMadeSelectable('test-selectable')); + $this->assertTrue($this->repo->selectableIsOverriden('test-selectable')); + } + + #[Test] + public function it_makes_fields_unselectable_in_forms() + { + $this->repo->makeSelectable('test-unselectable'); + $this->assertTrue($this->repo->hasBeenMadeSelectable('test-unselectable')); + + $this->repo->makeUnselectable('test-unselectable'); + $this->assertFalse($this->repo->hasBeenMadeSelectable('test-unselectable')); + $this->assertTrue($this->repo->selectableIsOverriden('test-unselectable')); + } +} + +class FooFormFieldtype extends FormFieldtype +{ + public static $handle = 'test'; + + public function toFieldArray(): array + { + // TODO: Implement toFieldArray() method. + } +} + +class BarFormFieldtype extends FormFieldtype +{ + public static $handle = 'test'; + + public function toFieldArray(): array + { + // TODO: Implement toFieldArray() method. + } +} diff --git a/tests/Forms/Fields/FormFieldtypeTest.php b/tests/Forms/Fields/FormFieldtypeTest.php new file mode 100644 index 00000000000..6bd95a7b591 --- /dev/null +++ b/tests/Forms/Fields/FormFieldtypeTest.php @@ -0,0 +1,57 @@ +assertFalse($formFieldtype->isSelectable()); + + $formFieldtype::makeSelectable(); + + $this->assertTrue($formFieldtype->isSelectable()); + $this->assertTrue(FormFieldtypeRepository::hasBeenMadeSelectable('test-selectable')); + $this->assertTrue(FormFieldtypeRepository::selectableIsOverriden('test-selectable')); + } + + #[Test] + public function it_can_make_a_fieldtype_unselectable_in_forms() + { + $formFieldtype = new class extends FormFieldtype + { + public static $handle = 'test-unselectable'; + protected $selectable = true; + + public function toFieldArray(): array + { + // TODO: Implement toFieldArray() method. + } + }; + + $this->assertTrue($formFieldtype->isSelectable()); + + $formFieldtype::makeUnselectable(); + + $this->assertFalse($formFieldtype->isSelectable()); + $this->assertFalse(FormFieldtypeRepository::hasBeenMadeSelectable('test-unselectable')); + $this->assertTrue(FormFieldtypeRepository::selectableIsOverriden('test-unselectable')); + } +} diff --git a/tests/Forms/Fields/LongAnswerTest.php b/tests/Forms/Fields/LongAnswerTest.php new file mode 100644 index 00000000000..9ad91e5cf26 --- /dev/null +++ b/tests/Forms/Fields/LongAnswerTest.php @@ -0,0 +1,45 @@ +setField(new FormField('message', [ + 'type' => 'long_answer', + 'placeholder' => 'Your message', + 'character_limit' => 30, + ])); + + $this->assertEquals([ + 'type' => 'textarea', + 'placeholder' => 'Your message', + 'character_limit' => 30, + ], $fieldtype->toFieldArray()); + } + + #[Test] + public function it_passes_through_extra_config() + { + $fieldtype = (new LongAnswer)->setField(new FormField('message', [ + 'type' => 'long_answer', + 'placeholder' => 'Your message', + 'character_limit' => 30, + 'default' => 'David Hasselhoff', + ])); + + $this->assertEquals([ + 'type' => 'textarea', + 'placeholder' => 'Your message', + 'character_limit' => 30, + 'default' => 'David Hasselhoff', + ], $fieldtype->toFieldArray()); + } +} diff --git a/tests/Forms/Fields/ShortAnswerTest.php b/tests/Forms/Fields/ShortAnswerTest.php new file mode 100644 index 00000000000..be3a2d72ac9 --- /dev/null +++ b/tests/Forms/Fields/ShortAnswerTest.php @@ -0,0 +1,45 @@ +setField(new FormField('name', [ + 'type' => 'short_answer', + 'placeholder' => 'Your name', + 'character_limit' => 30, + ])); + + $this->assertEquals([ + 'type' => 'text', + 'placeholder' => 'Your name', + 'character_limit' => 30, + ], $fieldtype->toFieldArray()); + } + + #[Test] + public function it_passes_through_extra_config() + { + $fieldtype = (new ShortAnswer)->setField(new FormField('name', [ + 'type' => 'short_answer', + 'placeholder' => 'Your name', + 'character_limit' => 30, + 'default' => 'David Hasselhoff', + ])); + + $this->assertEquals([ + 'type' => 'text', + 'placeholder' => 'Your name', + 'character_limit' => 30, + 'default' => 'David Hasselhoff', + ], $fieldtype->toFieldArray()); + } +} diff --git a/tests/Forms/FormTest.php b/tests/Forms/FormTest.php index 8401c3adb63..c0d24ff649e 100644 --- a/tests/Forms/FormTest.php +++ b/tests/Forms/FormTest.php @@ -11,8 +11,11 @@ use Statamic\Events\FormDeleting; use Statamic\Events\FormSaved; use Statamic\Events\FormSaving; +use Statamic\Facades\Blueprint; +use Statamic\Facades\File; use Statamic\Facades\Form; -use Statamic\Fields\Blueprint; +use Statamic\Facades\YAML; +use Statamic\Forms\Fields\FormFields; use Tests\TestCase; class FormTest extends TestCase @@ -29,8 +32,6 @@ public function it_saves_a_form() { Event::fake(); - $blueprint = (new Blueprint)->setHandle('post')->save(); - $form = Form::make('contact_us') ->title('Contact Us') ->honeypot('winnie') @@ -71,8 +72,6 @@ public function it_dispatches_form_created_only_once() { Event::fake(); - $blueprint = (new Blueprint)->setHandle('post')->save(); - $form = Form::make('contact_us') ->title('Contact Us') ->honeypot('winnie'); @@ -88,13 +87,31 @@ public function it_dispatches_form_created_only_once() Event::assertDispatched(FormCreated::class, 1); } + #[Test] + public function it_deletes_blueprint_after_saving() + { + Blueprint::make()->setHandle('contact_us')->setNamespace('forms')->save(); + + $this->assertNotNull(Blueprint::find('forms.contact_us')); + + $form = Form::make('contact_us') + ->title('Contact Us') + ->honeypot('winnie') + ->data([ + 'foo' => 'bar', + 'roo' => 'rar', + ]); + + $form->save(); + + $this->assertNull(Blueprint::find('forms.contact_us')); + } + #[Test] public function it_saves_quietly() { Event::fake(); - $blueprint = (new Blueprint)->setHandle('post')->save(); - $form = Form::make('contact_us') ->title('Contact Us') ->honeypot('winnie') @@ -115,8 +132,6 @@ public function if_creating_event_returns_false_the_form_doesnt_save() return false; }); - $blueprint = (new Blueprint)->setHandle('post')->save(); - $form = Form::make('contact_us') ->title('Contact Us') ->honeypot('winnie') @@ -134,8 +149,6 @@ public function if_saving_event_returns_false_the_form_doesnt_save() return false; }); - $blueprint = (new Blueprint)->setHandle('post')->save(); - $form = Form::make('contact_us') ->title('Contact Us') ->honeypot('winnie') @@ -268,4 +281,78 @@ public function it_clones_internal_collections() $this->assertEquals('A', $form->getSupplement('bar')); $this->assertEquals('B', $clone->getSupplement('bar')); } + + #[Test] + public function it_gets_and_sets_form_fields() + { + $fields = [ + 'sections' => [ + [ + 'fields' => [ + ['handle' => 'email', 'field' => ['type' => 'email']], + ], + ], + ], + ]; + + $form = Form::make('contact_us')->formFields($fields); + + $formFields = $form->formFields(); + + $this->assertInstanceOf(FormFields::class, $formFields); + $this->assertEquals($fields, $formFields->contents()); + } + + #[Test] + public function it_saves_form_fields_to_yaml() + { + $fields = [ + 'sections' => [ + [ + 'fields' => [ + ['handle' => 'name', 'field' => ['type' => 'short_answer']], + ['handle' => 'email', 'field' => ['type' => 'email']], + ], + ], + ], + ]; + + $form = tap(Form::make('contact_us') + ->title('Contact Us') + ->formFields($fields)) + ->save(); + + $saved = YAML::parse(File::get($form->path())); + + $this->assertEquals($fields, $saved['fields']); + } + + #[Test] + public function it_hydrates_form_fields_from_yaml() + { + $fields = [ + 'sections' => [ + [ + 'fields' => [ + ['handle' => 'name', 'field' => ['type' => 'short_answer']], + ['handle' => 'email', 'field' => ['type' => 'email']], + ], + ], + ], + ]; + + Form::make('contact_us') + ->title('Contact Us') + ->formFields($fields) + ->save(); + + $form = Form::find('contact_us'); + + $formFields = $form->formFields(); + + $this->assertInstanceOf(FormFields::class, $formFields); + $this->assertCount(2, $formFields->items()); + $this->assertEquals('email', $formFields->field('email')->handle()); + $this->assertEquals('name', $formFields->field('name')->handle()); + } } diff --git a/tests/Forms/SendEmailsTest.php b/tests/Forms/SendEmailsTest.php index 0360d85f685..f144b5afd1d 100644 --- a/tests/Forms/SendEmailsTest.php +++ b/tests/Forms/SendEmailsTest.php @@ -97,10 +97,12 @@ public function it_dispatches_delete_attachments_job_after_dispatching_email_job 'from' => 'first@sender.com', 'to' => 'first@recipient.com', 'foo' => 'bar', + ])->formFields([ + 'fields' => [ + ['handle' => 'attachments', 'field' => ['type' => 'files']], + ], ]))->save(); - $form->blueprint()->ensureField('attachments', ['type' => 'files'])->save(); - (new SendEmails( $submission = $form->makeSubmission(), $site = Site::default(), diff --git a/tests/Forms/SubmissionTest.php b/tests/Forms/SubmissionTest.php index a2700882bc4..d19fde2c5d3 100644 --- a/tests/Forms/SubmissionTest.php +++ b/tests/Forms/SubmissionTest.php @@ -12,7 +12,6 @@ use Statamic\Events\SubmissionDeleted; use Statamic\Events\SubmissionSaved; use Statamic\Events\SubmissionSaving; -use Statamic\Facades\Blueprint; use Statamic\Facades\Form; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -76,10 +75,18 @@ public static function utcProvider() #[Test] public function it_sets_and_gets_data() { - $submission = Form::make('test')->makeSubmission(); + $form = Form::make('test') + ->formFields([ + 'sections' => [ + [ + 'fields' => [ + ['handle' => 'foo', 'field' => ['type' => 'short_answer']], + ], + ], + ], + ]); - $blueprint = Blueprint::makeFromFields(['foo' => ['type' => 'text']]); - Blueprint::shouldReceive('find')->with('forms.test')->andReturn($blueprint); + $submission = $form->makeSubmission(); $this->assertInstanceOf(Collection::class, $data = $submission->data()); $this->assertEquals([], $data->all()); diff --git a/tests/StaticCaching/InvalidateTest.php b/tests/StaticCaching/InvalidateTest.php index d720571c2c2..d772097142b 100644 --- a/tests/StaticCaching/InvalidateTest.php +++ b/tests/StaticCaching/InvalidateTest.php @@ -5,10 +5,8 @@ use Mockery; use PHPUnit\Framework\Attributes\Test; use Statamic\Contracts\Entries\Entry; -use Statamic\Events\BlueprintSaved; use Statamic\Events\CollectionTreeEntriesMovedOrRemoved; use Statamic\Facades\Entry as EntryFacade; -use Statamic\Facades\Form; use Statamic\StaticCaching\Cacher; use Statamic\StaticCaching\Invalidate; use Statamic\StaticCaching\Invalidator; @@ -19,22 +17,6 @@ class InvalidateTest extends TestCase { use PreventSavingStacheItemsToDisk; - #[Test] - public function it_invalidates_a_form_when_its_blueprint_is_saved() - { - $form = tap(Form::make('contact'))->save(); - - $event = new BlueprintSaved($form->blueprint()); - - $invalidator = Mockery::mock(Invalidator::class)->shouldReceive('invalidate')->once()->withArgs(function ($form) { - return $form->handle() === 'contact'; - })->getMock(); - - $invalidate = new Invalidate($invalidator, Mockery::mock(Cacher::class)); - - $invalidate->invalidateByBlueprint($event); - } - private function mockEntry(string $url): Entry { $entry = Mockery::mock(Entry::class); diff --git a/tests/Tags/Form/FormTestCase.php b/tests/Tags/Form/FormTestCase.php index 17a3f89e2b2..203055d2946 100644 --- a/tests/Tags/Form/FormTestCase.php +++ b/tests/Tags/Form/FormTestCase.php @@ -3,7 +3,6 @@ namespace Tests\Tags\Form; use Illuminate\Support\Facades\Blade; -use Statamic\Facades\Blueprint; use Statamic\Facades\Form; use Statamic\Facades\Parse; use Statamic\Support\Arr; @@ -78,20 +77,20 @@ protected function blade($string, $context = []) return Blade::render($string, $context); } - protected function createForm($blueprintContents = null, $handle = null) + protected function createForm($fieldContents = null, $handle = null) { - $defaultBlueprintContents = [ - 'fields' => $this->defaultFields, + $defaultFieldsContents = [ + 'sections' => [ + ['fields' => $this->defaultFields], + ], ]; - $blueprint = Blueprint::make()->setContents($blueprintContents ?? $defaultBlueprintContents); - $handle = $handle ?? 'contact'; - Blueprint::shouldReceive('find')->with("forms.{$handle}")->andReturn($blueprint); - Blueprint::makePartial(); - - $form = Form::make()->handle($handle)->honeypot('winnie'); + $form = Form::make() + ->handle($handle) + ->honeypot('winnie') + ->formFields($fieldContents ?? $defaultFieldsContents); Form::shouldReceive('find')->with($handle)->andReturn($form); Form::makePartial();