diff --git a/packages/actions/docs/07-prebuilt-actions/08-import.md b/packages/actions/docs/07-prebuilt-actions/08-import.md index d3b4a978cbe..3e7f90a32b2 100644 --- a/packages/actions/docs/07-prebuilt-actions/08-import.md +++ b/packages/actions/docs/07-prebuilt-actions/08-import.md @@ -133,6 +133,19 @@ ImportColumn::make('sku') If you require a column in the database, you also need to make sure that it has a [`rules(['required'])` validation rule](#validating-csv-data). +If a column is not mapped, it will not be validated since there is no data to validate. + +If you allow an import to create records as well as [update existing ones](#updating-existing-records-when-importing), but only require a column to be mapped when creating records as it's a required field, you can use the `requiredMappingForNewRecordsOnly()` method instead of `requiredMapping()`: + +```php +use Filament\Actions\Imports\ImportColumn; + +ImportColumn::make('sku') + ->requiredMappingForNewRecordsOnly() +``` + +If the `resolveRecord()` method returns a model instance that is not saved in the database yet, the column will be required to be mapped, just for that row. If the user does not map the column, and one of the rows in the import does not yet exist in the database, just that row will fail and a message will be added to the failed rows CSV after every row has been analyzed. + ### Validating CSV data You can call the `rules()` method to add validation rules to a column. These rules will check the data in each row from the CSV before it is saved to the database: diff --git a/packages/actions/resources/lang/en/import.php b/packages/actions/resources/lang/en/import.php index 75b7aeb340c..459319874cd 100644 --- a/packages/actions/resources/lang/en/import.php +++ b/packages/actions/resources/lang/en/import.php @@ -79,6 +79,7 @@ 'file_name' => 'import-:import_id-:csv_name-failed-rows', 'error_header' => 'error', 'system_error' => 'System error, please contact support.', + 'column_mapping_required_for_new_record' => 'The :attribute column was not mapped to a column in the file, but it is required for creating new records.', ], ]; diff --git a/packages/actions/src/Imports/ImportColumn.php b/packages/actions/src/Imports/ImportColumn.php index e6f05a6e0d7..4518dc54b3d 100644 --- a/packages/actions/src/Imports/ImportColumn.php +++ b/packages/actions/src/Imports/ImportColumn.php @@ -20,6 +20,8 @@ class ImportColumn extends Component protected bool | Closure $isMappingRequired = false; + protected bool | Closure $isMappingRequiredForNewRecordsOnly = false; + protected int | Closure | null $decimalPlaces = null; protected bool | Closure $isNumeric = false; @@ -147,6 +149,13 @@ public function requiredMapping(bool | Closure $condition = true): static return $this; } + public function requiredMappingForNewRecordsOnly(bool | Closure $condition = true): static + { + $this->isMappingRequiredForNewRecordsOnly = $condition; + + return $this; + } + public function numeric(bool | Closure $condition = true, int | Closure | null $decimalPlaces = null): static { $this->isNumeric = $condition; @@ -502,6 +511,11 @@ public function isMappingRequired(): bool return (bool) $this->evaluate($this->isMappingRequired); } + public function isMappingRequiredForNewRecordsOnly(): bool + { + return (bool) $this->evaluate($this->isMappingRequiredForNewRecordsOnly); + } + public function hasRelationship(): bool { return filled($this->getRelationshipName()); diff --git a/packages/actions/src/Imports/Importer.php b/packages/actions/src/Imports/Importer.php index 2c0b6441717..925592dc166 100644 --- a/packages/actions/src/Imports/Importer.php +++ b/packages/actions/src/Imports/Importer.php @@ -56,6 +56,12 @@ public function __invoke(array $data): void return; } + $recordExists = $this->record->exists; + + if (! $recordExists) { + $this->checkColumnMappingRequirementsForNewRecords(); + } + $this->callHook('beforeValidate'); $this->validateData(); $this->callHook('afterValidate'); @@ -64,8 +70,6 @@ public function __invoke(array $data): void $this->fillRecord(); $this->callHook('afterFill'); - $recordExists = $this->record->exists; - $this->callHook('beforeSave'); $this->callHook($recordExists ? 'beforeUpdate' : 'beforeCreate'); $this->saveRecord(); @@ -96,6 +100,31 @@ public function remapData(): void $this->data = $data; } + /** + * @throws ValidationException + */ + public function checkColumnMappingRequirementsForNewRecords(): void + { + foreach ($this->getCachedColumns() as $column) { + $columnName = $column->getName(); + + if (filled($this->columnMap[$columnName] ?? null)) { + continue; + } + + if (! $column->isMappingRequiredForNewRecordsOnly()) { + continue; + } + + Validator::validate( + data: [$columnName => null], + rules: [$columnName => ['required']], + messages: ["{$columnName}.required" => __('filament-actions::import.failure_csv.column_mapping_required_for_new_record')], + attributes: [$columnName => $column->getLabel()], + ); + } + } + public function castData(): void { foreach ($this->getCachedColumns() as $column) { @@ -125,14 +154,12 @@ public function resolveRecord(): ?Model */ public function validateData(): void { - $validator = Validator::make( + Validator::validate( $this->data, $this->getValidationRules(), $this->getValidationMessages(), $this->getValidationAttributes(), ); - - $validator->validate(); } /**