From 54a34a4ecf7399095355714628211da7fae1f5f1 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 20 Dec 2024 12:33:12 +0000 Subject: [PATCH] Multi-site taxonomy support (#67) * Show "Site" dropdown when configuring taxonomy import * Drop "Unique Field" option for term & user imports * Revert "Show "Site" dropdown when configuring taxonomy import" This reverts commit 935043451ce577d62e5babaf52e4bce3bf167c03. * Fix styling * Term. * Swapzies. * Update name of test. * Merge field conditions. * A slug is always going to be here. * Fallback to null. * Update validation to handle situation where it's null. * Tweak test names again. * Fix styling * The slug is now required... update tests. * Fix failing tests. * Fix styling * Revert "Revert "Show "Site" dropdown when configuring taxonomy import"" This reverts commit 8972c0c33e72b8dc765be35b62665e86ffcf9081. * Multi-site taxonomy support * Fix styling * Make the label more user-friendly. * Add tests. --------- Co-authored-by: duncanmcclean --- DOCUMENTATION.md | 1 + lang/en/validation.php | 1 + src/Imports/Blueprint.php | 22 +++- src/Imports/Import.php | 11 ++ src/Jobs/ImportItemJob.php | 25 ++++- tests/Jobs/ImportItemJobTest.php | 177 +++++++++++++++++++++++++++++++ 6 files changed, 229 insertions(+), 8 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index c47632a..dfc9508 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 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/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(); + } } }, ], diff --git a/src/Imports/Import.php b/src/Imports/Import.php index 3cf9cfe..42c26f5 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' => __('Slug in Default Site'), + ]); + } + } + return $blueprint->fields(); } } diff --git a/src/Jobs/ImportItemJob.php b/src/Jobs/ImportItemJob.php index a69549e..27ecf04 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(); } 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() {