From 3560affbccb931cdb56aab5eb4aab9b83be619c8 Mon Sep 17 00:00:00 2001 From: Duncan McClean <19637309+duncanmcclean@users.noreply.github.com> Date: Fri, 12 Jan 2024 00:14:20 +0000 Subject: [PATCH] [6.x] Manage blueprints in the Control Panel (#373) * Register blueprints * Add update script to migrate existing blueprints * Move blueprints into physical YAML files * Adjust how blueprints are mocked in tests * Remove blueprint from `runway:resources` command * Update tests * Fix final failing test * Ensure blueprints already exist * Add tests to cover updates to Resource * Fix styling * Move migrate blueprint logic into a command * wip * wip * Update documentation * Fix styling * Fix styling * Fix new tests that are failing due to blueprint changes * Delete old blueprints at the end of the migration script * Specify minimum version of statamic/cms * Run Laravel Pint --------- Co-authored-by: duncanmcclean --- composer.json | 2 +- docs/blueprints.md | 43 +-- docs/images/field-visibility-computed.png | Bin 0 -> 11753 bytes docs/images/runway-blueprints-in-the-cp.png | Bin 0 -> 49193 bytes src/Console/Commands/GenerateBlueprint.php | 15 +- src/Console/Commands/ListResources.php | 3 +- src/Console/Commands/MigrateBlueprints.php | 72 +++++ src/Resource.php | 9 +- src/Runway.php | 27 -- src/ServiceProvider.php | 14 + src/UpdateScripts/MigrateBlueprints.php | 19 ++ .../Commands/GenerateBlueprintTest.php | 8 +- .../Commands/GenerateMigrationTest.php | 296 ++++++------------ tests/Console/Commands/ListResourcesTest.php | 6 +- tests/Fieldtypes/HasManyFieldtypeTest.php | 7 +- .../Controllers/CP/ResourceControllerTest.php | 78 ++--- .../CP/ResourceListingControllerTest.php | 19 -- tests/ResourceTest.php | 69 ++-- tests/Tags/RunwayTagTest.php | 4 + tests/TestCase.php | 5 + tests/__fixtures__/config/runway.php | 125 -------- .../resources/blueprints/.gitignore | 1 + .../blueprints/vendor/runway/author.yaml | 10 + .../blueprints/vendor/runway/post.yaml | 73 +++++ 24 files changed, 416 insertions(+), 489 deletions(-) create mode 100644 docs/images/field-visibility-computed.png create mode 100644 docs/images/runway-blueprints-in-the-cp.png create mode 100644 src/Console/Commands/MigrateBlueprints.php create mode 100644 src/UpdateScripts/MigrateBlueprints.php create mode 100644 tests/__fixtures__/resources/blueprints/.gitignore create mode 100644 tests/__fixtures__/resources/blueprints/vendor/runway/author.yaml create mode 100644 tests/__fixtures__/resources/blueprints/vendor/runway/post.yaml diff --git a/composer.json b/composer.json index 1a64ac9a..18aa8b35 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "php": "^8.1", "doctrine/dbal": "^4.0@RC", "pixelfear/composer-dist-plugin": "^0.1.5", - "statamic/cms": "^4.0" + "statamic/cms": "^4.44" }, "require-dev": { "nunomaduro/collision": "^6.1", diff --git a/docs/blueprints.md b/docs/blueprints.md index 0071c7b6..bcaaee3d 100644 --- a/docs/blueprints.md +++ b/docs/blueprints.md @@ -4,28 +4,13 @@ title: 'Blueprints' As explained in the [Statamic Docs](https://statamic.dev/blueprints#content), Blueprints are a key component to the content modeling process. They let you define the fields that should be available in the Control Panel and the way your data is stored. -### Creating & managing blueprints +## Creating & managing blueprints -Unfortunately, it's not yet possible to manage Runway blueprints in the Control Panel as there's no way for addons to "register" their own blueprints. +Every resource will have it's own blueprint. Just like with collections, you can manage the blueprints in the Control Panel. -In the meantime, you can create a blueprint for a collection, then move the outputted YAML file to the `resources/blueprints` directory. +![Runway blueprints in the Control Panel](/img/runway/runway-blueprints-in-the-cp.png) -:::note Note! -Remember that the field handles in your blueprint should match up exactly with the column names in the database, otherwise bad things will happen. -::: - -Now, to use the blueprint you just created, simply specify it's "namespace" (usually just its filename, minus the `.yaml` extension) as a `blueprint` key in your resources's config array: - -```php -'resources' => [ - \App\Models\Order::class => [ - 'name' => 'Orders', - 'blueprint' => 'order', - ], -], -``` - -If you want to store your resource's blueprint inside a directory, like `resources/blueprints/runway`, you'll need to specify the blueprint as `runway.blueprint_name`. +When configuring fields, make sure that the field handles in your blueprint should match up *exactly* with the column names in the database, otherwise bad things will happen. You'll also want to ensure the database column type matches the fieldtype you're trying to use (see [Supported Fieldtypes](#supported-fieldtypes)). ## Supported Fieldtypes @@ -76,15 +61,7 @@ Float|`float`| ## Nesting fields inside JSON columns -To avoid creating a migration for every new field you add to a blueprint, fields can be stored within a JSON column. To do so, use the `->` symbol within the field handle: - -```yaml -fields: - - - handle: 'values->excerpt' - field: - type: text -``` +To avoid creating a migration for every new field you add to a blueprint, fields can be stored within JSON columns. Simply use `->` within the field handle, like `values->excerpt`. Your table will need to have a suitable column: @@ -161,12 +138,6 @@ public function fullName(): Attribute } ``` -Then, in your user blueprint, you'd define `visibility: computed` in the field's config, like you would normally: +Then, in your user blueprint, you'd set the field's visibility to "Computed": -```yaml -- - handle: full_name - field: - type: text - visibility: computed -``` +![Field's visibility set to computed](/img/runway/field-visibility-computed.png) diff --git a/docs/images/field-visibility-computed.png b/docs/images/field-visibility-computed.png new file mode 100644 index 0000000000000000000000000000000000000000..80b1b2e32d47df7beb0df7bc2d9aaa7ef93420a4 GIT binary patch literal 11753 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 yXWN~cfALFpc^?2Yyhandle()) - ->setContents([ - 'tabs' => [ - 'main' => ['fields' => $mainSection], - 'sidebar' => ['fields' => $sidebarSection], - ], - ]) - ->save(); + $resource->blueprint()->setContents([ + 'tabs' => [ + 'main' => ['fields' => $mainSection], + 'sidebar' => ['fields' => $sidebarSection], + ], + ])->save(); } } diff --git a/src/Console/Commands/ListResources.php b/src/Console/Commands/ListResources.php index 22d93c04..d9c4adef 100644 --- a/src/Console/Commands/ListResources.php +++ b/src/Console/Commands/ListResources.php @@ -47,11 +47,10 @@ public function handle() } $this->table( - ['Handle', 'Model', 'Blueprint', 'Route'], + ['Handle', 'Model', 'Route'], Runway::allResources()->map(fn (Resource $resource) => [ $resource->handle(), $resource->model()::class, - optional($resource->blueprint())->namespace().optional($resource->blueprint())->handle(), $resource->hasRouting() ? $resource->route() : 'N/A', ])->values()->toArray() ); diff --git a/src/Console/Commands/MigrateBlueprints.php b/src/Console/Commands/MigrateBlueprints.php new file mode 100644 index 00000000..66cb2a0d --- /dev/null +++ b/src/Console/Commands/MigrateBlueprints.php @@ -0,0 +1,72 @@ +each(function (Resource $resource) { + $originalBlueprint = $this->resolveOriginalBlueprint($resource); + + $resource->blueprint()->setContents($originalBlueprint->contents())->save(); + + if (File::exists($originalBlueprint->path())) { + File::delete($originalBlueprint->path()); + } + }); + } + + protected function resolveOriginalBlueprint(Resource $resource): FieldsBlueprint + { + if (is_string($resource->config()->get('blueprint'))) { + return Blueprint::find($resource->config()->get('blueprint')); + } + + if (is_array($resource->config()->get('blueprint'))) { + return Blueprint::make()->setHandle($resource->handle())->setContents($resource->config()->get('blueprint')); + } + + throw new \Exception("Could not resolve the original blueprint for the [{$resource->handle()}] resource."); + } +} diff --git a/src/Resource.php b/src/Resource.php index 1fabe42c..388f779f 100644 --- a/src/Resource.php +++ b/src/Resource.php @@ -18,7 +18,6 @@ public function __construct( protected string $handle, protected Model $model, protected string $name, - protected Blueprint $blueprint, protected Collection $config ) { } @@ -50,7 +49,13 @@ public function plural(): string public function blueprint(): Blueprint { - return $this->blueprint; + $blueprint = Blueprint::find("runway::{$this->handle}"); + + if (! $blueprint) { + $blueprint = Blueprint::make($this->handle)->setNamespace('runway')->save(); + } + + return $blueprint; } public function config(): Collection diff --git a/src/Runway.php b/src/Runway.php index a4735e77..8f4af8bb 100644 --- a/src/Runway.php +++ b/src/Runway.php @@ -6,7 +6,6 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\Str; -use Statamic\Fields\Blueprint; class Runway { @@ -16,9 +15,7 @@ public static function discoverResources(): self { static::$resources = collect(config('runway.resources')) ->mapWithKeys(function ($config, $model) { - $blueprint = null; $config = collect($config); - $handle = $config->get('handle', Str::lower(class_basename($model))); throw_if( @@ -26,34 +23,10 @@ public static function discoverResources(): self new \Exception(__('The HasRunwayResource trait is missing from the [:model] model.', ['model' => $model])) ); - throw_if( - ! $config->has('blueprint'), - new \Exception(__('The [:model] model is missing a blueprint.', ['model' => $model])) - ); - - if (is_string($config->get('blueprint'))) { - try { - $blueprint = Blueprint::find($config['blueprint']); - } catch (\Exception $e) { - // If we're running in a console & the blueprint doesn't exist, let's ignore the resource. - // https://github.com/duncanmcclean/runway/pull/320 - if (app()->runningInConsole()) { - return [$handle => null]; - } - - throw $e; - } - } - - if (is_array($config->get('blueprint'))) { - $blueprint = Blueprint::make()->setHandle($handle)->setContents($config['blueprint']); - } - $resource = new Resource( handle: $handle, model: $model instanceof Model ? $model : new $model(), name: $config['name'] ?? Str::title($handle), - blueprint: $blueprint, config: $config, ); diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index eb01a28b..24f6822b 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Route; use Illuminate\Support\Traits\Conditionable; use Statamic\API\Middleware\Cache; +use Statamic\Facades\Blueprint; use Statamic\Facades\CP\Nav; use Statamic\Facades\GraphQL; use Statamic\Facades\Permission; @@ -32,6 +33,7 @@ class ServiceProvider extends AddonServiceProvider Console\Commands\GenerateBlueprint::class, Console\Commands\GenerateMigration::class, Console\Commands\ListResources::class, + Console\Commands\MigrateBlueprints::class, Console\Commands\RebuildUriCache::class, ]; @@ -54,6 +56,7 @@ class ServiceProvider extends AddonServiceProvider protected $updateScripts = [ UpdateScripts\ChangePermissionNames::class, + UpdateScripts\MigrateBlueprints::class, ]; protected $vite = [ @@ -79,6 +82,7 @@ public function boot() ], 'runway-config'); Statamic::booted(function () { + Runway::discoverResources(); $this @@ -86,6 +90,7 @@ public function boot() ->registerPermissions() ->registerPolicies() ->registerNavigation() + ->registerBlueprints() ->registerSearchProvider() ->bootGraphQl() ->bootApi() @@ -154,6 +159,15 @@ protected function registerNavigation(): self return $this; } + protected function registerBlueprints(): self + { + Blueprint::addNamespace('runway', base_path('resources/blueprints/runway')); + + Runway::allResources()->each(fn (Resource $resource) => $resource->blueprint()); + + return $this; + } + protected function bootGraphQl(): self { Runway::allResources() diff --git a/src/UpdateScripts/MigrateBlueprints.php b/src/UpdateScripts/MigrateBlueprints.php new file mode 100644 index 00000000..f3a210e4 --- /dev/null +++ b/src/UpdateScripts/MigrateBlueprints.php @@ -0,0 +1,19 @@ +isUpdatingTo('6.0.0'); + } + + public function update() + { + Artisan::call('runway:migrate-blueprints'); + } +} diff --git a/tests/Console/Commands/GenerateBlueprintTest.php b/tests/Console/Commands/GenerateBlueprintTest.php index 62ff0a1a..f1c3fafa 100644 --- a/tests/Console/Commands/GenerateBlueprintTest.php +++ b/tests/Console/Commands/GenerateBlueprintTest.php @@ -71,8 +71,8 @@ public function can_generate_blueprint() Blueprint::shouldReceive('find')->with('')->andReturnNull(); - Blueprint::shouldReceive('make') - ->with('post') + Blueprint::shouldReceive('find') + ->with('runway::post') ->andReturn(new FieldsBlueprint('post')) ->shouldReceive('setContents') ->with([ @@ -130,8 +130,8 @@ public function can_generate_resource_with_column_that_can_not_be_matched_to_a_f Blueprint::shouldReceive('find')->with('')->andReturnNull(); - Blueprint::shouldReceive('make') - ->with('post') + Blueprint::shouldReceive('find') + ->with('runway::post') ->andReturn(new FieldsBlueprint('post')) ->shouldReceive('setContents') ->with([ diff --git a/tests/Console/Commands/GenerateMigrationTest.php b/tests/Console/Commands/GenerateMigrationTest.php index 57350c63..8508458c 100644 --- a/tests/Console/Commands/GenerateMigrationTest.php +++ b/tests/Console/Commands/GenerateMigrationTest.php @@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Schema; use Spatie\TestTime\TestTime; use SplFileInfo; +use Statamic\Facades\Blueprint; class GenerateMigrationTest extends TestCase { @@ -23,11 +24,13 @@ public function setUp(): void Config::set('runway', [ 'resources' => [ - Food::class => [''], - Drink::class => [''], + Food::class => ['handle' => 'food'], + Drink::class => ['handle' => 'drink'], ], ]); + Runway::discoverResources(); + collect(File::glob(database_path('migrations/*')))->each(function ($path) { File::delete($path); }); @@ -47,38 +50,23 @@ public function can_generate_migrations_for_multiple_resources() { TestTime::freeze(); - Config::set('runway', [ - 'resources' => [ - Food::class => [ - 'name' => 'Food', - 'blueprint' => [ - 'tabs' => [ - 'main' => [ - 'fields' => [ - ['handle' => 'name', 'field' => ['type' => 'text', 'validate' => 'required']], - ['handle' => 'metadata->calories', 'field' => ['type' => 'integer', 'validate' => 'required']], - ], - ], - ], - ], - ], - Drink::class => [ - 'name' => 'Drink', - 'blueprint' => [ - 'tabs' => [ - 'main' => [ - 'fields' => [ - ['handle' => 'name', 'field' => ['type' => 'text', 'validate' => 'required']], - ['handle' => 'metadata->calories', 'field' => ['type' => 'integer', 'validate' => 'required']], - ], - ], - ], - ], - ], - ], + $postBlueprint = Blueprint::find('runway::post'); + $authorBlueprint = Blueprint::find('runway::author'); + + $foodBlueprint = Blueprint::makeFromFields([ + 'name' => ['type' => 'text', 'validate' => 'required'], + 'metadata->calories' => ['type' => 'integer', 'validate' => 'required'], ]); - Runway::discoverResources(); + $drinkBlueprint = Blueprint::makeFromFields([ + 'name' => ['type' => 'text', 'validate' => 'required'], + 'metadata->calories' => ['type' => 'integer', 'validate' => 'required'], + ]); + + Blueprint::shouldReceive('find')->with('runway::post')->andReturn($postBlueprint); + Blueprint::shouldReceive('find')->with('runway::author')->andReturn($authorBlueprint); + Blueprint::shouldReceive('find')->with('runway::food')->andReturn($foodBlueprint); + Blueprint::shouldReceive('find')->with('runway::drink')->andReturn($drinkBlueprint); $this ->artisan('runway:generate-migrations') @@ -112,25 +100,23 @@ public function can_generate_migration_for_single_resource() { TestTime::freeze(); - Config::set('runway', [ - 'resources' => [ - Food::class => [ - 'name' => 'Food', - 'blueprint' => [ - 'tabs' => [ - 'main' => [ - 'fields' => [ - ['handle' => 'name', 'field' => ['type' => 'text', 'validate' => 'required']], - ['handle' => 'metadata->calories', 'field' => ['type' => 'integer', 'validate' => 'required']], - ], - ], - ], - ], - ], - ], + $postBlueprint = Blueprint::find('runway::post'); + $authorBlueprint = Blueprint::find('runway::author'); + + $foodBlueprint = Blueprint::makeFromFields([ + 'name' => ['type' => 'text', 'validate' => 'required'], + 'metadata->calories' => ['type' => 'integer', 'validate' => 'required'], ]); - Runway::discoverResources(); + Blueprint::shouldReceive('find')->with('runway::post')->andReturn($postBlueprint); + Blueprint::shouldReceive('find')->with('runway::author')->andReturn($authorBlueprint); + Blueprint::shouldReceive('find')->with('runway::food')->andReturn($foodBlueprint); + + Schema::shouldReceive('hasTable') + ->with('foods') + ->andReturn(false); + + Schema::shouldReceive('dropIfExists'); $this ->artisan('runway:generate-migrations', [ @@ -155,25 +141,17 @@ public function cant_generate_migration_where_table_already_exists() { TestTime::freeze(); - Config::set('runway', [ - 'resources' => [ - Food::class => [ - 'name' => 'Food', - 'blueprint' => [ - 'tabs' => [ - 'main' => [ - 'fields' => [ - ['handle' => 'name', 'field' => ['type' => 'text', 'validate' => 'required']], - ['handle' => 'metadata->calories', 'field' => ['type' => 'integer', 'validate' => 'required']], - ], - ], - ], - ], - ], - ], + $postBlueprint = Blueprint::find('runway::post'); + $authorBlueprint = Blueprint::find('runway::author'); + + $foodBlueprint = Blueprint::makeFromFields([ + 'name' => ['type' => 'text', 'validate' => 'required'], + 'metadata->calories' => ['type' => 'integer', 'validate' => 'required'], ]); - Runway::discoverResources(); + Blueprint::shouldReceive('find')->with('runway::post')->andReturn($postBlueprint); + Blueprint::shouldReceive('find')->with('runway::author')->andReturn($authorBlueprint); + Blueprint::shouldReceive('find')->with('runway::food')->andReturn($foodBlueprint); Schema::shouldReceive('hasTable') ->with('foods') @@ -197,25 +175,17 @@ public function can_generate_migration_and_run_them_afterwards() { TestTime::freeze(); - Config::set('runway', [ - 'resources' => [ - Food::class => [ - 'name' => 'Food', - 'blueprint' => [ - 'tabs' => [ - 'main' => [ - 'fields' => [ - ['handle' => 'name', 'field' => ['type' => 'text', 'validate' => 'required']], - ['handle' => 'metadata->calories', 'field' => ['type' => 'integer', 'validate' => 'required']], - ], - ], - ], - ], - ], - ], + $postBlueprint = Blueprint::find('runway::post'); + $authorBlueprint = Blueprint::find('runway::author'); + + $foodBlueprint = Blueprint::makeFromFields([ + 'name' => ['type' => 'text', 'validate' => 'required'], + 'metadata->calories' => ['type' => 'integer', 'validate' => 'required'], ]); - Runway::discoverResources(); + Blueprint::shouldReceive('find')->with('runway::post')->andReturn($postBlueprint); + Blueprint::shouldReceive('find')->with('runway::author')->andReturn($authorBlueprint); + Blueprint::shouldReceive('find')->with('runway::food')->andReturn($foodBlueprint); $this->assertFalse(Schema::hasTable('foods')); @@ -236,59 +206,21 @@ public function can_generate_migration_and_ensure_normal_field_is_correct() { TestTime::freeze(); - Config::set('runway', [ - 'resources' => [ - Food::class => [ - 'name' => 'Food', - 'blueprint' => [ - 'tabs' => [ - 'main' => [ - 'fields' => [ - [ - 'handle' => 'ramond_the_array', - 'field' => [ - 'type' => 'array', - ], - ], - [ - 'handle' => 'the_big_red_button', - 'field' => [ - 'type' => 'button_group', - ], - ], - [ - 'handle' => 'floating_away', - 'field' => [ - 'type' => 'float', - ], - ], - [ - 'handle' => 'int_the_ant', - 'field' => [ - 'type' => 'integer', - ], - ], - [ - 'handle' => 'toggle_me_smth', - 'field' => [ - 'type' => 'toggle', - ], - ], - [ - 'handle' => 'author_id', - 'field' => [ - 'type' => 'belongs_to', - ], - ], - ], - ], - ], - ], - ], - ], + $postBlueprint = Blueprint::find('runway::post'); + $authorBlueprint = Blueprint::find('runway::author'); + + $foodBlueprint = Blueprint::makeFromFields([ + 'ramond_the_array' => ['type' => 'array'], + 'the_big_red_button' => ['type' => 'button_group'], + 'floating_away' => ['type' => 'float'], + 'int_the_ant' => ['type' => 'integer'], + 'toggle_me_smth' => ['type' => 'toggle'], + 'author_id' => ['type' => 'belongs_to'], ]); - Runway::discoverResources(); + Blueprint::shouldReceive('find')->with('runway::post')->andReturn($postBlueprint); + Blueprint::shouldReceive('find')->with('runway::author')->andReturn($authorBlueprint); + Blueprint::shouldReceive('find')->with('runway::food')->andReturn($foodBlueprint); $this ->artisan('runway:generate-migrations', [ @@ -318,34 +250,26 @@ public function can_generate_migration_and_ensure_max_items_1_field_is_correct() { TestTime::freeze(); - Config::set('runway', [ - 'resources' => [ - Food::class => [ - 'name' => 'Food', - 'blueprint' => [ - 'tabs' => [ - 'main' => [ - 'fields' => [ - ['handle' => 'assets', 'field' => ['type' => 'assets']], - ['handle' => 'collections', 'field' => ['type' => 'collections']], - ['handle' => 'entries', 'field' => ['type' => 'entries']], - ['handle' => 'terms', 'field' => ['type' => 'terms']], - ['handle' => 'users', 'field' => ['type' => 'users']], - - ['handle' => 'asset', 'field' => ['type' => 'assets', 'max_items' => 1]], - ['handle' => 'collection', 'field' => ['type' => 'collections', 'max_items' => 1]], - ['handle' => 'entry', 'field' => ['type' => 'entries', 'max_items' => 1]], - ['handle' => 'term', 'field' => ['type' => 'terms', 'max_items' => 1]], - ['handle' => 'user', 'field' => ['type' => 'users', 'max_items' => 1]], - ], - ], - ], - ], - ], - ], + $postBlueprint = Blueprint::find('runway::post'); + $authorBlueprint = Blueprint::find('runway::author'); + + $foodBlueprint = Blueprint::makeFromFields([ + 'assets' => ['type' => 'assets'], + 'collections' => ['type' => 'collections'], + 'entries' => ['type' => 'entries'], + 'terms' => ['type' => 'terms'], + 'users' => ['type' => 'users'], + + 'asset' => ['type' => 'assets', 'max_items' => 1], + 'collection' => ['type' => 'collections', 'max_items' => 1], + 'entry' => ['type' => 'entries', 'max_items' => 1], + 'term' => ['type' => 'terms', 'max_items' => 1], + 'user' => ['type' => 'users', 'max_items' => 1], ]); - Runway::discoverResources(); + Blueprint::shouldReceive('find')->with('runway::post')->andReturn($postBlueprint); + Blueprint::shouldReceive('find')->with('runway::author')->andReturn($authorBlueprint); + Blueprint::shouldReceive('find')->with('runway::food')->andReturn($foodBlueprint); $this ->artisan('runway:generate-migrations', [ @@ -380,25 +304,17 @@ public function can_generate_migration_and_ensure_field_is_nullable_if_required_ { TestTime::freeze(); - Config::set('runway', [ - 'resources' => [ - Food::class => [ - 'name' => 'Food', - 'blueprint' => [ - 'tabs' => [ - 'main' => [ - 'fields' => [ - ['handle' => 'text', 'field' => ['type' => 'text']], - ['handle' => 'entries', 'field' => ['type' => 'entries']], - ], - ], - ], - ], - ], - ], + $postBlueprint = Blueprint::find('runway::post'); + $authorBlueprint = Blueprint::find('runway::author'); + + $foodBlueprint = Blueprint::makeFromFields([ + 'text' => ['type' => 'text'], + 'entries' => ['type' => 'entries'], ]); - Runway::discoverResources(); + Blueprint::shouldReceive('find')->with('runway::post')->andReturn($postBlueprint); + Blueprint::shouldReceive('find')->with('runway::author')->andReturn($authorBlueprint); + Blueprint::shouldReceive('find')->with('runway::food')->andReturn($foodBlueprint); $this ->artisan('runway:generate-migrations', [ @@ -423,25 +339,17 @@ public function can_generate_migration_and_ensure_field_is_not_nullable_if_requi { TestTime::freeze(); - Config::set('runway', [ - 'resources' => [ - Food::class => [ - 'name' => 'Food', - 'blueprint' => [ - 'tabs' => [ - 'main' => [ - 'fields' => [ - ['handle' => 'text', 'field' => ['type' => 'text', 'validate' => 'required']], - ['handle' => 'entries', 'field' => ['type' => 'entries', 'validate' => 'required']], - ], - ], - ], - ], - ], - ], + $postBlueprint = Blueprint::find('runway::post'); + $authorBlueprint = Blueprint::find('runway::author'); + + $foodBlueprint = Blueprint::makeFromFields([ + 'text' => ['type' => 'text', 'validate' => 'required'], + 'entries' => ['type' => 'entries', 'validate' => 'required'], ]); - Runway::discoverResources(); + Blueprint::shouldReceive('find')->with('runway::post')->andReturn($postBlueprint); + Blueprint::shouldReceive('find')->with('runway::author')->andReturn($authorBlueprint); + Blueprint::shouldReceive('find')->with('runway::food')->andReturn($foodBlueprint); $this ->artisan('runway:generate-migrations', [ diff --git a/tests/Console/Commands/ListResourcesTest.php b/tests/Console/Commands/ListResourcesTest.php index 4aa07761..1f2a1405 100644 --- a/tests/Console/Commands/ListResourcesTest.php +++ b/tests/Console/Commands/ListResourcesTest.php @@ -16,10 +16,10 @@ public function it_lists_resources() $this ->artisan('runway:resources') ->expectsTable( - ['Handle', 'Model', 'Blueprint', 'Route'], + ['Handle', 'Model', 'Route'], [ - ['post', Post::class, 'post', '/posts/{{ slug }}'], - ['author', Author::class, 'author', 'N/A'], + ['post', Post::class, '/posts/{{ slug }}'], + ['author', Author::class, 'N/A'], ] ); } diff --git a/tests/Fieldtypes/HasManyFieldtypeTest.php b/tests/Fieldtypes/HasManyFieldtypeTest.php index d662d217..b9ea72e1 100644 --- a/tests/Fieldtypes/HasManyFieldtypeTest.php +++ b/tests/Fieldtypes/HasManyFieldtypeTest.php @@ -12,6 +12,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Config; use Statamic\Facades\Blink; +use Statamic\Facades\Blueprint; use Statamic\Fields\Field; use Statamic\Http\Requests\FilteredRequest; @@ -27,7 +28,9 @@ public function setUp(): void { parent::setUp(); - Config::set('runway.resources.DoubleThreeDigital\Runway\Tests\Fixtures\Models\Author.blueprint.sections.main.fields', [ + $postBlueprint = Blueprint::find('runway::post'); + + Blueprint::shouldReceive('find')->with('runway::post')->andReturn($postBlueprint->ensureFieldsInTab([ [ 'handle' => 'name', 'field' => [ @@ -50,7 +53,7 @@ public function setUp(): void 'mode' => 'select', ], ], - ]); + ], 'main')); $this->fieldtype = tap(new HasManyFieldtype()) ->setField(new Field('posts', [ diff --git a/tests/Http/Controllers/CP/ResourceControllerTest.php b/tests/Http/Controllers/CP/ResourceControllerTest.php index 0c18e4e6..99c8b057 100644 --- a/tests/Http/Controllers/CP/ResourceControllerTest.php +++ b/tests/Http/Controllers/CP/ResourceControllerTest.php @@ -6,6 +6,7 @@ use DoubleThreeDigital\Runway\Tests\Fixtures\Models\Author; use DoubleThreeDigital\Runway\Tests\Fixtures\Models\Post; use DoubleThreeDigital\Runway\Tests\TestCase; +use Statamic\Facades\Blueprint; use Statamic\Facades\Config; use Statamic\Facades\User; @@ -276,21 +277,16 @@ public function cant_edit_resource_when_it_does_not_exist() /** @test */ public function can_edit_resource_with_simple_date_field() { - $fields = Config::get('runway.resources.'.Post::class.'.blueprint.sections.main.fields'); - - $fields[] = [ - 'handle' => 'created_at', - 'field' => [ - 'type' => 'date', - 'mode' => 'single', - 'time_enabled' => false, - 'time_required' => false, - ], - ]; - - Config::set('runway.resources.'.Post::class.'.blueprint.sections.main.fields', $fields); + $postBlueprint = Blueprint::find('runway::post'); - Runway::discoverResources(); + Blueprint::shouldReceive('find')->with('user')->andReturn(new \Statamic\Fields\Blueprint); + Blueprint::shouldReceive('find')->with('runway::author')->andReturn(new \Statamic\Fields\Blueprint); + Blueprint::shouldReceive('find')->with('runway::post')->andReturn($postBlueprint->ensureField('created_at', [ + 'type' => 'date', + 'mode' => 'single', + 'time_enabled' => false, + 'time_required' => false, + ])); $user = User::make()->makeSuper()->save(); $post = Post::factory()->create(); @@ -320,22 +316,17 @@ public function can_edit_resource_with_simple_date_field() /** @test */ public function can_edit_resource_with_date_field_with_default_format() { - $fields = Config::get('runway.resources.'.Post::class.'.blueprint.sections.main.fields'); - - $fields[] = [ - 'handle' => 'created_at', - 'field' => [ - 'type' => 'date', - 'mode' => 'single', - 'format' => 'Y-m-d', - 'time_enabled' => false, - 'time_required' => false, - ], - ]; - - Config::set('runway.resources.'.Post::class.'.blueprint.sections.main.fields', $fields); - - Runway::discoverResources(); + $postBlueprint = Blueprint::find('runway::post'); + + Blueprint::shouldReceive('find')->with('user')->andReturn(new \Statamic\Fields\Blueprint); + Blueprint::shouldReceive('find')->with('runway::author')->andReturn(new \Statamic\Fields\Blueprint); + Blueprint::shouldReceive('find')->with('runway::post')->andReturn($postBlueprint->ensureField('created_at', [ + 'type' => 'date', + 'mode' => 'single', + 'format' => 'Y-m-d', + 'time_enabled' => false, + 'time_required' => false, + ])); $post = Post::factory()->create(); $user = User::make()->makeSuper()->save(); @@ -365,22 +356,17 @@ public function can_edit_resource_with_date_field_with_default_format() /** @test */ public function can_edit_resource_with_date_field_with_custom_format() { - $fields = Config::get('runway.resources.'.Post::class.'.blueprint.sections.main.fields'); - - $fields[] = [ - 'handle' => 'created_at', - 'field' => [ - 'type' => 'date', - 'mode' => 'single', - 'format' => 'Y-m-d H:i', - 'time_enabled' => true, - 'time_required' => false, - ], - ]; - - Config::set('runway.resources.'.Post::class.'.blueprint.sections.main.fields', $fields); - - Runway::discoverResources(); + $postBlueprint = Blueprint::find('runway::post'); + + Blueprint::shouldReceive('find')->with('user')->andReturn(new \Statamic\Fields\Blueprint); + Blueprint::shouldReceive('find')->with('runway::author')->andReturn(new \Statamic\Fields\Blueprint); + Blueprint::shouldReceive('find')->with('runway::post')->andReturn($postBlueprint->ensureField('created_at', [ + 'type' => 'date', + 'mode' => 'single', + 'format' => 'Y-m-d H:i', + 'time_enabled' => true, + 'time_required' => false, + ])); $post = Post::factory()->create(); $user = User::make()->makeSuper()->save(); diff --git a/tests/Http/Controllers/CP/ResourceListingControllerTest.php b/tests/Http/Controllers/CP/ResourceListingControllerTest.php index 0e55a1ad..5914bfdf 100644 --- a/tests/Http/Controllers/CP/ResourceListingControllerTest.php +++ b/tests/Http/Controllers/CP/ResourceListingControllerTest.php @@ -107,25 +107,6 @@ public function can_search() /** @test */ public function can_search_records_with_has_many_relationship() { - Config::set('runway.resources.DoubleThreeDigital\Runway\Tests\Fixtures\Models\Author.blueprint.sections.main.fields', [ - [ - 'handle' => 'name', - 'field' => [ - 'type' => 'text', - ], - ], - [ - 'handle' => 'posts', - 'field' => [ - 'type' => 'has_many', - 'resource' => 'post', - 'mode' => 'select', - ], - ], - ]); - - Runway::discoverResources(); - $user = User::make()->makeSuper()->save(); $author = Author::factory()->withPosts()->create(['name' => 'Colin The Caterpillar']); diff --git a/tests/ResourceTest.php b/tests/ResourceTest.php index d20dc0a5..1a949dbc 100644 --- a/tests/ResourceTest.php +++ b/tests/ResourceTest.php @@ -4,6 +4,7 @@ use DoubleThreeDigital\Runway\Runway; use Illuminate\Support\Facades\Config; +use Statamic\Facades\Blueprint; use Statamic\Facades\Fieldset; class ResourceTest extends TestCase @@ -23,21 +24,16 @@ public function can_get_eloquent_relationships_for_belongs_to_field() /** @test */ public function can_get_eloquent_relationships_for_has_many_field() { - $fields = Config::get('runway.resources.DoubleThreeDigital\Runway\Tests\Fixtures\Models\Author.blueprint.sections.main.fields'); + $blueprint = Blueprint::find('runway::author'); - $fields[] = [ - 'handle' => 'posts', - 'field' => [ + Blueprint::shouldReceive('find') + ->with('runway::author') + ->andReturn($blueprint->ensureField('posts', [ 'type' => 'has_many', 'resource' => 'post', 'max_items' => 1, 'mode' => 'default', - ], - ]; - - Config::set('runway.resources.DoubleThreeDigital\Runway\Tests\Fixtures\Models\Author.blueprint.sections.main.fields', $fields); - - Runway::discoverResources(); + ])); $resource = Runway::findResource('author'); @@ -125,6 +121,35 @@ public function can_get_configured_plural() $this->assertEquals($plural, 'Bibliotheken'); } + /** @test */ + public function can_get_blueprint() + { + $resource = Runway::findResource('post'); + + $blueprint = $resource->blueprint(); + + $this->assertTrue($blueprint instanceof \Statamic\Fields\Blueprint); + $this->assertSame('runway', $blueprint->namespace()); + $this->assertSame('post', $blueprint->handle()); + } + + /** @test */ + public function can_create_blueprint_if_one_does_not_exist() + { + $resource = Runway::findResource('post'); + + Blueprint::shouldReceive('find')->with('runway::post')->andReturnNull()->once(); + Blueprint::shouldReceive('find')->with('runway.post')->andReturnNull()->once(); + Blueprint::shouldReceive('make')->with('post')->andReturn((new \Statamic\Fields\Blueprint)->setHandle('post'))->once(); + Blueprint::shouldReceive('save')->andReturnSelf()->once(); + + $blueprint = $resource->blueprint(); + + $this->assertTrue($blueprint instanceof \Statamic\Fields\Blueprint); + $this->assertSame('runway', $blueprint->namespace()); + $this->assertSame('post', $blueprint->handle()); + } + /** @test */ public function can_get_listable_columns() { @@ -135,21 +160,27 @@ public function can_get_listable_columns() ], ])->save(); - Config::set('runway.resources.DoubleThreeDigital\Runway\Tests\Fixtures\Models\Post.blueprint', [ - 'sections' => [ + $blueprint = Blueprint::make()->setContents([ + 'tabs' => [ 'main' => [ - 'fields' => [ - ['handle' => 'title', 'field' => ['type' => 'text', 'listable' => true]], - ['handle' => 'summary', 'field' => ['type' => 'textarea', 'listable' => true]], - ['handle' => 'body', 'field' => ['type' => 'markdown', 'listable' => 'hidden']], - ['handle' => 'thumbnail', 'field' => ['type' => 'assets', 'listable' => false]], - ['import' => 'seo'], + 'sections' => [ + [ + 'fields' => [ + ['handle' => 'title', 'field' => ['type' => 'text', 'listable' => true]], + ['handle' => 'summary', 'field' => ['type' => 'textarea', 'listable' => true]], + ['handle' => 'body', 'field' => ['type' => 'markdown', 'listable' => 'hidden']], + ['handle' => 'thumbnail', 'field' => ['type' => 'assets', 'listable' => false]], + ['import' => 'seo'], + ], + ], ], ], ], ]); - $resource = Runway::discoverResources()->findResource('post'); + Blueprint::shouldReceive('find')->with('runway::post')->andReturn($blueprint); + + $resource = Runway::findResource('post'); $this->assertEquals([ 'title', diff --git a/tests/Tags/RunwayTagTest.php b/tests/Tags/RunwayTagTest.php index 45f4d258..5aba78fd 100644 --- a/tests/Tags/RunwayTagTest.php +++ b/tests/Tags/RunwayTagTest.php @@ -9,6 +9,7 @@ use DoubleThreeDigital\Runway\Tests\TestCase; use Illuminate\Support\Facades\Config; use Statamic\Facades\Antlers; +use Statamic\Facades\Blueprint; use Statamic\Fields\Value; class RunwayTagTest extends TestCase @@ -310,6 +311,9 @@ public function can_get_models_and_non_blueprint_columns_are_returned() /** @test */ public function can_get_models_with_studly_case_resource_handle() { + $postBlueprint = Blueprint::find('runway::post'); + Blueprint::shouldReceive('find')->with('runway::BlogPosts')->andReturn($postBlueprint); + Config::set('runway.resources.'.Post::class.'.handle', 'BlogPosts'); Runway::discoverResources(); diff --git a/tests/TestCase.php b/tests/TestCase.php index eca24dca..b351c006 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -8,6 +8,7 @@ use Orchestra\Testbench\TestCase as OrchestraTestCase; use Rebing\GraphQL\GraphQLServiceProvider; use Statamic\Extend\Manifest; +use Statamic\Facades\Blueprint; use Statamic\Providers\StatamicServiceProvider; use Statamic\Stache\Stores\UsersStore; use Statamic\Statamic; @@ -101,5 +102,9 @@ protected function resolveApplicationConfiguration($app) ]); $app['config']->set('runway', require (__DIR__.'/__fixtures__/config/runway.php')); + + Statamic::booted(function () { + Blueprint::setDirectory(__DIR__.'/__fixtures__/resources/blueprints'); + }); } } diff --git a/tests/__fixtures__/config/runway.php b/tests/__fixtures__/config/runway.php index 01c8ee2b..f729b50f 100644 --- a/tests/__fixtures__/config/runway.php +++ b/tests/__fixtures__/config/runway.php @@ -7,108 +7,6 @@ 'resources' => [ Post::class => [ 'name' => 'Posts', - 'blueprint' => [ - 'sections' => [ - 'main' => [ - 'fields' => [ - [ - 'handle' => 'title', - 'field' => [ - 'type' => 'text', - 'listable' => true, - ], - ], - [ - 'handle' => 'slug', - 'field' => [ - 'type' => 'slug', - ], - ], - [ - 'handle' => 'body', - 'field' => [ - 'type' => 'textarea', - ], - ], - [ - 'handle' => 'values->alt_title', - 'field' => [ - 'type' => 'text', - ], - ], - [ - 'handle' => 'values->alt_body', - 'field' => [ - 'type' => 'markdown', - ], - ], - [ - 'handle' => 'external_links->links', - 'field' => [ - 'type' => 'grid', - 'fields' => [ - [ - 'handle' => 'label', - 'field' => ['type' => 'text'], - ], - [ - 'handle' => 'url', - 'field' => ['type' => 'text'], - ], - ], - ], - ], - [ - 'handle' => 'excerpt', - 'field' => [ - 'type' => 'textarea', - 'read_only' => true, - ], - ], - [ - 'handle' => 'author_id', - 'field' => [ - 'type' => 'belongs_to', - 'resource' => 'author', - 'max_items' => 1, - 'mode' => 'default', - ], - ], - [ - 'handle' => 'age', - 'field' => [ - 'type' => 'integer', - 'visibility' => 'computed', - ], - ], - [ - 'handle' => 'start_date', - 'field' => [ - 'type' => 'date', - 'time_enabled' => true, - 'validate' => [ - 'before:end_date', - ], - ], - ], - [ - 'handle' => 'end_date', - 'field' => [ - 'type' => 'date', - 'time_enabled' => true, - ], - ], - [ - 'handle' => 'dont_save', - 'field' => [ - 'type' => 'text', - 'save' => false, - ], - ], - ], - ], - ], - ], 'listing' => [ 'columns' => [ 'title', @@ -123,29 +21,6 @@ Author::class => [ 'name' => 'Author', - 'blueprint' => [ - 'sections' => [ - 'main' => [ - 'fields' => [ - [ - 'handle' => 'name', - 'field' => [ - 'type' => 'text', - 'listable' => true, - ], - ], - // [ - // 'handle' => 'posts', - // 'field' => [ - // 'type' => 'has_many', - // 'resource' => 'post', - // 'mode' => 'select', - // ], - // ], - ], - ], - ], - ], 'listing' => [ 'columns' => [ 'name', diff --git a/tests/__fixtures__/resources/blueprints/.gitignore b/tests/__fixtures__/resources/blueprints/.gitignore new file mode 100644 index 00000000..5e4176ac --- /dev/null +++ b/tests/__fixtures__/resources/blueprints/.gitignore @@ -0,0 +1 @@ +!vendor diff --git a/tests/__fixtures__/resources/blueprints/vendor/runway/author.yaml b/tests/__fixtures__/resources/blueprints/vendor/runway/author.yaml new file mode 100644 index 00000000..3f6b0038 --- /dev/null +++ b/tests/__fixtures__/resources/blueprints/vendor/runway/author.yaml @@ -0,0 +1,10 @@ +tabs: + main: + sections: + - + fields: + - + handle: name + field: + type: text + listable: true diff --git a/tests/__fixtures__/resources/blueprints/vendor/runway/post.yaml b/tests/__fixtures__/resources/blueprints/vendor/runway/post.yaml new file mode 100644 index 00000000..570936a3 --- /dev/null +++ b/tests/__fixtures__/resources/blueprints/vendor/runway/post.yaml @@ -0,0 +1,73 @@ +tabs: + main: + sections: + - + fields: + - + handle: title + field: + type: text + listable: true + - + handle: slug + field: + type: slug + - + handle: body + field: + type: textarea + - + handle: values->alt_title + field: + type: text + - + handle: values->alt_body + field: + type: markdown + - + handle: external_links->links + field: + type: grid + fields: + - + handle: label + field: + type: text + - + handle: url + field: + type: text + - + handle: excerpt + field: + type: textarea + read_only: true + - + handle: author_id + field: + type: belongs_to + resource: author + max_items: 1 + mode: default + - + handle: age + field: + type: integer + visibility: computed + - + handle: start_date + field: + type: date + time_enabled: true + validate: + - 'before:end_date' + - + handle: end_date + field: + type: date + time_enabled: true + - + handle: dont_save + field: + type: text + save: false