Skip to content

Commit

Permalink
feat(vfg): convert field-select + more tests [khcp-11336] (#1584)
Browse files Browse the repository at this point in the history
Convert VFG `FieldSelect` to use Kongponents + add more VFG component test coverage.
Addresses [KHCP-11336](https://konghq.atlassian.net/browse/KHCP-11336).
  • Loading branch information
kaiarrowood authored Sep 24, 2024
1 parent 8c1e99f commit c57df8e
Show file tree
Hide file tree
Showing 11 changed files with 511 additions and 130 deletions.
5 changes: 1 addition & 4 deletions packages/core/forms/sandbox/model.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
{
"service-id": null,
"name": null,
"protocols": [
"http",
"https"
],
"protocols": "http,https",
"snis": null,
"hosts": null,
"sources": null,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/forms/src/components/fields/FieldRadio.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
:key="option.value"
v-model="inputValue"
:label="option.name"
:label-attributes="{ info: schema.help }"
:name="schema.name"
:required="schema.required || undefined"
:selected-value="option.value"
@change="onChange"
/>
Expand Down
248 changes: 129 additions & 119 deletions packages/core/forms/src/components/fields/FieldSelect.vue
Original file line number Diff line number Diff line change
@@ -1,138 +1,148 @@
<template lang="pug">
select.form-control(v-model="value", :disabled="disabled || null", :name="schema.inputName", :id="getFieldID(schema)", :class="schema.fieldClasses", v-attributes="'input'")
option(v-if="!selectOptions.hideNoneSelectedText", :disabled="schema.required || null", :value="null") {{ selectOptions.noneSelectedText || "&lt;Nothing selected&gt;" }}

template(v-for="item in items")
optgroup(v-if="item.group", :label="getGroupName(item)")
option(v-if="item.ops", v-for="i in item.ops", :value="getItemValue(i)") {{ getItemName(i) }}

option(v-if="!item.group", :value="getItemValue(item)") {{ getItemName(item) }}
<template>
<KSelect
:id="getFieldID(schema)"
v-model="inputValue"
:class="schema.fieldClasses"
:clearable="!schema.required"
:disabled="disabled || undefined"
:items="items"
:kpop-attributes="{ 'data-testid': `${getFieldID(schema)}-items` }"
:label-attributes="{ info: schema.help }"
:name="schema.inputName"
:placeholder="!selectOptions.hideNoneSelectedText ? selectOptions.noneSelectedText || 'Nothing Selected' : undefined"
:required="schema.required || undefined"
width="100%"
/>
</template>

<script>
<script lang="ts" setup>
import { computed, toRefs, type PropType } from 'vue'
import type { SelectItem } from '@kong/kongponents'
import isObject from 'lodash-es/isObject'
import isNil from 'lodash-es/isNil'
import find from 'lodash-es/find'
import abstractField from './abstractField'
export default {
mixins: [abstractField],
computed: {
selectOptions() {
return this.schema.selectOptions || {}
},
import composables from '../../composables'
items() {
const values = this.schema.values
if (typeof values === 'function') {
return this.groupValues(values.apply(this, [this.model, this.schema]))
} else return this.groupValues(values)
},
const props = defineProps({
disabled: {
type: Boolean,
default: false,
},
formOptions: {
type: Object as PropType<Record<string, any>>,
default: () => undefined,
},
model: {
type: Object as PropType<Record<string, any>>,
default: () => undefined,
},
schema: {
type: Object as PropType<Record<string, any>>,
required: true,
},
vfg: {
type: Object,
required: true,
},
/**
* TODO: stronger type
* TODO: pass this down to KInput error and errorMessage
*/
errors: {
type: Array,
default: () => [],
},
hint: {
type: String,
default: '',
},
})
methods: {
formatValueToField(value) {
if (isNil(value)) {
return null
}
return value
},
groupValues(values) {
const array = []
let arrayElement = {}
values.forEach(item => {
arrayElement = null
if (item.group && isObject(item)) {
// There is in a group.
// Find element with this group.
arrayElement = find(array, i => i.group === item.group)
if (arrayElement) {
// There is such a group.
arrayElement.ops.push({
id: item.id,
name: item.name,
})
} else {
// There is not such a group.
// Initialising.
arrayElement = {
group: '',
ops: [],
}
// Set group.
arrayElement.group = item.group
// Set Group element.
arrayElement.ops.push({
id: item.id,
name: item.name,
})
const emit = defineEmits<{
(event: 'modelUpdated', value: any, model: Record<string, any>): void
}>()
// Add array.
array.push(arrayElement)
}
} else {
// There is not in a group.
array.push(item)
}
})
const selectOptions = computed((): Record<string, any> => props.schema.selectOptions || {})
// With Groups.
return array
},
const formatValueToField = (value: Record<string, any>) => {
if (isNil(value)) {
return null
}
return value
}
getGroupName(item) {
if (item && item.group) {
return item.group
}
const propsRefs = toRefs(props)
throw new Error('Group name is missing! https://icebob.gitbooks.io/vueformgenerator/content/fields/select.html#select-field-with-object-items')
},
const { getFieldID, clearValidationErrors, value: inputValue } = composables.useAbstractFields({
model: propsRefs.model,
schema: props.schema,
formOptions: props.formOptions,
formatValueToField,
emitModelUpdated: (data: { value: any, model: Record<string, any> }): void => {
emit('modelUpdated', data.value, data.model)
},
})
defineExpose({
clearValidationErrors,
})
const items = computed((): SelectItem[] => {
// values to be used in the select items
const selectOptions = props.schema.values
if (typeof selectOptions === 'function') {
return getItemsFromValues(selectOptions.apply(this, [props.model, props.schema]))
} else {
return getItemsFromValues(selectOptions)
}
})
const getItemsFromValues = (values: Record<string, any>[] | string[] | number[]): SelectItem[] => {
const itemArray: SelectItem[] = []
values.forEach(item => {
itemArray.push({
label: getItemName(item),
value: getItemValue(item),
disabled: typeof item === 'object' ? item.disabled || undefined : undefined,
group: typeof item === 'object' ? String(item.group || '').toUpperCase() || undefined : undefined,
})
})
// With Groups.
return itemArray
}
getItemValue(item) {
if (isObject(item)) {
if (typeof this.schema.selectOptions !== 'undefined' && typeof this.schema.selectOptions.value !== 'undefined') {
return item[this.schema.selectOptions.value]
} else {
// Use 'id' instead of 'value' cause of backward compatibility
if (typeof item.id !== 'undefined') {
return item.id
} else {
throw new Error('`id` is not defined. If you want to use another key name, add a `value` property under `selectOptions` in the schema. https://icebob.gitbooks.io/vueformgenerator/content/fields/select.html#select-field-with-object-items')
}
}
const getItemValue = (item: string | number | Record<string, any>): string | number => {
if (isObject(item)) {
if (selectOptions.value && typeof selectOptions.value.value !== 'undefined') {
return item[selectOptions.value.value]
} else {
// Use 'id' instead of 'value' cause of backward compatibility
if (typeof item.id !== 'undefined') {
return String(item.id)
} else {
return item
throw new Error('`id` is not defined. If you want to use another key name, add a `value` property under `selectOptions` in the schema. https://icebob.gitbooks.io/vueformgenerator/content/fields/select.html#select-field-with-object-items')
}
},
}
} else {
return item
}
}
getItemName(item) {
if (isObject(item)) {
if (typeof this.schema.selectOptions !== 'undefined' && typeof this.schema.selectOptions.name !== 'undefined') {
return item[this.schema.selectOptions.name]
} else {
if (typeof item.name !== 'undefined') {
return item.name
} else {
throw new Error('`name` is not defined. If you want to use another key name, add a `name` property under `selectOptions` in the schema. https://icebob.gitbooks.io/vueformgenerator/content/fields/select.html#select-field-with-object-items')
}
}
const getItemName = (item: string | number | Record<string, any>): string => {
if (isObject(item)) {
if (selectOptions.value && typeof selectOptions.value.name !== 'undefined') {
return item[selectOptions.value.name]
} else {
if (typeof item.name !== 'undefined') {
return String(item.name)
} else {
return item
throw new Error('`name` is not defined. If you want to use another key name, add a `name` property under `selectOptions` in the schema. https://icebob.gitbooks.io/vueformgenerator/content/fields/select.html#select-field-with-object-items')
}
},
},
}
} else {
return String(item)
}
}
</script>

<style lang="sass">
</style>
2 changes: 2 additions & 0 deletions packages/core/forms/src/components/fields/FieldSwitch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
:class="schema.fieldClasses"
:disabled="disabled || undefined"
:label="inputValue ? schema.textOn || t('vfg.labels.on') : schema.textOff || t('vfg.labels.off')"
:label-attributes="{ info: schema.help }"
:name="schema.inputName"
:required="schema.required || undefined"
/>
</div>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ describe('<FieldTester /> - FieldCheckbox', () => {
model: fieldKey,
id: fieldKey,
label: fieldLabel,
help: 'Check if the cat is cool.',
required: true,
}],
}

Expand All @@ -33,6 +35,19 @@ describe('<FieldTester /> - FieldCheckbox', () => {
// check VFG label is set correctly
cy.get(`.form-group-label[for="${fieldKey}"]`).should('be.visible')
cy.get(`.form-group-label[for="${fieldKey}"]`).should('contain.text', fieldLabel)

// check required state
if (schema.fields[0].required) {
cy.get('.required').find(`.form-group-label[for="${fieldKey}"]`).should('exist')
} else {
cy.get('.required').find(`.form-group-label[for="${fieldKey}"]`).should('not.exist')
}

// check help text
if (schema.fields[0].help) {
cy.get(`label[for="${fieldKey}"] .info-icon`).should('be.visible')
cy.get(`label[for="${fieldKey}"]`).should('contain.text', schema.fields[0].help)
}
})

it('renders default state correctly - with model', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ describe('<FieldTester /> - FieldInput', () => {
id: fieldKey,
inputType: 'text',
label: fieldLabel,
help: 'The name of the cat.',
required: true,
}],
}

Expand All @@ -34,6 +36,19 @@ describe('<FieldTester /> - FieldInput', () => {
// check VFG label is set correctly
cy.get(`.form-group-label[for="${fieldKey}"]`).should('be.visible')
cy.get(`.form-group-label[for="${fieldKey}"]`).should('contain.text', fieldLabel)

// check required state
if (schema.fields[0].required) {
cy.get('.required').find(`.form-group-label[for="${fieldKey}"]`).should('exist')
} else {
cy.get('.required').find(`.form-group-label[for="${fieldKey}"]`).should('not.exist')
}

// check help text
if (schema.fields[0].help) {
cy.get(`label[for="${fieldKey}"] .info-icon`).should('be.visible')
cy.get(`label[for="${fieldKey}"]`).should('contain.text', schema.fields[0].help)
}
})

it('renders default state correctly - with model', () => {
Expand Down
Loading

0 comments on commit c57df8e

Please sign in to comment.