From 0b854030a495d73438ac14963eaf59c0e212dc17 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 9 Aug 2024 21:06:20 +0100 Subject: [PATCH] [5.x] Adjust behavior of array fields (#10467) Co-authored-by: duncanmcclean Co-authored-by: Jason Varga --- .../components/fieldtypes/ArrayFieldtype.vue | 8 +- .../fieldtypes/ButtonGroupFieldtype.vue | 2 +- .../fieldtypes/CheckboxesFieldtype.vue | 2 +- .../components/fieldtypes/HasInputOptions.js | 22 +- .../components/fieldtypes/RadioFieldtype.vue | 2 +- .../components/fieldtypes/SelectFieldtype.vue | 4 +- .../js/tests/NormalizeInputOptions.test.js | 57 ++ resources/lang/en/fieldtypes.php | 1 + src/Fieldtypes/Arr.php | 84 +- src/Fieldtypes/ButtonGroup.php | 1 + src/Fieldtypes/Checkboxes.php | 1 + src/Fieldtypes/HasSelectOptions.php | 29 + src/Fieldtypes/Radio.php | 1 + src/Fieldtypes/Select.php | 1 + .../CP/Assets/AssetContainersController.php | 2 +- tests/Fieldtypes/ArrayTest.php | 749 ++++++++++++++++++ tests/Fieldtypes/ButtonGroupTest.php | 2 +- tests/Fieldtypes/CheckboxesTest.php | 2 +- tests/Fieldtypes/DictionaryFieldsTest.php | 18 +- tests/Fieldtypes/HasSelectOptionsTests.php | 53 ++ tests/Fieldtypes/RadioTest.php | 2 +- tests/Fieldtypes/SelectTest.php | 2 +- tests/Fieldtypes/WidthTest.php | 19 + 23 files changed, 1030 insertions(+), 34 deletions(-) create mode 100644 resources/js/tests/NormalizeInputOptions.test.js create mode 100644 tests/Fieldtypes/ArrayTest.php create mode 100644 tests/Fieldtypes/HasSelectOptionsTests.php create mode 100644 tests/Fieldtypes/WidthTest.php diff --git a/resources/js/components/fieldtypes/ArrayFieldtype.vue b/resources/js/components/fieldtypes/ArrayFieldtype.vue index b74a63f8be..b62dd7dc98 100644 --- a/resources/js/components/fieldtypes/ArrayFieldtype.vue +++ b/resources/js/components/fieldtypes/ArrayFieldtype.vue @@ -7,7 +7,7 @@ @@ -130,7 +130,7 @@ export default { computed: { isKeyed() { - return Boolean(Object.keys(this.config.keys).length); + return Boolean(Object.keys(this.meta.keys).length); }, isDynamic() { @@ -142,7 +142,7 @@ export default { }, keyedData() { - return this.data.filter(element => this.config.keys.hasOwnProperty(element.key)); + return this.data.filter(element => this.meta.keys.hasOwnProperty(element.key)); }, maxItems() { diff --git a/resources/js/components/fieldtypes/ButtonGroupFieldtype.vue b/resources/js/components/fieldtypes/ButtonGroupFieldtype.vue index d748e9d695..246833595f 100644 --- a/resources/js/components/fieldtypes/ButtonGroupFieldtype.vue +++ b/resources/js/components/fieldtypes/ButtonGroupFieldtype.vue @@ -39,7 +39,7 @@ export default { computed: { options() { - return this.normalizeInputOptions(this.config.options); + return this.normalizeInputOptions(this.meta.options); }, replicatorPreview() { diff --git a/resources/js/components/fieldtypes/CheckboxesFieldtype.vue b/resources/js/components/fieldtypes/CheckboxesFieldtype.vue index a1c9eb959e..93f35eaa65 100644 --- a/resources/js/components/fieldtypes/CheckboxesFieldtype.vue +++ b/resources/js/components/fieldtypes/CheckboxesFieldtype.vue @@ -30,7 +30,7 @@ export default { computed: { options() { - return this.normalizeInputOptions(this.config.options); + return this.normalizeInputOptions(this.meta.options); }, replicatorPreview() { diff --git a/resources/js/components/fieldtypes/HasInputOptions.js b/resources/js/components/fieldtypes/HasInputOptions.js index 44125bf0c3..e2e6e3b176 100644 --- a/resources/js/components/fieldtypes/HasInputOptions.js +++ b/resources/js/components/fieldtypes/HasInputOptions.js @@ -1,10 +1,26 @@ export default { methods: { normalizeInputOptions(options) { - return _.map(options, (value, key) => { + if (! Array.isArray(options)) { + return _.map(options, (value, key) => { + return { + 'value': Array.isArray(options) ? value : key, + 'label': __(value) || key + }; + }); + } + + return _.map(options, (option) => { + if (typeof option === 'object') { + return { + 'value': option.value, + 'label': __(option.label) || option.value + }; + } + return { - 'value': Array.isArray(options) ? value : key, - 'label': __(value) || key + 'value': option, + 'label': __(option) || option }; }); } diff --git a/resources/js/components/fieldtypes/RadioFieldtype.vue b/resources/js/components/fieldtypes/RadioFieldtype.vue index 2547a20f11..a9abd770ea 100644 --- a/resources/js/components/fieldtypes/RadioFieldtype.vue +++ b/resources/js/components/fieldtypes/RadioFieldtype.vue @@ -48,7 +48,7 @@ export default { computed: { options() { - return this.normalizeInputOptions(this.config.options); + return this.normalizeInputOptions(this.meta.options); }, replicatorPreview() { diff --git a/resources/js/components/fieldtypes/SelectFieldtype.vue b/resources/js/components/fieldtypes/SelectFieldtype.vue index c2e5319fec..1a06a08d76 100644 --- a/resources/js/components/fieldtypes/SelectFieldtype.vue +++ b/resources/js/components/fieldtypes/SelectFieldtype.vue @@ -96,7 +96,7 @@ export default { computed: { selectedOptions() { - let selections = this.value || []; + let selections = this.value === null ? [] : this.value; if (typeof selections === 'string' || typeof selections === 'number') { selections = [selections]; } @@ -106,7 +106,7 @@ export default { }, options() { - return this.normalizeInputOptions(this.config.options); + return this.normalizeInputOptions(this.meta.options); }, replicatorPreview() { diff --git a/resources/js/tests/NormalizeInputOptions.test.js b/resources/js/tests/NormalizeInputOptions.test.js new file mode 100644 index 0000000000..525c35c99c --- /dev/null +++ b/resources/js/tests/NormalizeInputOptions.test.js @@ -0,0 +1,57 @@ +import _ from 'underscore'; +import { __ } from '../bootstrap/globals'; +import hasInputOptions from "../components/fieldtypes/HasInputOptions"; +const normalizeInputOptions = hasInputOptions.methods.normalizeInputOptions; + +window._ = _; +window.__ = __; + +const config = { + translations: { + '*.One': 'Uno' + } +} + +window.Statamic = { + $config: { + get: (key) => config[key] + } +} + +it('normalizes input options with simple array', () => { + expect(normalizeInputOptions([ + 'one', + 'two' + ])).toEqual([ + {value: 'one', label: 'one'}, + {value: 'two', label: 'two'} + ]); + + expect(normalizeInputOptions([ + 'One', + 'Two' + ])).toEqual([ + {value: 'One', label: 'Uno'}, + {value: 'Two', label: 'Two'} + ]); +}); + +it('normalizes input options with object', () => { + expect(normalizeInputOptions({ + one: 'One', + two: 'Two' + })).toEqual([ + {value: 'one', label: 'Uno'}, + {value: 'two', label: 'Two'} + ]); +}); + +it('normalizes input options with array of objects', () => { + expect(normalizeInputOptions([ + {value: 'one', label: 'One'}, + {value: 'two', label: 'Two'} + ])).toEqual([ + {value: 'one', label: 'Uno'}, + {value: 'two', label: 'Two'} + ]); +}); diff --git a/resources/lang/en/fieldtypes.php b/resources/lang/en/fieldtypes.php index 604d7029e2..c1263a4240 100644 --- a/resources/lang/en/fieldtypes.php +++ b/resources/lang/en/fieldtypes.php @@ -4,6 +4,7 @@ 'any.config.antlers' => "Enable Antlers parsing in this field's content.", 'any.config.cast_booleans' => 'Options with values of true and false will be saved as booleans.', 'any.config.mode' => 'Choose your preferred UI style.', + 'array.config.expand' => 'Whether to save the array in the expanded format. Use this if you intend to have numeric values.', 'array.config.keys' => 'Set the array keys (variables) and optional labels.', 'array.config.mode' => 'The **dynamic** mode gives the user free control of the data, while **keyed** and **single** modes enforce strict keys.', 'array.title' => 'Array', diff --git a/src/Fieldtypes/Arr.php b/src/Fieldtypes/Arr.php index 74387e22bb..c269f6de50 100644 --- a/src/Fieldtypes/Arr.php +++ b/src/Fieldtypes/Arr.php @@ -6,6 +6,7 @@ use Statamic\Facades\GraphQL; use Statamic\Fields\Fieldtype; use Statamic\GraphQL\Types\ArrayType; +use Statamic\Support\Arr as SupportArr; class Arr extends Fieldtype { @@ -33,6 +34,7 @@ protected function configFieldItems(): array 'display' => __('Keys'), 'instructions' => __('statamic::fieldtypes.array.config.keys'), 'type' => 'array', + 'expand' => true, 'key_header' => __('Key'), 'value_header' => __('Label').' ('.__('Optional').')', 'add_button' => __('Add Key'), @@ -40,14 +42,57 @@ protected function configFieldItems(): array 'mode' => 'dynamic', ], ], + 'expand' => [ + 'type' => 'toggle', + 'display' => __('Expand'), + 'instructions' => __('statamic::fieldtypes.array.config.expand'), + ], ], ], ]; } + public function preload(): array + { + return [ + 'keys' => $this->keys()->mapWithKeys(fn ($item) => [$item['key'] => $item['value']]), + ]; + } + + private function keys() + { + return collect($this->config('keys'))->map(fn ($value, $key) => [ + 'key' => is_array($value) ? $value['key'] : $key, + 'value' => is_array($value) ? $value['value'] : $value, + ])->values(); + } + public function preProcess($data) { - return array_replace($this->blankKeyed(), $data ?? []); + if ($this->isKeyed()) { + $isMulti = is_array(SupportArr::first($data)); + + return $this->keys()->mapWithKeys(function ($item) use ($isMulti, $data) { + $key = $item['key']; + + $value = $isMulti + ? collect($data)->where('key', $key)->pluck('value')->first() + : $data[$key] ?? null; + + return [$key => $value]; + })->all(); + } + + // When using the legacy format, return the data as is. + if (! is_array(SupportArr::first($data))) { + return $data ?? []; + } + + return collect($data) + ->mapWithKeys(fn ($item) => [ + (string) $item['key'] => $item['value'], + ]) + ->all(); } public function preProcessConfig($data) @@ -57,11 +102,18 @@ public function preProcessConfig($data) public function process($data) { - return collect($data) - ->when($this->isKeyed(), function ($data) { - return $data->filter(); - }) - ->all(); + if (empty($data)) { + return null; + } + + if ($this->config('expand')) { + return collect($data) + ->map(fn ($value, $key) => ['key' => $key, 'value' => $value]) + ->values() + ->all(); + } + + return $data; } protected function isKeyed() @@ -69,15 +121,6 @@ protected function isKeyed() return (bool) $this->config('keys'); } - protected function blankKeyed() - { - return collect($this->config('keys')) - ->map(function () { - return null; - }) - ->all(); - } - public function toGqlType() { return GraphQL::type(ArrayType::NAME); @@ -97,4 +140,15 @@ public function rules(): array } }]; } + + public function augment($value) + { + if (is_array(SupportArr::first($value))) { + return collect($value) + ->mapWithKeys(fn ($item) => [$item['key'] => $item['value']]) + ->all(); + } + + return $value; + } } diff --git a/src/Fieldtypes/ButtonGroup.php b/src/Fieldtypes/ButtonGroup.php index f071746b20..318b138167 100644 --- a/src/Fieldtypes/ButtonGroup.php +++ b/src/Fieldtypes/ButtonGroup.php @@ -21,6 +21,7 @@ protected function configFieldItems(): array 'display' => __('Options'), 'instructions' => __('statamic::fieldtypes.radio.config.options'), 'type' => 'array', + 'expand' => true, 'value_header' => __('Label').' ('.__('Optional').')', 'add_button' => __('Add Option'), ], diff --git a/src/Fieldtypes/Checkboxes.php b/src/Fieldtypes/Checkboxes.php index b62b16652d..1bd7843856 100644 --- a/src/Fieldtypes/Checkboxes.php +++ b/src/Fieldtypes/Checkboxes.php @@ -22,6 +22,7 @@ protected function configFieldItems(): array 'display' => __('Options'), 'instructions' => __('statamic::fieldtypes.checkboxes.config.options'), 'type' => 'array', + 'expand' => true, 'field' => [ 'type' => 'text', ], diff --git a/src/Fieldtypes/HasSelectOptions.php b/src/Fieldtypes/HasSelectOptions.php index a5068e5863..5f21068388 100644 --- a/src/Fieldtypes/HasSelectOptions.php +++ b/src/Fieldtypes/HasSelectOptions.php @@ -14,6 +14,35 @@ protected function multiple() return $this->config('multiple'); } + public function preload(): array + { + return [ + 'options' => $this->getOptions(), + ]; + } + + protected function getOptions(): array + { + $options = $this->config('options') ?? []; + + if (array_is_list($options) && ! is_array(Arr::first($options))) { + $options = collect($options) + ->map(fn ($value) => ['key' => $value, 'value' => $value]) + ->all(); + } + + if (Arr::isAssoc($options)) { + $options = collect($options) + ->map(fn ($value, $key) => ['key' => $key, 'value' => $value]) + ->all(); + } + + return collect($options) + ->map(fn ($item) => ['value' => $item['key'], 'label' => $item['value']]) + ->values() + ->all(); + } + public function preProcessIndex($value) { $values = $this->preProcess($value); diff --git a/src/Fieldtypes/Radio.php b/src/Fieldtypes/Radio.php index de6344888b..79ad94fc17 100644 --- a/src/Fieldtypes/Radio.php +++ b/src/Fieldtypes/Radio.php @@ -22,6 +22,7 @@ protected function configFieldItems(): array 'display' => __('Options'), 'instructions' => __('statamic::fieldtypes.radio.config.options'), 'type' => 'array', + 'expand' => true, 'field' => [ 'type' => 'text', ], diff --git a/src/Fieldtypes/Select.php b/src/Fieldtypes/Select.php index 8e0d45f3b6..dc3b215a85 100644 --- a/src/Fieldtypes/Select.php +++ b/src/Fieldtypes/Select.php @@ -22,6 +22,7 @@ protected function configFieldItems(): array 'display' => __('Options'), 'instructions' => __('statamic::fieldtypes.select.config.options'), 'type' => 'array', + 'expand' => true, 'key_header' => __('Key'), 'value_header' => __('Label').' ('.__('Optional').')', 'add_button' => __('Add Option'), diff --git a/src/Http/Controllers/CP/Assets/AssetContainersController.php b/src/Http/Controllers/CP/Assets/AssetContainersController.php index 86ed6351b9..28e1995fe0 100644 --- a/src/Http/Controllers/CP/Assets/AssetContainersController.php +++ b/src/Http/Controllers/CP/Assets/AssetContainersController.php @@ -317,7 +317,7 @@ private function expandedGlidePresetOptions() return collect(config('statamic.assets.image_manipulation.presets')) ->mapWithKeys(function ($params, $handle) { return [$handle => $this->expandedGlidePresetLabel($handle, $params)]; - }); + })->all(); } private function expandedGlidePresetLabel($handle, $params) diff --git a/tests/Fieldtypes/ArrayTest.php b/tests/Fieldtypes/ArrayTest.php new file mode 100644 index 0000000000..b94dd0bf29 --- /dev/null +++ b/tests/Fieldtypes/ArrayTest.php @@ -0,0 +1,749 @@ + 'array', 'keys' => $options]); + + $this->assertEquals($expected, $field->meta()['keys']->all()); + } + + public static function keyedPreloadProvider() + { + return [ + 'dynamic null' => [ + null, + [], + ], + 'dynamic empty array' => [ + [], + [], + ], + 'associative array options' => [ + [ + 'food' => 'Food', + 'drink' => 'Drink', + 'side' => 'Side', + ], + [ + 'food' => 'Food', + 'drink' => 'Drink', + 'side' => 'Side', + ], + ], + 'multidimensional array options' => [ + [ + ['key' => 'food', 'value' => 'Food'], + ['key' => 'drink', 'value' => 'Drink'], + ['key' => 'side', 'value' => 'Side'], + ], + [ + 'food' => 'Food', + 'drink' => 'Drink', + 'side' => 'Side', + ], + ], + 'multidimensional array with numbers' => [ + [ + ['key' => 0, 'value' => 'Zero'], + ['key' => 1, 'value' => 'One'], + ['key' => 2, 'value' => 'Two'], + ], + [ + 0 => 'Zero', + 1 => 'One', + 2 => 'Two', + ], + ], + 'multidimensional array with non-sequential numbers' => [ + [ + ['key' => 2, 'value' => 'Two'], + ['key' => 0, 'value' => 'Zero'], + ['key' => 1, 'value' => 'One'], + ], + [ + 2 => 'Two', + 0 => 'Zero', + 1 => 'One', + ], + ], + ]; + } + + #[Test] + #[DataProvider('dynamicPreprocessProvider')] + public function it_preprocesses_dynamic($value, $expected) + { + $field = new Field('test', ['type' => 'array']); + + $field->setValue($value); + + $this->assertEquals($expected, $field->preProcess()->value()); + } + + public static function dynamicPreprocessProvider() + { + return [ + 'associative array value' => [ + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'multidimensional array value' => [ + [ + ['key' => 'food', 'value' => 'burger'], + ['key' => 'drink', 'value' => 'coke'], + ['key' => 'side', 'value' => 'fries'], + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'multidimensional array with numbers' => [ + [ + ['key' => 0, 'value' => 'none'], + ['key' => 1, 'value' => 'some'], + ['key' => 2, 'value' => 'more'], + ], + [ + 0 => 'none', + 1 => 'some', + 2 => 'more', + ], + ], + 'multidimensional array with non-sequential numbers' => [ + [ + ['key' => 2, 'value' => 'some'], + ['key' => 1, 'value' => 'one'], + ['key' => 0, 'value' => 'none'], + ], + [ + 2 => 'some', + 1 => 'one', + 0 => 'none', + ], + ], + ]; + } + + #[Test] + #[DataProvider('keyedPreprocessProvider')] + public function it_preprocesses_keyed($options, $value, $expected) + { + $field = new Field('test', ['type' => 'array', 'keys' => $options]); + + $field->setValue($value); + + $this->assertEquals($expected, $field->preProcess()->value()); + } + + public static function keyedPreprocessProvider() + { + return [ + 'associative array options, associative array value' => [ + [ + 'food' => 'Food', + 'drink' => 'Drink', + 'side' => 'Side', + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'multidimensional array options, associative array value' => [ + [ + ['key' => 'food', 'value' => 'Food'], + ['key' => 'drink', 'value' => 'Drink'], + ['key' => 'side', 'value' => 'Side'], + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'associative array options, multidimensional array value' => [ + [ + 'food' => 'Food', + 'drink' => 'Drink', + 'side' => 'Side', + ], + [ + ['key' => 'food', 'value' => 'burger'], + ['key' => 'drink', 'value' => 'coke'], + ['key' => 'side', 'value' => 'fries'], + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'multidimensional array options, multidimensional array value' => [ + [ + ['key' => 'food', 'value' => 'Food'], + ['key' => 'drink', 'value' => 'Drink'], + ['key' => 'side', 'value' => 'Side'], + ], + [ + ['key' => 'food', 'value' => 'burger'], + ['key' => 'drink', 'value' => 'coke'], + ['key' => 'side', 'value' => 'fries'], + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'multidimensional array with numbers' => [ + [ + ['key' => 0, 'value' => 'Zero'], + ['key' => 1, 'value' => 'One'], + ['key' => 2, 'value' => 'Two'], + ], + [ + ['key' => 0, 'value' => 'none'], + ['key' => 1, 'value' => 'some'], + ['key' => 2, 'value' => 'more'], + ], + [ + 0 => 'none', + 1 => 'some', + 2 => 'more', + ], + ], + 'multidimensional array with non-sequential numbers' => [ + [ + ['key' => 0, 'value' => 'Zero'], + ['key' => 1, 'value' => 'One'], + ['key' => 2, 'value' => 'Two'], + ], + [ + ['key' => 2, 'value' => 'some'], + ['key' => 1, 'value' => 'one'], + ['key' => 0, 'value' => 'none'], + ], + [ + 2 => 'some', + 1 => 'one', + 0 => 'none', + ], + ], + ]; + } + + #[Test] + #[DataProvider('dynamicProcessProvider')] + public function it_processes_dynamic($expand, $value, $expected) + { + $field = new Field('test', ['type' => 'array', 'expand' => $expand]); + + $field->setValue($value); + + $this->assertEquals($expected, $field->process()->value()); + } + + public static function dynamicProcessProvider() + { + return [ + 'null' => [ + false, + null, + null, + ], + 'string keys' => [ + false, + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'string keys with expanded setting' => [ + true, + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + [ + ['key' => 'food', 'value' => 'burger'], + ['key' => 'drink', 'value' => 'coke'], + ['key' => 'side', 'value' => 'fries'], + ], + ], + 'numeric keys' => [ + false, + [ + 0 => 'none', + 1 => 'some', + 2 => 'more', + ], + [ + 0 => 'none', + 1 => 'some', + 2 => 'more', + ], + ], + 'numeric keys with expanded setting' => [ + true, + [ + 0 => 'none', + 1 => 'some', + 2 => 'more', + ], + [ + ['key' => 0, 'value' => 'none'], + ['key' => 1, 'value' => 'some'], + ['key' => 2, 'value' => 'more'], + ], + ], + 'non-sequential numeric keys' => [ + false, + [ + 2 => 'some', + 1 => 'one', + 0 => 'none', + ], + [ + 2 => 'some', + 1 => 'one', + 0 => 'none', + ], + ], + 'non-sequential numeric keys with expanded setting' => [ + true, + [ + 2 => 'some', + 1 => 'one', + 0 => 'none', + ], + [ + ['key' => 2, 'value' => 'some'], + ['key' => 1, 'value' => 'one'], + ['key' => 0, 'value' => 'none'], + ], + ], + 'strings and numeric keys' => [ + false, + [ + 'one' => 'One', + 2 => 'Two', + 'three' => 'Three', + ], + [ + 'one' => 'One', + 2 => 'Two', + 'three' => 'Three', + ], + ], + 'strings and numeric keys with expanded setting' => [ + true, + [ + 'one' => 'One', + 2 => 'Two', + 'three' => 'Three', + ], + [ + ['key' => 'one', 'value' => 'One'], + ['key' => 2, 'value' => 'Two'], + ['key' => 'three', 'value' => 'Three'], + ], + ], + ]; + } + + #[Test] + #[DataProvider('keyedProcessProvider')] + public function it_processes_keyed($expand, $options, $value, $expected) + { + $field = new Field('test', ['type' => 'array', 'keys' => $options, 'expand' => $expand]); + + $field->setValue($value); + + $this->assertEquals($expected, $field->process()->value()); + } + + public static function keyedProcessProvider() + { + return [ + 'null' => [ + false, + ['foo' => 'Foo'], + null, + null, + ], + 'associative array options, associative array value' => [ + false, + [ + 'food' => 'Food', + 'drink' => 'Drink', + 'side' => 'Side', + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'associative array options, associative array value with expanded setting' => [ + true, + [ + 'food' => 'Food', + 'drink' => 'Drink', + 'side' => 'Side', + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + [ + ['key' => 'food', 'value' => 'burger'], + ['key' => 'drink', 'value' => 'coke'], + ['key' => 'side', 'value' => 'fries'], + ], + ], + 'multidimensional array options, associative array value' => [ + false, + [ + ['key' => 'food', 'value' => 'Food'], + ['key' => 'drink', 'value' => 'Drink'], + ['key' => 'side', 'value' => 'Side'], + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'multidimensional array options, associative array value with expanded setting' => [ + true, + [ + ['key' => 'food', 'value' => 'Food'], + ['key' => 'drink', 'value' => 'Drink'], + ['key' => 'side', 'value' => 'Side'], + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + [ + ['key' => 'food', 'value' => 'burger'], + ['key' => 'drink', 'value' => 'coke'], + ['key' => 'side', 'value' => 'fries'], + ], + ], + 'multidimensional array with numbers' => [ + false, + [ + ['key' => 0, 'value' => 'Zero'], + ['key' => 1, 'value' => 'One'], + ['key' => 2, 'value' => 'Two'], + ], + [ + 0 => 'none', + 1 => 'some', + 2 => 'more', + ], + [ + 0 => 'none', + 1 => 'some', + 2 => 'more', + ], + ], + 'multidimensional array with numbers with expanded setting' => [ + true, + [ + ['key' => 0, 'value' => 'Zero'], + ['key' => 1, 'value' => 'One'], + ['key' => 2, 'value' => 'Two'], + ], + [ + 0 => 'none', + 1 => 'some', + 2 => 'more', + ], + [ + ['key' => 0, 'value' => 'none'], + ['key' => 1, 'value' => 'some'], + ['key' => 2, 'value' => 'more'], + ], + ], + 'multidimensional array with non-sequential numbers' => [ + false, + [ + ['key' => 0, 'value' => 'Zero'], + ['key' => 1, 'value' => 'One'], + ['key' => 2, 'value' => 'Two'], + ], + [ + 2 => 'some', + 1 => 'one', + 0 => 'none', + ], + [ + 2 => 'some', + 1 => 'one', + 0 => 'none', + ], + ], + 'multidimensional array with non-sequential numbers with expanded setting' => [ + true, + [ + ['key' => 0, 'value' => 'Zero'], + ['key' => 1, 'value' => 'One'], + ['key' => 2, 'value' => 'Two'], + ], + [ + 2 => 'some', + 1 => 'one', + 0 => 'none', + ], + [ + ['key' => 2, 'value' => 'some'], + ['key' => 1, 'value' => 'one'], + ['key' => 0, 'value' => 'none'], + ], + ], + ]; + } + + #[Test] + #[DataProvider('dynamicAugmentProvider')] + public function it_augments_dynamic($value, $expected) + { + $fieldtype = (new Arr)->setField(new Field('test', ['type' => 'arr'])); + + $this->assertEquals($expected, $fieldtype->augment($value)); + } + + public static function dynamicAugmentProvider() + { + return [ + 'null' => [ + null, + null, + ], + 'associative array value' => [ + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'multidimensional array value' => [ + [ + ['key' => 'food', 'value' => 'burger'], + ['key' => 'drink', 'value' => 'coke'], + ['key' => 'side', 'value' => 'fries'], + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'multidimensional array with numbers' => [ + [ + ['key' => 0, 'value' => 'none'], + ['key' => 1, 'value' => 'some'], + ['key' => 2, 'value' => 'more'], + ], + [ + 0 => 'none', + 1 => 'some', + 2 => 'more', + ], + ], + 'multidimensional array with non-sequential numbers' => [ + [ + ['key' => 2, 'value' => 'some'], + ['key' => 1, 'value' => 'one'], + ['key' => 0, 'value' => 'none'], + ], + [ + 2 => 'some', + 1 => 'one', + 0 => 'none', + ], + ], + ]; + } + + #[Test] + #[DataProvider('keyedAugmentProvider')] + public function it_augments_keyed($options, $value, $expected) + { + $fieldtype = (new Arr)->setField(new Field('test', ['type' => 'arr', 'keys' => $options])); + + $this->assertEquals($expected, $fieldtype->augment($value)); + } + + public static function keyedAugmentProvider() + { + return [ + 'null' => [ + ['foo' => 'Foo'], + null, + null, + ], + 'associative array options, associative array value' => [ + [ + 'food' => 'Food', + 'drink' => 'Drink', + 'side' => 'Side', + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'multidimensional array options, associative array value' => [ + [ + ['key' => 'food', 'value' => 'Food'], + ['key' => 'drink', 'value' => 'Drink'], + ['key' => 'side', 'value' => 'Side'], + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'associative array options, multidimensional array value' => [ + [ + 'food' => 'Food', + 'drink' => 'Drink', + 'side' => 'Side', + ], + [ + ['key' => 'food', 'value' => 'burger'], + ['key' => 'drink', 'value' => 'coke'], + ['key' => 'side', 'value' => 'fries'], + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'multidimensional array options, multidimensional array value' => [ + [ + ['key' => 'food', 'value' => 'Food'], + ['key' => 'drink', 'value' => 'Drink'], + ['key' => 'side', 'value' => 'Side'], + ], + [ + ['key' => 'food', 'value' => 'burger'], + ['key' => 'drink', 'value' => 'coke'], + ['key' => 'side', 'value' => 'fries'], + ], + [ + 'food' => 'burger', + 'drink' => 'coke', + 'side' => 'fries', + ], + ], + 'multidimensional array with numbers' => [ + [ + ['key' => 0, 'value' => 'Zero'], + ['key' => 1, 'value' => 'One'], + ['key' => 2, 'value' => 'Two'], + ], + [ + ['key' => 0, 'value' => 'none'], + ['key' => 1, 'value' => 'some'], + ['key' => 2, 'value' => 'more'], + ], + [ + 0 => 'none', + 1 => 'some', + 2 => 'more', + ], + ], + 'multidimensional array with non-sequential numbers' => [ + [ + ['key' => 0, 'value' => 'Zero'], + ['key' => 1, 'value' => 'One'], + ['key' => 2, 'value' => 'Two'], + ], + [ + ['key' => 2, 'value' => 'some'], + ['key' => 1, 'value' => 'one'], + ['key' => 0, 'value' => 'none'], + ], + [ + 2 => 'some', + 1 => 'one', + 0 => 'none', + ], + ], + ]; + } +} diff --git a/tests/Fieldtypes/ButtonGroupTest.php b/tests/Fieldtypes/ButtonGroupTest.php index 329ea207be..5520047c3d 100644 --- a/tests/Fieldtypes/ButtonGroupTest.php +++ b/tests/Fieldtypes/ButtonGroupTest.php @@ -11,7 +11,7 @@ class ButtonGroupTest extends TestCase { - use CastsBooleansTests, LabeledValueTests; + use CastsBooleansTests, HasSelectOptionsTests, LabeledValueTests; private function field($config) { diff --git a/tests/Fieldtypes/CheckboxesTest.php b/tests/Fieldtypes/CheckboxesTest.php index a24315569f..d1e6ad26fa 100644 --- a/tests/Fieldtypes/CheckboxesTest.php +++ b/tests/Fieldtypes/CheckboxesTest.php @@ -8,7 +8,7 @@ class CheckboxesTest extends TestCase { - use CastsMultipleBooleansTests, MultipleLabeledValueTests; + use CastsMultipleBooleansTests, HasSelectOptionsTests, MultipleLabeledValueTests; private function field($config) { diff --git a/tests/Fieldtypes/DictionaryFieldsTest.php b/tests/Fieldtypes/DictionaryFieldsTest.php index 132204714c..2c49664183 100644 --- a/tests/Fieldtypes/DictionaryFieldsTest.php +++ b/tests/Fieldtypes/DictionaryFieldsTest.php @@ -30,7 +30,17 @@ public function it_returns_dictionary_fields_in_preload() 'fields' => [ ['handle' => 'type', 'type' => 'select'], ], - 'meta' => ['type' => null], + 'meta' => [ + 'type' => [ + 'options' => [ + ['value' => 'countries', 'label' => 'Countries'], + ['value' => 'currencies', 'label' => 'Currencies'], + ['value' => 'file', 'label' => 'File'], + ['value' => 'timezones', 'label' => 'Timezones'], + ['value' => 'fake_dictionary', 'label' => 'Fake Dictionary'], + ], + ], + ], ], ], $preload); @@ -39,7 +49,11 @@ public function it_returns_dictionary_fields_in_preload() 'fields' => [ ['handle' => 'category', 'type' => 'select'], ], - 'meta' => ['category' => null], + 'meta' => [ + 'category' => [ + 'options' => [], + ], + ], 'defaults' => ['category' => null], ], ], $preload['dictionaries']); diff --git a/tests/Fieldtypes/HasSelectOptionsTests.php b/tests/Fieldtypes/HasSelectOptionsTests.php new file mode 100644 index 0000000000..16719bf2cd --- /dev/null +++ b/tests/Fieldtypes/HasSelectOptionsTests.php @@ -0,0 +1,53 @@ +field(['options' => $options]); + + $this->assertArrayHasKey('options', $preloaded = $field->preload()); + $this->assertEquals($expected, $preloaded['options']); + } + + public static function optionsProvider() + { + return [ + 'list' => [ + ['one', 'two', 'three'], + [ + ['value' => 'one', 'label' => 'one'], + ['value' => 'two', 'label' => 'two'], + ['value' => 'three', 'label' => 'three'], + ], + ], + 'associative' => [ + ['one' => 'One', 'two' => 'Two', 'three' => 'Three'], + [ + ['value' => 'one', 'label' => 'One'], + ['value' => 'two', 'label' => 'Two'], + ['value' => 'three', 'label' => 'Three'], + ], + ], + 'multidimensional' => [ + [ + ['key' => 'one', 'value' => 'One'], + ['key' => 'two', 'value' => 'Two'], + ['key' => 'three', 'value' => 'Three'], + ], + [ + ['value' => 'one', 'label' => 'One'], + ['value' => 'two', 'label' => 'Two'], + ['value' => 'three', 'label' => 'Three'], + ], + ], + ]; + } +} diff --git a/tests/Fieldtypes/RadioTest.php b/tests/Fieldtypes/RadioTest.php index 4ec90c1ffa..e147acd5f8 100644 --- a/tests/Fieldtypes/RadioTest.php +++ b/tests/Fieldtypes/RadioTest.php @@ -8,7 +8,7 @@ class RadioTest extends TestCase { - use CastsBooleansTests, LabeledValueTests; + use CastsBooleansTests, HasSelectOptionsTests, LabeledValueTests; private function field($config) { diff --git a/tests/Fieldtypes/SelectTest.php b/tests/Fieldtypes/SelectTest.php index f019d6d015..3d9b236212 100644 --- a/tests/Fieldtypes/SelectTest.php +++ b/tests/Fieldtypes/SelectTest.php @@ -11,7 +11,7 @@ class SelectTest extends TestCase { - use CastsBooleansTests, CastsMultipleBooleansTests, LabeledValueTests, MultipleLabeledValueTests; + use CastsBooleansTests, CastsMultipleBooleansTests, HasSelectOptionsTests, LabeledValueTests, MultipleLabeledValueTests; private function field($config) { diff --git a/tests/Fieldtypes/WidthTest.php b/tests/Fieldtypes/WidthTest.php new file mode 100644 index 0000000000..b438e98805 --- /dev/null +++ b/tests/Fieldtypes/WidthTest.php @@ -0,0 +1,19 @@ +setField(new Field('test', array_merge($config, ['type' => $ft->handle()]))); + } +}