diff --git a/resources/js/components/blueprints/Fields.vue b/resources/js/components/blueprints/Fields.vue index 6d48bf3759a..5e0606befb3 100644 --- a/resources/js/components/blueprints/Fields.vue +++ b/resources/js/components/blueprints/Fields.vue @@ -128,16 +128,16 @@ export default { return field.type === 'import' ? 'ImportField' : 'RegularField'; }, - fieldtypeSelected(field) { + fieldtypeSelected({ config, icon }) { this.isSelectingNewFieldtype = false; const pending = { _id: uniqid(), type: 'inline', - fieldtype: field.type, - icon: field.icon, + fieldtype: config.type, + icon, config: { - ...field, + ...config, isNew: true, }, }; diff --git a/resources/js/components/fields/FieldtypeSelector.vue b/resources/js/components/fields/FieldtypeSelector.vue index ae835d7ad8e..00c510cb289 100644 --- a/resources/js/components/fields/FieldtypeSelector.vue +++ b/resources/js/components/fields/FieldtypeSelector.vue @@ -252,9 +252,7 @@ export default { return this.selectMeta(selection); } - const field = this.createField(selection.value); - - this.$emit('selected', field); + this.$emit('selected', this.createField(selection.value)); this.close(); }, @@ -265,19 +263,21 @@ export default { fieldtype = 'text'; } - let field = this.createField(fieldtype); - - field = Object.assign( - { - display: __(`cp.${selection.value}`), - handle: selection.value, - type: fieldtype, - isMeta: true, - }, - field, - ); + const { config, icon } = this.createField(fieldtype); + + this.$emit('selected', { + icon, + config: Object.assign( + { + display: __(`cp.${selection.value}`), + handle: selection.value, + type: fieldtype, + isMeta: true, + }, + config, + ), + }); - this.$emit('selected', field); this.close(); }, @@ -291,7 +291,6 @@ export default { type: fieldtype.handle, display: __(':title Field', { title: fieldtype.title }), handle: null, // The handle will be generated from the display by the "slug" fieldtype. - icon: fieldtype.icon, instructions: null, localizable: false, width: 100, @@ -307,8 +306,15 @@ export default { defaults[configField.handle] = configField.default || null; }); - // Smoosh the field together with the defaults. - return Object.assign(defaults, field); + // The icon is kept alongside the config rather than inside it. It belongs to the + // fieldtype, not the field, and would otherwise clobber the default value of a + // config field that happens to be handled "icon". + return { + icon: fieldtype.icon, + + // Smoosh the field together with the defaults. + config: Object.assign(defaults, field), + }; }, close() { diff --git a/resources/js/tests/components/FieldtypeSelector.test.js b/resources/js/tests/components/FieldtypeSelector.test.js new file mode 100644 index 00000000000..2e4f0cd2d92 --- /dev/null +++ b/resources/js/tests/components/FieldtypeSelector.test.js @@ -0,0 +1,106 @@ +import { flushPromises, mount, shallowMount } from '@vue/test-utils'; +import { expect, test } from 'vitest'; +import * as Globals from '@/bootstrap/globals'; +import FieldtypeSelector from '@/components/fields/FieldtypeSelector.vue'; +import Fields from '@/components/blueprints/Fields.vue'; + +Object.keys(Globals).forEach((fn) => (window[fn] = Globals[fn])); +window.__ = (key) => key; +window.cp_url = (url) => url; + +window.Statamic = { + $config: { get: (key) => (key === 'sites' ? [{ handle: 'default' }] : undefined) }, + $commandPalette: { add: () => {}, category: { Actions: 'actions' } }, + $toast: { success: () => {} }, +}; + +const fieldtypes = [ + { + handle: 'test', + title: 'Test', + icon: 'test-fieldtype-icon', + categories: ['special'], + keywords: [], + config: [ + { handle: 'icon', default: 'default-icon' }, + { handle: 'foo', default: 'bar' }, + ], + }, + { + handle: 'text', + title: 'Text', + icon: 'text-fieldtype-icon', + categories: ['text'], + keywords: [], + config: [], + }, +]; + +async function mountSelector() { + const wrapper = mount(FieldtypeSelector, { + props: { allowTitle: true }, + global: { + mocks: { + $axios: { get: () => Promise.resolve({ data: fieldtypes }) }, + $config: { get: () => undefined }, + $toast: { error: () => {} }, + }, + stubs: { + 'ui-input': true, + 'ui-panel': true, + 'ui-panel-header': true, + 'ui-description': true, + 'ui-icon': true, + }, + }, + }); + + await flushPromises(); + + return wrapper; +} + +test('the fieldtype icon is emitted alongside the config, leaving an "icon" config field intact', async () => { + const wrapper = await mountSelector(); + + wrapper.vm.select({ value: 'test' }); + + const [{ icon, config }] = wrapper.emitted('selected')[0]; + + expect(icon).toBe('test-fieldtype-icon'); + expect(config.type).toBe('test'); + expect(config.icon).toBe('default-icon'); + expect(config.foo).toBe('bar'); +}); + +test('meta fields also emit the icon alongside the config', async () => { + const wrapper = await mountSelector(); + + wrapper.vm.select({ value: 'title', isMeta: true }); + + const [{ icon, config }] = wrapper.emitted('selected')[0]; + + expect(icon).toBe('text-fieldtype-icon'); + expect(config.isMeta).toBe(true); + expect(config.type).toBe('text'); +}); + +test('the fieldtype icon does not leak into the created field config', () => { + const wrapper = shallowMount(Fields, { + props: { fields: [] }, + global: { + mocks: { $toast: { success: () => {} } }, + }, + }); + + wrapper.vm.fieldtypeSelected({ + icon: 'test-fieldtype-icon', + config: { type: 'test', icon: 'default-icon' }, + }); + + const pending = wrapper.vm.pendingCreatedField; + + expect(pending.icon).toBe('test-fieldtype-icon'); + expect(pending.fieldtype).toBe('test'); + expect(pending.config.icon).toBe('default-icon'); +}); diff --git a/src/Fields/FieldTransformer.php b/src/Fields/FieldTransformer.php index ab992143676..36d04352102 100644 --- a/src/Fields/FieldTransformer.php +++ b/src/Fields/FieldTransformer.php @@ -36,7 +36,7 @@ private static function inlineTabField(array $submitted) $field = collect($submitted['config']) ->reject(function ($value, $key) use ($fields) { - if (in_array($key, ['isNew', 'icon'])) { + if ($key === 'isNew') { return true; } diff --git a/tests/Fields/FieldTransformerTest.php b/tests/Fields/FieldTransformerTest.php index c594161bf70..442ffffc296 100644 --- a/tests/Fields/FieldTransformerTest.php +++ b/tests/Fields/FieldTransformerTest.php @@ -115,6 +115,35 @@ public function configFieldItems(): array ], $fromVue['field']); } + #[Test] + public function a_fieldtype_can_have_an_icon_config_field() + { + $fieldtype = new class extends Fieldtype + { + protected static $handle = 'test'; + + public function configFieldItems(): array + { + return [ + 'icon' => ['type' => 'text', 'default' => 'default-icon'], + ]; + } + }; + $fieldtype::register(); + + $fromVue = FieldTransformer::fromVue([ + 'fieldtype' => 'test', + 'handle' => 'test', + 'type' => 'inline', + 'config' => [ + 'icon' => 'chosen-icon', + 'foo' => 'bar', + ], + ]); + + $this->assertEquals(['icon' => 'chosen-icon', 'foo' => 'bar'], $fromVue['field']); + } + #[Test] public function it_removes_full_width_from_field_config() {