diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 9abc5b3..c47632a 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -32,7 +32,7 @@ Before importing, you will need to do some preparation: 3. You can then map fields from your blueprint to fields/columns in your file. * Depending on the fieldtype, some fields may have additional options, like "Related Key" or "Create when missing". You can read more about these below. * Mapping is disabled for some fieldtypes, like the [Replicator fieldtype](https://statamic.dev/fieldtypes/replicator#content). If you wish to import these fields, you will need to build a [custom transformer](#transformers). -4. You will also need to specify a "Unique Field". This field will be used to determine if an item already exists in Statamic. +4. If you're importing entries, you will also need to specify a "Unique Field". This field will be used to determine if an entry already exists in Statamic. 5. Then, run the import and watch the magic happen! ✨ You can run the importer as many times as you like as you tweak the mappings. It'll update existing content and create new content as needed. diff --git a/lang/en/validation.php b/lang/en/validation.php index c35a529..0663596 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -2,7 +2,9 @@ return [ 'file_type_not_allowed' => 'Only CSV and XML files can be imported at this time.', + 'mappings_email_missing' => 'The Email field must be mapped on user imports.', 'mappings_not_provided' => 'You must map at least one field.', + 'mappings_slug_missing' => 'The Slug field must be mapped on taxonomy term imports.', 'site_not_configured_in_collection' => 'The chosen collection is not available on this site.', 'unique_field_without_mapping' => 'Please configure a mapping for this field.', 'uploaded_file_not_found' => 'The uploaded file could not be found.', diff --git a/src/Http/Controllers/ImportController.php b/src/Http/Controllers/ImportController.php index 4e0544a..9ba07d0 100644 --- a/src/Http/Controllers/ImportController.php +++ b/src/Http/Controllers/ImportController.php @@ -176,7 +176,7 @@ public function update(Request $request, Import $import) 'strategy' => $values['strategy'], 'source' => $values['source'] ?? null, 'mappings' => $values['mappings'], - 'unique_field' => $values['unique_field'], + 'unique_field' => $values['unique_field'] ?? null, ])); $saved = $import->save(); diff --git a/src/Imports/Blueprint.php b/src/Imports/Blueprint.php index 741e85a..57aee79 100644 --- a/src/Imports/Blueprint.php +++ b/src/Imports/Blueprint.php @@ -187,9 +187,19 @@ function (string $attribute, mixed $value, Closure $fail) { 'required', 'array', function (string $attribute, mixed $value, Closure $fail) { + $type = Arr::get(request()->destination, 'type'); + if (collect($value)->reject(fn (array $mapping) => empty($mapping['key']))->isEmpty()) { $fail('importer::validation.mappings_not_provided')->translate(); } + + if ($type === 'terms' && Arr::get($value, 'slug.key') === null) { + $fail('importer::validation.mappings_slug_missing')->translate(); + } + + if ($type === 'users' && Arr::get($value, 'email.key') === null) { + $fail('importer::validation.mappings_email_missing')->translate(); + } }, ], 'if' => $import ? static::buildFieldConditions($import) : null, @@ -206,14 +216,16 @@ function (string $attribute, mixed $value, Closure $fail) { ->map(fn ($field) => ['key' => $field->handle(), 'value' => $field->display()]) ->values(), 'validate' => [ - 'required', + 'required_if:destination.type,entries', function (string $attribute, mixed $value, Closure $fail) { - if (! collect(request()->mappings)->reject(fn ($mapping) => empty($mapping['key']))->has($value)) { + if ($value && ! collect(request()->mappings)->reject(fn ($mapping) => empty($mapping['key']))->has($value)) { $fail('importer::validation.unique_field_without_mapping')->translate(); } }, ], - 'if' => $import ? static::buildFieldConditions($import) : null, + 'if' => $import + ? array_merge(static::buildFieldConditions($import), ['destination.type' => 'entries']) + : null, ], ], ], diff --git a/src/Jobs/ImportItemJob.php b/src/Jobs/ImportItemJob.php index c2110a9..a69549e 100644 --- a/src/Jobs/ImportItemJob.php +++ b/src/Jobs/ImportItemJob.php @@ -10,7 +10,6 @@ use Illuminate\Support\Arr; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Str; use Statamic\Facades\Collection; use Statamic\Facades\Entry; use Statamic\Facades\Site; @@ -121,7 +120,7 @@ protected function findOrCreateTerm(array $data): void { $term = Term::query() ->where('taxonomy', $this->import->get('destination.taxonomy')) - ->where($this->import->get('unique_field'), $data[$this->import->get('unique_field')]) + ->where('slug', $data['slug']) ->first(); if (! $term) { @@ -138,13 +137,7 @@ protected function findOrCreateTerm(array $data): void return; } - if (isset($data['slug'])) { - $term->slug(Arr::pull($data, 'slug')); - } - - if (! $term->slug()) { - $term->slug(Str::slug($data[$this->import->get('unique_field')])); - } + $term->slug(Arr::pull($data, 'slug')); $term->merge($data); @@ -153,9 +146,7 @@ protected function findOrCreateTerm(array $data): void protected function findOrCreateUser(array $data): void { - $user = User::query() - ->where($this->import->get('unique_field'), $data[$this->import->get('unique_field')]) - ->first(); + $user = User::findByEmail($data['email']); if (! $user) { if (! in_array('create', $this->import->get('strategy'))) { diff --git a/tests/Imports/UpdateImportTest.php b/tests/Imports/UpdateImportTest.php index e208079..6b6b592 100644 --- a/tests/Imports/UpdateImportTest.php +++ b/tests/Imports/UpdateImportTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Storage; use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\Collection; +use Statamic\Facades\Taxonomy; use Statamic\Facades\User; use Statamic\Importer\Facades\Import; use Statamic\Importer\Tests\TestCase; @@ -224,7 +225,76 @@ public function validation_error_is_thrown_without_any_mappings() } #[Test] - public function throws_validation_errors_for_mapping_fields() + public function validation_error_is_thrown_for_terms_import_without_slug_mapping() + { + Taxonomy::make('tags')->save(); + + Storage::disk('local')->put('statamic/imports/tags/tags.csv', ''); + + $import = Import::make()->name('Users')->config([ + 'type' => 'csv', + 'path' => Storage::disk('local')->path('statamic/imports/tags/tags.csv'), + 'destination' => ['type' => 'terms', 'taxonomy' => 'tags', 'blueprint' => 'tag'], + ]); + + $import->save(); + + $this + ->actingAs(User::make()->makeSuper()->save()) + ->patch("/cp/utilities/importer/{$import->id()}", [ + 'name' => 'Posts', + 'file' => ['posts.csv'], + 'destination' => ['type' => 'terms', 'taxonomy' => ['tags'], 'blueprint' => 'tag'], + 'strategy' => ['create', 'update'], + 'mappings' => [ + 'title' => ['key' => 'Title'], + 'slug' => ['key' => null], + ], + ]) + ->assertSessionHasErrors('mappings'); + } + + #[Test] + public function validation_error_is_thrown_for_users_import_without_email_mapping() + { + User::blueprint()->setContents([ + 'sections' => [ + 'main' => [ + 'fields' => [ + ['handle' => 'name', 'field' => ['type' => 'text']], + ['handle' => 'email', 'field' => ['type' => 'text']], + ], + ], + ], + ]); + + Storage::disk('local')->put('statamic/imports/users/users.csv', ''); + + $import = Import::make()->name('Users')->config([ + 'type' => 'csv', + 'path' => Storage::disk('local')->path('statamic/imports/users/users.csv'), + 'destination' => ['type' => 'users'], + ]); + + $import->save(); + + $this + ->actingAs(User::make()->makeSuper()->save()) + ->patch("/cp/utilities/importer/{$import->id()}", [ + 'name' => 'Posts', + 'file' => ['posts.csv'], + 'destination' => ['type' => 'users'], + 'strategy' => ['create', 'update'], + 'mappings' => [ + 'name' => ['key' => 'Name'], + 'email' => ['key' => null], + ], + ]) + ->assertSessionHasErrors('mappings'); + } + + #[Test] + public function validation_errors_are_thrown_for_transformer_fields() { $this ->actingAs(User::make()->makeSuper()->save()) @@ -242,7 +312,7 @@ public function throws_validation_errors_for_mapping_fields() } #[Test] - public function validation_error_is_thrown_without_unique_field() + public function unique_field_is_required_for_entry_imports() { $this ->actingAs(User::make()->makeSuper()->save()) @@ -264,7 +334,7 @@ public function validation_error_is_thrown_without_unique_field() } #[Test] - public function validation_error_is_thrown_when_no_mapping_is_configured_for_unique_field() + public function ensure_unique_field_has_a_mapping() { $this ->actingAs(User::make()->makeSuper()->save()) diff --git a/tests/Jobs/ImportItemJobTest.php b/tests/Jobs/ImportItemJobTest.php index 06223cd..44c97c9 100644 --- a/tests/Jobs/ImportItemJobTest.php +++ b/tests/Jobs/ImportItemJobTest.php @@ -381,12 +381,14 @@ public function it_imports_a_new_term() 'unique_field' => 'title', 'mappings' => [ 'title' => ['key' => 'Title'], + 'slug' => ['key' => 'Slug'], ], 'strategy' => ['create'], ]); ImportItemJob::dispatch($import, [ 'Title' => 'Statamic', + 'Slug' => 'statamic', ]); $term = Term::query()->where('title', 'Statamic')->first(); @@ -416,12 +418,14 @@ public function it_imports_a_new_term_with_a_specific_blueprint() 'unique_field' => 'title', 'mappings' => [ 'title' => ['key' => 'Title'], + 'slug' => ['key' => 'Slug'], ], 'strategy' => ['create'], ]); ImportItemJob::dispatch($import, [ 'Title' => 'Statamic', + 'Slug' => 'statamic', ]); $term = Term::query()->where('title', 'Statamic')->first(); @@ -442,12 +446,14 @@ public function it_doesnt_import_a_new_term_when_creation_is_disabled() 'unique_field' => 'title', 'mappings' => [ 'title' => ['key' => 'Title'], + 'slug' => ['key' => 'Slug'], ], 'strategy' => ['update'], ]); ImportItemJob::dispatch($import, [ 'Title' => 'Statamic', + 'Slug' => 'statamic', ]); $this->assertNull(Term::query()->where('title', 'Statamic')->first()); @@ -464,6 +470,7 @@ public function it_updates_an_existing_term() 'unique_field' => 'title', 'mappings' => [ 'title' => ['key' => 'Title'], + 'slug' => ['key' => 'Slug'], 'foo' => ['key' => 'Foo'], ], 'strategy' => ['update'], @@ -471,6 +478,7 @@ public function it_updates_an_existing_term() ImportItemJob::dispatch($import, [ 'Title' => 'Statamic', + 'Slug' => 'statamic', 'Foo' => 'Baz', ]); @@ -493,6 +501,7 @@ public function it_doesnt_update_an_existing_term_when_updating_is_disabled() 'unique_field' => 'title', 'mappings' => [ 'title' => ['key' => 'Title'], + 'slug' => ['key' => 'Slug'], 'foo' => ['key' => 'Foo'], ], 'strategy' => ['create'], @@ -500,6 +509,7 @@ public function it_doesnt_update_an_existing_term_when_updating_is_disabled() ImportItemJob::dispatch($import, [ 'Title' => 'Statamic', + 'Slug' => 'statamic', 'Foo' => 'Baz', ]);