fix: FileUpload requiredIf() (#18818)

This commit is contained in:
Dan Harrin
2026-01-01 21:40:35 +00:00
committed by GitHub
parent ce679f317f
commit 219101c2ab
3 changed files with 162 additions and 28 deletions
+1 -26
View File
@@ -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",
@@ -77,6 +77,24 @@ class BaseFileUpload extends Field implements Contracts\HasNestedRecursiveValida
protected ?Closure $saveUploadedFileUsing = null;
/**
* @var array<string>
*/
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
@@ -1,10 +1,12 @@
<?php
use Filament\Forms\Components\Field;
use Filament\Forms\Components\FileUpload;
use Filament\Schemas\Schema;
use Filament\Tests\Fixtures\Livewire\Livewire;
use Filament\Tests\TestCase;
use Illuminate\Http\UploadedFile;
use Illuminate\Validation\ValidationException;
use Livewire\Exceptions\RootTagMissingFromViewException;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
@@ -81,6 +83,118 @@ describe('visibility', function (): void {
});
});
describe('validation', function (): void {
it('can use `requiredIf()` and fails validation when condition is met', function (): void {
$rules = [];
$errors = [];
try {
Schema::make(Livewire::make())
->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