Skip to content

Commit 7eb817f

Browse files
authored
fix(forms): scoped entities selected default value (#2490)
* fix(forms): scoped entities selected default value * Show `uuid` as selected value when certain scoped entity does not own a name * Add test cases against the component `FieldAutoSuggestV2`. * fix(forms): show `-` for name with value `-` * fix(forms): remove data contamination * feat(forms): use em dash `—` as the label placeholder instead of hyphen
1 parent aac4be5 commit 7eb817f

File tree

3 files changed

+206
-17
lines changed

3 files changed

+206
-17
lines changed

packages/core/forms/src/components/fields/FieldAutoSuggestV2.vue

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<template #item="{ item }">
3333
<div class="entity-suggestion-item">
3434
<span class="entity-label">
35-
{{ item.label || '–' }}
35+
{{ item.label ?? EMPTY_VALUE_PLACEHOLDER }}
3636
</span>
3737
<span class="entity-id">
3838
{{ item.id }}
@@ -42,7 +42,7 @@
4242

4343
<template #selected-item="{ item }">
4444
<span class="selected-entity-item">
45-
<span class="selected-entity-label">{{ item.label || '–' }}</span>
45+
<span class="selected-entity-label">{{ item.label ?? item.id }}</span>
4646
</span>
4747
</template>
4848
</FieldScopedEntitySelect>
@@ -56,7 +56,7 @@ import { SearchIcon } from '@kong/icons'
5656
import { KUI_ICON_SIZE_40, KUI_COLOR_TEXT_NEUTRAL } from '@kong/design-tokens'
5757
import FieldScopedEntitySelect from './FieldScopedEntitySelect.vue'
5858
import { getFieldState } from '../../utils/autoSuggest'
59-
import { FORMS_API_KEY, FIELD_STATES } from '../../const'
59+
import { FORMS_API_KEY, FIELD_STATES, EMPTY_VALUE_PLACEHOLDER } from '../../const'
6060
import english from '../../locales/en.json'
6161
6262
const requestResultsLimit = 1000
@@ -76,6 +76,7 @@ export default {
7676
t,
7777
KUI_ICON_SIZE_40,
7878
KUI_COLOR_TEXT_NEUTRAL,
79+
EMPTY_VALUE_PLACEHOLDER,
7980
}
8081
},
8182
@@ -183,23 +184,12 @@ export default {
183184
transformItem(item) {
184185
return {
185186
...item,
187+
// This field is for select dropdown item first column.
186188
label: this.getSuggestionLabel(item),
187189
value: item.id,
188190
}
189191
},
190192
191-
dedupeSuggestions(items, filteredIds) {
192-
const dedupedItems = []
193-
items.forEach((item) => {
194-
if (!filteredIds.has(item.id)) {
195-
filteredIds.add(item.id)
196-
dedupedItems.push(item)
197-
}
198-
})
199-
200-
return dedupedItems
201-
},
202-
203193
getItem(data) {
204194
if (data.data) {
205195
return data.data.length > 0 ? data.data[0] : []
@@ -230,8 +220,7 @@ export default {
230220
231221
getSuggestionLabel(item) {
232222
const labelKey = this.schema?.labelField || 'id'
233-
234-
return (labelKey && item ? item[labelKey] : '') || ''
223+
return labelKey && item ? item[labelKey] : ''
235224
},
236225
237226
updateModel(value) {
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import FieldAutoSuggestV2 from '../FieldAutoSuggestV2.vue'
2+
import { v4 } from 'uuid'
3+
import { FORMS_API_KEY, EMPTY_VALUE_PLACEHOLDER } from '../../../const'
4+
5+
const schema = {
6+
entity: 'services',
7+
inputValues: {
8+
fields: ['name', 'id'],
9+
primaryField: 'name',
10+
},
11+
labelField: 'name',
12+
model: 'service-id',
13+
disabled: false,
14+
}
15+
16+
const generateMockServiceData = (idx: number) => {
17+
return {
18+
id: v4(),
19+
name: `test-service-${idx}`,
20+
'connect_timeout': 60000,
21+
'created_at': 1751534248,
22+
'enabled': true,
23+
'host': 'http.bin',
24+
'port': 443,
25+
'protocol': 'https',
26+
'read_timeout': 60000,
27+
'retries': 5,
28+
'updated_at': 1751534248,
29+
'write_timeout': 60000,
30+
}
31+
}
32+
33+
const generateServices = (count: number, append?: ReturnType<typeof generateMockServiceData>) => {
34+
const ret: { data: any[], offset: string | null, next: string | null } = {
35+
data: [],
36+
offset: null,
37+
next: null,
38+
}
39+
if (count > 50) {
40+
ret.offset = 'next-page-offset'
41+
ret.next = `/services?offset=${ret.offset}`
42+
}
43+
44+
ret.data.push(...Array.from({ length: count }).map((_, idx) => generateMockServiceData(idx)))
45+
46+
if (append) {
47+
ret.data.push(append)
48+
}
49+
50+
return {
51+
data: ret,
52+
}
53+
}
54+
55+
const defaultGetOneReturn = generateMockServiceData(1)
56+
57+
const defaultAPIReturns = {
58+
getOneReturn: defaultGetOneReturn,
59+
getAllReturn: generateServices(99, defaultGetOneReturn),
60+
peekReturn: generateServices(49, defaultGetOneReturn),
61+
}
62+
63+
describe.only('<FieldAutoSuggestV2 />', function() {
64+
let getOne: () => Promise<ReturnType<typeof generateMockServiceData>>
65+
let getAll: () => Promise<ReturnType<typeof generateServices>>
66+
let peek: () => Promise<ReturnType<typeof generateServices>>
67+
68+
const createComponent = (overrideSchema = {}, { getOneReturn, getAllReturn, peekReturn } = defaultAPIReturns) => {
69+
getOne = cy.stub().as('getOne').resolves({ data: getOneReturn })
70+
getAll = cy.stub().as('getAll').resolves(getAllReturn)
71+
peek = cy.stub().as('peek').resolves(peekReturn)
72+
73+
cy.mount(FieldAutoSuggestV2, {
74+
props: {
75+
model: {
76+
'service-id': null,
77+
},
78+
schema: {
79+
...schema,
80+
...overrideSchema,
81+
},
82+
},
83+
global: {
84+
provide: {
85+
[FORMS_API_KEY]: {
86+
getOne: getOne,
87+
getAllV2: getAll,
88+
peek: peek,
89+
},
90+
},
91+
},
92+
})
93+
}
94+
95+
describe('initial fetch with suggestions less than 50 or equal records', () => {
96+
beforeEach(() => {
97+
createComponent()
98+
})
99+
100+
it('should load all suggestions at once', () => {
101+
expect(peek).to.have.been.callCount(1)
102+
})
103+
104+
it('should only perform inline search', () => {
105+
cy.getTestId('select-input').type('test-service-1')
106+
expect(getAll).to.have.been.callCount(0)
107+
})
108+
109+
it('can perform inline search with uuid', () => {
110+
cy.getTestId('select-input').type(defaultGetOneReturn.id)
111+
expect(getAll).to.have.been.callCount(0)
112+
expect(getOne).to.have.been.callCount(0)
113+
})
114+
})
115+
116+
describe('initial fetch with suggestions more than 50 records', () => {
117+
beforeEach(() => {
118+
const getOneReturn = {
119+
...defaultGetOneReturn,
120+
name: null,
121+
} as any
122+
createComponent({}, {
123+
getOneReturn,
124+
getAllReturn: generateServices(49, getOneReturn),
125+
peekReturn: generateServices(99, getOneReturn),
126+
})
127+
})
128+
129+
it('should load suggestions until reaching 50 records', () => {
130+
expect(peek).to.have.been.callCount(1)
131+
})
132+
133+
it('should send request to the server for search', () => {
134+
cy.clock()
135+
cy.getTestId('select-input').click()
136+
cy.getTestId('select-input').type('test-service-1')
137+
// called on the edge trigger
138+
cy.tick(0).then(() => {
139+
expect(getAll).to.have.been.callCount(1)
140+
})
141+
142+
cy.tick(500).then(() => {
143+
expect(getAll).to.have.been.callCount(2)
144+
cy.clock().then(clock => clock.restore())
145+
})
146+
})
147+
148+
it('can perform online search with uuid', () => {
149+
cy.clock()
150+
cy.getTestId('select-input').click()
151+
cy.getTestId('select-input').type(defaultGetOneReturn.id)
152+
// The cypress.type puts text into the text input sequentially, so that the first request fired
153+
// from the input event is `getAll`, after the debounce time, the uuid is put into the text input box
154+
// now the exact fetch event is then fired.
155+
cy.tick(500).then(() => {
156+
expect(getOne).to.have.been.callCount(1)
157+
cy.get(`[data-testid="select-item-${defaultGetOneReturn.id}"]`).should('exist')
158+
cy.clock().then(clock => {
159+
clock.restore()
160+
cy.get(`[data-testid="select-item-${defaultGetOneReturn.id}"] button`).should('include.text', EMPTY_VALUE_PLACEHOLDER)
161+
cy.get(`[data-testid="select-item-${defaultGetOneReturn.id}"] button`).click()
162+
// The selected item without a label field should show the id instead of `—`.
163+
cy.get('.custom-selected-item-wrapper').should('contain.text', defaultGetOneReturn.id)
164+
})
165+
})
166+
})
167+
})
168+
169+
describe('suggestions with name `-` as name', () => {
170+
it('can fetch and display the entity with name `-` correctly', () => {
171+
const getOneReturn = {
172+
...defaultGetOneReturn,
173+
name: '-',
174+
}
175+
createComponent({}, {
176+
getOneReturn,
177+
getAllReturn: generateServices(49, getOneReturn),
178+
peekReturn: generateServices(99, getOneReturn),
179+
})
180+
cy.clock()
181+
cy.getTestId('select-input').click()
182+
cy.getTestId('select-input').type(getOneReturn.id)
183+
// The cypress.type puts text into the text input sequentially, so that the first request fired
184+
// from the input event is `getAll`, after the debounce time, the uuid is put into the text input box
185+
// now the exact fetch event is then fired.
186+
cy.tick(500).then(() => {
187+
expect(getOne).to.have.been.callCount(1)
188+
cy.get(`[data-testid="select-item-${getOneReturn.id}"]`).should('exist')
189+
cy.clock().then(clock => {
190+
clock.restore()
191+
cy.get(`[data-testid="select-item-${getOneReturn.id}"] button`).click()
192+
// The selected item without a label field should show the id instead of `–`.
193+
cy.get('.custom-selected-item-wrapper').should('contain.text', getOneReturn.name)
194+
})
195+
})
196+
})
197+
})
198+
})

packages/core/forms/src/const.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ export const FIELD_STATES = {
1818
UPDATE_ENTITY: 'UPDATE_ENTITY',
1919
SET_REFERRAL_VALUE: 'SET_REFERRAL_VALUE',
2020
}
21+
22+
export const EMPTY_VALUE_PLACEHOLDER = '—' // em dash

0 commit comments

Comments
 (0)