From 4c3c620e82dbc23f82c95fbb0b4727a2b593d60d Mon Sep 17 00:00:00 2001 From: Joshua Blum Date: Fri, 4 Sep 2026 16:56:02 +0200 Subject: [PATCH] Add info fieldtype --- lang/en/fieldtypes.php | 4 ++ resources/js/bootstrap/fieldtypes.js | 2 + .../components/fieldtypes/InfoFieldtype.vue | 29 +++++++++ .../fieldtypes/InfoFieldtype.test.js | 49 +++++++++++++++ src/Fieldtypes/Info.php | 60 +++++++++++++++++++ src/Providers/ExtensionServiceProvider.php | 1 + tests/Fieldtypes/InfoTest.php | 41 +++++++++++++ 7 files changed, 186 insertions(+) create mode 100644 resources/js/components/fieldtypes/InfoFieldtype.vue create mode 100644 resources/js/tests/components/fieldtypes/InfoFieldtype.test.js create mode 100644 src/Fieldtypes/Info.php create mode 100644 tests/Fieldtypes/InfoTest.php diff --git a/lang/en/fieldtypes.php b/lang/en/fieldtypes.php index 7a3c9e0d7e8..3b75a19f65e 100644 --- a/lang/en/fieldtypes.php +++ b/lang/en/fieldtypes.php @@ -117,6 +117,10 @@ 'icon.config.mode' => 'Choose a dropdown or compact picker.', 'icon.config.set' => 'Default CP icons, or a set registered with `Icons::register()`.', 'icon.title' => 'Icon', + 'info.config.content' => 'The information to display. Markdown is supported, including links and lists.', + 'info.config.icon' => 'Choose an icon, or leave empty to use the state\'s default icon.', + 'info.config.state' => 'Choose the visual importance of the information.', + 'info.title' => 'Info', 'integer.config.max' => 'The maximum allowed value.', 'integer.config.min' => 'The minimum allowed value.', 'integer.config.step' => 'The interval between valid numbers.', diff --git a/resources/js/bootstrap/fieldtypes.js b/resources/js/bootstrap/fieldtypes.js index 5cb99a0c35e..414809b79b7 100644 --- a/resources/js/bootstrap/fieldtypes.js +++ b/resources/js/bootstrap/fieldtypes.js @@ -34,6 +34,7 @@ import GroupFieldtype from '../components/fieldtypes/GroupFieldtype.vue'; import HiddenFieldtype from '../components/fieldtypes/HiddenFieldtype.vue'; import HtmlFieldtype from '../components/fieldtypes/HtmlFieldtype.vue'; import IconFieldtype from '../components/fieldtypes/IconFieldtype.vue'; +import InfoFieldtype from '../components/fieldtypes/InfoFieldtype.vue'; import IntegerFieldtype from '../components/fieldtypes/IntegerFieldtype.vue'; import LinkFieldtype from '../components/fieldtypes/LinkFieldtype.vue'; import LinkIndexFieldtype from '../components/fieldtypes/LinkIndexFieldtype.vue'; @@ -108,6 +109,7 @@ export default function registerFieldtypes(app) { app.component('hidden-fieldtype', HiddenFieldtype); app.component('html-fieldtype', HtmlFieldtype); app.component('icon-fieldtype', IconFieldtype); + app.component('info-fieldtype', InfoFieldtype); app.component('integer-fieldtype', IntegerFieldtype); app.component('link-fieldtype', LinkFieldtype); app.component('link-fieldtype-index', LinkIndexFieldtype); diff --git a/resources/js/components/fieldtypes/InfoFieldtype.vue b/resources/js/components/fieldtypes/InfoFieldtype.vue new file mode 100644 index 00000000000..466ae43584f --- /dev/null +++ b/resources/js/components/fieldtypes/InfoFieldtype.vue @@ -0,0 +1,29 @@ + + + diff --git a/resources/js/tests/components/fieldtypes/InfoFieldtype.test.js b/resources/js/tests/components/fieldtypes/InfoFieldtype.test.js new file mode 100644 index 00000000000..6a83b6edd24 --- /dev/null +++ b/resources/js/tests/components/fieldtypes/InfoFieldtype.test.js @@ -0,0 +1,49 @@ +import { mount } from '@vue/test-utils'; +import { describe, expect, test } from 'vitest'; +import InfoFieldtype from '@/components/fieldtypes/InfoFieldtype.vue'; +import { Icon } from '@/components/ui'; + +window.__ = (key) => key; + +function mountField(config = {}) { + return mount(InfoFieldtype, { + props: { + value: null, + handle: 'info', + config, + }, + }); +} + +describe.each([ + ['notice', 'default'], + ['tip', 'tip'], + ['warning', 'warning'], + ['important', 'error'], + ['success', 'success'], +])('%s state', (state, variant) => { + test(`uses the ${variant} alert variant`, () => { + const wrapper = mountField({ state, content: 'Something happened.' }); + + expect(wrapper.get('[data-ui-alert]').attributes('data-variant')).toBe(variant); + }); +}); + +test('renders sanitized markdown with links and lists', () => { + const wrapper = mountField({ + content: '- First item\n- [Statamic](https://statamic.com)\n\n', + }); + + expect(wrapper.findAll('li')).toHaveLength(2); + expect(wrapper.get('a').attributes()).toMatchObject({ + href: 'https://statamic.com', + target: '_blank', + }); + expect(wrapper.find('script').exists()).toBe(false); +}); + +test('uses a configured icon', () => { + const wrapper = mountField({ content: 'Hello', icon: 'lightbulb-idea' }); + + expect(wrapper.findComponent(Icon).props('name')).toBe('lightbulb-idea'); +}); diff --git a/src/Fieldtypes/Info.php b/src/Fieldtypes/Info.php new file mode 100644 index 00000000000..c982ddc2b8b --- /dev/null +++ b/src/Fieldtypes/Info.php @@ -0,0 +1,60 @@ + __('Content'), + 'fields' => [ + 'content' => [ + 'display' => __('Content'), + 'instructions' => __('statamic::fieldtypes.info.config.content'), + 'type' => 'textarea', + ], + ], + ], + [ + 'display' => __('Appearance'), + 'fields' => [ + 'state' => [ + 'display' => __('State'), + 'instructions' => __('statamic::fieldtypes.info.config.state'), + 'type' => 'select', + 'default' => 'notice', + 'options' => [ + 'notice' => __('Notice'), + 'tip' => __('Tip'), + 'warning' => __('Warning'), + 'important' => __('Important Warning'), + 'success' => __('Success'), + ], + 'width' => 50, + ], + 'icon' => [ + 'display' => __('Icon'), + 'instructions' => __('statamic::fieldtypes.info.config.icon'), + 'type' => 'icon', + 'set' => 'default', + 'mode' => 'compact', + 'width' => 50, + ], + ], + ], + ]; + } +} diff --git a/src/Providers/ExtensionServiceProvider.php b/src/Providers/ExtensionServiceProvider.php index b237ba4ae0d..8510e539540 100644 --- a/src/Providers/ExtensionServiceProvider.php +++ b/src/Providers/ExtensionServiceProvider.php @@ -91,6 +91,7 @@ class ExtensionServiceProvider extends ServiceProvider Fieldtypes\Hidden::class, Fieldtypes\Html::class, Fieldtypes\Icon::class, + Fieldtypes\Info::class, Fieldtypes\Integer::class, Fieldtypes\Link::class, Fieldtypes\Lists::class, diff --git a/tests/Fieldtypes/InfoTest.php b/tests/Fieldtypes/InfoTest.php new file mode 100644 index 00000000000..dc7f2f899ec --- /dev/null +++ b/tests/Fieldtypes/InfoTest.php @@ -0,0 +1,41 @@ +assertSame(['special'], $fieldtype->categories()); + $this->assertFalse($fieldtype->localizable()); + $this->assertFalse($fieldtype->validatable()); + $this->assertFalse($fieldtype->defaultable()); + } + + #[Test] + public function it_has_configurable_content_state_and_icon() + { + $fields = (new Info)->configFields(); + + $this->assertSame('textarea', $fields->get('content')->type()); + $this->assertSame('select', $fields->get('state')->type()); + $this->assertSame('notice', $fields->get('state')->get('default')); + $this->assertSame([ + 'notice' => 'Notice', + 'tip' => 'Tip', + 'warning' => 'Warning', + 'important' => 'Important Warning', + 'success' => 'Success', + ], $fields->get('state')->get('options')); + $this->assertSame('icon', $fields->get('icon')->type()); + $this->assertSame('default', $fields->get('icon')->get('set')); + $this->assertSame('compact', $fields->get('icon')->get('mode')); + } +}