Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<template #item="{ item }">
<div class="entity-suggestion-item">
<span class="entity-label">
{{ item.label || '–' }}
{{ item.label ?? EMPTY_VALUE_PLACEHOLDER }}
</span>
<span class="entity-id">
{{ item.id }}
Expand All @@ -42,7 +42,7 @@

<template #selected-item="{ item }">
<span class="selected-entity-item">
<span class="selected-entity-label">{{ item.label || '–' }}</span>
<span class="selected-entity-label">{{ item.label ?? item.id }}</span>
</span>
</template>
</FieldScopedEntitySelect>
Expand All @@ -56,7 +56,7 @@ import { SearchIcon } from '@kong/icons'
import { KUI_ICON_SIZE_40, KUI_COLOR_TEXT_NEUTRAL } from '@kong/design-tokens'
import FieldScopedEntitySelect from './FieldScopedEntitySelect.vue'
import { getFieldState } from '../../utils/autoSuggest'
import { FORMS_API_KEY, FIELD_STATES } from '../../const'
import { FORMS_API_KEY, FIELD_STATES, EMPTY_VALUE_PLACEHOLDER } from '../../const'
import english from '../../locales/en.json'

const requestResultsLimit = 1000
Expand All @@ -76,6 +76,7 @@ export default {
t,
KUI_ICON_SIZE_40,
KUI_COLOR_TEXT_NEUTRAL,
EMPTY_VALUE_PLACEHOLDER,
}
},

