diff --git a/package-lock.json b/package-lock.json index 9374afd9fe..fa99a7efbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -139,7 +139,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -3523,7 +3522,6 @@ "integrity": "sha512-iUelgiTMgPVMpY5ZqASUpk8mC8HuR9FWKaDzK27w9oWip9tuB54Z8mePTxNcQaSPb6ErzEaC8x8egrRt7OsdGQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -3849,7 +3847,6 @@ "integrity": "sha512-LcH9KE4QBUJ6IPwt1Uo5iU7zatFjUUvXbctIu2fKQ9nqJ7nNSFxRhkNyporVFkTWYH7/rb0qMoF1VxSUGefG5w==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -3964,7 +3961,6 @@ "integrity": "sha512-i7O0ptSibEtTy+2PIPsNKEvhTvMaFJg1W4Oxfnbuxvaigs7cJV9Q0lwDUcc7CPsNw2T1+44wcxg431CzTvdYoA==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -3980,7 +3976,6 @@ "integrity": "sha512-WKR4ucALq+lwx0WJZW17CspeTpXorbIOpvKv5mulZica6QxqfMhn8n1IXCkDws/mCoLRx4Drk5d377tIjFNsvQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", @@ -4012,7 +4007,6 @@ "integrity": "sha512-IXNvyLITpPiuXHn/q1ntztPYJZMFjPAokKj+OQz3MFNYlzAX3I409KD/EwwCubisRIAFiNX0ZjIIXxxZ3AhFTw==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -4718,7 +4712,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4745,7 +4738,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5404,7 +5396,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5600,7 +5591,6 @@ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -7020,7 +7010,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7483,8 +7472,7 @@ "resolved": "https://registry.npmjs.org/filepond/-/filepond-4.32.10.tgz", "integrity": "sha512-uflpIL+rfAgsYk7HgXugh7ELlHNHLTPtIHfvuq0gv3x2qNfVxdGA6T7RPxMh9Vji8ysXstXkDHVXQ4hY38MPTQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/filepond-plugin-file-validate-size": { "version": "2.2.8", @@ -8501,7 +8489,6 @@ "integrity": "sha512-33AmZ+xjZhg2JMCe+vDf6a9mzWukE7l+wAtesjE7KyteqqKjzxv7aVQeWnul1Ve26mWvEQqyPwl0OctNBfSR9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "file-type": "^12.0.0", "globby": "^10.0.0", @@ -9844,7 +9831,6 @@ "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" } @@ -11052,7 +11038,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11665,7 +11650,6 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11926,7 +11910,6 @@ "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -11959,7 +11942,6 @@ "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -12012,7 +11994,6 @@ "integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -13785,7 +13766,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14371,7 +14351,6 @@ "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -14421,7 +14400,6 @@ "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", @@ -14558,7 +14536,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14676,7 +14653,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14779,7 +14755,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/packages/forms/src/Components/BaseFileUpload.php b/packages/forms/src/Components/BaseFileUpload.php index 7ff3a3d697..27ab333f8b 100644 --- a/packages/forms/src/Components/BaseFileUpload.php +++ b/packages/forms/src/Components/BaseFileUpload.php @@ -77,6 +77,24 @@ class BaseFileUpload extends Field implements Contracts\HasNestedRecursiveValida protected ?Closure $saveUploadedFileUsing = null; + /** + * @var array + */ + protected const ARRAY_VALIDATION_RULES = [ + 'filled', + 'prohibited', + 'prohibited_if', + 'prohibited_unless', + 'required_if', + 'required_if_accepted', + 'required_if_declined', + 'required_unless', + 'required_with', + 'required_with_all', + 'required_without', + 'required_without_all', + ]; + protected function setUp(): void { parent::setUp(); @@ -598,7 +616,23 @@ class BaseFileUpload extends Field implements Contracts\HasNestedRecursiveValida $rules[] = "min:{$count}"; } - $rules[] = function (string $attribute, array $value, Closure $fail): void { + $arrayRules = []; + $fileRules = []; + + foreach (parent::getValidationRules() as $rule) { + if ($this->isArrayValidationRule($rule)) { + $arrayRules[] = $rule; + } else { + $fileRules[] = $rule; + } + } + + $rules = [ + ...$rules, + ...$arrayRules, + ]; + + $rules[] = function (string $attribute, array $value, Closure $fail) use ($fileRules): void { $files = array_filter($value, fn (TemporaryUploadedFile | string $file): bool => $file instanceof TemporaryUploadedFile); $name = $this->getName(); @@ -607,7 +641,7 @@ class BaseFileUpload extends Field implements Contracts\HasNestedRecursiveValida $validator = Validator::make( [$name => $files], - ["{$name}.*" => ['file', ...parent::getValidationRules()]], + ["{$name}.*" => ['file', ...$fileRules]], $validationMessages ? ["{$name}.*" => $validationMessages] : [], ["{$name}.*" => $this->getValidationAttribute()], ); @@ -622,6 +656,17 @@ class BaseFileUpload extends Field implements Contracts\HasNestedRecursiveValida return $rules; } + protected function isArrayValidationRule(mixed $rule): bool + { + if (! is_string($rule)) { + return false; + } + + $ruleName = strtolower(explode(':', $rule)[0]); + + return in_array($ruleName, static::ARRAY_VALIDATION_RULES, strict: true); + } + #[ExposedLivewireMethod] #[Renderless] public function deleteUploadedFile(string $fileKey): static diff --git a/tests/src/Forms/Components/FileUploadTest.php b/tests/src/Forms/Components/FileUploadTest.php index 0b2379ece1..64d0e59e0c 100644 --- a/tests/src/Forms/Components/FileUploadTest.php +++ b/tests/src/Forms/Components/FileUploadTest.php @@ -1,10 +1,12 @@ statePath('data') + ->components([ + $field1 = (new Field('type')) + ->default('file'), + $field2 = FileUpload::make('document') + ->requiredIf('type', 'file'), + ]) + ->fill() + ->validate(); + } catch (ValidationException $exception) { + $rules = array_keys($exception->validator->failed()[$field2->getStatePath()] ?? []); + $errors = $exception->validator->errors()->get($field2->getStatePath()); + } + + expect($rules) + ->toContain('RequiredIf'); + + expect($errors) + ->toContain('The document field is required when type is file.'); + }); + + it('can use `requiredIf()` and passes validation when condition is not met', function (): void { + $validationPassed = false; + + try { + Schema::make(Livewire::make()) + ->statePath('data') + ->components([ + (new Field('type')) + ->default('text'), + FileUpload::make('document') + ->requiredIf('type', 'file'), + ]) + ->fill() + ->validate(); + + $validationPassed = true; + } catch (ValidationException) { + $validationPassed = false; + } + + expect($validationPassed)->toBeTrue(); + }); + + it('can use `requiredUnless()` and fails validation when condition is not met', function (): void { + $rules = []; + $errors = []; + + try { + Schema::make(Livewire::make()) + ->statePath('data') + ->components([ + $field1 = (new Field('type')) + ->default('text'), + $field2 = FileUpload::make('document') + ->requiredUnless('type', 'file'), + ]) + ->fill() + ->validate(); + } catch (ValidationException $exception) { + $rules = array_keys($exception->validator->failed()[$field2->getStatePath()] ?? []); + $errors = $exception->validator->errors()->get($field2->getStatePath()); + } + + expect($rules) + ->toContain('RequiredUnless'); + + expect($errors) + ->toContain('The document field is required unless type is in file.'); + }); + + it('can use `requiredUnless()` and passes validation when condition is met', function (): void { + $validationPassed = false; + + try { + Schema::make(Livewire::make()) + ->statePath('data') + ->components([ + (new Field('type')) + ->default('file'), + FileUpload::make('document') + ->requiredUnless('type', 'file'), + ]) + ->fill() + ->validate(); + + $validationPassed = true; + } catch (ValidationException) { + $validationPassed = false; + } + + expect($validationPassed)->toBeTrue(); + }); + + it('applies `rule()` to individual files, not the array', function (): void { + $field = FileUpload::make('document') + ->rule('mimetypes:image/png'); + + $rules = $field->getValidationRules(); + + $stringRules = array_filter($rules, fn ($rule) => is_string($rule)); + expect($stringRules)->not->toContain('mimetypes:image/png'); + }); +}); + class TestComponentWithFileUpload extends Livewire { public function form(Schema $form): Schema