From 94b66f06cef7c5e1c0821c742f39c1d7bc7aaab3 Mon Sep 17 00:00:00 2001 From: Kevin Meijer Date: Fri, 1 May 2026 16:33:48 +0200 Subject: [PATCH] Added support for group fields and show imported fieldset fields --- .../AvailableVariablesFieldtype.php | 137 +++++++- .../AvailableVariablesFieldtypeTest.php | 298 ++++++++++++++++++ 2 files changed, 423 insertions(+), 12 deletions(-) diff --git a/src/Fieldtypes/AvailableVariablesFieldtype.php b/src/Fieldtypes/AvailableVariablesFieldtype.php index f6a0455..fb95f5c 100644 --- a/src/Fieldtypes/AvailableVariablesFieldtype.php +++ b/src/Fieldtypes/AvailableVariablesFieldtype.php @@ -6,6 +6,7 @@ use Statamic\Entries\Collection; use Statamic\Entries\Entry; use Statamic\Facades\Collection as CollectionFacade; +use Statamic\Facades\Fieldset as FieldsetFacade; use Statamic\Facades\GlobalSet; use Statamic\Fields\Blueprint; use Statamic\Fields\Fieldtype; @@ -58,6 +59,7 @@ protected function fieldTypeIsEligible(string $fieldType): bool 'assets', 'bard', 'toggle', + 'select', 'integer', 'slug', 'date', @@ -208,11 +210,16 @@ protected function getCollectionVariables(string $collectionHandle, array $field protected function setFieldData(array $field, ?string $name = null, ?string $description = null, bool $recursive = true): ?array { $fieldHandle = $field['handle'] ?? null; - /** @var array|null $fieldConfig */ - $fieldConfig = $field['field'] ?? null; - $fieldType = is_array($fieldConfig) ? ($fieldConfig['type'] ?? null) : null; + /** @var array $fieldConfig */ + $fieldConfig = is_array($field['field'] ?? null) ? $field['field'] : []; + $fieldType = $fieldConfig['type'] ?? null; - if (! is_string($fieldHandle) || $fieldHandle === 'parent' || ! is_string($fieldType) || ! $this->fieldTypeIsEligible($fieldType)) { + if (! is_string($fieldHandle) || $fieldHandle === 'parent' || ! is_string($fieldType)) { + return null; + } + + $isGroupField = $fieldType === 'group'; + if (! $isGroupField && ! $this->fieldTypeIsEligible($fieldType)) { return null; } @@ -229,9 +236,17 @@ protected function setFieldData(array $field, ?string $name = null, ?string $des $children = $this->getCollectionVariables($collections[0], $field); } + if ($isGroupField && $recursive) { + $children = $this->getGroupVariables($field, $fieldConfig); + } + $descriptionValue = is_string($description) ? $description : null; $display = is_string($fieldConfig['display'] ?? null) ? $fieldConfig['display'] : null; + if ($isGroupField && empty($children)) { + return null; + } + return [ 'name' => $name ?? $fieldHandle, 'description' => $descriptionValue ?? $display ?? $fieldHandle, @@ -239,6 +254,37 @@ protected function setFieldData(array $field, ?string $name = null, ?string $des ]; } + /** + * @param array $field + * @param array $fieldConfig + * @return array> + */ + protected function getGroupVariables(array $field, array $fieldConfig): array + { + $groupFields = $this->normalizeBlueprintItems($fieldConfig['fields'] ?? []); + $groupHandle = is_string($field['handle'] ?? null) ? $field['handle'] : ''; + $groupDisplay = is_string($fieldConfig['display'] ?? null) ? $fieldConfig['display'] : $groupHandle; + + return collect($groupFields) + ->map(function (array $groupField) use ($groupHandle, $groupDisplay): ?array { + /** @var string $childHandle */ + $childHandle = $groupField['handle']; + + $childConfig = is_array($groupField['field'] ?? null) ? $groupField['field'] : []; + $childDisplay = is_string($childConfig['display'] ?? null) ? $childConfig['display'] : $childHandle; + + return $this->setFieldData( + $groupField, + $groupHandle.':'.$childHandle, + $groupDisplay.': '.$childDisplay, + false + ); + }) + ->filter() + ->values() + ->all(); + } + /** * Normalizes blueprint items to ensure they are an array. * @@ -247,18 +293,85 @@ protected function setFieldData(array $field, ?string $name = null, ?string $des */ protected function normalizeBlueprintItems($items): array { - if (is_array($items)) { - /** @var array> $items */ - return $items; + if ($items instanceof SupportCollection) { + $items = $items->all(); } - if ($items instanceof SupportCollection) { - /** @var array> $result */ - $result = $items->all(); + if (! is_array($items)) { + return []; + } + + $normalizedItems = []; + + foreach ($items as $item) { + if ($item instanceof SupportCollection) { + $item = $item->all(); + } + + if (! is_array($item)) { + continue; + } + + $import = $item['import'] ?? null; + if (is_string($import) && $import !== '') { + $normalizedItems = array_merge($normalizedItems, $this->getImportedFieldsetItems($item)); - return $result; + continue; + } + + if (isset($item['handle']) && is_array($item['field'] ?? null)) { + /** @var array $item */ + $normalizedItems[] = $item; + } + + $nestedItems = $item['fields'] ?? null; + if (is_array($nestedItems) || $nestedItems instanceof SupportCollection) { + $normalizedItems = array_merge( + $normalizedItems, + $this->normalizeBlueprintItems($nestedItems) + ); + } + } + + return $normalizedItems; + } + + /** + * @param array $importConfig + * @return array> + */ + protected function getImportedFieldsetItems(array $importConfig): array + { + $fieldsetHandle = $importConfig['import'] ?? null; + if (! is_string($fieldsetHandle) || $fieldsetHandle === '') { + return []; } - return []; + $prefix = $importConfig['prefix'] ?? null; + if (! is_string($prefix)) { + $prefix = ''; + } + + $fieldset = FieldsetFacade::find($fieldsetHandle); + + if (! $fieldset) { + return []; + } + + $items = $this->normalizeBlueprintItems($fieldset->fields()->items()); + + if ($prefix === '') { + return $items; + } + + return array_map(function (array $item) use ($prefix): array { + $handle = $item['handle'] ?? null; + + if (is_string($handle) && $handle !== '') { + $item['handle'] = $prefix.$handle; + } + + return $item; + }, $items); } } diff --git a/tests/Unit/Fieldtypes/AvailableVariablesFieldtypeTest.php b/tests/Unit/Fieldtypes/AvailableVariablesFieldtypeTest.php index 872761a..72413a9 100644 --- a/tests/Unit/Fieldtypes/AvailableVariablesFieldtypeTest.php +++ b/tests/Unit/Fieldtypes/AvailableVariablesFieldtypeTest.php @@ -11,12 +11,14 @@ use Statamic\Entries\Entry; use Statamic\Facades\Blueprint as BlueprintFacade; use Statamic\Facades\Collection as CollectionFacade; +use Statamic\Facades\Fieldset as FieldsetFacade; use Statamic\Facades\GlobalSet; use Statamic\Facades\GlobalSet as GlobalSetFacade; use Statamic\Facades\Taxonomy as TaxonomyFacade; use Statamic\Fields\Blueprint; use Statamic\Fields\Field; use Statamic\Fields\Fields; +use Statamic\Fields\Fieldset; use Statamic\Globals\GlobalCollection; use Statamic\Taxonomies\Taxonomy; @@ -522,6 +524,98 @@ public function set_field_data_returns_null_when_entries_field_has_no_collection $this->assertNull($result); } + #[Test] + public function set_field_data_returns_group_field_with_supported_children(): void + { + $fieldtype = new AvailableVariablesFieldtype; + + $reflection = new \ReflectionClass($fieldtype); + $method = $reflection->getMethod('setFieldData'); + $method->setAccessible(true); + + $field = [ + 'handle' => 'media_options', + 'field' => [ + 'type' => 'group', + 'display' => 'Media opties', + 'fields' => [ + [ + 'handle' => 'object_fit', + 'field' => [ + 'type' => 'select', + 'display' => 'Object fit', + ], + ], + [ + 'handle' => 'video', + 'field' => [ + 'type' => 'toggle', + 'display' => 'Video', + ], + ], + [ + 'handle' => 'layout', + 'field' => [ + 'type' => 'grid', + 'display' => 'Layout', + ], + ], + ], + ], + ]; + + $result = $method->invoke($fieldtype, $field); + + $this->assertIsArray($result); + /** @var array $result */ + $this->assertSame('media_options', $result['name']); + $this->assertSame('Media opties', $result['description']); + $this->assertArrayHasKey('children', $result); + $this->assertIsArray($result['children']); + $this->assertCount(2, $result['children']); + + /** @var array> $children */ + $children = $result['children']; + $childNames = array_column($children, 'name'); + $childDescriptions = array_column($children, 'description'); + + $this->assertContains('media_options:object_fit', $childNames); + $this->assertContains('Media opties: Object fit', $childDescriptions); + $this->assertContains('media_options:video', $childNames); + $this->assertContains('Media opties: Video', $childDescriptions); + $this->assertNotContains('media_options:layout', $childNames); + } + + #[Test] + public function set_field_data_returns_null_for_group_without_supported_children(): void + { + $fieldtype = new AvailableVariablesFieldtype; + $reflection = new \ReflectionClass($fieldtype); + $method = $reflection->getMethod('setFieldData'); + $method->setAccessible(true); + + $field = [ + 'handle' => 'media_options', + 'field' => [ + 'type' => 'group', + 'display' => 'Media opties', + 'fields' => [ + [ + 'handle' => 'layout', + 'field' => [ + 'type' => 'grid', + 'display' => 'Layout', + ], + ], + ], + ], + ]; + + $result = $method->invoke($fieldtype, $field); + + $this->assertNull($result); + } + #[Test] public function normalize_blueprint_items_converts_collection_to_array(): void { @@ -575,6 +669,168 @@ public function normalize_blueprint_items_returns_empty_array_for_invalid_input( $this->assertEmpty($result); } + #[Test] + public function normalize_blueprint_items_includes_imported_fieldset_items(): void + { + $fieldtype = new AvailableVariablesFieldtype; + $reflection = new \ReflectionClass($fieldtype); + $method = $reflection->getMethod('normalizeBlueprintItems'); + $method->setAccessible(true); + + $fieldsetFields = $this->mock(Fields::class, function (MockInterface $mock): void { + $mock->shouldReceive('items')->andReturn([ + [ + 'handle' => 'imported_title', + 'field' => [ + 'type' => 'text', + 'display' => 'Imported Title', + ], + ], + ]); + }); + + $fieldset = $this->mock(Fieldset::class, function (MockInterface $mock) use ($fieldsetFields): void { + $mock->shouldReceive('fields')->andReturn($fieldsetFields); + }); + + FieldsetFacade::shouldReceive('find')->with('seo_fields')->andReturn($fieldset); + + $result = $method->invoke($fieldtype, [ + ['import' => 'seo_fields'], + ]); + + $this->assertIsArray($result); + $this->assertCount(1, $result); + /** @var array $firstResult */ + $firstResult = $result[0]; + $this->assertSame('imported_title', $firstResult['handle']); + } + + #[Test] + public function normalize_blueprint_items_handles_missing_imported_fieldset(): void + { + $fieldtype = new AvailableVariablesFieldtype; + $reflection = new \ReflectionClass($fieldtype); + $method = $reflection->getMethod('normalizeBlueprintItems'); + $method->setAccessible(true); + + FieldsetFacade::shouldReceive('find')->with('missing_fields')->andReturn(null); + + $result = $method->invoke($fieldtype, [ + ['import' => 'missing_fields'], + ]); + + $this->assertIsArray($result); + $this->assertEmpty($result); + } + + #[Test] + public function normalize_blueprint_items_applies_prefix_to_imported_fieldset_items(): void + { + $fieldtype = new AvailableVariablesFieldtype; + $reflection = new \ReflectionClass($fieldtype); + $method = $reflection->getMethod('normalizeBlueprintItems'); + $method->setAccessible(true); + + $fieldsetFields = $this->mock(Fields::class, function (MockInterface $mock): void { + $mock->shouldReceive('items')->andReturn([ + [ + 'handle' => 'image', + 'field' => [ + 'type' => 'assets', + 'display' => 'Image', + ], + ], + ]); + }); + + $fieldset = $this->mock(Fieldset::class, function (MockInterface $mock) use ($fieldsetFields): void { + $mock->shouldReceive('fields')->andReturn($fieldsetFields); + }); + + FieldsetFacade::shouldReceive('find')->with('media')->andReturn($fieldset); + + $result = $method->invoke($fieldtype, [ + ['import' => 'media', 'prefix' => 'featured_'], + ]); + + $this->assertIsArray($result); + $this->assertCount(1, $result); + /** @var array $firstResult */ + $firstResult = $result[0]; + $this->assertSame('featured_image', $firstResult['handle']); + } + + #[Test] + public function normalize_blueprint_items_handles_nested_support_collection_and_invalid_items(): void + { + $fieldtype = new AvailableVariablesFieldtype; + $reflection = new \ReflectionClass($fieldtype); + $method = $reflection->getMethod('normalizeBlueprintItems'); + $method->setAccessible(true); + + $nestedFields = collect([ + [ + 'handle' => 'nested_title', + 'field' => [ + 'type' => 'text', + 'display' => 'Nested Title', + ], + ], + ]); + + $items = [ + collect([ + 'handle' => 'wrapped_field', + 'field' => [ + 'type' => 'text', + 'display' => 'Wrapped Field', + ], + ]), + 'invalid-item', + [ + 'fields' => $nestedFields, + ], + ]; + + $result = $method->invoke($fieldtype, $items); + + $this->assertIsArray($result); + $fieldHandles = array_column($result, 'handle'); + $this->assertContains('wrapped_field', $fieldHandles); + $this->assertContains('nested_title', $fieldHandles); + } + + #[Test] + public function normalize_blueprint_items_handles_invalid_import_handle(): void + { + $fieldtype = new AvailableVariablesFieldtype; + $reflection = new \ReflectionClass($fieldtype); + $method = $reflection->getMethod('normalizeBlueprintItems'); + $method->setAccessible(true); + + $result = $method->invoke($fieldtype, [ + ['import' => null], + ]); + + $this->assertIsArray($result); + $this->assertEmpty($result); + } + + #[Test] + public function get_imported_fieldset_items_returns_empty_for_invalid_import_config(): void + { + $fieldtype = new AvailableVariablesFieldtype; + $reflection = new \ReflectionClass($fieldtype); + $method = $reflection->getMethod('getImportedFieldsetItems'); + $method->setAccessible(true); + + $result = $method->invoke($fieldtype, ['import' => null]); + + $this->assertIsArray($result); + $this->assertEmpty($result); + } + #[Test] public function get_entry_fields_includes_base_fields_and_maps_collection_fields(): void { @@ -630,6 +886,48 @@ public function get_entry_fields_includes_base_fields_and_maps_collection_fields $this->assertContains('title', $fieldNames); } + #[Test] + public function set_field_data_group_skips_children_without_handle(): void + { + $fieldtype = new AvailableVariablesFieldtype; + $reflection = new \ReflectionClass($fieldtype); + $method = $reflection->getMethod('setFieldData'); + $method->setAccessible(true); + + $field = [ + 'handle' => 'media_options', + 'field' => [ + 'type' => 'group', + 'display' => 'Media opties', + 'fields' => [ + [ + 'field' => [ + 'type' => 'toggle', + 'display' => 'Missing Handle', + ], + ], + [ + 'handle' => 'video', + 'field' => [ + 'type' => 'toggle', + 'display' => 'Video', + ], + ], + ], + ], + ]; + + $result = $method->invoke($fieldtype, $field); + + $this->assertIsArray($result); + /** @var array $result */ + $this->assertArrayHasKey('children', $result); + /** @var array> $children */ + $children = $result['children']; + $this->assertCount(1, $children); + $this->assertSame('media_options:video', $children[0]['name'] ?? null); + } + #[Test] public function get_entry_fields_handles_empty_fields_after_merge_gracefully(): void {