Expand Down Expand Up @@ -183,23 +184,12 @@ export default {
transformItem(item) {
return {
...item,
// This field is for select dropdown item first column.
label: this.getSuggestionLabel(item),
value: item.id,
}
},

dedupeSuggestions(items, filteredIds) {
const dedupedItems = []
items.forEach((item) => {
if (!filteredIds.has(item.id)) {
filteredIds.add(item.id)
dedupedItems.push(item)
}
})

return dedupedItems
},

getItem(data) {
if (data.data) {
return data.data.length > 0 ? data.data[0] : []
Expand Down Expand Up @@ -230,8 +220,7 @@ export default {

getSuggestionLabel(item) {
const labelKey = this.schema?.labelField || 'id'

return (labelKey && item ? item[labelKey] : '') || '–'
return labelKey && item ? item[labelKey] : ''
},

updateModel(value) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import FieldAutoSuggestV2 from '../FieldAutoSuggestV2.vue'
import { v4 } from 'uuid'
import { FORMS_API_KEY, EMPTY_VALUE_PLACEHOLDER } from '../../../const'

const schema = {
entity: 'services',
inputValues: {
fields: ['name', 'id'],
primaryField: 'name',
},
labelField: 'name',
model: 'service-id',
disabled: false,
}

const generateMockServiceData = (idx: number) => {
return {
id: v4(),
name: `test-service-${idx}`,
'connect_timeout': 60000,
'created_at': 1751534248,
'enabled': true,
'host': 'http.bin',
'port': 443,
'protocol': 'https',
'read_timeout': 60000,
'retries': 5,
'updated_at': 1751534248,
'write_timeout': 60000,
}
}

const generateServices = (count: number, append?: ReturnType<typeof generateMockServiceData>) => {
const ret: { data: any[], offset: string | null, next: string | null } = {
data: [],
offset: null,
next: null,
}
if (count > 50) {
ret.offset = 'next-page-offset'
ret.next = `/services?offset=${ret.offset}`
}

ret.data.push(...Array.from({ length: count }).map((_, idx) => generateMockServiceData(idx)))

if (append) {
ret.data.push(append)
}

return {
data: ret,
}
}

const defaultGetOneReturn = generateMockServiceData(1)

const defaultAPIReturns = {
getOneReturn: defaultGetOneReturn,
getAllReturn: generateServices(99, defaultGetOneReturn),
peekReturn: generateServices(49, defaultGetOneReturn),
}

describe.only('<FieldAutoSuggestV2 />', function() {
let getOne: () => Promise<ReturnType<typeof generateMockServiceData>>
let getAll: () => Promise<ReturnType<typeof generateServices>>
let peek: () => Promise<ReturnType<typeof generateServices>>

const createComponent = (overrideSchema = {}, { getOneReturn, getAllReturn, peekReturn } = defaultAPIReturns) => {
getOne = cy.stub().as('getOne').resolves({ data: getOneReturn })
getAll = cy.stub().as('getAll').resolves(getAllReturn)
peek = cy.stub().as('peek').resolves(peekReturn)

cy.mount(FieldAutoSuggestV2, {
props: {
model: {
'service-id': null,
},
schema: {
...schema,
...overrideSchema,
},
},
global: {
provide: {
[FORMS_API_KEY]: {
getOne: getOne,
getAllV2: getAll,
peek: peek,
},
},
},
})
}

describe('initial fetch with suggestions less than 50 or equal records', () => {
beforeEach(() => {
createComponent()
})

it('should load all suggestions at once', () => {
expect(peek).to.have.been.callCount(1)
})

it('should only perform inline search', () => {
cy.getTestId('select-input').type('test-service-1')
expect(getAll).to.have.been.callCount(0)
})

it('can perform inline search with uuid', () => {
cy.getTestId('select-input').type(defaultGetOneReturn.id)
expect(getAll).to.have.been.callCount(0)
expect(getOne).to.have.been.callCount(0)
})
})

describe('initial fetch with suggestions more than 50 records', () => {
beforeEach(() => {
const getOneReturn = {
...defaultGetOneReturn,
name: null,
} as any
createComponent({}, {
getOneReturn,
getAllReturn: generateServices(49, getOneReturn),
peekReturn: generateServices(99, getOneReturn),
})
})

it('should load suggestions until reaching 50 records', () => {
expect(peek).to.have.been.callCount(1)
})

it('should send request to the server for search', () => {
cy.clock()
cy.getTestId('select-input').click()
cy.getTestId('select-input').type('test-service-1')
// called on the edge trigger
cy.tick(0).then(() => {
expect(getAll).to.have.been.callCount(1)
})

cy.tick(500).then(() => {
expect(getAll).to.have.been.callCount(2)
cy.clock().then(clock => clock.restore())
})
})

it('can perform online search with uuid', () => {
cy.clock()
cy.getTestId('select-input').click()
cy.getTestId('select-input').type(defaultGetOneReturn.id)
// The cypress.type puts text into the text input sequentially, so that the first request fired
// from the input event is `getAll`, after the debounce time, the uuid is put into the text input box
// now the exact fetch event is then fired.
cy.tick(500).then(() => {
expect(getOne).to.have.been.callCount(1)
cy.get(`[data-testid="select-item-${defaultGetOneReturn.id}"]`).should('exist')
cy.clock().then(clock => {
clock.restore()
cy.get(`[data-testid="select-item-${defaultGetOneReturn.id}"] button`).should('include.text', EMPTY_VALUE_PLACEHOLDER)
cy.get(`[data-testid="select-item-${defaultGetOneReturn.id}"] button`).click()
// The selected item without a label field should show the id instead of `—`.
cy.get('.custom-selected-item-wrapper').should('contain.text', defaultGetOneReturn.id)
})
})
})
})

describe('suggestions with name `-` as name', () => {
it('can fetch and display the entity with name `-` correctly', () => {
const getOneReturn = {
...defaultGetOneReturn,
name: '-',
}
createComponent({}, {
getOneReturn,
getAllReturn: generateServices(49, getOneReturn),
peekReturn: generateServices(99, getOneReturn),
})
cy.clock()
cy.getTestId('select-input').click()
cy.getTestId('select-input').type(getOneReturn.id)
// The cypress.type puts text into the text input sequentially, so that the first request fired
// from the input event is `getAll`, after the debounce time, the uuid is put into the text input box
// now the exact fetch event is then fired.
cy.tick(500).then(() => {
expect(getOne).to.have.been.callCount(1)
cy.get(`[data-testid="select-item-${getOneReturn.id}"]`).should('exist')
cy.clock().then(clock => {
clock.restore()
cy.get(`[data-testid="select-item-${getOneReturn.id}"] button`).click()
// The selected item without a label field should show the id instead of `–`.
cy.get('.custom-selected-item-wrapper').should('contain.text', getOneReturn.name)
})
})
})
})
})
2 changes: 2 additions & 0 deletions packages/core/forms/src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ export const FIELD_STATES = {
UPDATE_ENTITY: 'UPDATE_ENTITY',
SET_REFERRAL_VALUE: 'SET_REFERRAL_VALUE',
}

export const EMPTY_VALUE_PLACEHOLDER = '—' // em dash
Loading