mirror of https://github.com/rancher/dashboard.git
Put ArrayList in component library and storybook, add unit tests
This commit is contained in:
parent
215d2466ed
commit
a279d22fc7
|
|
@ -0,0 +1,72 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { ArrayList } from './index';
|
||||
import { _EDIT, _VIEW } from "@shell/config/query-params";
|
||||
|
||||
describe('ArrayList.vue', () => {
|
||||
|
||||
it('is empty', () => {
|
||||
const wrapper = mount(ArrayList, {
|
||||
propsData: {
|
||||
value: [],
|
||||
mode: _EDIT
|
||||
},
|
||||
});
|
||||
const elements = wrapper.findAll('[data-testid^="array-list-box"]');
|
||||
|
||||
expect(elements.length).toBe(0);
|
||||
});
|
||||
|
||||
it('shows an initial empty row', () => {
|
||||
const wrapper = mount(ArrayList, {
|
||||
propsData: {
|
||||
value: [],
|
||||
mode: _EDIT,
|
||||
initialEmptyRow: true
|
||||
},
|
||||
});
|
||||
const arrayListBoxes = wrapper.findAll('[data-testid="array-list-box"]');
|
||||
|
||||
expect(arrayListBoxes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('expands when the add button is clicked', async () => {
|
||||
const wrapper = mount(ArrayList, {
|
||||
propsData: {
|
||||
value: [],
|
||||
mode: _EDIT,
|
||||
},
|
||||
});
|
||||
|
||||
const arrayListButton = wrapper.find('[data-testid="array-list-button"]').element as HTMLElement;
|
||||
await arrayListButton.click()
|
||||
await arrayListButton.click()
|
||||
const arrayListBoxes = wrapper.findAll('[data-testid="array-list-box"]')
|
||||
|
||||
expect(arrayListBoxes.length).toBe(2);
|
||||
});
|
||||
|
||||
it('contracts when a delete button is clicked', async () => {
|
||||
const wrapper = mount(ArrayList, {
|
||||
propsData: {
|
||||
value: ['string 1', 'string 2'],
|
||||
mode: _EDIT,
|
||||
},
|
||||
});
|
||||
const deleteButton = wrapper.get('[data-testid^="remove-item"]').element as HTMLElement;
|
||||
await deleteButton.click()
|
||||
const arrayListBoxes = wrapper.findAll('[data-testid="array-list-box"]')
|
||||
|
||||
expect(arrayListBoxes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('Add button is hidden in read-only mode', () => {
|
||||
const wrapper = mount(ArrayList, {
|
||||
propsData: {
|
||||
value: ['read-only example'],
|
||||
mode: _VIEW,
|
||||
},
|
||||
});
|
||||
const arrayListButtons = wrapper.findAll('[data-testid="array-list-button"]')
|
||||
expect(arrayListButtons.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,375 @@
|
|||
<script lang='ts'>
|
||||
import Vue, { PropType } from 'vue';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||
import { removeAt } from '@shell/utils/array';
|
||||
import TextAreaAutoGrow from '@components/Form/TextArea/TextAreaAutoGrow.vue';
|
||||
import { clone } from '@shell/utils/object';
|
||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||
import { ArrayListData, ArrayListRow } from './types';
|
||||
|
||||
const DEFAULT_PROTIP =
|
||||
'Tip: Paste lines into any list field for easy bulk entry';
|
||||
|
||||
export default Vue.extend({
|
||||
components: { TextAreaAutoGrow, LabeledInput },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array as PropType<Array<string>>,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
initialEmptyRow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
protip: {
|
||||
type: [String, Boolean],
|
||||
default: DEFAULT_PROTIP,
|
||||
},
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
valueLabel: {
|
||||
type: String,
|
||||
default: 'Value',
|
||||
},
|
||||
valuePlaceholder: {
|
||||
type: String,
|
||||
default: 'e.g. bar',
|
||||
},
|
||||
valueMultiline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
addLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
addAllowed: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
removeLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
removeAllowed: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
defaultAddValue: {
|
||||
type: [String, Number, Object, Array],
|
||||
default: '',
|
||||
},
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
rules: {
|
||||
default: () => [],
|
||||
type: Array,
|
||||
// we only want functions in the rules array
|
||||
validator: (rules) =>
|
||||
rules.every((rule) => ['function'].includes(typeof rule)),
|
||||
},
|
||||
},
|
||||
|
||||
data(): ArrayListData {
|
||||
const input: string[] = (this.value || []).slice();
|
||||
const rows: ArrayListRow[] = [];
|
||||
|
||||
for (const value of input) {
|
||||
rows.push({ value });
|
||||
}
|
||||
|
||||
if (!rows.length && this.initialEmptyRow) {
|
||||
const value = this.defaultAddValue ? clone(this.defaultAddValue) : '';
|
||||
|
||||
rows.push({ value });
|
||||
}
|
||||
|
||||
return { rows, lastUpdateWasFromValue: false };
|
||||
},
|
||||
|
||||
computed: {
|
||||
isView(): boolean {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
showAdd(): boolean {
|
||||
return this.addAllowed;
|
||||
},
|
||||
|
||||
showRemove(): boolean {
|
||||
return this.removeAllowed;
|
||||
},
|
||||
|
||||
isDefaultProtip(): boolean {
|
||||
return this.protip === DEFAULT_PROTIP;
|
||||
},
|
||||
|
||||
showProtip(): boolean {
|
||||
if (this.protip && !this.isDefaultProtip) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !this.valueMultiline && !!this.protip;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
value() {
|
||||
this.lastUpdateWasFromValue = true;
|
||||
this.rows = (this.value || []).map((v) => ({ value: v }));
|
||||
},
|
||||
rows: {
|
||||
deep: true,
|
||||
handler() {
|
||||
// lastUpdateWasFromValue is used to break a cycle where when rows are updated
|
||||
// this was called which then forced rows to updated again
|
||||
if (!this.lastUpdateWasFromValue) {
|
||||
this.queueUpdate();
|
||||
}
|
||||
this.lastUpdateWasFromValue = false;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
debounce(this.update, 50);
|
||||
},
|
||||
|
||||
methods: {
|
||||
queueUpdate() {
|
||||
debounce(this.update, 50);
|
||||
},
|
||||
add() {
|
||||
this.rows.push({ value: clone(this.defaultAddValue) });
|
||||
if (this.defaultAddValue) {
|
||||
this.queueUpdate();
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
const inputs: any = this.$refs.value;
|
||||
|
||||
if (inputs && inputs.length > 0) {
|
||||
inputs[inputs.length - 1].focus();
|
||||
}
|
||||
|
||||
this.$emit('add');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove item and emits removed row and its own index value
|
||||
*/
|
||||
remove(row: ArrayListRow, index: number): void {
|
||||
this.$emit('remove', { row, index });
|
||||
removeAt(this.rows, index);
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
update() {
|
||||
if (this.isView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const out = [];
|
||||
|
||||
for (const row of this.rows) {
|
||||
const trim = !this.valueMultiline && typeof row.value === 'string';
|
||||
const value = trim ? row.value.trim() : row.value;
|
||||
|
||||
if (typeof value !== 'undefined') {
|
||||
out.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('input', out);
|
||||
},
|
||||
|
||||
onPaste(index: number, event: any) {
|
||||
if (this.valueMultiline) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const text = event.clipboardData.getData('text/plain');
|
||||
const split = text.split('\n').map((value: string) => ({ value }));
|
||||
|
||||
this.rows.splice(index, 1, ...split);
|
||||
this.update();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="title" class="clearfix">
|
||||
<slot name="title">
|
||||
<h3>
|
||||
{{ title }}
|
||||
<i v-if="showProtip" v-tooltip="protip" class="icon icon-info" />
|
||||
</h3>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<template v-if="rows.length">
|
||||
<div v-if="showHeader">
|
||||
<slot name="column-headers">
|
||||
<label class="value text-label mb-10">
|
||||
{{ valueLabel }}
|
||||
</label>
|
||||
</slot>
|
||||
</div>
|
||||
<div
|
||||
v-for="(row, idx) in rows"
|
||||
:key="idx"
|
||||
class="box"
|
||||
data-testid="array-list-box"
|
||||
>
|
||||
<slot
|
||||
name="columns"
|
||||
:queueUpdate="queueUpdate"
|
||||
:i="idx"
|
||||
:rows="rows"
|
||||
:row="row"
|
||||
:mode="mode"
|
||||
:isView="isView"
|
||||
>
|
||||
<div class="value">
|
||||
<slot
|
||||
name="value"
|
||||
:row="row"
|
||||
:mode="mode"
|
||||
:isView="isView"
|
||||
:queue-update="queueUpdate"
|
||||
>
|
||||
<TextAreaAutoGrow
|
||||
v-if="valueMultiline"
|
||||
ref="value"
|
||||
v-model="row.value"
|
||||
:placeholder="valuePlaceholder"
|
||||
:mode="mode"
|
||||
:disabled="disabled"
|
||||
@paste="onPaste(idx, $event)"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-else-if="rules.length > 0"
|
||||
ref="value"
|
||||
v-model="row.value"
|
||||
:placeholder="valuePlaceholder"
|
||||
:disabled="isView || disabled"
|
||||
:rules="rules"
|
||||
:compact="false"
|
||||
@paste="onPaste(idx, $event)"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
ref="value"
|
||||
v-model="row.value"
|
||||
:placeholder="valuePlaceholder"
|
||||
:disabled="isView || disabled"
|
||||
@paste="onPaste(idx, $event)"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</slot>
|
||||
<div v-if="showRemove" class="remove">
|
||||
<slot
|
||||
name="remove-button"
|
||||
:remove="() => remove(row, idx)"
|
||||
:i="idx"
|
||||
:row="row"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="isView"
|
||||
class="btn role-link"
|
||||
:data-testid="`remove-item-${idx}`"
|
||||
@click="remove(row, idx)"
|
||||
>
|
||||
{{ removeLabel }}
|
||||
</button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="mode === 'view'" class="text-muted">—</div>
|
||||
<div v-else>
|
||||
<slot name="empty" />
|
||||
</div>
|
||||
<div v-if="showAdd && !isView" class="footer">
|
||||
<slot v-if="showAdd" name="add" :add="add">
|
||||
<button
|
||||
data-testid="array-list-button"
|
||||
type="button"
|
||||
class="btn role-tertiary add"
|
||||
:disabled="loading"
|
||||
@click="add()"
|
||||
>
|
||||
<i v-if="loading" class="mr-5 icon icon-spinner icon-spin icon-lg" />
|
||||
{{ addLabel }}
|
||||
</button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: grid;
|
||||
grid-template-columns: auto $array-list-remove-margin;
|
||||
align-items: center;
|
||||
|
||||
margin-bottom: 10px;
|
||||
.value {
|
||||
flex: 1;
|
||||
INPUT {
|
||||
height: $input-height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footer {
|
||||
.protip {
|
||||
float: right;
|
||||
padding: 5px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as ArrayList } from './ArrayList.vue';
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export type ArrayListRow = {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type ArrayListData = {
|
||||
lastUpdateWasFromValue: boolean;
|
||||
rows: Array<ArrayListRow>,
|
||||
}
|
||||
|
|
@ -3,3 +3,4 @@ export * from './LabeledInput';
|
|||
export * from './Radio';
|
||||
export * from './TextArea';
|
||||
export * from './ToggleSwitch';
|
||||
export * from './ArrayList';
|
||||
|
|
|
|||
|
|
@ -11,3 +11,12 @@ export const RadioGroup: VueConstructor;
|
|||
export const StringList: VueConstructor;
|
||||
export const TextAreaAutoGrow: VueConstructor;
|
||||
export const ToggleSwitch: VueConstructor;
|
||||
|
||||
type ArrayListRow = {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type ArrayListData = {
|
||||
lastUpdateWasFromValue: boolean;
|
||||
rows: Array<ArrayListRow>,
|
||||
}
|
||||
|
|
@ -1,396 +1,49 @@
|
|||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||
import { removeAt } from '@shell/utils/array';
|
||||
import { TextAreaAutoGrow } from '@components/Form/TextArea';
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import ArrayList from '@components/Form/ArrayList/ArrayList';
|
||||
|
||||
const DEFAULT_PROTIP = 'Tip: Paste lines into any list field for easy bulk entry';
|
||||
/*
|
||||
The purpose of this component is to allow the ArrayList component
|
||||
to use translations from Rancher's Vuex store, while also separating out
|
||||
that dependency on the store so that the ArrayList can be added to the Rancher
|
||||
component library.
|
||||
*/
|
||||
|
||||
export default {
|
||||
components: { TextAreaAutoGrow, LabeledInput },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
initialEmptyRow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
protip: {
|
||||
type: [String, Boolean],
|
||||
default: DEFAULT_PROTIP,
|
||||
},
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
valueLabel: {
|
||||
type: String,
|
||||
default: 'Value',
|
||||
},
|
||||
valuePlaceholder: {
|
||||
type: String,
|
||||
default: 'e.g. bar'
|
||||
},
|
||||
valueMultiline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
name: 'ArrayListWrapper',
|
||||
components: { ArrayList },
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
addLabel: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$store.getters['i18n/t']('generic.add');
|
||||
},
|
||||
},
|
||||
addAllowed: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
removeLabel: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$store.getters['i18n/t']('generic.remove');
|
||||
},
|
||||
},
|
||||
removeAllowed: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
defaultAddValue: {
|
||||
type: [String, Number, Object, Array],
|
||||
default: ''
|
||||
},
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
rules: {
|
||||
default: () => [],
|
||||
type: Array,
|
||||
// we only want functions in the rules array
|
||||
validator: rules => rules.every(rule => ['function'].includes(typeof rule))
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const input = (this.value || []).slice();
|
||||
const rows = [];
|
||||
|
||||
for ( const value of input ) {
|
||||
rows.push({ value });
|
||||
}
|
||||
|
||||
if ( !rows.length && this.initialEmptyRow ) {
|
||||
const value = this.defaultAddValue ? clone(this.defaultAddValue) : '';
|
||||
|
||||
rows.push({ value });
|
||||
}
|
||||
|
||||
return { rows, lastUpdateWasFromValue: false };
|
||||
},
|
||||
|
||||
computed: {
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
|
||||
showAdd() {
|
||||
return this.addAllowed;
|
||||
},
|
||||
|
||||
showRemove() {
|
||||
return this.removeAllowed;
|
||||
},
|
||||
|
||||
isDefaultProtip() {
|
||||
return this.protip === DEFAULT_PROTIP;
|
||||
},
|
||||
|
||||
showProtip() {
|
||||
if (this.protip && !this.isDefaultProtip) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !this.valueMultiline && this.protip;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value() {
|
||||
this.lastUpdateWasFromValue = true;
|
||||
this.rows = (this.value || []).map(v => ({ value: v }));
|
||||
},
|
||||
rows: {
|
||||
deep: true,
|
||||
handler(newValue, oldValue) {
|
||||
// lastUpdateWasFromValue is used to break a cycle where when rows are updated
|
||||
// this was called which then forced rows to updated again
|
||||
if (!this.lastUpdateWasFromValue) {
|
||||
this.queueUpdate();
|
||||
}
|
||||
this.lastUpdateWasFromValue = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.queueUpdate = debounce(this.update, 50);
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
this.rows.push({ value: clone(this.defaultAddValue) });
|
||||
if (this.defaultAddValue) {
|
||||
this.queueUpdate();
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
const inputs = this.$refs.value;
|
||||
|
||||
if ( inputs && inputs.length > 0 ) {
|
||||
inputs[inputs.length - 1].focus();
|
||||
}
|
||||
|
||||
this.$emit('add');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove item and emits removed row and its own index value
|
||||
*/
|
||||
remove(row, index) {
|
||||
this.$emit('remove', { row, index });
|
||||
removeAt(this.rows, index);
|
||||
this.queueUpdate();
|
||||
},
|
||||
|
||||
update() {
|
||||
if ( this.isView ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const out = [];
|
||||
|
||||
for ( const row of this.rows ) {
|
||||
const trim = !this.valueMultiline && (typeof row.value === 'string');
|
||||
const value = trim ? row.value.trim() : row.value;
|
||||
|
||||
if ( typeof value !== 'undefined' ) {
|
||||
out.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('input', out);
|
||||
},
|
||||
|
||||
onPaste(index, event) {
|
||||
if (this.valueMultiline) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const text = event.clipboardData.getData('text/plain');
|
||||
const split = text.split('\n').map(value => ({ value }));
|
||||
|
||||
this.rows.splice(index, 1, ...split);
|
||||
this.update();
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="title"
|
||||
class="clearfix"
|
||||
>
|
||||
<slot name="title">
|
||||
<h3>
|
||||
{{ title }}
|
||||
<i
|
||||
v-if="showProtip"
|
||||
v-tooltip="protip"
|
||||
class="icon icon-info"
|
||||
/>
|
||||
</h3>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<template v-if="rows.length">
|
||||
<div v-if="showHeader">
|
||||
<slot name="column-headers">
|
||||
<label class="value text-label mb-10">
|
||||
{{ valueLabel }}
|
||||
</label>
|
||||
</slot>
|
||||
</div>
|
||||
<div
|
||||
v-for="(row, idx) in rows"
|
||||
:key="idx"
|
||||
class="box"
|
||||
>
|
||||
<slot
|
||||
name="columns"
|
||||
:queueUpdate="queueUpdate"
|
||||
:i="idx"
|
||||
:rows="rows"
|
||||
:row="row"
|
||||
:mode="mode"
|
||||
:isView="isView"
|
||||
>
|
||||
<div class="value">
|
||||
<slot
|
||||
name="value"
|
||||
:row="row"
|
||||
:mode="mode"
|
||||
:isView="isView"
|
||||
:queue-update="queueUpdate"
|
||||
>
|
||||
<TextAreaAutoGrow
|
||||
v-if="valueMultiline"
|
||||
ref="value"
|
||||
v-model="row.value"
|
||||
:placeholder="valuePlaceholder"
|
||||
:mode="mode"
|
||||
:disabled="disabled"
|
||||
@paste="onPaste(idx, $event)"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-else-if="rules.length > 0"
|
||||
ref="value"
|
||||
v-model="row.value"
|
||||
:placeholder="valuePlaceholder"
|
||||
:disabled="isView || disabled"
|
||||
:rules="rules"
|
||||
:compact="false"
|
||||
@paste="onPaste(idx, $event)"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
ref="value"
|
||||
v-model="row.value"
|
||||
:placeholder="valuePlaceholder"
|
||||
:disabled="isView || disabled"
|
||||
@paste="onPaste(idx, $event)"
|
||||
@input="queueUpdate"
|
||||
>
|
||||
</slot>
|
||||
</div>
|
||||
</slot>
|
||||
<div
|
||||
v-if="showRemove"
|
||||
class="remove"
|
||||
>
|
||||
<slot
|
||||
name="remove-button"
|
||||
:remove="() => remove(row, idx)"
|
||||
:i="idx"
|
||||
:row="row"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="isView"
|
||||
class="btn role-link"
|
||||
:data-testid="`remove-item-${idx}`"
|
||||
@click="remove(row, idx)"
|
||||
>
|
||||
{{ removeLabel }}
|
||||
</button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-else-if="mode==='view'"
|
||||
class="text-muted"
|
||||
>
|
||||
—
|
||||
</div>
|
||||
<div v-else>
|
||||
<slot name="empty" />
|
||||
</div>
|
||||
<div
|
||||
v-if="showAdd && !isView"
|
||||
class="footer"
|
||||
<array-list
|
||||
v-bind="$attrs"
|
||||
:add-label="addLabel"
|
||||
:remove-label="removeLabel"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<template
|
||||
v-for="(_, slot) of $scopedSlots"
|
||||
v-slot:[slot]="scope"
|
||||
>
|
||||
<slot
|
||||
v-if="showAdd"
|
||||
name="add"
|
||||
:add="add"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-tertiary add"
|
||||
:disabled="loading"
|
||||
data-testid="add-item"
|
||||
@click="add()"
|
||||
>
|
||||
<i
|
||||
v-if="loading"
|
||||
class="mr-5 icon icon-spinner icon-spin icon-lg"
|
||||
/>
|
||||
{{ addLabel }}
|
||||
</button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
margin-bottom: 10px;
|
||||
|
||||
}
|
||||
|
||||
.box {
|
||||
display: grid;
|
||||
grid-template-columns: auto $array-list-remove-margin;
|
||||
align-items: center;
|
||||
|
||||
margin-bottom: 10px;
|
||||
.value {
|
||||
flex: 1;
|
||||
INPUT {
|
||||
height: $input-height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footer {
|
||||
.protip {
|
||||
float: right;
|
||||
padding: 5px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
:name="slot"
|
||||
v-bind="scope"
|
||||
/>
|
||||
</template>
|
||||
</array-list>
|
||||
</template>
|
||||
|
|
@ -4,7 +4,7 @@ import ArrayListGrouped from '@shell/components/form/ArrayListGrouped.vue';
|
|||
describe('component: ArrayListGrouped', () => {
|
||||
it('should display enabled add button', () => {
|
||||
const wrapper = mount(ArrayListGrouped);
|
||||
const button = wrapper.find('[data-testid^="add-item"]').element as HTMLInputElement;
|
||||
const button = wrapper.find('[data-testid^="array-list-button"]').element as HTMLInputElement;
|
||||
|
||||
expect(button.disabled).toBe(false);
|
||||
});
|
||||
|
|
@ -14,7 +14,7 @@ describe('component: ArrayListGrouped', () => {
|
|||
mocks: { propsData: { value: ['a', 'b'] } },
|
||||
slots: { default: '<div id="test"/>' }
|
||||
});
|
||||
const button = wrapper.find('[data-testid^="add-item"]');
|
||||
const button = wrapper.find('[data-testid^="array-list-button"]');
|
||||
|
||||
await button.trigger('click');
|
||||
await button.trigger('click');
|
||||
|
|
@ -29,7 +29,7 @@ describe('component: ArrayListGrouped', () => {
|
|||
slots: { default: '<div id="test"/>' }
|
||||
});
|
||||
|
||||
await wrapper.find('[data-testid^="add-item"]').trigger('click');
|
||||
await wrapper.find('[data-testid^="array-list-button"]').trigger('click');
|
||||
await wrapper.find('[data-testid^="remove-item"]').trigger('click');
|
||||
|
||||
const elements = wrapper.findAll('#test');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import { Meta, Canvas, Story, ArgsTable, Source } from '@storybook/addon-docs';
|
||||
import ArrayList from '@/pkg/rancher-components/src/components/Form/ArrayList/ArrayList';
|
||||
import { _EDIT, _VIEW } from "@shell/config/query-params";
|
||||
|
||||
export const Template = (args, { argTypes }) => ({
|
||||
components: { ArrayList },
|
||||
props: Object.keys(argTypes),
|
||||
template: '<ArrayList v-bind="$props" />',
|
||||
})
|
||||
|
||||
<Meta
|
||||
title="Components/Form/ArrayList"
|
||||
decorators={[]}
|
||||
/>
|
||||
|
||||
# ArrayList
|
||||
The ArrayList is used to input a list of strings. The list is of variable length.
|
||||
|
||||
## Default Empty ArrayList (Button only)
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Empty"
|
||||
>
|
||||
{Template.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
|
||||
## Showing an Initial Empty Row
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Showing an initial empty row"
|
||||
args={{
|
||||
initialEmptyRow: true
|
||||
}}>
|
||||
{Template.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Read-only Mode
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Disabled"
|
||||
args={{
|
||||
value: ['string 1', 'string 2'],
|
||||
mode: _VIEW
|
||||
}}>
|
||||
{Template.bind({})}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
### Import
|
||||
<Source
|
||||
language='ts'
|
||||
code={`
|
||||
import ArrayList from '@/pkg/rancher-components/src/components/Form/ArrayList/ArrayList';
|
||||
`}
|
||||
/>
|
||||
|
||||
|
||||
### Props table
|
||||
|
||||
<ArgsTable of={ArrayList} />
|
||||
Loading…
Reference in New Issue