From 9fb48fedfed99e6a104ba6e72002a6527574cbc1 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 2 Sep 2026 15:21:10 +0200 Subject: [PATCH 1/4] Suggestions: demo and back API --- demo/app/Sharp/Profile/ProfileSingleForm.php | 15 +++++++++++---- demo/app/Sharp/Profile/ProfileSingleShow.php | 16 +++++++++++----- demo/database/factories/UserFactory.php | 1 + .../2014_10_12_000000_create_users_table.php | 1 + src/Data/Form/Fields/FormTextFieldData.php | 2 ++ src/Form/Fields/SharpFormTextField.php | 11 +++++++++++ .../Unit/Form/Fields/SharpFormTextFieldTest.php | 8 ++++++++ 7 files changed, 45 insertions(+), 9 deletions(-) diff --git a/demo/app/Sharp/Profile/ProfileSingleForm.php b/demo/app/Sharp/Profile/ProfileSingleForm.php index 9cbb41b33..6ee34564a 100644 --- a/demo/app/Sharp/Profile/ProfileSingleForm.php +++ b/demo/app/Sharp/Profile/ProfileSingleForm.php @@ -34,6 +34,12 @@ public function buildFormFields(FieldsContainer $formFields): void ->setMaxLength(300) ->setHelpMessage('It will be displayed publicly'), ) + ->addField( + SharpFormTextField::make('occupation') + ->setLabel('Occupation') + ->setMaxLength(300) + ->setSuggestions(['Developer', 'Designer', 'Manager', 'Other']) + ) ->addField( SharpFormTextField::make('email') ->setLabel('Email address') @@ -48,6 +54,7 @@ public function buildFormLayout(FormLayout $formLayout): void ->addColumn(6, function (FormLayoutColumn $column) { $column ->withField('name') + ->withField('occupation') ->withField('email'); }) ->addColumn(6, function (FormLayoutColumn $column) { @@ -65,10 +72,10 @@ public function findSingle(): array protected function updateSingle(array $data) { - $this->validate( - $data, - ['name' => ['required', 'string', 'max:300']] - ); + $this->validate($data, [ + 'name' => ['required', 'string', 'max:300'], + 'occupation' => ['nullable', 'string', 'max:200'], + ]); $user = auth()->user(); diff --git a/demo/app/Sharp/Profile/ProfileSingleShow.php b/demo/app/Sharp/Profile/ProfileSingleShow.php index e4b7fc697..a11016db1 100644 --- a/demo/app/Sharp/Profile/ProfileSingleShow.php +++ b/demo/app/Sharp/Profile/ProfileSingleShow.php @@ -25,6 +25,10 @@ protected function buildShowFields(FieldsContainer $showFields): void SharpShowTextField::make('email') ->setLabel('Email address'), ) + ->addField( + SharpShowTextField::make('occupation') + ->setLabel('Occupation'), + ) ->addField( SharpShowPictureField::make('avatar'), ) @@ -37,11 +41,13 @@ protected function buildShowFields(FieldsContainer $showFields): void protected function buildShowLayout(ShowLayout $showLayout): void { $showLayout - ->addSection('', function (ShowLayoutSection $section) { - $section - ->addColumn(6, fn (ShowLayoutColumn $column) => $column->withField('email')) - ->addColumn(6, fn (ShowLayoutColumn $column) => $column->withField('avatar')); - }) + ->addSection(fn (ShowLayoutSection $section) => $section + ->addColumn(6, fn (ShowLayoutColumn $column) => $column + ->withField('email') + ->withField('occupation') + ) + ->addColumn(6, fn (ShowLayoutColumn $column) => $column->withField('avatar')) + ) ->when(config('demo.enable_passkeys'))->addEntityListSection(PasskeyEntity::class); } diff --git a/demo/database/factories/UserFactory.php b/demo/database/factories/UserFactory.php index b913ce4b3..66aac0083 100644 --- a/demo/database/factories/UserFactory.php +++ b/demo/database/factories/UserFactory.php @@ -15,6 +15,7 @@ public function definition() 'email_verified_at' => now(), 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'role' => 'editor', + 'occupation' => $this->faker->jobTitle(), 'remember_token' => Str::random(10), ]; } diff --git a/demo/database/migrations/2014_10_12_000000_create_users_table.php b/demo/database/migrations/2014_10_12_000000_create_users_table.php index b511d5046..d2fe71829 100644 --- a/demo/database/migrations/2014_10_12_000000_create_users_table.php +++ b/demo/database/migrations/2014_10_12_000000_create_users_table.php @@ -15,6 +15,7 @@ public function up() $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->string('role'); + $table->string('occupation')->nullable(); $table->rememberToken(); $table->timestamps(); }); diff --git a/src/Data/Form/Fields/FormTextFieldData.php b/src/Data/Form/Fields/FormTextFieldData.php index c70a8fd00..fb1d80e69 100644 --- a/src/Data/Form/Fields/FormTextFieldData.php +++ b/src/Data/Form/Fields/FormTextFieldData.php @@ -24,6 +24,8 @@ public function __construct( public ?string $placeholder = null, public ?int $maxLength = null, public ?bool $localized = null, + #[LiteralTypeScriptType('string[] | null')] + public ?array $suggestions = null, public ?string $label = null, public ?bool $readOnly = null, public ?FormConditionalDisplayData $conditionalDisplay = null, diff --git a/src/Form/Fields/SharpFormTextField.php b/src/Form/Fields/SharpFormTextField.php index 59e907f67..1b67d86c6 100644 --- a/src/Form/Fields/SharpFormTextField.php +++ b/src/Form/Fields/SharpFormTextField.php @@ -20,6 +20,7 @@ class SharpFormTextField extends SharpFormField implements IsSharpFieldWithHtmlS const FIELD_TYPE = 'text'; protected string $inputType = 'text'; + private ?array $suggestions = null; public static function make(string $key): self { @@ -61,10 +62,19 @@ public function setInputTypeUrl(): self return $this; } + public function setSuggestions(?array $suggestions = null): self + { + $this->suggestions = $suggestions; + + return $this; + } + protected function validationRules(): array { return [ 'inputType' => 'required|in:text,password,email,tel,url', + 'suggestions' => 'nullable|array', + 'suggestions.*' => 'string', ]; } @@ -75,6 +85,7 @@ public function toArray(): array 'placeholder' => $this->placeholder, 'maxLength' => $this->maxLength, 'localized' => $this->localized, + 'suggestions' => $this->suggestions, ]); } } diff --git a/tests/Unit/Form/Fields/SharpFormTextFieldTest.php b/tests/Unit/Form/Fields/SharpFormTextFieldTest.php index 0da8e5560..024e7b1ca 100644 --- a/tests/Unit/Form/Fields/SharpFormTextFieldTest.php +++ b/tests/Unit/Form/Fields/SharpFormTextFieldTest.php @@ -44,3 +44,11 @@ expect($formField->toArray()) ->toHaveKey('maxLength', 10); }); + +it('allows to define suggestions', function () { + $formField = SharpFormTextField::make('text') + ->setSuggestions(['a', 'b', 'c']); + + expect($formField->toArray()) + ->toHaveKey('suggestions', ['a', 'b', 'c']); +}); From 6c335c33559e72d5b0014d10d16bb0898d4b11a3 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 2 Sep 2026 15:44:49 +0200 Subject: [PATCH 2/4] Suggestions: front --- resources/js/form/components/fields/Text.vue | 82 +++++++++++++++++++- resources/js/types/generated.d.ts | 3 +- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/resources/js/form/components/fields/Text.vue b/resources/js/form/components/fields/Text.vue index b86b9e83b..bc418a88b 100644 --- a/resources/js/form/components/fields/Text.vue +++ b/resources/js/form/components/fields/Text.vue @@ -1,6 +1,6 @@ + + diff --git a/resources/js/components/ui/combobox/ComboboxAnchor.vue b/resources/js/components/ui/combobox/ComboboxAnchor.vue new file mode 100644 index 000000000..969f04368 --- /dev/null +++ b/resources/js/components/ui/combobox/ComboboxAnchor.vue @@ -0,0 +1,23 @@ + + + diff --git a/resources/js/components/ui/combobox/ComboboxEmpty.vue b/resources/js/components/ui/combobox/ComboboxEmpty.vue new file mode 100644 index 000000000..5688dbb61 --- /dev/null +++ b/resources/js/components/ui/combobox/ComboboxEmpty.vue @@ -0,0 +1,21 @@ + + + diff --git a/resources/js/components/ui/combobox/ComboboxGroup.vue b/resources/js/components/ui/combobox/ComboboxGroup.vue new file mode 100644 index 000000000..a9199108a --- /dev/null +++ b/resources/js/components/ui/combobox/ComboboxGroup.vue @@ -0,0 +1,27 @@ + + + diff --git a/resources/js/components/ui/combobox/ComboboxInput.vue b/resources/js/components/ui/combobox/ComboboxInput.vue new file mode 100644 index 000000000..1b76d279b --- /dev/null +++ b/resources/js/components/ui/combobox/ComboboxInput.vue @@ -0,0 +1,42 @@ + + + diff --git a/resources/js/components/ui/combobox/ComboboxItem.vue b/resources/js/components/ui/combobox/ComboboxItem.vue new file mode 100644 index 000000000..e83e3d337 --- /dev/null +++ b/resources/js/components/ui/combobox/ComboboxItem.vue @@ -0,0 +1,24 @@ + + + diff --git a/resources/js/components/ui/combobox/ComboboxItemIndicator.vue b/resources/js/components/ui/combobox/ComboboxItemIndicator.vue new file mode 100644 index 000000000..fde095952 --- /dev/null +++ b/resources/js/components/ui/combobox/ComboboxItemIndicator.vue @@ -0,0 +1,23 @@ + + + diff --git a/resources/js/components/ui/combobox/ComboboxList.vue b/resources/js/components/ui/combobox/ComboboxList.vue new file mode 100644 index 000000000..d154e750d --- /dev/null +++ b/resources/js/components/ui/combobox/ComboboxList.vue @@ -0,0 +1,35 @@ + + + diff --git a/resources/js/components/ui/combobox/ComboboxSeparator.vue b/resources/js/components/ui/combobox/ComboboxSeparator.vue new file mode 100644 index 000000000..2aa53b584 --- /dev/null +++ b/resources/js/components/ui/combobox/ComboboxSeparator.vue @@ -0,0 +1,21 @@ + + + diff --git a/resources/js/components/ui/combobox/ComboboxTrigger.vue b/resources/js/components/ui/combobox/ComboboxTrigger.vue new file mode 100644 index 000000000..ab83aec77 --- /dev/null +++ b/resources/js/components/ui/combobox/ComboboxTrigger.vue @@ -0,0 +1,24 @@ + + + diff --git a/resources/js/components/ui/combobox/ComboboxViewport.vue b/resources/js/components/ui/combobox/ComboboxViewport.vue new file mode 100644 index 000000000..bac3905f5 --- /dev/null +++ b/resources/js/components/ui/combobox/ComboboxViewport.vue @@ -0,0 +1,27 @@ + + + diff --git a/resources/js/components/ui/combobox/index.ts b/resources/js/components/ui/combobox/index.ts new file mode 100644 index 000000000..5e599b96c --- /dev/null +++ b/resources/js/components/ui/combobox/index.ts @@ -0,0 +1,13 @@ +export { default as Combobox } from "./Combobox.vue" +export { default as ComboboxAnchor } from "./ComboboxAnchor.vue" +export { default as ComboboxEmpty } from "./ComboboxEmpty.vue" +export { default as ComboboxGroup } from "./ComboboxGroup.vue" +export { default as ComboboxInput } from "./ComboboxInput.vue" +export { default as ComboboxItem } from "./ComboboxItem.vue" +export { default as ComboboxItemIndicator } from "./ComboboxItemIndicator.vue" +export { default as ComboboxList } from "./ComboboxList.vue" +export { default as ComboboxSeparator } from "./ComboboxSeparator.vue" +export { default as ComboboxTrigger } from "./ComboboxTrigger.vue" +export { default as ComboboxViewport } from "./ComboboxViewport.vue" + +export { ComboboxCancel } from "reka-ui" diff --git a/resources/js/form/components/fields/Text.vue b/resources/js/form/components/fields/Text.vue index bc418a88b..2edf774d7 100644 --- a/resources/js/form/components/fields/Text.vue +++ b/resources/js/form/components/fields/Text.vue @@ -1,43 +1,54 @@