From 935043451ce577d62e5babaf52e4bce3bf167c03 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 18:26:16 +0000 Subject: [PATCH 01/21] Show "Site" dropdown when configuring taxonomy import --- lang/en/validation.php | 1 + src/Imports/Blueprint.php | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lang/en/validation.php b/lang/en/validation.php index c35a529..6d093c0 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -4,6 +4,7 @@ 'file_type_not_allowed' => 'Only CSV and XML files can be imported at this time.', 'mappings_not_provided' => 'You must map at least one field.', 'site_not_configured_in_collection' => 'The chosen collection is not available on this site.', + 'site_not_configured_in_taxonomy' => 'The chosen taxonomy 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/Imports/Blueprint.php b/src/Imports/Blueprint.php index 741e85a..b099350 100644 --- a/src/Imports/Blueprint.php +++ b/src/Imports/Blueprint.php @@ -140,14 +140,26 @@ function (string $attribute, mixed $value, Closure $fail) use ($import) { 'width' => 50, 'max_items' => 1, 'mode' => 'select', - 'if' => ['type' => 'entries'], + 'unless' => ['type' => 'users'], 'validate' => [ - 'required_if:destination.type,entries', + 'required_unless:destination.type,users', function (string $attribute, mixed $value, Closure $fail) { - $collection = Collection::find(Arr::get(request()->destination, 'collection.0')); + $type = Arr::get(request()->destination, 'type'); + + if ($type === 'entries') { + $collection = Collection::find(Arr::get(request()->destination, 'collection.0')); + + if (count($value) && ! $collection->sites()->contains($value[0])) { + $fail('importer::validation.site_not_configured_in_collection')->translate(); + } + } + + if ($type === 'terms') { + $taxonomy = Facades\Taxonomy::findByHandle(Arr::get(request()->destination, 'taxonomy.0')); - if (count($value) && ! $collection->sites()->contains($value[0])) { - $fail('importer::validation.site_not_configured_in_collection')->translate(); + if (count($value) && ! $taxonomy->sites()->contains($value[0])) { + $fail('importer::validation.site_not_configured_in_taxonomy')->translate(); + } } }, ], From c826e88c458288f7f33bd8ac1a3b60b8336e3ebf Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 20:46:28 +0000 Subject: [PATCH 02/21] Drop "Unique Field" option for term & user imports --- DOCUMENTATION.md | 2 +- lang/en/validation.php | 2 + src/Fieldtypes/ImportMappingsFieldtype.php | 1 + src/Imports/Blueprint.php | 19 ++++++-- src/Jobs/ImportItemJob.php | 6 +-- tests/Imports/UpdateImportTest.php | 52 +++++++++++++++++++++- 6 files changed, 73 insertions(+), 9 deletions(-) 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 6d093c0..21bd931 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 imports.', 'site_not_configured_in_collection' => 'The chosen collection is not available on this site.', 'site_not_configured_in_taxonomy' => 'The chosen taxonomy is not available on this site.', 'unique_field_without_mapping' => 'Please configure a mapping for this field.', diff --git a/src/Fieldtypes/ImportMappingsFieldtype.php b/src/Fieldtypes/ImportMappingsFieldtype.php index d396621..71681b5 100644 --- a/src/Fieldtypes/ImportMappingsFieldtype.php +++ b/src/Fieldtypes/ImportMappingsFieldtype.php @@ -69,6 +69,7 @@ public function extraRules(): array return collect($this->field->value()) ->reject(fn ($row) => empty($row['key'])) + ->filter(fn ($row) => $fields->has($row['key'])) ->flatMap(function (array $row, string $field) use ($fields) { $rules = $fields ->get($field) diff --git a/src/Imports/Blueprint.php b/src/Imports/Blueprint.php index b099350..be3c577 100644 --- a/src/Imports/Blueprint.php +++ b/src/Imports/Blueprint.php @@ -199,9 +199,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, @@ -213,19 +223,22 @@ function (string $attribute, mixed $value, Closure $fail) { 'type' => 'radio', 'display' => __('Unique Field'), 'instructions' => __('importer::messages.unique_field_instructions'), - 'options' => $import?->destinationBlueprint()->fields()->all() + 'options' => $import?->mappingFields()->all() ->filter(fn ($field) => in_array($field->type(), ['text', 'integer', 'slug'])) ->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)) { $fail('importer::validation.unique_field_without_mapping')->translate(); } }, ], - 'if' => $import ? static::buildFieldConditions($import) : null, + 'if' => [ + 'destination.type' => 'entries', + ], +// 'if' => $import ? static::buildFieldConditions($import) : null, ], ], ], diff --git a/src/Jobs/ImportItemJob.php b/src/Jobs/ImportItemJob.php index c2110a9..e12aa6d 100644 --- a/src/Jobs/ImportItemJob.php +++ b/src/Jobs/ImportItemJob.php @@ -120,8 +120,8 @@ protected function findOrCreateEntry(array $data): void protected function findOrCreateTerm(array $data): void { $term = Term::query() + ->where('slug', $data['slug']) ->where('taxonomy', $this->import->get('destination.taxonomy')) - ->where($this->import->get('unique_field'), $data[$this->import->get('unique_field')]) ->first(); if (! $term) { @@ -153,9 +153,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..6a2b319 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,56 @@ 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(); + + $this + ->actingAs(User::make()->makeSuper()->save()) + ->patch("/cp/utilities/importer/{$this->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']], + ], + ], + ], + ]); + + $this + ->actingAs(User::make()->makeSuper()->save()) + ->patch("/cp/utilities/importer/{$this->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()) From 8972c0c33e72b8dc765be35b62665e86ffcf9081 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 20:50:04 +0000 Subject: [PATCH 03/21] Revert "Show "Site" dropdown when configuring taxonomy import" This reverts commit 935043451ce577d62e5babaf52e4bce3bf167c03. --- lang/en/validation.php | 1 - src/Imports/Blueprint.php | 22 +++++----------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/lang/en/validation.php b/lang/en/validation.php index 21bd931..df56681 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -6,7 +6,6 @@ 'mappings_not_provided' => 'You must map at least one field.', 'mappings_slug_missing' => 'The Slug field must be mapped on taxonomy imports.', 'site_not_configured_in_collection' => 'The chosen collection is not available on this site.', - 'site_not_configured_in_taxonomy' => 'The chosen taxonomy 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/Imports/Blueprint.php b/src/Imports/Blueprint.php index be3c577..e0093e4 100644 --- a/src/Imports/Blueprint.php +++ b/src/Imports/Blueprint.php @@ -140,26 +140,14 @@ function (string $attribute, mixed $value, Closure $fail) use ($import) { 'width' => 50, 'max_items' => 1, 'mode' => 'select', - 'unless' => ['type' => 'users'], + 'if' => ['type' => 'entries'], 'validate' => [ - 'required_unless:destination.type,users', + 'required_if:destination.type,entries', function (string $attribute, mixed $value, Closure $fail) { - $type = Arr::get(request()->destination, 'type'); - - if ($type === 'entries') { - $collection = Collection::find(Arr::get(request()->destination, 'collection.0')); - - if (count($value) && ! $collection->sites()->contains($value[0])) { - $fail('importer::validation.site_not_configured_in_collection')->translate(); - } - } - - if ($type === 'terms') { - $taxonomy = Facades\Taxonomy::findByHandle(Arr::get(request()->destination, 'taxonomy.0')); + $collection = Collection::find(Arr::get(request()->destination, 'collection.0')); - if (count($value) && ! $taxonomy->sites()->contains($value[0])) { - $fail('importer::validation.site_not_configured_in_taxonomy')->translate(); - } + if (count($value) && ! $collection->sites()->contains($value[0])) { + $fail('importer::validation.site_not_configured_in_collection')->translate(); } }, ], From 98a8b8a87fd631db7d4efe8a6d0762b081d0fe94 Mon Sep 17 00:00:00 2001 From: duncanmcclean Date: Thu, 19 Dec 2024 20:50:38 +0000 Subject: [PATCH 04/21] Fix styling --- src/Imports/Blueprint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Imports/Blueprint.php b/src/Imports/Blueprint.php index e0093e4..86af4ce 100644 --- a/src/Imports/Blueprint.php +++ b/src/Imports/Blueprint.php @@ -226,7 +226,7 @@ function (string $attribute, mixed $value, Closure $fail) { 'if' => [ 'destination.type' => 'entries', ], -// 'if' => $import ? static::buildFieldConditions($import) : null, + // 'if' => $import ? static::buildFieldConditions($import) : null, ], ], ], From 4ed8f7a5c5aeef60fce98c82bafa19525f55b75b Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 20:53:16 +0000 Subject: [PATCH 05/21] Term. --- lang/en/validation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en/validation.php b/lang/en/validation.php index df56681..0663596 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -4,7 +4,7 @@ '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 imports.', + '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.', From b484a2a60030080994ac505fc0ae260455bcb5f0 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 20:54:51 +0000 Subject: [PATCH 06/21] Swapzies. --- src/Jobs/ImportItemJob.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jobs/ImportItemJob.php b/src/Jobs/ImportItemJob.php index e12aa6d..b095022 100644 --- a/src/Jobs/ImportItemJob.php +++ b/src/Jobs/ImportItemJob.php @@ -120,8 +120,8 @@ protected function findOrCreateEntry(array $data): void protected function findOrCreateTerm(array $data): void { $term = Term::query() - ->where('slug', $data['slug']) ->where('taxonomy', $this->import->get('destination.taxonomy')) + ->where('slug', $data['slug']) ->first(); if (! $term) { From 2bdef790159865842da6fece289a077bdb74fa52 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 20:54:59 +0000 Subject: [PATCH 07/21] Update name of test. --- tests/Imports/UpdateImportTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Imports/UpdateImportTest.php b/tests/Imports/UpdateImportTest.php index 6a2b319..5648a3f 100644 --- a/tests/Imports/UpdateImportTest.php +++ b/tests/Imports/UpdateImportTest.php @@ -314,7 +314,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 unique_field_is_required_for_entry_imports() { $this ->actingAs(User::make()->makeSuper()->save()) From 0b5b84c5be82185d0230f2fc24c8d66667684067 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 20:57:36 +0000 Subject: [PATCH 08/21] Merge field conditions. --- src/Imports/Blueprint.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Imports/Blueprint.php b/src/Imports/Blueprint.php index 86af4ce..15588ef 100644 --- a/src/Imports/Blueprint.php +++ b/src/Imports/Blueprint.php @@ -223,10 +223,7 @@ function (string $attribute, mixed $value, Closure $fail) { } }, ], - 'if' => [ - 'destination.type' => 'entries', - ], - // 'if' => $import ? static::buildFieldConditions($import) : null, + 'if' => $import ? array_merge(static::buildFieldConditions($import), ['destination.type' => 'entries']) : null, ], ], ], From 54639d327f8d2e3768cab2c942390c96b5c7d8a7 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 20:59:38 +0000 Subject: [PATCH 09/21] A slug is always going to be here. --- src/Jobs/ImportItemJob.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Jobs/ImportItemJob.php b/src/Jobs/ImportItemJob.php index b095022..e8554ce 100644 --- a/src/Jobs/ImportItemJob.php +++ b/src/Jobs/ImportItemJob.php @@ -138,13 +138,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); From 58b7f97cea3cc381065bfacdee1d623b1fb60793 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 21:02:45 +0000 Subject: [PATCH 10/21] Fallback to null. --- src/Http/Controllers/ImportController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); From 04867ca2f2b287d6ac11c055f87511c7d5467564 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 21:03:22 +0000 Subject: [PATCH 11/21] Update validation to handle situation where it's null. --- src/Imports/Blueprint.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Imports/Blueprint.php b/src/Imports/Blueprint.php index 15588ef..4459518 100644 --- a/src/Imports/Blueprint.php +++ b/src/Imports/Blueprint.php @@ -218,12 +218,14 @@ function (string $attribute, mixed $value, Closure $fail) { 'validate' => [ '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 ? array_merge(static::buildFieldConditions($import), ['destination.type' => 'entries']) : null, + 'if' => $import + ? array_merge(static::buildFieldConditions($import), ['destination.type' => 'entries']) + : null, ], ], ], From 7ef5326047781e833218768f52cc4d6dc7be1a3c Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 21:03:30 +0000 Subject: [PATCH 12/21] Tweak test names again. --- tests/Imports/UpdateImportTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Imports/UpdateImportTest.php b/tests/Imports/UpdateImportTest.php index 5648a3f..1741d9f 100644 --- a/tests/Imports/UpdateImportTest.php +++ b/tests/Imports/UpdateImportTest.php @@ -292,7 +292,7 @@ public function validation_errors_are_thrown_for_transformer_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()) @@ -314,7 +314,7 @@ public function validation_error_is_thrown_without_unique_field() } #[Test] - public function unique_field_is_required_for_entry_imports() + public function ensure_unique_field_has_a_mapping() { $this ->actingAs(User::make()->makeSuper()->save()) From 72bd7fe02baf2a80fab533a5b910e6862f95c025 Mon Sep 17 00:00:00 2001 From: duncanmcclean Date: Thu, 19 Dec 2024 21:04:12 +0000 Subject: [PATCH 13/21] Fix styling --- src/Jobs/ImportItemJob.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Jobs/ImportItemJob.php b/src/Jobs/ImportItemJob.php index e8554ce..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; From 17a37f1682d88ce00be0ea47cf6929282a2f93ad Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 21:08:24 +0000 Subject: [PATCH 14/21] The slug is now required... update tests. --- tests/Jobs/ImportItemJobTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) 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', ]); From eb4fc89254a5399b700196e3fe0e036aaf5b9359 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 21:25:03 +0000 Subject: [PATCH 15/21] Fix failing tests. --- src/Fieldtypes/ImportMappingsFieldtype.php | 1 - src/Imports/Blueprint.php | 2 +- tests/Imports/UpdateImportTest.php | 25 ++++++++++++++++++++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Fieldtypes/ImportMappingsFieldtype.php b/src/Fieldtypes/ImportMappingsFieldtype.php index 71681b5..d396621 100644 --- a/src/Fieldtypes/ImportMappingsFieldtype.php +++ b/src/Fieldtypes/ImportMappingsFieldtype.php @@ -69,7 +69,6 @@ public function extraRules(): array return collect($this->field->value()) ->reject(fn ($row) => empty($row['key'])) - ->filter(fn ($row) => $fields->has($row['key'])) ->flatMap(function (array $row, string $field) use ($fields) { $rules = $fields ->get($field) diff --git a/src/Imports/Blueprint.php b/src/Imports/Blueprint.php index 4459518..57aee79 100644 --- a/src/Imports/Blueprint.php +++ b/src/Imports/Blueprint.php @@ -211,7 +211,7 @@ function (string $attribute, mixed $value, Closure $fail) { 'type' => 'radio', 'display' => __('Unique Field'), 'instructions' => __('importer::messages.unique_field_instructions'), - 'options' => $import?->mappingFields()->all() + 'options' => $import?->destinationBlueprint()->fields()->all() ->filter(fn ($field) => in_array($field->type(), ['text', 'integer', 'slug'])) ->map(fn ($field) => ['key' => $field->handle(), 'value' => $field->display()]) ->values(), diff --git a/tests/Imports/UpdateImportTest.php b/tests/Imports/UpdateImportTest.php index 1741d9f..c0e8375 100644 --- a/tests/Imports/UpdateImportTest.php +++ b/tests/Imports/UpdateImportTest.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Storage; use PHPUnit\Framework\Attributes\Test; +use Statamic\Facades\Blink; use Statamic\Facades\Collection; use Statamic\Facades\Taxonomy; use Statamic\Facades\User; @@ -229,9 +230,19 @@ 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/{$this->import->id()}", [ + ->patch("/cp/utilities/importer/{$import->id()}", [ 'name' => 'Posts', 'file' => ['posts.csv'], 'destination' => ['type' => 'terms', 'taxonomy' => ['tags'], 'blueprint' => 'tag'], @@ -258,9 +269,19 @@ public function validation_error_is_thrown_for_users_import_without_email_mappin ], ]); + 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/{$this->import->id()}", [ + ->patch("/cp/utilities/importer/{$import->id()}", [ 'name' => 'Posts', 'file' => ['posts.csv'], 'destination' => ['type' => 'users'], From f1be20fa007766265d9ad89fb1cba32340aca132 Mon Sep 17 00:00:00 2001 From: duncanmcclean Date: Thu, 19 Dec 2024 21:25:34 +0000 Subject: [PATCH 16/21] Fix styling --- tests/Imports/UpdateImportTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Imports/UpdateImportTest.php b/tests/Imports/UpdateImportTest.php index c0e8375..6b6b592 100644 --- a/tests/Imports/UpdateImportTest.php +++ b/tests/Imports/UpdateImportTest.php @@ -5,7 +5,6 @@ use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Storage; use PHPUnit\Framework\Attributes\Test; -use Statamic\Facades\Blink; use Statamic\Facades\Collection; use Statamic\Facades\Taxonomy; use Statamic\Facades\User; From 006cfd6942d8b014b9c09b8827c935bc479c40a3 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 21:32:31 +0000 Subject: [PATCH 17/21] Revert "Revert "Show "Site" dropdown when configuring taxonomy import"" This reverts commit 8972c0c33e72b8dc765be35b62665e86ffcf9081. --- lang/en/validation.php | 1 + src/Imports/Blueprint.php | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lang/en/validation.php b/lang/en/validation.php index 0663596..15cd1f5 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -6,6 +6,7 @@ '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.', + 'site_not_configured_in_taxonomy' => 'The chosen taxonomy 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/Imports/Blueprint.php b/src/Imports/Blueprint.php index 57aee79..49a043b 100644 --- a/src/Imports/Blueprint.php +++ b/src/Imports/Blueprint.php @@ -140,14 +140,26 @@ function (string $attribute, mixed $value, Closure $fail) use ($import) { 'width' => 50, 'max_items' => 1, 'mode' => 'select', - 'if' => ['type' => 'entries'], + 'unless' => ['type' => 'users'], 'validate' => [ - 'required_if:destination.type,entries', + 'required_unless:destination.type,users', function (string $attribute, mixed $value, Closure $fail) { - $collection = Collection::find(Arr::get(request()->destination, 'collection.0')); + $type = Arr::get(request()->destination, 'type'); + + if ($type === 'entries') { + $collection = Collection::find(Arr::get(request()->destination, 'collection.0')); + + if (count($value) && ! $collection->sites()->contains($value[0])) { + $fail('importer::validation.site_not_configured_in_collection')->translate(); + } + } + + if ($type === 'terms') { + $taxonomy = Facades\Taxonomy::findByHandle(Arr::get(request()->destination, 'taxonomy.0')); - if (count($value) && ! $collection->sites()->contains($value[0])) { - $fail('importer::validation.site_not_configured_in_collection')->translate(); + if (count($value) && ! $taxonomy->sites()->contains($value[0])) { + $fail('importer::validation.site_not_configured_in_taxonomy')->translate(); + } } }, ], From 7a83e8756c90632de6612f8da716c2da80bc5c12 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 21:37:23 +0000 Subject: [PATCH 18/21] Multi-site taxonomy support --- DOCUMENTATION.md | 1 + src/Imports/Import.php | 11 +++++++++++ src/Jobs/ImportItemJob.php | 25 ++++++++++++++++++++++--- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index c47632a..e10d822 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -32,6 +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). + * When you're importing taxonomy terms into a non-default site (eg. not the first site) and the slugs differ between sites, you can use the "Default Slug" field to specify the slug of the term in the default site. 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! ✨ diff --git a/src/Imports/Import.php b/src/Imports/Import.php index 3cf9cfe..2b0b7fc 100644 --- a/src/Imports/Import.php +++ b/src/Imports/Import.php @@ -171,6 +171,17 @@ public function mappingFields(): Fields ]); } + if ($this->get('destination.type') === 'terms') { + $taxonomy = Taxonomy::find($this->get('destination.taxonomy')); + + if ($this->get('destination.site') !== $taxonomy->sites()->first()) { + $blueprint->ensureField('default_slug', [ + 'type' => 'slug', + 'display' => __('Default Slug'), + ]); + } + } + return $blueprint->fields(); } } diff --git a/src/Jobs/ImportItemJob.php b/src/Jobs/ImportItemJob.php index a69549e..9e5615d 100644 --- a/src/Jobs/ImportItemJob.php +++ b/src/Jobs/ImportItemJob.php @@ -13,6 +13,7 @@ use Statamic\Facades\Collection; use Statamic\Facades\Entry; use Statamic\Facades\Site; +use Statamic\Facades\Taxonomy; use Statamic\Facades\Term; use Statamic\Facades\User; use Statamic\Importer\Importer; @@ -120,8 +121,9 @@ protected function findOrCreateTerm(array $data): void { $term = Term::query() ->where('taxonomy', $this->import->get('destination.taxonomy')) - ->where('slug', $data['slug']) - ->first(); + ->where('id', $this->import->get('destination.taxonomy') . '::' . Arr::get($data, 'default_slug', $data['slug'])) + ->first() + ?->term(); if (! $term) { if (! in_array('create', $this->import->get('strategy'))) { @@ -133,14 +135,31 @@ protected function findOrCreateTerm(array $data): void ->blueprint($this->import->get('destination.blueprint')); } - if (Term::find($term->id()) && ! in_array('update', $this->import->get('strategy'))) { + if ( + Term::find($term->id())?->in($this->import->get('destination.site') ?? Site::default()->handle()) + && ! in_array('update', $this->import->get('strategy')) + ) { return; } + $term = $term->in($this->import->get('destination.site') ?? Site::default()->handle()); + $term->slug(Arr::pull($data, 'slug')); $term->merge($data); + $site = $this->import->get('destination.site') ?? Site::default()->handle(); + $defaultSite = Taxonomy::find($this->import->get('destination.taxonomy'))->sites()->first(); + + // If the term is *not* being created in the default site, we'll copy all the + // appropriate values into the default localization since it needs to exist. + if (! Term::find($term->id()) && $site !== $defaultSite) { + $term + ->in($defaultSite) + ->data($data) + ->slug($data['default_slug'] ?? $term->slug()); + } + $term->save(); } From a5c39acada29363c1b7a00bb8c09acfeeaf5362d Mon Sep 17 00:00:00 2001 From: duncanmcclean Date: Thu, 19 Dec 2024 21:37:51 +0000 Subject: [PATCH 19/21] Fix styling --- src/Jobs/ImportItemJob.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jobs/ImportItemJob.php b/src/Jobs/ImportItemJob.php index 9e5615d..27ecf04 100644 --- a/src/Jobs/ImportItemJob.php +++ b/src/Jobs/ImportItemJob.php @@ -121,7 +121,7 @@ protected function findOrCreateTerm(array $data): void { $term = Term::query() ->where('taxonomy', $this->import->get('destination.taxonomy')) - ->where('id', $this->import->get('destination.taxonomy') . '::' . Arr::get($data, 'default_slug', $data['slug'])) + ->where('id', $this->import->get('destination.taxonomy').'::'.Arr::get($data, 'default_slug', $data['slug'])) ->first() ?->term(); From cde59f6e9de13cb6e1cc657725b97b4e5e4a9a2c Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 19 Dec 2024 21:48:25 +0000 Subject: [PATCH 20/21] Make the label more user-friendly. --- DOCUMENTATION.md | 2 +- src/Imports/Import.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index e10d822..dfc9508 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). - * When you're importing taxonomy terms into a non-default site (eg. not the first site) and the slugs differ between sites, you can use the "Default Slug" field to specify the slug of the term in the default site. + * When you're importing taxonomy terms into a non-default site (eg. not the _first_ site you created) and the slugs differ between sites, you can map the "Slug in Default Site" field to the slug of the term in the default site in order for the importer to match up the terms correctly. 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! ✨ diff --git a/src/Imports/Import.php b/src/Imports/Import.php index 2b0b7fc..42c26f5 100644 --- a/src/Imports/Import.php +++ b/src/Imports/Import.php @@ -177,7 +177,7 @@ public function mappingFields(): Fields if ($this->get('destination.site') !== $taxonomy->sites()->first()) { $blueprint->ensureField('default_slug', [ 'type' => 'slug', - 'display' => __('Default Slug'), + 'display' => __('Slug in Default Site'), ]); } } From c7bb2ede41b2d3b5126f10dce5382bf792acac5e Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 20 Dec 2024 12:19:51 +0000 Subject: [PATCH 21/21] Add tests. --- tests/Jobs/ImportItemJobTest.php | 177 +++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/tests/Jobs/ImportItemJobTest.php b/tests/Jobs/ImportItemJobTest.php index 44c97c9..097b7fc 100644 --- a/tests/Jobs/ImportItemJobTest.php +++ b/tests/Jobs/ImportItemJobTest.php @@ -398,6 +398,91 @@ public function it_imports_a_new_term() $this->assertEquals('Statamic', $term->get('title')); } + #[Test] + public function it_imports_a_new_term_in_a_multisite_into_the_default_site() + { + $this->setSites([ + 'en' => ['locale' => 'en', 'url' => '/'], + 'fr' => ['locale' => 'fr', 'url' => '/fr/'], + ]); + + Taxonomy::find('tags')->sites(['en', 'fr']); + + $this->assertNull(Term::query()->where('title', 'Statamic')->first()); + + $import = Import::make()->config([ + 'destination' => ['type' => 'terms', 'taxonomy' => 'tags', 'blueprint' => 'tag', 'site' => 'en'], + 'unique_field' => 'title', + 'mappings' => [ + 'title' => ['key' => 'Title'], + 'slug' => ['key' => 'Slug'], + ], + 'strategy' => ['create'], + ]); + + ImportItemJob::dispatch($import, [ + 'Title' => 'Statamic', + 'Slug' => 'statamic', + ]); + + $term = Term::query() + ->where('site', 'en') + ->where('title', 'Statamic') + ->first(); + + $this->assertNotNull($term); + $this->assertEquals('statamic', $term->slug()); + $this->assertEquals('Statamic', $term->get('title')); + $this->assertEquals('en', $term->site()); + } + + #[Test] + public function it_imports_a_new_term_in_a_multisite_into_a_specific_site() + { + $this->setSites([ + 'en' => ['locale' => 'en', 'url' => '/'], + 'fr' => ['locale' => 'fr', 'url' => '/fr/'], + ]); + + Taxonomy::find('tags')->sites(['en', 'fr']); + + $this->assertNull(Term::query()->where('title', 'Statamic')->first()); + + $import = Import::make()->config([ + 'destination' => ['type' => 'terms', 'taxonomy' => 'tags', 'blueprint' => 'tag', 'site' => 'fr'], + 'unique_field' => 'title', + 'mappings' => [ + 'title' => ['key' => 'Title'], + 'slug' => ['key' => 'Slug'], + ], + 'strategy' => ['create'], + ]); + + ImportItemJob::dispatch($import, [ + 'Title' => 'Statamic', + 'Slug' => 'statamic', + ]); + + $term = Term::query() + ->where('site', 'fr') + ->where('title', 'Statamic') + ->first(); + + $this->assertNotNull($term); + + // Both the default site and the chosen site should have the same data + // (because of the way taxonomies work). + $en = $term->in('en'); + $this->assertEquals('statamic', $en->slug()); + $this->assertEquals('Statamic', $en->get('title')); + $this->assertEquals('en', $en->site()); + + $fr = $term->in('fr'); + $this->assertEquals('statamic', $fr->slug()); + $this->assertEquals('Statamic', $fr->get('title')); + $this->assertEquals('fr', $fr->site()); + } + #[Test] public function it_imports_a_new_term_with_a_specific_blueprint() { @@ -490,6 +575,98 @@ public function it_updates_an_existing_term() $this->assertEquals('Baz', $term->get('foo')); } + #[Test] + public function it_updates_an_existing_term_in_a_multisite_with_the_same_slug() + { + $this->setSites([ + 'en' => ['locale' => 'en', 'url' => '/'], + 'fr' => ['locale' => 'fr', 'url' => '/fr/'], + ]); + + $term = Term::make()->taxonomy('tags')->slug('statamic')->set('title', 'Statamic')->set('foo', 'bar'); + $term->save(); + + $import = Import::make()->config([ + 'destination' => ['type' => 'terms', 'taxonomy' => 'tags', 'blueprint' => 'tag', 'site' => 'fr'], + 'unique_field' => 'title', + 'mappings' => [ + 'title' => ['key' => 'Title'], + 'slug' => ['key' => 'Slug'], + 'foo' => ['key' => 'Foo'], + ], + 'strategy' => ['update'], + ]); + + ImportItemJob::dispatch($import, [ + 'Title' => 'Statamic', + 'Slug' => 'statamic', + 'Foo' => 'Baz', + ]); + + $term->fresh(); + + // The importer is updating the French localization of the term, so the English + // localization should stay the same. + $en = $term->in('en'); + $this->assertEquals('statamic', $en->slug()); + $this->assertEquals('Statamic', $en->get('title')); + $this->assertEquals('bar', $en->get('foo')); + $this->assertEquals('en', $en->site()); + + $fr = $term->in('fr'); + $this->assertEquals('statamic', $fr->slug()); + $this->assertEquals('Statamic', $fr->get('title')); + $this->assertEquals('Baz', $fr->get('foo')); + $this->assertEquals('fr', $fr->site()); + } + + #[Test] + public function it_updates_an_existing_term_in_a_multisite_with_the_default_slug_mapping() + { + $this->setSites([ + 'en' => ['locale' => 'en', 'url' => '/'], + 'fr' => ['locale' => 'fr', 'url' => '/fr/'], + ]); + + $term = Term::make()->taxonomy('tags')->slug('statamic')->set('title', 'Statamic')->set('foo', 'bar'); + $term->save(); + + $import = Import::make()->config([ + 'destination' => ['type' => 'terms', 'taxonomy' => 'tags', 'blueprint' => 'tag', 'site' => 'fr'], + 'unique_field' => 'title', + 'mappings' => [ + 'title' => ['key' => 'Title'], + 'slug' => ['key' => 'Slug'], + 'default_slug' => ['key' => 'Default Slug'], + 'foo' => ['key' => 'Foo'], + ], + 'strategy' => ['update'], + ]); + + ImportItemJob::dispatch($import, [ + 'Title' => 'Statique Dynamique', + 'Slug' => 'statique-dynamique', + 'Default Slug' => 'statamic', + 'Foo' => 'Baz', + ]); + + $term->fresh(); + + // The importer is updating the French localization of the term, so the English + // localization should stay the same. + $en = $term->in('en'); + $this->assertEquals('statamic', $en->slug()); + $this->assertEquals('Statamic', $en->get('title')); + $this->assertEquals('bar', $en->get('foo')); + $this->assertEquals('en', $en->site()); + + $fr = $term->in('fr'); + $this->assertEquals('statique-dynamique', $fr->slug()); + $this->assertEquals('Statique Dynamique', $fr->get('title')); + $this->assertEquals('Baz', $fr->get('foo')); + $this->assertEquals('fr', $fr->site()); + } + #[Test] public function it_doesnt_update_an_existing_term_when_updating_is_disabled() {