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
10 changes: 10 additions & 0 deletions resources/js/components/inputs/relationship/RelationshipInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,16 @@ export default {
this.$emit('item-data-updated', data);
},

value(value) {
if (this.initializing || this.loading) return;

// A value set from outside (e.g. a save response normalizing a typed term) may
// reference an id we have no data for yet, which would render as a raw id.
if (value?.some((selection) => !this.data?.find((item) => item.id == selection))) {
this.getDataForSelections(value);
}
},

items(items, oldItems) {
if (items.length > 0 && oldItems.length === 0) {
if (this.canReorder) {
Expand Down
10 changes: 9 additions & 1 deletion resources/js/components/inputs/relationship/SelectField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
:max-selections="maxSelections"
:model-value="items.map((item) => item.id)"
:multiple
:options
:options="comboboxOptions"
:placeholder="__(config.placeholder) || __('Choose...')"
:read-only="readOnly"
:taggable="isTaggable"
Expand Down Expand Up @@ -95,6 +95,14 @@ export default {
return JSON.stringify({ ...this.parameters, url: this.url });
},

comboboxOptions() {
// Combobox resolves the selected label from this list, so a selected item missing
// from it (e.g. a just-created term) would otherwise display as its raw id.
const missing = this.items.filter((item) => !this.options.some((option) => option.id === item.id));

return [...this.options, ...missing];
},

noOptionsText() {
return this.typeahead && !this.requested ? __('Start typing to search.') : __('No options to choose from.');
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,41 @@ describe('RelationshipInput in-flight request deduplication', () => {
b.unmount();
});
});

describe('RelationshipInput refetches item data for unresolved values', () => {
test('fetches item data when the value prop changes to include an id with no matching data', async () => {
const d = deferred();
const post = vi.fn(() => d.promise);

const wrapper = mountInput({ axiosPost: post, itemDataUrl: '/test/stale-value' });
await flushPromises();

await wrapper.setProps({ value: ['taxonomy::bob'] });
await flushPromises();

expect(post).toHaveBeenCalledWith(
'/test/stale-value',
{ site: 'default', selections: ['taxonomy::bob'] },
expect.anything(),
);

d.resolve({ data: { data: [] } });
await flushPromises();

wrapper.unmount();
});

test('does not refetch when the new value is already covered by existing data', async () => {
const post = vi.fn(() => Promise.resolve({ data: { data: [] } }));

const wrapper = mountInput({ axiosPost: post, itemDataUrl: '/test/stale-value-known' });
await flushPromises();

await wrapper.setProps({ data: [{ id: '1', title: 'One' }], value: ['1'] });
await flushPromises();

expect(post).not.toHaveBeenCalled();

wrapper.unmount();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { mount } from '@vue/test-utils';
import { describe, expect, test, vi } from 'vitest';

vi.mock('@inertiajs/vue3', () => ({
router: { on: () => () => {} },
}));

globalThis.__ = (key) => key;

import { data_get } from '@/bootstrap/globals.js';
globalThis.data_get = data_get;

import SelectField from '@/components/inputs/relationship/SelectField.vue';

const stubs = {
Combobox: true,
StatusIndicator: true,
};

function mountSelectField({ items = [] } = {}) {
return mount(SelectField, {
props: {
items,
url: '/test/select-field',
config: {},
},
global: {
mocks: {
$axios: { get: () => Promise.resolve({ data: { data: [] } }) },
},
stubs,
},
});
}

describe('SelectField comboboxOptions', () => {
test('includes a selected item missing from the fetched options list', () => {
const wrapper = mountSelectField({ items: [{ id: 'tags::bob', title: 'Bob' }] });

expect(wrapper.vm.comboboxOptions).toContainEqual({ id: 'tags::bob', title: 'Bob' });

wrapper.unmount();
});

test('does not duplicate an item already present in the fetched options list', async () => {
const wrapper = mountSelectField({ items: [{ id: '1', title: 'One' }] });
await wrapper.setData({ options: [{ id: '1', title: 'One' }] });

expect(wrapper.vm.comboboxOptions).toEqual([{ id: '1', title: 'One' }]);

wrapper.unmount();
});
});
Loading