diff --git a/resources/js/components/blueprints/ImportField.vue b/resources/js/components/blueprints/ImportField.vue
index babeb9da327..590fab51e3e 100644
--- a/resources/js/components/blueprints/ImportField.vue
+++ b/resources/js/components/blueprints/ImportField.vue
@@ -11,6 +11,7 @@
+
-
+
@@ -76,6 +82,14 @@ export default {
type: Boolean,
default: false,
},
+ excludeFieldset: {
+ type: String,
+ default: null,
+ },
+ withCommandPalette: {
+ type: Boolean,
+ default: false,
+ },
},
data() {
diff --git a/resources/js/components/fields/ImportSettings.vue b/resources/js/components/fields/ImportSettings.vue
index 3f4dcc2f815..1bcc72dcea1 100644
--- a/resources/js/components/fields/ImportSettings.vue
+++ b/resources/js/components/fields/ImportSettings.vue
@@ -16,16 +16,36 @@
+
+
+
+
+
+
diff --git a/src/Fields/FieldTransformer.php b/src/Fields/FieldTransformer.php
index 156e5441a65..ab992143676 100644
--- a/src/Fields/FieldTransformer.php
+++ b/src/Fields/FieldTransformer.php
@@ -21,6 +21,9 @@ private static function importTabField(array $submitted)
return array_filter([
'import' => $submitted['fieldset'],
'prefix' => $submitted['prefix'] ?? null,
+ 'section_behavior' => ($submitted['section_behavior'] ?? 'preserve') === 'flatten'
+ ? 'flatten'
+ : null,
]);
}
@@ -179,11 +182,17 @@ private static function inlineFieldToVue($field): array
private static function importFieldToVue($field): array
{
- return [
+ $import = [
'type' => 'import',
'fieldset' => $field['import'],
'prefix' => $field['prefix'] ?? null,
];
+
+ if (isset($field['section_behavior'])) {
+ $import['section_behavior'] = $field['section_behavior'];
+ }
+
+ return $import;
}
public static function fieldsetFields()
diff --git a/src/Fields/Fieldset.php b/src/Fields/Fieldset.php
index cf01cabb96a..8ddd4467d6e 100644
--- a/src/Fields/Fieldset.php
+++ b/src/Fields/Fieldset.php
@@ -2,6 +2,7 @@
namespace Statamic\Fields;
+use Illuminate\Support\Collection as IlluminateCollection;
use Statamic\CommandPalette\Category;
use Statamic\CommandPalette\Link;
use Statamic\Events\FieldsetCreated;
@@ -62,18 +63,24 @@ public function initialPath($path = null)
return $this;
}
+ /**
+ * Canonical storage: either top-level `fields` (flat) or non-empty `sections`, never both.
+ * Hand-edited YAML or mixed input is normalized on save.
+ */
public function setContents(array $contents)
{
- $fields = Arr::get($contents, 'fields', []);
-
- // Support legacy syntax
- if (! empty($fields) && array_keys($fields)[0] !== 0) {
- $fields = collect($fields)->map(function ($field, $handle) {
- return compact('handle', 'field');
- })->values()->all();
+ if (array_key_exists('sections', $contents)) {
+ $contents['sections'] = $this->normalizeSections(Arr::get($contents, 'sections', []));
}
- $contents['fields'] = $fields;
+ $usesSections = array_key_exists('sections', $contents) && ! empty($contents['sections']);
+
+ if ($usesSections) {
+ unset($contents['fields']);
+ } else {
+ unset($contents['sections']);
+ $contents['fields'] = $this->normalizeFields(Arr::get($contents, 'fields', []));
+ }
$this->contents = $contents;
@@ -100,11 +107,23 @@ public function validateRecursion()
public function fields(): Fields
{
- $fields = Arr::get($this->contents, 'fields', []);
+ $fields = $this->hasSections()
+ ? $this->sections()->flatMap(fn ($section) => Arr::get($section, 'fields', []))->values()->all()
+ : Arr::get($this->contents, 'fields', []);
return new Fields($fields);
}
+ public function sections(): IlluminateCollection
+ {
+ return collect(Arr::get($this->contents, 'sections', []));
+ }
+
+ public function hasSections(): bool
+ {
+ return $this->sections()->isNotEmpty();
+ }
+
public function field(string $handle): ?Field
{
return $this->fields()->get($handle);
@@ -159,9 +178,12 @@ public function importedBy(): array
})->values();
$fieldsets = \Statamic\Facades\Fieldset::all()
- ->filter(fn (Fieldset $fieldset) => isset($fieldset->contents()['fields']))
->filter(function (Fieldset $fieldset) {
- return collect($fieldset->contents()['fields'])
+ $fields = $fieldset->hasSections()
+ ? $fieldset->sections()->flatMap(fn ($section) => Arr::get($section, 'fields', []))->all()
+ : Arr::get($fieldset->contents(), 'fields', []);
+
+ return collect($fields)
->filter(fn ($field) => $this->fieldImportsFieldset($field))
->isNotEmpty();
})
@@ -311,4 +333,25 @@ public static function __callStatic($method, $parameters)
{
return Facades\Fieldset::{$method}(...$parameters);
}
+
+ private function normalizeFields(array $fields): array
+ {
+ // Support legacy syntax where handles are array keys.
+ if (! empty($fields) && array_keys($fields)[0] !== 0) {
+ $fields = collect($fields)->map(function ($field, $handle) {
+ return compact('handle', 'field');
+ })->values()->all();
+ }
+
+ return $fields;
+ }
+
+ private function normalizeSections(array $sections): array
+ {
+ return collect($sections)->map(function ($section) {
+ $section['fields'] = $this->normalizeFields(Arr::get($section, 'fields', []));
+
+ return $section;
+ })->all();
+ }
}
diff --git a/src/Fields/Tab.php b/src/Fields/Tab.php
index bbc50f2a2bc..0c67dfaabc1 100644
--- a/src/Fields/Tab.php
+++ b/src/Fields/Tab.php
@@ -2,6 +2,7 @@
namespace Statamic\Fields;
+use Statamic\Facades\Fieldset as FieldsetRepository;
use Statamic\Support\Arr;
use Statamic\Support\Str;
@@ -81,7 +82,7 @@ public function toPublishArray()
'display' => $this->display(),
'instructions' => $this->instructions(),
'handle' => $this->handle,
- 'sections' => $this->sections()->map->toPublishArray()->all(),
+ 'sections' => $this->expandedSections(),
];
}
@@ -94,4 +95,89 @@ public function instructions()
{
return Arr::get($this->contents, 'instructions');
}
+
+ private function expandedSections(): array
+ {
+ return $this->sections()->reduce(function ($carry, Section $section) {
+ $fields = Arr::get($section->contents(), 'fields', []);
+
+ if (empty($fields)) {
+ return $carry->push($section->toPublishArray());
+ }
+
+ $sectionContents = $section->contents();
+ $sectionMeta = Arr::except($sectionContents, ['fields']);
+ $bufferedFields = [];
+
+ foreach ($fields as $field) {
+ if (! $this->isSectionedFieldsetImport($field)) {
+ $bufferedFields[] = $field;
+
+ continue;
+ }
+
+ if (! empty($bufferedFields)) {
+ $carry->push((new Section($sectionMeta + ['fields' => $bufferedFields]))->toPublishArray());
+ $bufferedFields = [];
+ }
+
+ $importedSections = FieldsetRepository::find($field['import'])->sections();
+
+ foreach ($importedSections as $importedSection) {
+ $carry->push($this->toImportedPublishSection($importedSection, $field));
+ }
+ }
+
+ if (! empty($bufferedFields)) {
+ $carry->push((new Section($sectionMeta + ['fields' => $bufferedFields]))->toPublishArray());
+ }
+
+ return $carry;
+ }, collect())->all();
+ }
+
+ private function isSectionedFieldsetImport(array $field): bool
+ {
+ if (! isset($field['import'])) {
+ return false;
+ }
+
+ if (($field['section_behavior'] ?? 'preserve') === 'flatten') {
+ return false;
+ }
+
+ $fieldset = FieldsetRepository::find($field['import']);
+
+ return $fieldset && $fieldset->hasSections();
+ }
+
+ private function toImportedPublishSection(array $section, array $import): array
+ {
+ $fields = (new Fields(Arr::get($section, 'fields', [])))->all();
+
+ if ($overrides = $import['config'] ?? null) {
+ $fields = $fields->map(function ($field, $handle) use ($overrides) {
+ return $field->setConfig(array_merge($field->config(), $overrides[$handle] ?? []));
+ });
+ }
+
+ if ($prefix = Arr::get($import, 'prefix')) {
+ $fields = $fields->mapWithKeys(function ($field) use ($prefix) {
+ $field = clone $field;
+ $handle = $prefix.$field->handle();
+ $prefix = $prefix.$field->prefix();
+
+ return [$handle => $field->setHandle($handle)->setPrefix($prefix)];
+ });
+ }
+
+ return Arr::removeNullValues([
+ 'display' => Arr::get($section, 'display'),
+ 'instructions' => Arr::get($section, 'instructions'),
+ 'collapsible' => ($collapsible = Arr::get($section, 'collapsible')) ?: null,
+ 'collapsed' => ($collapsible && Arr::get($section, 'collapsed')) ?: null,
+ ]) + [
+ 'fields' => $fields->map->toPublishArray()->values()->all(),
+ ];
+ }
}
diff --git a/src/Http/Controllers/CP/Fields/FieldsetController.php b/src/Http/Controllers/CP/Fields/FieldsetController.php
index 0254a72f06f..7027ff80e42 100644
--- a/src/Http/Controllers/CP/Fields/FieldsetController.php
+++ b/src/Http/Controllers/CP/Fields/FieldsetController.php
@@ -112,9 +112,7 @@ public function edit($fieldset)
$vue = [
'title' => $fieldset->title(),
'handle' => $fieldset->handle(),
- 'fields' => collect(Arr::get($fieldset->contents(), 'fields'))->map(function ($field, $i) {
- return array_merge(FieldTransformer::toVue($field), ['_id' => $i]);
- })->all(),
+ 'sections' => $this->sectionsToVue($fieldset),
];
return Inertia::render('fieldsets/Edit', [
@@ -130,15 +128,30 @@ public function update(Request $request, $fieldset)
$request->validate([
'title' => 'required',
+ 'sections' => 'array',
'fields' => 'array',
]);
- $fieldset->setContents(array_merge($fieldset->contents(), [
- 'title' => $request->title,
- 'fields' => collect($request->fields)->map(function ($field) {
- return FieldTransformer::fromVue($field);
- })->all(),
- ]));
+ $base = array_merge(
+ Arr::except($fieldset->contents(), ['fields', 'sections']),
+ ['title' => $request->title],
+ );
+
+ if ($request->has('sections')) {
+ $sections = $this->sectionsFromVueRequest($request->sections);
+
+ $fieldset->setContents(
+ $this->shouldStoreAsFlatFields($sections)
+ ? $base + ['fields' => Arr::get($sections, '0.fields', [])]
+ : $base + ['sections' => $sections]
+ );
+ } else {
+ $fieldset->setContents($base + [
+ 'fields' => collect($request->fields)
+ ->map(fn ($field) => FieldTransformer::fromVue($field))
+ ->all(),
+ ]);
+ }
$fieldset->validateRecursion();
@@ -217,4 +230,58 @@ private function groupKey(Fieldset $fieldset): string
return __('My Fieldsets');
}
+
+ private function sectionsFromVueRequest(array $sections): array
+ {
+ return collect($sections)->map(function ($section) {
+ return Arr::removeNullValues([
+ 'display' => Arr::get($section, 'display'),
+ 'instructions' => Arr::get($section, 'instructions'),
+ 'collapsible' => ($collapsible = Arr::get($section, 'collapsible')) ?: null,
+ 'collapsed' => ($collapsible && Arr::get($section, 'collapsed')) ?: null,
+ 'fields' => collect(Arr::get($section, 'fields', []))
+ ->map(fn ($field) => FieldTransformer::fromVue($field))
+ ->all(),
+ ]);
+ })->all();
+ }
+
+ private function sectionsToVue(Fieldset $fieldset): array
+ {
+ $sections = $fieldset->hasSections()
+ ? Arr::get($fieldset->contents(), 'sections', [])
+ : [[
+ 'display' => __('Fields'),
+ 'fields' => Arr::get($fieldset->contents(), 'fields', []),
+ ]];
+
+ return collect($sections)->map(function ($section, $sectionIndex) {
+ return Arr::removeNullValues([
+ '_id' => "section-{$sectionIndex}",
+ 'display' => Arr::get($section, 'display'),
+ 'instructions' => Arr::get($section, 'instructions'),
+ 'collapsible' => Arr::get($section, 'collapsible'),
+ 'collapsed' => Arr::get($section, 'collapsed'),
+ ]) + [
+ 'fields' => collect(Arr::get($section, 'fields', []))->map(function ($field, $fieldIndex) use ($sectionIndex) {
+ return array_merge(FieldTransformer::toVue($field), ['_id' => "section-{$sectionIndex}-{$fieldIndex}"]);
+ })->all(),
+ ];
+ })->all();
+ }
+
+ private function shouldStoreAsFlatFields(array $sections): bool
+ {
+ if (count($sections) !== 1) {
+ return false;
+ }
+
+ $section = $sections[0];
+ $display = Arr::get($section, 'display');
+
+ return in_array($display, [null, '', __('Fields')], true)
+ && Arr::get($section, 'instructions') === null
+ && Arr::get($section, 'collapsible') !== true
+ && Arr::get($section, 'collapsed') !== true;
+ }
}
diff --git a/src/Http/Controllers/CP/Fields/ManagesFields.php b/src/Http/Controllers/CP/Fields/ManagesFields.php
index 7df69f95c9f..98394fb9ff6 100644
--- a/src/Http/Controllers/CP/Fields/ManagesFields.php
+++ b/src/Http/Controllers/CP/Fields/ManagesFields.php
@@ -21,10 +21,16 @@ private function fieldProps()
private function fieldsets()
{
return Fieldset::all()->mapWithKeys(function ($fieldset) {
+ $fields = $fieldset->hasSections()
+ ? $fieldset->sections()->flatMap(fn ($section) => Arr::get($section, 'fields', []))
+ : collect(Arr::get($fieldset->contents(), 'fields', []));
+
return [$fieldset->handle() => [
'handle' => $fieldset->handle(),
'title' => $fieldset->title(),
- 'fields' => collect(Arr::get($fieldset->contents(), 'fields'))->map(function ($field) {
+ 'has_sections' => $fieldset->hasSections(),
+ 'sections_count' => $fieldset->hasSections() ? $fieldset->sections()->count() : 0,
+ 'fields' => $fields->map(function ($field) {
return FieldTransformer::toVue($field);
})->sortBy('config.display')->values()->all(),
]];
diff --git a/tests/Feature/Fieldsets/EditFieldsetTest.php b/tests/Feature/Fieldsets/EditFieldsetTest.php
index df82c1882d0..24d88b92f13 100644
--- a/tests/Feature/Fieldsets/EditFieldsetTest.php
+++ b/tests/Feature/Fieldsets/EditFieldsetTest.php
@@ -43,12 +43,46 @@ public function it_provides_the_fieldset()
{
$this->withoutExceptionHandling();
$user = Facades\User::make()->makeSuper()->save();
- $fieldset = (new Fieldset)->setHandle('test')->setContents(['title' => 'Test'])->save();
+ $fieldset = (new Fieldset)->setHandle('test')->setContents([
+ 'title' => 'Test',
+ 'fields' => [
+ ['handle' => 'title', 'field' => ['type' => 'text']],
+ ],
+ ])->save();
+
+ $this
+ ->actingAs($user)
+ ->get($fieldset->editUrl())
+ ->assertStatus(200)
+ ->assertInertia(fn ($page) => $page
+ ->where('initialFieldset.handle', $fieldset->handle())
+ ->where('initialFieldset.sections.0.fields.0.handle', 'title')
+ );
+ }
+
+ #[Test]
+ public function it_provides_sectioned_fieldsets()
+ {
+ $user = Facades\User::make()->makeSuper()->save();
+ $fieldset = (new Fieldset)->setHandle('test')->setContents([
+ 'title' => 'Test',
+ 'sections' => [
+ [
+ 'display' => 'SEO',
+ 'fields' => [
+ ['handle' => 'meta_title', 'field' => ['type' => 'text']],
+ ],
+ ],
+ ],
+ ])->save();
$this
->actingAs($user)
->get($fieldset->editUrl())
->assertStatus(200)
- ->assertInertia(fn ($page) => $page->where('initialFieldset.handle', $fieldset->handle()));
+ ->assertInertia(fn ($page) => $page
+ ->where('initialFieldset.sections.0.display', 'SEO')
+ ->where('initialFieldset.sections.0.fields.0.handle', 'meta_title')
+ );
}
}
diff --git a/tests/Feature/Fieldsets/UpdateFieldsetTest.php b/tests/Feature/Fieldsets/UpdateFieldsetTest.php
index 619a75c10bf..ff9e694bd4c 100644
--- a/tests/Feature/Fieldsets/UpdateFieldsetTest.php
+++ b/tests/Feature/Fieldsets/UpdateFieldsetTest.php
@@ -105,6 +105,231 @@ public function fieldset_gets_saved()
], Facades\Fieldset::find('test')->contents());
}
+ #[Test]
+ public function fieldset_gets_saved_with_sections()
+ {
+ $user = tap(Facades\User::make()->makeSuper())->save();
+ (new Fieldset)->setHandle('seo_defaults')->setContents([
+ 'title' => 'SEO Defaults',
+ 'fields' => [
+ ['handle' => 'canonical_url', 'field' => ['type' => 'text']],
+ ],
+ ])->save();
+
+ $fieldset = (new Fieldset)->setHandle('test')->setContents([
+ 'title' => 'Test',
+ 'foo' => 'bar',
+ 'fields' => [
+ ['handle' => 'legacy', 'field' => ['type' => 'text']],
+ ],
+ ])->save();
+
+ $this
+ ->actingAs($user)
+ ->submit($fieldset, [
+ 'title' => 'Updated title',
+ 'sections' => [
+ [
+ '_id' => 'section-1',
+ 'display' => 'SEO',
+ 'instructions' => 'SEO fields',
+ 'collapsible' => true,
+ 'collapsed' => true,
+ 'fields' => [
+ [
+ '_id' => 'section-1-field-1',
+ 'handle' => 'meta_title',
+ 'type' => 'inline',
+ 'config' => [
+ 'type' => 'text',
+ 'display' => 'Meta title',
+ ],
+ ],
+ [
+ '_id' => 'section-1-field-2',
+ 'type' => 'import',
+ 'fieldset' => 'seo_defaults',
+ 'prefix' => 'seo_',
+ ],
+ ],
+ ],
+ ],
+ ])
+ ->assertStatus(204);
+
+ $this->assertEquals([
+ 'title' => 'Updated title',
+ 'foo' => 'bar',
+ 'sections' => [
+ [
+ 'display' => 'SEO',
+ 'instructions' => 'SEO fields',
+ 'collapsible' => true,
+ 'collapsed' => true,
+ 'fields' => [
+ [
+ 'handle' => 'meta_title',
+ 'field' => [
+ 'type' => 'text',
+ 'display' => 'Meta title',
+ ],
+ ],
+ [
+ 'import' => 'seo_defaults',
+ 'prefix' => 'seo_',
+ ],
+ ],
+ ],
+ ],
+ ], Facades\Fieldset::find('test')->contents());
+ }
+
+ #[Test]
+ public function fieldset_sections_are_removed_when_updating_with_flat_fields()
+ {
+ $user = tap(Facades\User::make()->makeSuper())->save();
+ $fieldset = (new Fieldset)->setHandle('test')->setContents([
+ 'title' => 'Test',
+ 'sections' => [
+ [
+ 'display' => 'SEO',
+ 'fields' => [
+ ['handle' => 'legacy', 'field' => ['type' => 'text']],
+ ],
+ ],
+ ],
+ ])->save();
+
+ $this
+ ->actingAs($user)
+ ->submit($fieldset, [
+ 'title' => 'Updated title',
+ 'fields' => [
+ [
+ '_id' => 'flat-1',
+ 'handle' => 'meta_title',
+ 'type' => 'inline',
+ 'config' => [
+ 'type' => 'text',
+ 'display' => 'Meta title',
+ ],
+ ],
+ ],
+ ])
+ ->assertStatus(204);
+
+ $this->assertEquals([
+ 'title' => 'Updated title',
+ 'fields' => [
+ [
+ 'handle' => 'meta_title',
+ 'field' => [
+ 'type' => 'text',
+ 'display' => 'Meta title',
+ ],
+ ],
+ ],
+ ], Facades\Fieldset::find('test')->contents());
+ }
+
+ #[Test]
+ public function import_section_behavior_is_saved_when_flattening_sections()
+ {
+ $user = tap(Facades\User::make()->makeSuper())->save();
+ (new Fieldset)->setHandle('seo_defaults')->setContents([
+ 'title' => 'SEO Defaults',
+ 'fields' => [
+ ['handle' => 'canonical_url', 'field' => ['type' => 'text']],
+ ],
+ ])->save();
+
+ $fieldset = (new Fieldset)->setHandle('test')->setContents([
+ 'title' => 'Test',
+ 'fields' => [],
+ ])->save();
+
+ $this
+ ->actingAs($user)
+ ->submit($fieldset, [
+ 'sections' => [
+ [
+ '_id' => 'section-1',
+ 'display' => 'Main',
+ 'fields' => [
+ [
+ '_id' => 'section-1-field-1',
+ 'type' => 'import',
+ 'fieldset' => 'seo_defaults',
+ 'section_behavior' => 'flatten',
+ ],
+ ],
+ ],
+ ],
+ ])
+ ->assertStatus(204);
+
+ $this->assertEquals([
+ 'title' => 'Updated',
+ 'sections' => [
+ [
+ 'display' => 'Main',
+ 'fields' => [
+ [
+ 'import' => 'seo_defaults',
+ 'section_behavior' => 'flatten',
+ ],
+ ],
+ ],
+ ],
+ ], Facades\Fieldset::find('test')->contents());
+ }
+
+ #[Test]
+ public function single_default_section_is_collapsed_back_to_flat_fields()
+ {
+ $user = tap(Facades\User::make()->makeSuper())->save();
+ $fieldset = (new Fieldset)->setHandle('test')->setContents([
+ 'title' => 'Test',
+ 'fields' => [],
+ ])->save();
+
+ $this
+ ->actingAs($user)
+ ->submit($fieldset, [
+ 'sections' => [
+ [
+ '_id' => 'section-1',
+ 'display' => 'Fields',
+ 'fields' => [
+ [
+ '_id' => 'section-1-field-1',
+ 'handle' => 'meta_title',
+ 'type' => 'inline',
+ 'config' => [
+ 'type' => 'text',
+ 'display' => 'Meta title',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ])
+ ->assertStatus(204);
+
+ $this->assertEquals([
+ 'title' => 'Updated',
+ 'fields' => [
+ [
+ 'handle' => 'meta_title',
+ 'field' => [
+ 'type' => 'text',
+ 'display' => 'Meta title',
+ ],
+ ],
+ ],
+ ], Facades\Fieldset::find('test')->contents());
+ }
+
#[Test]
public function title_is_required()
{
diff --git a/tests/Fields/FieldsetTest.php b/tests/Fields/FieldsetTest.php
index ff8fb006d08..559edcbee9f 100644
--- a/tests/Fields/FieldsetTest.php
+++ b/tests/Fields/FieldsetTest.php
@@ -117,6 +117,107 @@ public function it_gets_fields()
$this->assertEquals(['text', 'textarea'], $fields->map->type()->values()->all());
}
+ #[Test]
+ public function it_gets_fields_from_sections()
+ {
+ $fieldset = new Fieldset;
+
+ $fieldset->setContents([
+ 'sections' => [
+ [
+ 'display' => 'First section',
+ 'fields' => [
+ [
+ 'handle' => 'one',
+ 'field' => ['type' => 'text'],
+ ],
+ ],
+ ],
+ [
+ 'display' => 'Second section',
+ 'fields' => [
+ [
+ 'handle' => 'two',
+ 'field' => ['type' => 'textarea'],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $fields = $fieldset->fields();
+
+ $this->assertTrue($fieldset->hasSections());
+ $this->assertCount(2, $fieldset->sections());
+ $this->assertInstanceOf(Fields::class, $fields);
+ $this->assertEveryItemIsInstanceOf(Field::class, $fields = $fields->all());
+ $this->assertEquals(['one', 'two'], $fields->map->handle()->values()->all());
+ $this->assertEquals(['text', 'textarea'], $fields->map->type()->values()->all());
+ }
+
+ #[Test]
+ public function it_drops_empty_sections_when_storing_flat_fields()
+ {
+ $fieldset = new Fieldset;
+
+ $fieldset->setContents([
+ 'title' => 'Test',
+ 'sections' => [],
+ 'fields' => [
+ ['handle' => 'one', 'field' => ['type' => 'text']],
+ ],
+ ]);
+
+ $this->assertArrayNotHasKey('sections', $fieldset->contents());
+ $this->assertEquals('one', $fieldset->fields()->all()->first()->handle());
+ }
+
+ #[Test]
+ public function it_drops_top_level_fields_when_storing_sections()
+ {
+ $fieldset = new Fieldset;
+
+ $fieldset->setContents([
+ 'sections' => [
+ [
+ 'display' => 'A',
+ 'fields' => [
+ ['handle' => 'one', 'field' => ['type' => 'text']],
+ ],
+ ],
+ ],
+ 'fields' => [
+ ['handle' => 'stale', 'field' => ['type' => 'text']],
+ ],
+ ]);
+
+ $this->assertArrayNotHasKey('fields', $fieldset->contents());
+ $this->assertEquals(['one'], $fieldset->fields()->all()->keys()->all());
+ }
+
+ #[Test]
+ public function it_normalizes_legacy_section_fields_syntax()
+ {
+ $fieldset = new Fieldset;
+
+ $fieldset->setContents([
+ 'sections' => [
+ [
+ 'display' => 'First section',
+ 'fields' => [
+ 'one' => ['type' => 'text'],
+ 'two' => ['type' => 'textarea'],
+ ],
+ ],
+ ],
+ ]);
+
+ $fields = $fieldset->fields()->all();
+
+ $this->assertEquals(['one', 'two'], $fields->map->handle()->values()->all());
+ $this->assertEquals(['text', 'textarea'], $fields->map->type()->values()->all());
+ }
+
#[Test]
public function it_gets_fields_using_legacy_syntax()
{
@@ -445,6 +546,32 @@ public function gets_fieldsets_importing_single_field_from_fieldset()
$this->assertEquals($fieldsetA->handle(), $importedBy['fieldsets']->first()->handle());
}
+ #[Test]
+ public function gets_fieldsets_importing_fieldset_inside_sections()
+ {
+ $fieldset = Fieldset::make('seo')->setContents(['fields' => [
+ ['handle' => 'meta_title', 'field' => ['type' => 'text']],
+ ]])->save();
+
+ $fieldsetA = Fieldset::make('one')
+ ->setContents([
+ 'sections' => [
+ [
+ 'display' => 'SEO',
+ 'fields' => [
+ ['import' => 'seo'],
+ ],
+ ],
+ ],
+ ])
+ ->save();
+
+ $importedBy = $fieldset->importedBy();
+
+ $this->assertCount(1, $importedBy['fieldsets']);
+ $this->assertEquals($fieldsetA->handle(), $importedBy['fieldsets']->first()->handle());
+ }
+
#[Test]
public function it_saves_through_the_repository()
{
diff --git a/tests/Fields/TabTest.php b/tests/Fields/TabTest.php
index 69094f5abb1..c26e642a433 100644
--- a/tests/Fields/TabTest.php
+++ b/tests/Fields/TabTest.php
@@ -4,8 +4,10 @@
use Facades\Statamic\Fields\FieldRepository;
use PHPUnit\Framework\Attributes\Test;
+use Statamic\Facades\Fieldset as FieldsetRepository;
use Statamic\Fields\Field;
use Statamic\Fields\Fields;
+use Statamic\Fields\Fieldset;
use Statamic\Fields\Tab;
use Tests\TestCase;
@@ -194,4 +196,121 @@ public function converts_to_array_suitable_for_rendering_fields_in_publish_compo
],
], $tab->toPublishArray());
}
+
+ #[Test]
+ public function it_expands_sectioned_fieldset_imports_into_publish_sections()
+ {
+ FieldsetRepository::shouldReceive('find')
+ ->with('seo')
+ ->andReturn((new Fieldset)->setHandle('seo')->setContents([
+ 'sections' => [
+ [
+ 'display' => 'SEO',
+ 'fields' => [
+ ['handle' => 'meta_title', 'field' => ['type' => 'text']],
+ ],
+ ],
+ [
+ 'display' => 'Social',
+ 'fields' => [
+ ['handle' => 'og_title', 'field' => ['type' => 'text']],
+ ],
+ ],
+ ],
+ ]));
+
+ $tab = (new Tab('main'))->setContents([
+ 'sections' => [
+ [
+ 'display' => 'Main',
+ 'fields' => [
+ ['handle' => 'title', 'field' => ['type' => 'text']],
+ ['import' => 'seo'],
+ ['handle' => 'summary', 'field' => ['type' => 'textarea']],
+ ],
+ ],
+ ],
+ ]);
+
+ $publish = $tab->toPublishArray();
+
+ $this->assertCount(4, $publish['sections']);
+ $this->assertEquals('Main', $publish['sections'][0]['display']);
+ $this->assertEquals(['title'], collect($publish['sections'][0]['fields'])->pluck('handle')->all());
+ $this->assertEquals('SEO', $publish['sections'][1]['display']);
+ $this->assertEquals(['meta_title'], collect($publish['sections'][1]['fields'])->pluck('handle')->all());
+ $this->assertEquals('Social', $publish['sections'][2]['display']);
+ $this->assertEquals(['og_title'], collect($publish['sections'][2]['fields'])->pluck('handle')->all());
+ $this->assertEquals('Main', $publish['sections'][3]['display']);
+ $this->assertEquals(['summary'], collect($publish['sections'][3]['fields'])->pluck('handle')->all());
+ }
+
+ #[Test]
+ public function it_applies_prefixes_to_fields_inside_imported_fieldset_sections()
+ {
+ FieldsetRepository::shouldReceive('find')
+ ->with('seo')
+ ->andReturn((new Fieldset)->setHandle('seo')->setContents([
+ 'sections' => [
+ [
+ 'display' => 'SEO',
+ 'fields' => [
+ ['handle' => 'meta_title', 'field' => ['type' => 'text']],
+ ],
+ ],
+ ],
+ ]));
+
+ $tab = (new Tab('main'))->setContents([
+ 'sections' => [
+ [
+ 'fields' => [
+ ['import' => 'seo', 'prefix' => 'seo_'],
+ ],
+ ],
+ ],
+ ]);
+
+ $publish = $tab->toPublishArray();
+ $field = $publish['sections'][0]['fields'][0];
+
+ $this->assertEquals('seo_meta_title', $field['handle']);
+ $this->assertEquals('seo_', $field['prefix']);
+ }
+
+ #[Test]
+ public function it_can_flatten_imported_fieldset_sections_in_place()
+ {
+ FieldsetRepository::shouldReceive('find')
+ ->with('seo')
+ ->andReturn((new Fieldset)->setHandle('seo')->setContents([
+ 'sections' => [
+ [
+ 'display' => 'SEO',
+ 'fields' => [
+ ['handle' => 'meta_title', 'field' => ['type' => 'text']],
+ ],
+ ],
+ ],
+ ]));
+
+ $tab = (new Tab('main'))->setContents([
+ 'sections' => [
+ [
+ 'display' => 'Main',
+ 'fields' => [
+ ['handle' => 'title', 'field' => ['type' => 'text']],
+ ['import' => 'seo', 'section_behavior' => 'flatten'],
+ ['handle' => 'summary', 'field' => ['type' => 'textarea']],
+ ],
+ ],
+ ],
+ ]);
+
+ $publish = $tab->toPublishArray();
+
+ $this->assertCount(1, $publish['sections']);
+ $this->assertEquals('Main', $publish['sections'][0]['display']);
+ $this->assertEquals(['title', 'meta_title', 'summary'], collect($publish['sections'][0]['fields'])->pluck('handle')->all());
+ }
}