Skip to content

Commit 071c397

Browse files
mynetxclaude
andcommitted
Fix dictionary single-select showing the raw id instead of the label
The Combobox resolves selected labels from the options it's given, but an API-backed dictionary's options endpoint may not include the stored value, so a single-select field rendered the raw id until re-selected. Merge the preloaded selected options into the options passed to the Combobox so their labels always resolve. Multi-select wasn't affected since it renders its badges from the preloaded data directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5171897 commit 071c397

2 files changed

Lines changed: 82 additions & 1 deletion

File tree

resources/js/components/fieldtypes/DictionaryFieldtype.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,17 @@ export default {
8989
},
9090
9191
normalizedOptions() {
92-
return this.normalizeInputOptions(this.options);
92+
const options = this.normalizeInputOptions(this.options);
93+
94+
// Multi-select renders its selections from the slot below, so the options can be left alone.
95+
if (this.multiple) return options;
96+
97+
// In single mode the Combobox resolves the selected label from these options, and the fetched
98+
// options may not include the stored value (API-backed dictionaries only return a page of
99+
// results), so merge the selected options in or it would display the raw value instead.
100+
const values = new Set(options.map((option) => option.value));
101+
102+
return [...options, ...this.selectedOptions.filter((option) => !values.has(option.value))];
93103
},
94104
95105
selectedOptions() {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { mount, flushPromises } from '@vue/test-utils';
2+
import { describe, expect, test, vi } from 'vitest';
3+
4+
window.__ = (key) => key;
5+
window.__n = (key) => key;
6+
window.utf8btoa = (string) => btoa(string);
7+
8+
import DictionaryFieldtype from '@/components/fieldtypes/DictionaryFieldtype.vue';
9+
10+
function mountFieldtype({ value, maxItems, selectedOptions, fetchedOptions, shallow = false }) {
11+
return mount(DictionaryFieldtype, {
12+
shallow,
13+
props: {
14+
handle: 'country',
15+
value,
16+
config: { max_items: maxItems },
17+
meta: { url: '/cp/fieldtypes/dictionaries/partial_countries', selectedOptions },
18+
},
19+
global: {
20+
mocks: {
21+
$axios: { get: vi.fn(() => Promise.resolve({ data: { data: fetchedOptions } })) },
22+
},
23+
},
24+
});
25+
}
26+
27+
describe('DictionaryFieldtype options', () => {
28+
// API-backed dictionaries only return a page of options, which may not include the stored value.
29+
// The preloaded selected options must be merged into the options passed to the Combobox so it can
30+
// resolve their labels — otherwise a single-select field displays the raw id instead.
31+
// https://github.com/statamic/cms/issues/14835
32+
test('single-select renders the label of a stored value missing from the fetched options', async () => {
33+
const fieldtype = mountFieldtype({
34+
value: 'de',
35+
maxItems: 1,
36+
selectedOptions: [{ value: 'de', label: 'Germany', invalid: false }],
37+
fetchedOptions: [],
38+
});
39+
await flushPromises();
40+
41+
const selected = fieldtype.find('[data-ui-combobox-selected-option]');
42+
expect(selected.text()).toContain('Germany');
43+
expect(selected.text()).not.toContain('de');
44+
});
45+
46+
test('selected options already present in the fetched options are not duplicated', async () => {
47+
const fieldtype = mountFieldtype({
48+
value: 'de',
49+
maxItems: 1,
50+
selectedOptions: [{ value: 'de', label: 'Germany', invalid: false }],
51+
fetchedOptions: [{ value: 'de', label: 'Germany' }],
52+
shallow: true,
53+
});
54+
await flushPromises();
55+
56+
expect(fieldtype.vm.normalizedOptions).toEqual([{ value: 'de', label: 'Germany' }]);
57+
});
58+
59+
test('multi-select options are not polluted with selected options', async () => {
60+
const fieldtype = mountFieldtype({
61+
value: ['de'],
62+
maxItems: null,
63+
selectedOptions: [{ value: 'de', label: 'Germany', invalid: false }],
64+
fetchedOptions: [{ value: 'ca', label: 'Canada' }],
65+
shallow: true,
66+
});
67+
await flushPromises();
68+
69+
expect(fieldtype.vm.normalizedOptions).toEqual([{ value: 'ca', label: 'Canada' }]);
70+
});
71+
});

0 commit comments

Comments
 (0)