Skip to content
Open
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
12 changes: 11 additions & 1 deletion resources/js/components/fieldtypes/DictionaryFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,17 @@ export default {
},

normalizedOptions() {
return this.normalizeInputOptions(this.options);
const options = this.normalizeInputOptions(this.options);

// Multi-select renders its selections from the slot below, so the options can be left alone.
if (this.multiple) return options;

// In single mode the Combobox resolves the selected label from these options, and the fetched
// options may not include the stored value (API-backed dictionaries only return a page of
// results), so merge the selected options in or it would display the raw value instead.
const values = new Set(options.map((option) => option.value));

return [...options, ...this.selectedOptions.filter((option) => !values.has(option.value))];
},

selectedOptions() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { mount, flushPromises } from '@vue/test-utils';
import { describe, expect, test, vi } from 'vitest';

window.__ = (key) => key;
window.__n = (key) => key;
window.utf8btoa = (string) => btoa(string);

import DictionaryFieldtype from '@/components/fieldtypes/DictionaryFieldtype.vue';

function mountFieldtype({ value, maxItems, selectedOptions, fetchedOptions, shallow = false }) {
return mount(DictionaryFieldtype, {
shallow,
props: {
handle: 'country',
value,
config: { max_items: maxItems },
meta: { url: '/cp/fieldtypes/dictionaries/partial_countries', selectedOptions },
},
global: {
mocks: {
$axios: { get: vi.fn(() => Promise.resolve({ data: { data: fetchedOptions } })) },
},
},
});
}

describe('DictionaryFieldtype options', () => {
// API-backed dictionaries only return a page of options, which may not include the stored value.
// The preloaded selected options must be merged into the options passed to the Combobox so it can
// resolve their labels — otherwise a single-select field displays the raw id instead.
// https://github.com/statamic/cms/issues/14835
test('single-select renders the label of a stored value missing from the fetched options', async () => {
const fieldtype = mountFieldtype({
value: 'de',
maxItems: 1,
selectedOptions: [{ value: 'de', label: 'Germany', invalid: false }],
fetchedOptions: [],
});
await flushPromises();

const selected = fieldtype.find('[data-ui-combobox-selected-option]');
expect(selected.text()).toContain('Germany');
expect(selected.text()).not.toContain('de');
});

test('selected options already present in the fetched options are not duplicated', async () => {
const fieldtype = mountFieldtype({
value: 'de',
maxItems: 1,
selectedOptions: [{ value: 'de', label: 'Germany', invalid: false }],
fetchedOptions: [{ value: 'de', label: 'Germany' }],
shallow: true,
});
await flushPromises();

expect(fieldtype.vm.normalizedOptions).toEqual([{ value: 'de', label: 'Germany' }]);
});

test('multi-select options are not polluted with selected options', async () => {
const fieldtype = mountFieldtype({
value: ['de'],
maxItems: null,
selectedOptions: [{ value: 'de', label: 'Germany', invalid: false }],
fetchedOptions: [{ value: 'ca', label: 'Canada' }],
shallow: true,
});
await flushPromises();

expect(fieldtype.vm.normalizedOptions).toEqual([{ value: 'ca', label: 'Canada' }]);
});
});
Loading