Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions resources/js/components/blueprints/Fields.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
Expand Down
42 changes: 24 additions & 18 deletions resources/js/components/fields/FieldtypeSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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();
},

Expand All @@ -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();
},

Expand All @@ -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,
Expand All @@ -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() {
Expand Down
106 changes: 106 additions & 0 deletions resources/js/tests/components/FieldtypeSelector.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
2 changes: 1 addition & 1 deletion src/Fields/FieldTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
29 changes: 29 additions & 0 deletions tests/Fields/FieldTransformerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading