Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi-site taxonomy support #67

Merged
merged 25 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9350434
Show "Site" dropdown when configuring taxonomy import
duncanmcclean Dec 19, 2024
c826e88
Drop "Unique Field" option for term & user imports
duncanmcclean Dec 19, 2024
8972c0c
Revert "Show "Site" dropdown when configuring taxonomy import"
duncanmcclean Dec 19, 2024
98a8b8a
Fix styling
duncanmcclean Dec 19, 2024
4ed8f7a
Term.
duncanmcclean Dec 19, 2024
b484a2a
Swapzies.
duncanmcclean Dec 19, 2024
2bdef79
Update name of test.
duncanmcclean Dec 19, 2024
a89004e
Merge branch 'unique-field-changes' of github.com:statamic/importer i…
duncanmcclean Dec 19, 2024
0b5b84c
Merge field conditions.
duncanmcclean Dec 19, 2024
54639d3
A slug is always going to be here.
duncanmcclean Dec 19, 2024
58b7f97
Fallback to null.
duncanmcclean Dec 19, 2024
04867ca
Update validation to handle situation where it's null.
duncanmcclean Dec 19, 2024
7ef5326
Tweak test names again.
duncanmcclean Dec 19, 2024
72bd7fe
Fix styling
duncanmcclean Dec 19, 2024
17a37f1
The slug is now required... update tests.
duncanmcclean Dec 19, 2024
eb4fc89
Fix failing tests.
duncanmcclean Dec 19, 2024
f1be20f
Fix styling
duncanmcclean Dec 19, 2024
acd257f
Merge remote-tracking branch 'origin/main' into unique-field-changes
duncanmcclean Dec 19, 2024
006cfd6
Revert "Revert "Show "Site" dropdown when configuring taxonomy import""
duncanmcclean Dec 19, 2024
7a83e87
Multi-site taxonomy support
duncanmcclean Dec 19, 2024
a5c39ac
Fix styling
duncanmcclean Dec 19, 2024
cde59f6
Make the label more user-friendly.
duncanmcclean Dec 19, 2024
52b7543
Merge branch 'multisite-taxonomies' of github.com:statamic/importer i…
duncanmcclean Dec 19, 2024
a456e97
Merge remote-tracking branch 'origin/main' into multisite-taxonomies
duncanmcclean Dec 20, 2024
c7bb2ed
Add tests.
duncanmcclean Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading