Skip to content

Commit

Permalink
Multi-site taxonomy support (#67)
Browse files Browse the repository at this point in the history
* 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 9350434.

* 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 8972c0c.

* Multi-site taxonomy support

* Fix styling

* Make the label more user-friendly.

* Add tests.

---------

Co-authored-by: duncanmcclean <[email protected]>
  • Loading branch information
duncanmcclean and duncanmcclean authored Dec 20, 2024
1 parent 046f219 commit 54a34a4
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 8 deletions.
1 change: 1 addition & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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! ✨

Expand Down
1 change: 1 addition & 0 deletions lang/en/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
];
22 changes: 17 additions & 5 deletions src/Imports/Blueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
},
],
Expand Down
11 changes: 11 additions & 0 deletions src/Imports/Import.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
25 changes: 22 additions & 3 deletions src/Jobs/ImportItemJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'))) {
Expand All @@ -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();
}

Expand Down
177 changes: 177 additions & 0 deletions tests/Jobs/ImportItemJobTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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()
{
Expand Down

0 comments on commit 54a34a4

Please sign in to comment.