From e34e5cd6abde980b1a713ca539186cdff4149fa1 Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Mon, 12 Aug 2024 17:20:09 +0100 Subject: [PATCH] feature: Required import mapping for new records only --- .../docs/07-prebuilt-actions/08-import.md | 13 +++++++ packages/actions/resources/lang/en/import.php | 1 + packages/actions/src/Imports/ImportColumn.php | 14 +++++++ packages/actions/src/Imports/Importer.php | 37 ++++++++++++++++--- 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/packages/actions/docs/07-prebuilt-actions/08-import.md b/packages/actions/docs/07-prebuilt-actions/08-import.md index d3b4a978cb..3e7f90a32b 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 75b7aeb340..459319874c 100644 --- a/packages/actions/resources/lang/en/import.php +++ b/packages/actions/resources/lang/en/import.php @@ -79,6 +79,7 @@ return [ '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 e6f05a6e0d..4518dc54b3 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 @@ class ImportColumn extends Component 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 @@ class ImportColumn extends Component 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 2c0b644171..925592dc16 100644 --- a/packages/actions/src/Imports/Importer.php +++ b/packages/actions/src/Imports/Importer.php @@ -56,6 +56,12 @@ abstract class Importer return; } + $recordExists = $this->record->exists; + + if (! $recordExists) { + $this->checkColumnMappingRequirementsForNewRecords(); + } + $this->callHook('beforeValidate'); $this->validateData(); $this->callHook('afterValidate'); @@ -64,8 +70,6 @@ abstract class Importer $this->fillRecord(); $this->callHook('afterFill'); - $recordExists = $this->record->exists; - $this->callHook('beforeSave'); $this->callHook($recordExists ? 'beforeUpdate' : 'beforeCreate'); $this->saveRecord(); @@ -96,6 +100,31 @@ abstract class Importer $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 @@ abstract class Importer */ public function validateData(): void { - $validator = Validator::make( + Validator::validate( $this->data, $this->getValidationRules(), $this->getValidationMessages(), $this->getValidationAttributes(), ); - - $validator->validate(); } /**