diff --git a/packages/framework/src/Console/Commands/MakePublicationTypeCommand.php b/packages/framework/src/Console/Commands/MakePublicationTypeCommand.php index a006042690e..a8476e27b4a 100644 --- a/packages/framework/src/Console/Commands/MakePublicationTypeCommand.php +++ b/packages/framework/src/Console/Commands/MakePublicationTypeCommand.php @@ -4,19 +4,18 @@ namespace Hyde\Console\Commands; -use function array_flip; use function array_keys; -use function array_merge; -use function file_exists; use Hyde\Console\Concerns\ValidatingCommand; use Hyde\Framework\Actions\CreatesNewPublicationType; use Hyde\Framework\Features\Publications\Models\PublicationField; use Hyde\Framework\Features\Publications\PublicationFieldTypes; -use Hyde\Framework\Features\Publications\PublicationService; +use Hyde\Hyde; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use function in_array; use InvalidArgumentException; use function is_dir; +use function is_file; use LaravelZero\Framework\Commands\Command; use function scandir; use function strtolower; @@ -32,36 +31,29 @@ class MakePublicationTypeCommand extends ValidatingCommand { /** @var string */ protected $signature = 'make:publicationType - {title? : The name of the Publication Type to create. Will be used to generate the storage directory}'; + {name? : The name of the publication type to create} + {--use-defaults : Select the default options wherever possible}'; /** @var string */ protected $description = 'Create a new publication type definition'; + protected Collection $fields; + public function safeHandle(): int { $this->title('Creating a new Publication Type!'); - $title = $this->argument('title'); - if (! $title) { - $title = trim($this->askWithValidation('name', 'Publication type name', ['required', 'string'])); - $dirname = Str::slug($title); - if (file_exists($dirname) && is_dir($dirname) && count(scandir($dirname)) > 2) { - throw new InvalidArgumentException("Storage path [$dirname] already exists"); - } - } - - $fields = $this->captureFieldsDefinitions(); + $title = $this->getTitle(); - $sortField = $this->getSortField($fields); + $this->validateStorageDirectory(Str::slug($title)); - $sortAscending = $this->getSortDirection(); + $this->fields = $this->captureFieldsDefinitions(); - $pageSize = $this->getPageSize(); - $prevNextLinks = $this->getPrevNextLinks(); + [$sortField, $sortAscending, $prevNextLinks, $pageSize] = ($this->getPaginationSettings()); - $canonicalField = $this->getCanonicalField($fields); + $canonicalField = $this->getCanonicalField(); - $creator = new CreatesNewPublicationType($title, $fields, $canonicalField, $sortField, $sortAscending, $prevNextLinks, $pageSize, $this->output); + $creator = new CreatesNewPublicationType($title, $this->fields, $canonicalField->name, $sortField, $sortAscending, $prevNextLinks, $pageSize, $this->output); $creator->create(); $this->info('Publication type created successfully!'); @@ -69,142 +61,148 @@ public function safeHandle(): int return Command::SUCCESS; } + protected function getTitle(): string + { + return $this->argument('name') ?: trim($this->askWithValidation('name', 'Publication type name', ['required', 'string'])); + } + + protected function validateStorageDirectory(string $directoryName): void + { + if (is_file(Hyde::path($directoryName)) || (is_dir(Hyde::path($directoryName)) && (count(scandir($directoryName)) > 2))) { + throw new InvalidArgumentException("Storage path [$directoryName] already exists"); + } + } + protected function captureFieldsDefinitions(): Collection { - $this->output->writeln('You now need to define the fields in your publication type:'); - $count = 1; - $fields = Collection::make(); - do { - $this->line(''); - $this->output->writeln("Field #$count:"); + $this->line('You now need to define the fields in your publication type:'); + $this->fields = Collection::make(); - $fieldData = []; - do { - $fieldData['name'] = Str::kebab(trim($this->askWithValidation('name', 'Field name', ['required']))); - $duplicate = $this->checkIfFieldIsDuplicate($fields, $fieldData['name']); - } while ($duplicate); + $this->addCreatedAtMetaField(); - $type = $this->getFieldType(); + do { + $this->fields->add($this->captureFieldDefinition()); - if ($type === 10) { - $fieldData = $this->getFieldDataForTag($fieldData); + if ($this->option('use-defaults') === true) { + $addAnother = false; + } else { + $addAnother = $this->confirm("Field #{$this->getCount(-1)} added! Add another field?"); } - $addAnother = $this->askWithValidation('addAnother', 'Add another field (y/n)', ['required', 'string', 'in:y,n'], 'n'); + } while ($addAnother); - // map field choice to actual field type - $fieldData['type'] = PublicationFieldTypes::values()[$type - 1]; + return $this->fields; + } + + protected function captureFieldDefinition(): PublicationField + { + $this->line(''); + + $fieldName = $this->getFieldName(); + + $fieldType = $this->getFieldType(); + + if ($fieldType === PublicationFieldTypes::Tag) { + $this->comment('Tip: Hyde will look for tags matching the name of the publication!'); + } - $fields->add(PublicationField::fromArray($fieldData)); - $count++; - } while (strtolower($addAnother) !== 'n'); + // TODO: Here we could collect other data like the "rules" array for the field. - return $fields; + return new PublicationField($fieldType, $fieldName); } - protected function getFieldType(): int + protected function getFieldName(?string $message = null): string { - $options = PublicationFieldTypes::cases(); - foreach ($options as $key => $value) { - $options[$key] = $value->name; + $selected = Str::kebab(trim($this->askWithValidation('name', $message ?? "Enter name for field #{$this->getCount()}", ['required']))); + + if ($this->checkIfFieldIsDuplicate($selected)) { + return $this->getFieldName("Try again: Enter name for field #{$this->getCount()}"); } - $options[4] = 'Datetime (YYYY-MM-DD (HH:MM:SS))'; - $options[5] = 'URL'; - $options[8] = 'Local Image'; - $options[9] = 'Tag (select value from list)'; - return (int) $this->choice('Field type', $options, 1) + 1; + return $selected; } - protected function getSortField(Collection $fields): string + protected function getFieldType(): PublicationFieldTypes { - $options = array_merge(['dateCreated (meta field)'], $fields->pluck('name')->toArray()); + $options = PublicationFieldTypes::names(); - $selected = $this->choice('Choose the default field you wish to sort by', $options, 'dateCreated (meta field)'); + $choice = $this->choice("Enter type for field #{$this->getCount()}", $options, 'String'); - return $selected === 'dateCreated (meta field)' ? '__createdAt' : $options[(array_flip($options)[$selected])]; + return PublicationFieldTypes::from(strtolower($choice)); } - protected function getSortDirection(): bool + protected function getCanonicalField(): PublicationField { - $options = [ - 'Ascending (oldest items first if sorting by dateCreated)' => true, - 'Descending (newest items first if sorting by dateCreated)' => false, - ]; + $selectableFields = $this->fields->reject(function (PublicationField $field): bool { + return in_array($field, PublicationFieldTypes::canonicable()); + }); + + if ($this->option('use-defaults')) { + return $selectableFields->first(); + } + + $options = $selectableFields->pluck('name'); + + $selected = $this->choice('Choose a canonical name field (this will be used to generate filenames, so the values need to be unique)', + $options->toArray(), + $options->first() + ); - return $options[$this->choice('Choose the default sort direction', array_keys($options), 'Ascending (oldest items first if sorting by dateCreated)')]; + return $this->fields->firstWhere('name', $selected); } - protected function getPageSize(): int + protected function checkIfFieldIsDuplicate($name): bool { - return (int) $this->askWithValidation( - 'pageSize', - 'Enter the pageSize (0 for no limit)', - ['required', 'integer', 'between:0,100'], - 25 - ); + if ($this->fields->where('name', $name)->count() > 0) { + $this->error("Field name [$name] already exists!"); + + return true; + } + + return false; } - protected function getPrevNextLinks(): bool + protected function addCreatedAtMetaField(): void { - return (bool) $this->askWithValidation( - 'prevNextLinks', - 'Generate previous/next links in detail view (y/n)', - ['required', 'string', 'in:y,n'], - 'y' - ); + $this->fields->add(new PublicationField(PublicationFieldTypes::Datetime, '__createdAt')); } - protected function getCanonicalField(Collection $fields): string + protected function getPaginationSettings(): array { - $options = $fields->reject(function (PublicationField $field): bool { - // Temporary verbose check to see code coverage - if ($field->type === 'image') { - return true; - } elseif ($field->type === 'tag') { - return true; - } else { - return false; - } - })->pluck('name'); + if ($this->option('use-defaults') || ! $this->confirm('Do you want to configure pagination settings?')) { + return [null, null, null, null]; + } - return $this->choice('Choose a canonical name field (the values of this field have to be unique!)', $options->toArray(), $options->first()); + return [$this->getSortField(), $this->getSortDirection(), $this->getPrevNextLinks(), $this->getPageSize()]; } - protected function validateLengths(string $min, string $max): bool + protected function getSortField(): string { - if ($max < $min) { - $this->error('Field length [max] cannot be less than [min]'); + return $this->choice('Choose the default field you wish to sort by', $this->fields->pluck('name')->toArray(), '__dateCreated'); + } - return false; - } + protected function getSortDirection(): bool + { + $options = ['Ascending' => true, 'Descending' => false]; - return true; + return $options[$this->choice('Choose the default sort direction', array_keys($options), 'Ascending')]; } - protected function getFieldDataForTag(array $fieldData): array + protected function getPrevNextLinks(): bool { - $allTags = PublicationService::getAllTags(); - $offset = 1; - foreach ($allTags as $k => $v) { - $this->line(" $offset - $k"); - $offset++; - } - $offset--; // The above loop overcounts by 1 - $selected = $this->askWithValidation('tagGroup', 'Tag Group', ['required', 'integer', "between:1,$offset"], 0); - $fieldData['tagGroup'] = $allTags->keys()->{$selected - 1}; - $fieldData['min'] = 0; - $fieldData['max'] = 0; - - return $fieldData; + return $this->confirm('Generate previous/next links in detail view?', true); } - protected function checkIfFieldIsDuplicate(Collection $fields, $name): bool + protected function getPageSize(): int { - $duplicate = $fields->where('name', $name)->count(); - if ($duplicate) { - $this->error("Field name [$name] already exists!"); - } + return (int) $this->askWithValidation('pageSize', + 'Enter the page size (0 for no limit)', + ['required', 'integer', 'between:0,100'], + 25 + ); + } - return (bool) $duplicate; + protected function getCount(int $offset = 0): int + { + return $this->fields->count() + $offset; } } diff --git a/packages/framework/src/Framework/Actions/CreatesNewPublicationType.php b/packages/framework/src/Framework/Actions/CreatesNewPublicationType.php index 7886e3c0e3a..d48e0d73cbe 100644 --- a/packages/framework/src/Framework/Actions/CreatesNewPublicationType.php +++ b/packages/framework/src/Framework/Actions/CreatesNewPublicationType.php @@ -24,10 +24,10 @@ public function __construct( protected string $name, protected Collection $fields, protected string $canonicalField, - protected string $sortField, - protected bool $sortAscending, - protected bool $prevNextLinks, - protected int $pageSize, + protected ?string $sortField, + protected ?bool $sortAscending, + protected ?bool $prevNextLinks, + protected ?int $pageSize, protected ?OutputStyle $output = null, ) { $this->dirName = $this->formatStringForStorage($this->name); @@ -42,10 +42,10 @@ protected function handleCreate(): void "{$this->dirName}_detail", "{$this->dirName}_list", [ - $this->sortField, - $this->sortAscending, - $this->prevNextLinks, - $this->pageSize, + $this->sortField ?? '__createdAt', + $this->sortAscending ?? true, + $this->prevNextLinks ?? true, + $this->pageSize ?? 25, ], $this->fields->toArray() ); diff --git a/packages/framework/src/Framework/Features/Publications/Models/PaginationSettings.php b/packages/framework/src/Framework/Features/Publications/Models/PaginationSettings.php index d133e104991..81a2f5cee4f 100644 --- a/packages/framework/src/Framework/Features/Publications/Models/PaginationSettings.php +++ b/packages/framework/src/Framework/Features/Publications/Models/PaginationSettings.php @@ -13,6 +13,7 @@ class PaginationSettings implements SerializableContract public string $sortField = '__createdAt'; public bool $sortAscending = true; + /** @deprecated This setting might be deprecated as its unlikely one would enable page size limits without a way to traverse them */ public bool $prevNextLinks = true; public int $pageSize = 25; diff --git a/packages/framework/src/Framework/Features/Publications/Models/PublicationField.php b/packages/framework/src/Framework/Features/Publications/Models/PublicationField.php index 88f7ef7f2e9..6a0ef005f51 100644 --- a/packages/framework/src/Framework/Features/Publications/Models/PublicationField.php +++ b/packages/framework/src/Framework/Features/Publications/Models/PublicationField.php @@ -13,6 +13,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use function str_starts_with; use function strtolower; /** @@ -37,7 +38,7 @@ public static function fromArray(array $array): static public function __construct(PublicationFieldTypes|string $type, string $name, array $rules = []) { $this->type = $type instanceof PublicationFieldTypes ? $type : PublicationFieldTypes::from(strtolower($type)); - $this->name = Str::kebab($name); + $this->name = str_starts_with($name, '__') ? $name : Str::kebab($name); $this->rules = $rules; } diff --git a/packages/framework/src/Framework/Features/Publications/PublicationFieldTypes.php b/packages/framework/src/Framework/Features/Publications/PublicationFieldTypes.php index 41323c2d6c8..2642c83cac2 100644 --- a/packages/framework/src/Framework/Features/Publications/PublicationFieldTypes.php +++ b/packages/framework/src/Framework/Features/Publications/PublicationFieldTypes.php @@ -15,14 +15,14 @@ enum PublicationFieldTypes: string { case String = 'string'; + case Datetime = 'datetime'; case Boolean = 'boolean'; case Integer = 'integer'; case Float = 'float'; - case Datetime = 'datetime'; - case Url = 'url'; + case Image = 'image'; case Array = 'array'; case Text = 'text'; - case Image = 'image'; + case Url = 'url'; case Tag = 'tag'; public function rules(): array @@ -40,20 +40,40 @@ public static function values(): array return self::collect()->pluck('value')->toArray(); } + public static function names(): array + { + return self::collect()->pluck('name')->toArray(); + } + public static function getRules(self $type): array { /** @noinspection PhpDuplicateMatchArmBodyInspection */ return match ($type) { self::String => ['string'], + self::Datetime => ['date'], self::Boolean => ['boolean'], self::Integer => ['integer', 'numeric'], self::Float => ['numeric'], - self::Datetime => ['date'], - self::Url => ['url'], - self::Text => ['string'], - self::Array => ['array'], self::Image => [], + self::Array => ['array'], + self::Text => ['string'], + self::Url => ['url'], self::Tag => [], }; } + + /** + * The types that can be used for canonical fields (used to generate file names). + * + * @return \Hyde\Framework\Features\Publications\PublicationFieldTypes[] + */ + public static function canonicable(): array + { + return [ + self::String, + self::Integer, + self::Datetime, + self::Text, + ]; + } } diff --git a/packages/framework/tests/Feature/Commands/MakePublicationTypeCommandTest.php b/packages/framework/tests/Feature/Commands/MakePublicationTypeCommandTest.php index 8dca69f48e9..682eca863c3 100644 --- a/packages/framework/tests/Feature/Commands/MakePublicationTypeCommandTest.php +++ b/packages/framework/tests/Feature/Commands/MakePublicationTypeCommandTest.php @@ -6,6 +6,7 @@ use function config; use Hyde\Facades\Filesystem; +use Hyde\Framework\Features\Publications\PublicationFieldTypes; use Hyde\Hyde; use Hyde\Testing\TestCase; @@ -22,35 +23,44 @@ protected function setUp(): void config(['app.throw_on_console_exception' => true]); } + protected function tearDown(): void + { + Filesystem::deleteDirectory('test-publication'); + + parent::tearDown(); + } + public function test_command_creates_publication_type() { $this->artisan('make:publicationType') ->expectsQuestion('Publication type name', 'Test Publication') - ->expectsQuestion('Field name', 'Publication Title') - ->expectsChoice('Field type', 'String', [ - 1 => 'String', - 2 => 'Boolean', - 3 => 'Integer', - 4 => 'Float', - 5 => 'Datetime (YYYY-MM-DD (HH:MM:SS))', - 6 => 'URL', - 7 => 'Array', - 8 => 'Text', - 9 => 'Local Image', - 10 => 'Tag (select value from list)', - ]) - ->expectsQuestion('Add another field (y/n)', 'n') - ->expectsChoice('Choose the default field you wish to sort by', 'dateCreated (meta field)', [ - 'dateCreated (meta field)', + ->expectsQuestion('Enter name for field #1', 'Publication Title') + ->expectsChoice('Enter type for field #1', 'String', [ + 'String', + 'Datetime', + 'Boolean', + 'Integer', + 'Float', + 'Image', + 'Array', + 'Text', + 'Url', + 'Tag', + ], true) + ->expectsConfirmation('Field #1 added! Add another field?') + ->expectsConfirmation('Do you want to configure pagination settings?', 'yes') + ->expectsChoice('Choose the default field you wish to sort by', '__createdAt', [ + '__createdAt', 'publication-title', ]) - ->expectsChoice('Choose the default sort direction', 'Ascending (oldest items first if sorting by dateCreated)', [ - 'Ascending (oldest items first if sorting by dateCreated)', - 'Descending (newest items first if sorting by dateCreated)', + ->expectsChoice('Choose the default sort direction', 'Ascending', [ + 'Ascending', + 'Descending', ]) - ->expectsQuestion('Enter the pageSize (0 for no limit)', 10) - ->expectsQuestion('Generate previous/next links in detail view (y/n)', 'n') - ->expectsChoice('Choose a canonical name field (the values of this field have to be unique!)', 'publication-title', [ + ->expectsConfirmation('Generate previous/next links in detail view?', 'yes') + ->expectsQuestion('Enter the page size (0 for no limit)', 10) + ->expectsChoice('Choose a canonical name field (this will be used to generate filenames, so the values need to be unique)', 'publication-title', [ + '__createdAt', 'publication-title', ]) ->expectsOutputToContain('Creating a new Publication Type!') @@ -73,6 +83,10 @@ public function test_command_creates_publication_type() "pageSize": 10 }, "fields": [ + { + "type": "datetime", + "name": "__createdAt" + }, { "type": "string", "name": "publication-title" @@ -84,7 +98,63 @@ public function test_command_creates_publication_type() ); // TODO: Assert Blade templates were created? + } - Filesystem::deleteDirectory('test-publication'); + public function test_with_default_values() + { + $this->artisan('make:publicationType --use-defaults') + ->expectsQuestion('Publication type name', 'Test Publication') + ->expectsQuestion('Enter name for field #1', 'foo') + ->expectsChoice('Enter type for field #1', 'String', PublicationFieldTypes::names()) + ->expectsOutput('Saving publication data to [test-publication/schema.json]') + ->expectsOutput('Publication type created successfully!') + ->assertExitCode(0); + } + + public function test_with_multiple_fields_of_the_same_name() + { + $this->artisan('make:publicationType "Test Publication"') + ->expectsQuestion('Enter name for field #1', 'foo') + ->expectsChoice('Enter type for field #1', 'String', PublicationFieldTypes::names()) + + ->expectsConfirmation('Field #1 added! Add another field?', 'yes') + + ->expectsQuestion('Enter name for field #2', 'foo') + ->expectsOutput('Field name [foo] already exists!') + ->expectsQuestion('Try again: Enter name for field #2', 'bar') + ->expectsChoice('Enter type for field #2', 'String', PublicationFieldTypes::names()) + + ->expectsConfirmation('Field #2 added! Add another field?') + + ->expectsConfirmation('Do you want to configure pagination settings?') + ->expectsChoice('Choose a canonical name field (this will be used to generate filenames, so the values need to be unique)', 'foo', [ + '__createdAt', + 'bar', + 'foo', + ]) + ->assertExitCode(0); + } + + public function test_with_existing_file_of_the_same_name() + { + config(['app.throw_on_console_exception' => false]); + + $this->file('test-publication'); + + $this->artisan('make:publicationType "Test Publication"') + ->expectsOutput('Error: Storage path [test-publication] already exists') + ->assertExitCode(1); + } + + public function test_with_existing_publication_of_the_same_name() + { + config(['app.throw_on_console_exception' => false]); + + $this->directory('test-publication'); + $this->file('test-publication/foo'); + + $this->artisan('make:publicationType "Test Publication"') + ->expectsOutput('Error: Storage path [test-publication] already exists') + ->assertExitCode(1); } } diff --git a/packages/framework/tests/Feature/PublicationFieldTypesEnumTest.php b/packages/framework/tests/Feature/PublicationFieldTypesEnumTest.php index 5c0e2a51908..365633691d3 100644 --- a/packages/framework/tests/Feature/PublicationFieldTypesEnumTest.php +++ b/packages/framework/tests/Feature/PublicationFieldTypesEnumTest.php @@ -51,15 +51,41 @@ public function testValuesReturnsArrayOfCaseValues() { $this->assertSame([ 0 => 'string', - 1 => 'boolean', - 2 => 'integer', - 3 => 'float', - 4 => 'datetime', - 5 => 'url', + 1 => 'datetime', + 2 => 'boolean', + 3 => 'integer', + 4 => 'float', + 5 => 'image', 6 => 'array', 7 => 'text', - 8 => 'image', + 8 => 'url', 9 => 'tag', ], PublicationFieldTypes::values()); } + + public function testNamesReturnsArrayOfCaseNames() + { + $this->assertSame([ + 0 => 'String', + 1 => 'Datetime', + 2 => 'Boolean', + 3 => 'Integer', + 4 => 'Float', + 5 => 'Image', + 6 => 'Array', + 7 => 'Text', + 8 => 'Url', + 9 => 'Tag', + ], PublicationFieldTypes::names()); + } + + public function testCanonicable() + { + $this->assertSame([ + PublicationFieldTypes::String, + PublicationFieldTypes::Integer, + PublicationFieldTypes::Datetime, + PublicationFieldTypes::Text, + ], PublicationFieldTypes::canonicable()); + } }