diff --git a/lang/en/messages.php b/lang/en/messages.php index 14b6d51..b36b22e 100644 --- a/lang/en/messages.php +++ b/lang/en/messages.php @@ -5,6 +5,7 @@ 'utility_description' => 'Import entries, taxonomies, and users from XML and CSV files.', 'configuration_instructions' => 'You can add or modify your Blueprint fields to customize what data is imported and what fieldtype it will be stored in. You can save, refresh, and come back to this import config later until it\'s ready to run.', + 'destination_blueprint_instructions' => 'Select which blueprint should be used for imported content.', 'destination_collection_instructions' => 'Select the collection to import entries into.', 'destination_site_instructions' => 'Which site should the entries be imported into?', 'destination_taxonomy_instructions' => 'Select the taxonomy to import terms into.', diff --git a/resources/js/components/Fieldtypes/BlueprintFieldtype.vue b/resources/js/components/Fieldtypes/BlueprintFieldtype.vue new file mode 100644 index 0000000..234fc10 --- /dev/null +++ b/resources/js/components/Fieldtypes/BlueprintFieldtype.vue @@ -0,0 +1,63 @@ + + + diff --git a/resources/js/cp.js b/resources/js/cp.js index 53398ad..9ea7dce 100644 --- a/resources/js/cp.js +++ b/resources/js/cp.js @@ -1,9 +1,11 @@ import CreateImportForm from "./components/CreateImportForm.vue"; import EditImportForm from "./components/EditImportForm.vue"; import ImportsListing from "./components/ImportsListing.vue"; +import BlueprintFieldtype from "./components/Fieldtypes/BlueprintFieldtype.vue"; import ImportMappingsFieldtype from "./components/Fieldtypes/ImportMappingsFieldtype.vue"; Statamic.$components.register('create-import-form', CreateImportForm); Statamic.$components.register('edit-import-form', EditImportForm); Statamic.$components.register('imports-listing', ImportsListing); +Statamic.$components.register('blueprint-fieldtype', BlueprintFieldtype); Statamic.$components.register('import_mappings-fieldtype', ImportMappingsFieldtype); diff --git a/src/Fieldtypes/BlueprintFieldtype.php b/src/Fieldtypes/BlueprintFieldtype.php new file mode 100644 index 0000000..d0063df --- /dev/null +++ b/src/Fieldtypes/BlueprintFieldtype.php @@ -0,0 +1,22 @@ + Collection::all()->mapWithKeys(function ($collection) { + return [$collection->handle() => $collection->entryBlueprints()->values()]; + })->all(), + 'taxonomyBlueprints' => Taxonomy::all()->mapWithKeys(function ($taxonomy) { + return [$taxonomy->handle() => $taxonomy->termBlueprints()->values()]; + })->all(), + ]; + } +} diff --git a/src/Imports/Blueprint.php b/src/Imports/Blueprint.php index 72dd292..741e85a 100644 --- a/src/Imports/Blueprint.php +++ b/src/Imports/Blueprint.php @@ -120,6 +120,17 @@ function (string $attribute, mixed $value, Closure $fail) use ($import) { 'validate' => 'required_if:destination.type,terms', ], ], + [ + 'handle' => 'blueprint', + 'field' => [ + 'type' => 'blueprint', + 'display' => __('Blueprint'), + 'instructions' => __('importer::messages.destination_blueprint_instructions'), + 'width' => 50, + 'unless' => ['destination.type' => 'users'], + 'validate' => 'required_unless:destination.type,users', + ], + ], Site::hasMultiple() ? [ 'handle' => 'site', 'field' => [ @@ -255,6 +266,10 @@ private static function buildFieldConditions(Import $import): array $conditions['destination.taxonomy'] = 'contains '.$import->get('destination.taxonomy'); } + if ($import->get('destination.blueprint')) { + $conditions['destination.blueprint'] = 'equals '.$import->get('destination.blueprint'); + } + if ($import->get('destination.site')) { $conditions['destination.site'] = 'contains '.$import->get('destination.site'); } diff --git a/src/Imports/Import.php b/src/Imports/Import.php index 61fdcb4..3cf9cfe 100644 --- a/src/Imports/Import.php +++ b/src/Imports/Import.php @@ -137,8 +137,20 @@ public function blueprint(): StatamicBlueprint public function destinationBlueprint(): StatamicBlueprint { return match ($this->get('destination.type')) { - 'entries' => Collection::find($this->get('destination.collection'))->entryBlueprint(), - 'terms' => Taxonomy::find($this->get('destination.taxonomy'))->termBlueprint(), + 'entries' => Collection::find($this->get('destination.collection')) + ->entryBlueprints() + ->when( + $this->get('destination.blueprint'), + fn ($collection) => $collection->filter(fn ($blueprint) => $blueprint->handle() === $this->get('destination.blueprint')) + ) + ->first(), + 'terms' => Taxonomy::find($this->get('destination.taxonomy')) + ->termBlueprints() + ->when( + $this->get('destination.blueprint'), + fn ($taxonomy) => $taxonomy->filter(fn ($blueprint) => $blueprint->handle() === $this->get('destination.blueprint')) + ) + ->first(), 'users' => User::blueprint(), }; } diff --git a/src/Jobs/ImportItemJob.php b/src/Jobs/ImportItemJob.php index d7283d3..c2110a9 100644 --- a/src/Jobs/ImportItemJob.php +++ b/src/Jobs/ImportItemJob.php @@ -72,7 +72,10 @@ protected function findOrCreateEntry(array $data): void return; } - $entry = Entry::make()->collection($collection)->locale($site); + $entry = Entry::make() + ->collection($collection) + ->blueprint($this->import->get('destination.blueprint')) + ->locale($site); } if ($entry->id() && ! in_array('update', $this->import->get('strategy'))) { @@ -126,7 +129,9 @@ protected function findOrCreateTerm(array $data): void return; } - $term = Term::make()->taxonomy($this->import->get('destination.taxonomy')); + $term = Term::make() + ->taxonomy($this->import->get('destination.taxonomy')) + ->blueprint($this->import->get('destination.blueprint')); } if (Term::find($term->id()) && ! in_array('update', $this->import->get('strategy'))) { diff --git a/tests/Imports/StoreImportTest.php b/tests/Imports/StoreImportTest.php index ec8e28f..7bf832a 100644 --- a/tests/Imports/StoreImportTest.php +++ b/tests/Imports/StoreImportTest.php @@ -40,6 +40,7 @@ public function it_stores_a_collection_import() 'destination' => [ 'type' => 'entries', 'collection' => ['posts'], + 'blueprint' => 'post', ], 'strategy' => ['create', 'update'], ]) @@ -75,6 +76,7 @@ public function it_stores_a_collection_import_with_a_site() 'destination' => [ 'type' => 'entries', 'collection' => ['posts'], + 'blueprint' => 'post', 'site' => ['en'], ], 'strategy' => ['create', 'update'], @@ -113,6 +115,7 @@ public function it_cant_store_a_collection_import_when_the_collection_is_not_ava 'destination' => [ 'type' => 'entries', 'collection' => ['posts'], + 'blueprint' => 'post', 'site' => ['fr'], ], 'strategy' => ['create', 'update'], @@ -143,6 +146,7 @@ public function it_cant_store_a_collection_import_without_a_site_when_multisite_ 'destination' => [ 'type' => 'entries', 'collection' => ['posts'], + 'blueprint' => 'post', ], 'strategy' => ['create', 'update'], ]) @@ -167,6 +171,7 @@ public function it_stores_a_taxonomy_import() 'destination' => [ 'type' => 'terms', 'taxonomy' => ['categories'], + 'blueprint' => 'post', ], 'strategy' => ['create', 'update'], ]) diff --git a/tests/Imports/UpdateImportTest.php b/tests/Imports/UpdateImportTest.php index 303f72b..e208079 100644 --- a/tests/Imports/UpdateImportTest.php +++ b/tests/Imports/UpdateImportTest.php @@ -63,7 +63,7 @@ public function can_update_an_import() ->patch("/cp/utilities/importer/{$this->import->id()}", [ 'name' => 'Old Posts', 'file' => ['posts.csv'], - 'destination' => ['type' => 'entries', 'collection' => ['posts']], + 'destination' => ['type' => 'entries', 'collection' => ['posts'], 'blueprint' => 'post'], 'strategy' => ['create', 'update'], 'source' => ['csv_delimiter' => ','], 'mappings' => [ @@ -95,7 +95,7 @@ public function can_replace_the_file() ->patch("/cp/utilities/importer/{$this->import->id()}", [ 'name' => 'Posts', 'file' => ['123456789/latest-posts.csv'], - 'destination' => ['type' => 'entries', 'collection' => ['posts']], + 'destination' => ['type' => 'entries', 'collection' => ['posts'], 'blueprint' => 'post'], 'strategy' => ['create', 'update'], 'source' => ['csv_delimiter' => ','], 'mappings' => [ @@ -124,7 +124,7 @@ public function validation_error_is_thrown_when_file_does_not_exist() ->patch("/cp/utilities/importer/{$this->import->id()}", [ 'name' => 'Posts', 'file' => ['123456789/latest-posts.pdf'], - 'destination' => ['type' => 'entries', 'collection' => ['posts']], + 'destination' => ['type' => 'entries', 'collection' => ['posts'], 'blueprint' => 'post'], 'strategy' => ['create', 'update'], 'mappings' => [ 'title' => ['key' => 'Title'], @@ -155,7 +155,7 @@ public function validation_error_is_thrown_when_file_mime_type_is_not_allowed() ->patch("/cp/utilities/importer/{$this->import->id()}", [ 'name' => 'Posts', 'file' => ['123456789/latest-posts.pdf'], - 'destination' => ['type' => 'entries', 'collection' => ['posts']], + 'destination' => ['type' => 'entries', 'collection' => ['posts'], 'blueprint' => 'post'], 'strategy' => ['create', 'update'], 'mappings' => [ 'title' => ['key' => 'Title'], @@ -183,7 +183,7 @@ public function validation_error_is_thrown_without_an_import_strategy() ->patch("/cp/utilities/importer/{$this->import->id()}", [ 'name' => 'Posts', 'file' => ['posts.csv'], - 'destination' => ['type' => 'entries', 'collection' => ['posts']], + 'destination' => ['type' => 'entries', 'collection' => ['posts'], 'blueprint' => 'post'], 'strategy' => [], 'mappings' => [ 'title' => ['key' => 'Title'], @@ -209,7 +209,7 @@ public function validation_error_is_thrown_without_any_mappings() ->patch("/cp/utilities/importer/{$this->import->id()}", [ 'name' => 'Posts', 'file' => ['posts.csv'], - 'destination' => ['type' => 'entries', 'collection' => ['posts']], + 'destination' => ['type' => 'entries', 'collection' => ['posts'], 'blueprint' => 'post'], 'strategy' => ['create', 'update'], 'mappings' => [ 'title' => ['key' => null], @@ -231,7 +231,7 @@ public function throws_validation_errors_for_mapping_fields() ->patch("/cp/utilities/importer/{$this->import->id()}", [ 'name' => 'Posts', 'file' => ['posts.csv'], - 'destination' => ['type' => 'entries', 'collection' => ['posts']], + 'destination' => ['type' => 'entries', 'collection' => ['posts'], 'blueprint' => 'post'], 'strategy' => ['create', 'update'], 'mappings' => [ 'author' => ['key' => 'Author Email', 'related_field' => null], @@ -249,7 +249,7 @@ public function validation_error_is_thrown_without_unique_field() ->patch("/cp/utilities/importer/{$this->import->id()}", [ 'name' => 'Posts', 'file' => ['posts.csv'], - 'destination' => ['type' => 'entries', 'collection' => ['posts']], + 'destination' => ['type' => 'entries', 'collection' => ['posts'], 'blueprint' => 'post'], 'strategy' => ['create', 'update'], 'mappings' => [ 'title' => ['key' => 'Title'], @@ -271,7 +271,7 @@ public function validation_error_is_thrown_when_no_mapping_is_configured_for_uni ->patch("/cp/utilities/importer/{$this->import->id()}", [ 'name' => 'Posts', 'file' => ['posts.csv'], - 'destination' => ['type' => 'entries', 'collection' => ['posts']], + 'destination' => ['type' => 'entries', 'collection' => ['posts'], 'blueprint' => 'post'], 'strategy' => ['create', 'update'], 'mappings' => [ 'title' => ['key' => 'Title'], diff --git a/tests/Jobs/ImportItemJobTest.php b/tests/Jobs/ImportItemJobTest.php index e8b2489..06223cd 100644 --- a/tests/Jobs/ImportItemJobTest.php +++ b/tests/Jobs/ImportItemJobTest.php @@ -3,6 +3,8 @@ namespace Statamic\Importer\Tests\Jobs; use PHPUnit\Framework\Attributes\Test; +use Statamic\Facades\Blink; +use Statamic\Facades\Blueprint; use Statamic\Facades\Collection; use Statamic\Facades\Entry; use Statamic\Facades\Taxonomy; @@ -68,7 +70,7 @@ public function it_imports_a_new_entry() $this->assertNull(Entry::query()->where('email', 'john.doe@example.com')->first()); $import = Import::make()->config([ - 'destination' => ['type' => 'entries', 'collection' => 'team'], + 'destination' => ['type' => 'entries', 'collection' => 'team', 'blueprint' => 'team'], 'unique_field' => 'email', 'mappings' => [ 'first_name' => ['key' => 'First Name'], @@ -108,7 +110,7 @@ public function it_imports_a_new_entry_in_a_multisite() $this->assertNull(Entry::query()->where('email', 'john.doe@example.com')->first()); $import = Import::make()->config([ - 'destination' => ['type' => 'entries', 'collection' => 'team', 'site' => 'fr'], + 'destination' => ['type' => 'entries', 'collection' => 'team', 'blueprint' => 'team', 'site' => 'fr'], 'unique_field' => 'email', 'mappings' => [ 'first_name' => ['key' => 'First Name'], @@ -136,13 +138,54 @@ public function it_imports_a_new_entry_in_a_multisite() $this->assertEquals('fr', $entry->site()); } + #[Test] + public function it_imports_a_new_entry_with_a_specific_blueprint() + { + Blueprint::make('volunteers')->setNamespace('collections/team')->setContents([ + 'sections' => [ + 'main' => [ + 'fields' => [ + ['handle' => 'first_name', 'field' => ['type' => 'text']], + ['handle' => 'last_name', 'field' => ['type' => 'text']], + ], + ], + ], + ])->save(); + + Blink::forget('collection-entry-blueprints-team'); + + $this->assertNull(Entry::query()->where('email', 'John')->first()); + + $import = Import::make()->config([ + 'destination' => ['type' => 'entries', 'collection' => 'team', 'blueprint' => 'volunteers'], + 'unique_field' => 'last_name', + 'mappings' => [ + 'first_name' => ['key' => 'First Name'], + 'last_name' => ['key' => 'Last Name'], + ], + 'strategy' => ['create'], + ]); + + ImportItemJob::dispatch($import, [ + 'First Name' => 'John', + 'Last Name' => 'Doe', + ]); + + $entry = Entry::query()->where('first_name', 'John')->first(); + + $this->assertNotNull($entry); + $this->assertEquals('John', $entry->get('first_name')); + $this->assertEquals('Doe', $entry->get('last_name')); + $this->assertEquals('volunteers', $entry->blueprint()->handle()); + } + #[Test] public function it_doesnt_import_a_new_entry_when_creation_is_disabled() { $this->assertNull(Entry::query()->where('email', 'john.doe@example.com')->first()); $import = Import::make()->config([ - 'destination' => ['type' => 'entries', 'collection' => 'team'], + 'destination' => ['type' => 'entries', 'collection' => 'team', 'blueprint' => 'team'], 'unique_field' => 'email', 'mappings' => [ 'first_name' => ['key' => 'First Name'], @@ -172,7 +215,7 @@ public function it_updates_an_existing_entry() $entry->save(); $import = Import::make()->config([ - 'destination' => ['type' => 'entries', 'collection' => 'team'], + 'destination' => ['type' => 'entries', 'collection' => 'team', 'blueprint' => 'team'], 'unique_field' => 'email', 'mappings' => [ 'first_name' => ['key' => 'First Name'], @@ -213,7 +256,7 @@ public function it_updates_an_existing_entry_when_entry_is_in_the_same_site() $entry->save(); $import = Import::make()->config([ - 'destination' => ['type' => 'entries', 'collection' => 'team', 'site' => 'fr'], + 'destination' => ['type' => 'entries', 'collection' => 'team', 'blueprint' => 'team', 'site' => 'fr'], 'unique_field' => 'email', 'mappings' => [ 'first_name' => ['key' => 'First Name'], @@ -255,7 +298,7 @@ public function it_doesnt_update_an_existing_entry_when_entry_is_in_a_different_ $entry->save(); $import = Import::make()->config([ - 'destination' => ['type' => 'entries', 'collection' => 'team', 'site' => 'fr'], + 'destination' => ['type' => 'entries', 'collection' => 'team', 'blueprint' => 'team', 'site' => 'fr'], 'unique_field' => 'email', 'mappings' => [ 'first_name' => ['key' => 'First Name'], @@ -301,7 +344,7 @@ public function it_doesnt_update_an_existing_entry_when_updating_is_disabled() $entry->save(); $import = Import::make()->config([ - 'destination' => ['type' => 'entries', 'collection' => 'team'], + 'destination' => ['type' => 'entries', 'collection' => 'team', 'blueprint' => 'team'], 'unique_field' => 'email', 'mappings' => [ 'first_name' => ['key' => 'First Name'], @@ -334,7 +377,42 @@ public function it_imports_a_new_term() $this->assertNull(Term::query()->where('title', 'Statamic')->first()); $import = Import::make()->config([ - 'destination' => ['type' => 'terms', 'taxonomy' => 'tags'], + 'destination' => ['type' => 'terms', 'taxonomy' => 'tags', 'blueprint' => 'tag'], + 'unique_field' => 'title', + 'mappings' => [ + 'title' => ['key' => 'Title'], + ], + 'strategy' => ['create'], + ]); + + ImportItemJob::dispatch($import, [ + 'Title' => 'Statamic', + ]); + + $term = Term::query()->where('title', 'Statamic')->first(); + + $this->assertNotNull($term); + $this->assertEquals('statamic', $term->slug()); + $this->assertEquals('Statamic', $term->get('title')); + } + + #[Test] + public function it_imports_a_new_term_with_a_specific_blueprint() + { + Blueprint::make('special_tag')->setNamespace('taxonomies/tags')->setContents([ + 'sections' => [ + 'main' => [ + 'fields' => [ + ['handle' => 'title', 'field' => ['type' => 'text']], + ], + ], + ], + ])->save(); + + $this->assertNull(Term::query()->where('title', 'Statamic')->first()); + + $import = Import::make()->config([ + 'destination' => ['type' => 'terms', 'taxonomy' => 'tags', 'blueprint' => 'special_tag'], 'unique_field' => 'title', 'mappings' => [ 'title' => ['key' => 'Title'], @@ -351,6 +429,7 @@ public function it_imports_a_new_term() $this->assertNotNull($term); $this->assertEquals('statamic', $term->slug()); $this->assertEquals('Statamic', $term->get('title')); + $this->assertEquals('special_tag', $term->blueprint()->handle()); } #[Test] @@ -359,7 +438,7 @@ public function it_doesnt_import_a_new_term_when_creation_is_disabled() $this->assertNull(Term::query()->where('title', 'Statamic')->first()); $import = Import::make()->config([ - 'destination' => ['type' => 'terms', 'taxonomy' => 'tags'], + 'destination' => ['type' => 'terms', 'taxonomy' => 'tags', 'blueprint' => 'tag'], 'unique_field' => 'title', 'mappings' => [ 'title' => ['key' => 'Title'], @@ -381,7 +460,7 @@ public function it_updates_an_existing_term() $term->save(); $import = Import::make()->config([ - 'destination' => ['type' => 'terms', 'taxonomy' => 'tags'], + 'destination' => ['type' => 'terms', 'taxonomy' => 'tags', 'blueprint' => 'tag'], 'unique_field' => 'title', 'mappings' => [ 'title' => ['key' => 'Title'], @@ -410,7 +489,7 @@ public function it_doesnt_update_an_existing_term_when_updating_is_disabled() $term->save(); $import = Import::make()->config([ - 'destination' => ['type' => 'terms', 'taxonomy' => 'tags'], + 'destination' => ['type' => 'terms', 'taxonomy' => 'tags', 'blueprint' => 'tag'], 'unique_field' => 'title', 'mappings' => [ 'title' => ['key' => 'Title'],