Add dropdown interface (#461)

* Add show-deselect option to v-select

* Add parse-choices util

* Add dropdown interface

* Add allow-other prop to v-select (single only)

* Check for custom state correctly

* Treat empty custom value as null

* Set full-width to true by default for inputs / selects

* Add allow-other support to multiple dropdown

* Upgrade display value to show item count

* Fix custom deletion

* Fix tests

* Pass allow other on in dropdown interface
This commit is contained in:
Rijk van Zanten
2020-04-23 18:16:17 -04:00
committed by GitHub
parent 571412ff2c
commit 0b05613a55
42 changed files with 663 additions and 150 deletions

View File

@@ -58,7 +58,7 @@ export const monospace = () => ({
export const disabled = () => `<v-input value="I'm disabled" disabled />`;
export const fullWidth = () => `
<v-input placeholder="Enter content..." full-width />
<v-input placeholder="Enter content..." />
`;
export const forceSlug = () =>

View File

@@ -74,7 +74,7 @@ export default defineComponent({
},
fullWidth: {
type: Boolean,
default: false,
default: true,
},
value: {
type: [String, Number],

View File

@@ -132,13 +132,17 @@ export default defineComponent({
background-color: var(--v-list-item-background-color-hover);
}
&:not(.disabled):active,
&.active {
&:not(.disabled):active {
color: var(--v-list-item-color-active);
background-color: var(--v-list-item-background-color-active);
}
}
&.active {
color: var(--v-list-item-color-active);
background-color: var(--v-list-item-background-color-active);
}
&.disabled {
--v-list-item-color: var(--foreground-subdued);
}

View File

@@ -22,16 +22,17 @@ Renders a dropdown input.
## Props
| Prop | Description | Default |
| ------------- | --------------------------------------------------- | ------- |
| `items`\* | Items to render in the select | |
| `itemText` | What item value to use for the display text | `text` |
| `itemValue` | What item value to use for the item value | `value` |
| `value` | Currently selected item(s) | |
| `multiple` | Allow multiple items to be selected | `false` |
| `placeholder` | What placeholder to show when no items are selected | |
| `full-width` | Render the select at full width | |
| `disabled` | Disable the select | |
| Prop | Description | Default |
|-----------------|-----------------------------------------------------|---------|
| `items`\* | Items to render in the select | |
| `itemText` | What item value to use for the display text | `text` |
| `itemValue` | What item value to use for the item value | `value` |
| `value` | Currently selected item(s) | |
| `multiple` | Allow multiple items to be selected | `false` |
| `placeholder` | What placeholder to show when no items are selected | |
| `full-width` | Render the select at full width | |
| `disabled` | Disable the select | |
| `show-deselect` | Show the deselect option when a value has been set | |
## Events

View File

@@ -1,8 +1,9 @@
import readme from './readme.md';
import { defineComponent, ref } from '@vue/composition-api';
import withPadding from '../../../.storybook/decorators/with-padding';
import { withKnobs, array, text } from '@storybook/addon-knobs';
import { withKnobs, array, text, boolean } from '@storybook/addon-knobs';
import RawValue from '../../../.storybook/raw-value.vue';
import i18n from '@/lang';
import VSelect from './v-select.vue';
@@ -16,6 +17,7 @@ export default {
export const basic = () =>
defineComponent({
i18n,
components: { VSelect, RawValue },
props: {
items: {
@@ -24,14 +26,26 @@ export const basic = () =>
placeholder: {
default: text('Placeholder', 'Enter value...'),
},
showDeselect: {
default: boolean('Show Deselect', false),
},
allowOther: {
default: boolean('Allow Other', false),
},
},
setup() {
const value = ref(null);
return { value };
},
template: `
<div>
<v-select :placeholder="placeholder" v-model="value" :items="items" />
<div style="max-width: 300px;">
<v-select
:show-deselect="showDeselect"
:allow-other="allowOther"
:placeholder="placeholder"
v-model="value"
:items="items"
/>
<raw-value>{{ value }}</raw-value>
</div>
`,
@@ -39,6 +53,7 @@ export const basic = () =>
export const multiple = () =>
defineComponent({
i18n,
components: { VSelect, RawValue },
props: {
items: {
@@ -47,14 +62,27 @@ export const multiple = () =>
placeholder: {
default: text('Placeholder', 'Enter value...'),
},
showDeselect: {
default: boolean('Show Deselect', false),
},
allowOther: {
default: boolean('Allow Other', false),
},
},
setup() {
const value = ref(null);
return { value };
},
template: `
<div>
<v-select :placeholder="placeholder" v-model="value" :items="items" multiple />
<div style="max-width: 300px;">
<v-select
:show-deselect="showDeselect"
:allow-other="allowOther"
:placeholder="placeholder"
v-model="value"
:items="items"
multiple
/>
<raw-value>{{ value }}</raw-value>
</div>
`,

View File

@@ -1,10 +1,5 @@
<template>
<v-menu
:disabled="disabled"
class="v-select"
attached
:close-on-content-click="multiple === false"
>
<v-menu :disabled="disabled" class="v-select" attached>
<template #activator="{ toggle }">
<v-input
:full-width="fullWidth"
@@ -14,17 +9,31 @@
:placeholder="placeholder"
:disabled="disabled"
>
<template #prepend><slot name="prepend" /></template>
<template #append><v-icon name="expand_more" /></template>
</v-input>
</template>
<v-list dense>
<template v-if="showDeselect">
<v-list-item @click="$emit('input', null)" :disabled="value === null">
<v-list-item-icon v-if="multiple === true">
<v-icon name="close" />
</v-list-item-icon>
<v-list-item-content>
{{ multiple ? $t('deselect_all') : $t('deselect') }}
</v-list-item-content>
<v-list-item-icon v-if="multiple === false">
<v-icon name="close" />
</v-list-item-icon>
</v-list-item>
<v-divider />
</template>
<v-list-item
v-for="item in _items"
:key="item.value"
:class="{
active: multiple ? (value || []).includes(item.value) : value === item.value,
}"
:active="multiple ? (value || []).includes(item.value) : value === item.value"
@click="multiple ? null : $emit('input', item.value)"
>
<v-list-item-content>
@@ -38,22 +47,70 @@
/>
</v-list-item-content>
</v-list-item>
<v-list-item v-if="allowOther && multiple === false" :active="usesOtherValue">
<v-list-item-content>
<input
class="other-input"
@focus="otherValue ? $emit('input', otherValue) : null"
v-model="otherValue"
:placeholder="$t('other')"
/>
</v-list-item-content>
</v-list-item>
<template v-if="allowOther && multiple === true">
<v-list-item
v-for="otherValue in otherValues"
:key="otherValue.key"
:active="(value || []).includes(otherValue.value)"
>
<v-list-item-icon>
<v-checkbox
:inputValue="value || []"
:value="otherValue.value"
@change="$emit('input', $event.length > 0 ? $event : null)"
/>
</v-list-item-icon>
<v-list-item-content>
<input
class="other-input"
:value="otherValue.value"
:placeholder="$t('other')"
v-focus
@input="setOtherValue(otherValue.key, $event.target.value)"
@blur="
otherValue.value.length === 0 && setOtherValue(otherValue.key, null)
"
/>
</v-list-item-content>
<v-list-item-icon>
<v-icon name="close" @click="setOtherValue(otherValue.key, null)" />
</v-list-item-icon>
</v-list-item>
<v-list-item @click="addOtherValue()">
<v-list-item-icon><v-icon name="add" /></v-list-item-icon>
<v-list-item-content>{{ $t('other') }}</v-list-item-content>
</v-list-item>
</template>
</v-list>
</v-menu>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from '@vue/composition-api';
import { defineComponent, PropType, computed, ref, watch } from '@vue/composition-api';
import { nanoid } from 'nanoid';
import i18n from '@/lang';
type Item = {
text: string;
value: string | number;
value: string;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ItemsRaw = (string | any)[];
type InputValue = (string | number)[] | string | number;
type InputValue = string[] | string;
export default defineComponent({
props: {
@@ -83,82 +140,221 @@ export default defineComponent({
},
fullWidth: {
type: Boolean,
default: false,
},
allowNull: {
type: Boolean,
default: false,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
showDeselect: {
type: Boolean,
default: false,
},
allowOther: {
type: Boolean,
default: false,
},
},
setup(props) {
const _items = computed(() => {
const items = props.items.map((item) => {
if (typeof item === 'string') {
return {
text: item,
value: item,
};
}
setup(props, { emit }) {
const { _items } = useItems();
const { displayValue } = useDisplayValue();
const { otherValue, usesOtherValue } = useCustomValue();
const { otherValues, addOtherValue, setOtherValue } = useMultipleCustomValues();
return {
text: item[props.itemText],
value: item[props.itemValue],
};
return {
_items,
displayValue,
otherValue,
usesOtherValue,
otherValues,
addOtherValue,
setOtherValue,
};
function useItems() {
const _items = computed(() => {
const items = props.items.map((item) => {
if (typeof item === 'string') {
return {
text: item,
value: item,
};
}
return {
text: item[props.itemText],
value: item[props.itemValue],
};
});
return items;
});
if (props.allowNull) {
items.unshift({
text: i18n.t('none'),
value: null,
});
return { _items };
}
function useDisplayValue() {
const displayValue = computed(() => {
if (Array.isArray(props.value)) {
if (props.value.length < 3) {
return props.value
.map((value) => {
return getTextForValue(value) || value;
})
.join(', ');
} else {
const itemCount = _items.value.length + otherValues.value.length;
const selectionCount = props.value.length;
if (itemCount === selectionCount) {
return i18n.t('all_items');
} else {
return i18n.tc('item_count', selectionCount);
}
}
}
return getTextForValue(props.value) || props.value;
});
return { displayValue };
function getTextForValue(value: string | number) {
return _items.value.find((item) => item.value === value)?.['text'];
}
}
function useCustomValue() {
const localOtherValue = ref('');
const otherValue = computed({
get() {
return localOtherValue.value;
},
set(newValue: string | null) {
if (newValue === null) {
localOtherValue.value = '';
emit('input', null);
} else {
localOtherValue.value = newValue;
emit('input', newValue);
}
},
});
const usesOtherValue = computed(() => {
// Check if set value is one of the existing keys
const values = _items.value.map((item) => item.value);
return (
props.value !== null &&
props.value.length > 0 &&
values.includes(props.value) === false
);
});
return { otherValue, usesOtherValue };
}
function useMultipleCustomValues() {
type OtherValue = {
key: string;
value: string;
};
const otherValues = ref<OtherValue[]>([]);
watch(
() => props.value,
(newValue) => {
if (newValue === null) return;
if (Array.isArray(newValue) === false) return;
(newValue as string[]).forEach((value) => {
const values = _items.value.map((item) => item.value);
const existsInValues = values.includes(value) === true;
if (existsInValues === false) {
const other = otherValues.value.map((o) => o.value);
const existsInOtherValues = other.includes(value) === true;
if (existsInOtherValues === false) {
addOtherValue(value);
}
}
});
}
);
return { otherValues, addOtherValue, setOtherValue };
function addOtherValue(value = '') {
otherValues.value = [
...otherValues.value,
{
key: nanoid(),
value: value,
},
];
}
return items;
});
function setOtherValue(key: string, newValue: string | null) {
const previousValue = otherValues.value.find((o) => o.key === key);
const displayValue = computed(() => {
if (Array.isArray(props.value)) {
return props.value
.map((value) => {
return getTextForValue(value);
})
.join(', ');
const valueWithoutPrevious = ((props.value || []) as string[]).filter(
(val) => val !== previousValue?.value
);
if (newValue === null) {
otherValues.value = otherValues.value.filter((o) => o.key !== key);
if (valueWithoutPrevious.length === 0) {
emit('input', null);
} else {
emit('input', valueWithoutPrevious);
}
} else {
otherValues.value = otherValues.value.map((otherValue) => {
if (otherValue.key === key) otherValue.value = newValue;
return otherValue;
});
const newEmitValue = [...valueWithoutPrevious, newValue];
emit('input', newEmitValue);
}
}
return getTextForValue(props.value);
});
return { _items, displayValue };
function getTextForValue(value: string | number) {
return _items.value.find((item) => item.value === value)?.['text'];
}
},
});
</script>
<style lang="scss" scoped>
.v-select {
<style>
:root {
--v-select-font-family: var(--family-sans-serif);
font-family: var(--v-select-font-family);
.item-text {
font-family: var(--v-select-font-family);
}
.v-input {
--v-input-font-family: var(--v-select-font-family);
cursor: pointer;
::v-deep input {
cursor: pointer;
}
}
}
</style>
<style lang="scss" scoped>
.item-text {
font-family: var(--v-select-font-family);
}
.v-input {
--v-input-font-family: var(--v-select-font-family);
cursor: pointer;
::v-deep input {
cursor: pointer;
}
}
.other-input {
margin: 0;
padding: 0;
line-height: 1.2;
background-color: transparent;
border: none;
border-radius: 0;
}
</style>

View File

@@ -21,7 +21,11 @@ describe('Textarea', () => {
await component.vm.$nextTick();
expect(component.find('.v-textarea').classes()).toEqual(['v-textarea', 'disabled']);
expect(component.find('.v-textarea').classes()).toEqual([
'v-textarea',
'disabled',
'full-width',
]);
});
it('Emits just the value for the input event', async () => {

View File

@@ -36,7 +36,7 @@ export default defineComponent({
},
fullWidth: {
type: Boolean,
default: false,
default: true,
},
value: {
type: String,

View File

@@ -0,0 +1,62 @@
import withPadding from '../../../.storybook/decorators/with-padding';
import { defineComponent, ref } from '@vue/composition-api';
import { boolean, withKnobs, text } from '@storybook/addon-knobs';
import readme from './readme.md';
import i18n from '@/lang';
import RawValue from '../../../.storybook/raw-value.vue';
export default {
title: 'Interfaces / Dropdown',
decorators: [withPadding, withKnobs],
parameters: {
notes: readme,
},
};
export const basic = () =>
defineComponent({
components: { RawValue },
i18n,
props: {
allowOther: {
default: boolean('Allow Other', false),
},
allowNone: {
default: boolean('Allow None', false),
},
placeholder: {
default: text('Placeholder', 'Select something'),
},
choices: {
default: text(
'Choices',
`
Option A
Option B
custom_value::Option C
trim :: Option D
`
),
},
icon: {
default: text('Icon', 'person'),
},
},
setup() {
const value = ref(null);
return { value };
},
template: `
<div style="max-width: 300px;">
<interface-dropdown
v-model="value"
:allow-other="allowOther"
:allow-none="allowNone"
:placeholder="placeholder"
:choices="choices"
:icon="icon"
/>
<raw-value>{{ value }}</raw-value>
</div>
`,
});

View File

@@ -0,0 +1,45 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import VNotice from '@/components/v-notice';
import VSelect from '@/components/v-select';
import VIcon from '@/components/v-icon';
import InterfaceDropdown from './dropdown.vue';
import VueI18n from 'vue-i18n';
import i18n from '@/lang';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.use(VueI18n);
localVue.component('v-select', VSelect);
localVue.component('v-notice', VNotice);
localVue.component('v-icon', VIcon);
describe('Interfaces / Dropdown', () => {
it('Renders a notice when choices arent set', async () => {
const component = shallowMount(InterfaceDropdown, {
localVue,
i18n,
listeners: {
input: () => undefined,
},
});
expect(component.find(VNotice).exists()).toBe(true);
});
it('Renders select when choices exist', async () => {
const component = shallowMount(InterfaceDropdown, {
localVue,
i18n,
listeners: {
input: () => undefined,
},
propsData: {
choices: `
test
`,
},
});
expect(component.find(VSelect).exists()).toBe(true);
});
});

View File

@@ -0,0 +1,66 @@
<template>
<v-notice v-if="!items" warning>
{{ $t('choices_option_configured_incorrectly') }}
</v-notice>
<v-select
v-else
:value="value"
@input="$listeners.input"
:items="items"
:disabled="disabled"
:show-deselect="allowNone"
:placeholder="placeholder"
:allow-other="allowOther"
>
<template #prepend v-if="icon">
<v-icon :name="icon" />
</template>
</v-select>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import parseChoices from '@/utils/parse-choices';
export default defineComponent({
props: {
disabled: {
type: Boolean,
default: false,
},
value: {
type: String,
default: null,
},
choices: {
type: String,
default: null,
},
icon: {
type: String,
default: null,
},
allowNone: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: null,
},
allowOther: {
type: Boolean,
default: false,
},
},
setup(props) {
const items = computed(() => {
if (props.choices === null || props.choices.length === 0) return null;
return parseChoices(props.choices);
});
return { items };
},
});
</script>

View File

@@ -0,0 +1,38 @@
import { defineInterface } from '@/interfaces/define';
import InterfaceDropdown from './dropdown.vue';
export default defineInterface(({ i18n }) => ({
id: 'dropdown',
name: i18n.t('dropdown'),
icon: 'arrow_drop_down_circle',
component: InterfaceDropdown,
options: [
{
field: 'choices',
name: i18n.t('choices'),
note: i18n.t('use_double_colon_for_key'),
width: 'full',
interface: 'textarea',
},
{
field: 'allowOther',
name: i18n.t('allow_other'),
width: 'half',
interface: 'toggle',
default_value: false,
},
{
field: 'allowNone',
name: i18n.t('allow_none'),
width: 'half',
interface: 'toggle',
default_value: false,
},
{
field: 'icon',
name: i18n.t('icon'),
width: 'half',
interface: 'icon',
},
],
}));

View File

@@ -0,0 +1,12 @@
# Dropdown
Pick one from a list of options.
## Options
| Option | Description | Default |
|---------------|----------------------------------------|---------|
| `placeholder` | Text to show when no input is entered | `null` |
| `allow-none` | Allow the user to deselect the value | `false` |
| `allow-other` | Allow the user to enter a custom value | `false` |
| `choices` | What choices to present to the user | `null` |

View File

@@ -5,6 +5,7 @@ import InterfaceNumeric from './numeric/';
import InterfaceSlider from './slider/';
import InterfaceToggle from './toggle/';
import InterfaceWYSIWYG from './wysiwyg/';
import InterfaceDropdown from './dropdown/';
export const interfaces = [
InterfaceTextInput,
@@ -14,6 +15,7 @@ export const interfaces = [
InterfaceDivider,
InterfaceToggle,
InterfaceWYSIWYG,
InterfaceDropdown,
];
export default interfaces;

View File

@@ -8,7 +8,6 @@
:min="minValue"
:max="maxValue"
:step="stepInterval"
full-width
@input="$listeners.input"
>
<template v-if="iconLeft" #prepend>

View File

@@ -6,7 +6,6 @@
:trim="trim"
:type="masked ? 'password' : 'text'"
:class="font"
full-width
@input="$listeners.input"
>
<template v-if="iconLeft" #prepend><v-icon :name="iconLeft" /></template>

View File

@@ -5,7 +5,6 @@
:disabled="disabled"
:class="font"
@input="$listeners.input"
full-width
/>
</template>

View File

@@ -13,6 +13,9 @@ describe('Interfaces / Toggle', () => {
it('Renders a v-checkbox', () => {
const component = shallowMount(InterfaceToggle, {
localVue,
listeners: {
input: () => undefined,
},
});
expect(component.find(VCheckbox).exists()).toBe(true);

View File

@@ -117,6 +117,7 @@
"comments": "Comments",
"item_count": "No Items | One Item | {count} Items",
"all_items": "All Items",
"users": "Users",
"files": "Files",
@@ -335,6 +336,17 @@
"custom_formats": "Custom Formats",
"tinymce_options_override": "TinyMCE Options Override",
"dropdown": "Dropdown",
"allow_other": "Allow Other",
"allow_none": "Allow None",
"choices": "Choices",
"use_double_colon_for_key": "Use double colon for dedicated keys, eg: `value_saved::Option Displayed`",
"choices_option_configured_incorrectly": "Choices option configured incorrectly",
"deselect": "Deselect",
"deselect_all": "Deselect All",
"other": "Other...",
"about_directus": "About Directus",
"activity_log": "Activity Log",
"add_field_filter": "Add a field filter",
@@ -485,7 +497,6 @@
"delete_role_are_you_sure": "Are you sure to delete the role \"{name}\"? This action cannot be undone.",
"desc": "desc",
"description": "Description",
"deselect": "Deselect",
"dialog_beginning": "Beginning of dialog window.",
"discard_changes": "Discard Changes",
"display_name": "Display Name",
@@ -673,7 +684,6 @@
"operator": "Operator",
"optional": "Optional",
"options": "Options",
"other": "Other",
"otp": "One-Time Password",
"password": "Password",
"password_reset_sending": "Sending email...",

View File

@@ -6,7 +6,6 @@
<div class="label type-text">{{ $t('layouts.cards.image_source') }}</div>
<v-select
v-model="imageSource"
full-width
allow-null
item-value="field"
item-text="name"
@@ -19,7 +18,6 @@
<v-select
v-model="imageFit"
:disabled="imageSource === null"
full-width
:items="[
{
text: $t('layouts.cards.crop'),
@@ -35,17 +33,17 @@
<div class="setting">
<div class="label type-text">{{ $t('layouts.cards.title') }}</div>
<v-input full-width v-model="title" />
<v-input v-model="title" />
</div>
<div class="setting">
<div class="label type-text">{{ $t('layouts.cards.subtitle') }}</div>
<v-input full-width v-model="subtitle" />
<v-input v-model="subtitle" />
</div>
<div class="setting">
<div class="label type-text">{{ $t('layouts.cards.fallback_icon') }}</div>
<v-input full-width v-model="icon" />
<v-input v-model="icon" />
</div>
</drawer-detail>
</portal>

View File

@@ -30,7 +30,6 @@
<drawer-detail icon="format_line_spacing" :title="$t('layouts.tabular.spacing')">
<v-select
full-width
v-model="tableSpacing"
:items="[
{

View File

@@ -9,7 +9,7 @@
<v-card>
<v-card-title>{{ $t('add_new_folder') }}</v-card-title>
<v-card-text>
<v-input :placeholder="$t('folder_name')" v-model="newFolderName" full-width />
<v-input :placeholder="$t('folder_name')" v-model="newFolderName" />
</v-card-text>
<v-card-actions>
<v-button secondary @click="dialogActive = false">{{ $t('cancel') }}</v-button>

View File

@@ -33,17 +33,16 @@
<v-tab-item value="collection">
<h2 class="type-title">{{ $t('creating_collection_info') }}</h2>
<div class="type-label">{{ $t('name') }}</div>
<v-input full-width class="monospace" v-model="collectionName" />
<v-input class="monospace" v-model="collectionName" />
<v-divider />
<div class="grid">
<div>
<div class="type-label">{{ $t('primary_key_field') }}</div>
<v-input full-width class="monospace" v-model="primaryKeyFieldName" />
<v-input class="monospace" v-model="primaryKeyFieldName" />
</div>
<div>
<div class="type-label">{{ $t('type') }}</div>
<v-select
full-width
:items="[
{
text: $t('auto_increment_integer'),

View File

@@ -7,7 +7,6 @@
readonly
@click="toggle"
:value="field.name"
full-width
>
<template #prepend>
<v-icon class="drag-handle" name="drag_indicator" @click.stop />
@@ -41,15 +40,10 @@
<v-card-title>{{ $t('duplicate_where_to') }}</v-card-title>
<v-card-text>
<span class="label">{{ $tc('collection', 0) }}</span>
<v-select
class="monospace"
:items="collections"
v-model="duplicateTo"
full-width
/>
<v-select class="monospace" :items="collections" v-model="duplicateTo" />
<span class="label">{{ $tc('field', 0) }}</span>
<v-input class="monospace" v-model="duplicateName" full-width />
<v-input class="monospace" v-model="duplicateName" />
</v-card-text>
<v-card-actions>
<v-button secondary @click="duplicateActive = false">

View File

@@ -7,7 +7,6 @@
:disabled="isNew === false"
id="name"
v-model="_field"
full-width
:placeholder="$t('enter_field_name')"
/>

View File

@@ -16,15 +16,7 @@
/>
</draggable>
<v-button
class="add-field"
align="left"
dashed
outlined
full-width
large
@click="openFieldSetup()"
>
<v-button class="add-field" align="left" dashed outlined large @click="openFieldSetup()">
<v-icon name="add" />
{{ $t('add_field') }}
</v-button>

View File

@@ -5,27 +5,27 @@
<div class="pane-form">
<div class="field">
<div class="type-label label">{{ $t('host') }}</div>
<v-input full-width v-model="_value.db_host" />
<v-input v-model="_value.db_host" />
</div>
<div class="field">
<div class="type-label label">{{ $t('port') }}</div>
<v-input type="number" full-width v-model="_value.db_port" />
<v-input type="number" v-model="_value.db_port" />
</div>
<div class="field">
<div class="type-label label">{{ $t('db_user') }}</div>
<v-input full-width v-model="_value.db_user" />
<v-input v-model="_value.db_user" />
</div>
<div class="field">
<div class="type-label label">{{ $t('db_password') }}</div>
<v-input type="password" full-width v-model="_value.db_password" />
<v-input type="password" v-model="_value.db_password" />
</div>
<div class="field">
<div class="type-label label">{{ $t('db_name') }}</div>
<v-input full-width v-model="_value.db_name" class="db" />
<v-input v-model="_value.db_name" class="db" />
</div>
<div class="field">
<div class="type-label label">{{ $t('db_type') }}</div>
<v-input full-width value="MySQL" disabled />
<v-input value="MySQL" disabled />
</div>
</div>
</div>

View File

@@ -5,19 +5,19 @@
<div class="pane-form">
<div class="field">
<div class="type-label label">{{ $t('project_name') }}</div>
<v-input full-width v-model="_value.project_name" />
<v-input v-model="_value.project_name" />
</div>
<div class="field">
<div class="type-label label">{{ $t('project_key') }}</div>
<v-input slug full-width v-model="_value.project" class="key" />
<v-input slug v-model="_value.project" class="key" />
</div>
<div class="field">
<div class="type-label label">{{ $t('admin_email') }}</div>
<v-input full-width type="email" v-model="_value.user_email" />
<v-input type="email" v-model="_value.user_email" />
</div>
<div class="field">
<div class="type-label label">{{ $t('admin_password') }}</div>
<v-input full-width type="password" v-model="_value.user_password" />
<v-input type="password" v-model="_value.user_password" />
</div>
</div>
</div>

View File

@@ -14,7 +14,6 @@
@input="setToken"
:value="token"
:placeholder="$t('super_admin_token')"
full-width
class="token"
>
<template #append>

View File

@@ -6,14 +6,12 @@
type="email"
v-model="email"
:placeholder="$t('email')"
full-width
/>
<v-input
type="password"
autocomplete="current-password"
v-model="password"
:placeholder="$t('password')"
full-width
/>
<v-notice danger v-if="error">
{{ errorFormatted }}

View File

@@ -6,7 +6,6 @@
type="email"
v-model="email"
:placeholder="$t('email')"
full-width
/>
<v-notice success v-if="done">{{ $t('password_reset_sent') }}</v-notice>
<v-notice danger v-if="error">

View File

@@ -1,12 +1,11 @@
<template>
<form @submit.prevent="onSubmit">
<v-input :value="email" disabled full-width />
<v-input :value="email" disabled />
<v-input
:placeholder="$t('password')"
autofocus
autocomplete="username"
type="password"
full-width
v-model="password"
:disabled="done"
/>

View File

@@ -16,7 +16,7 @@ export interface FieldRaw {
primary_key: boolean;
auto_increment: boolean;
default_value: any; // eslint-disable-line @typescript-eslint/no-explicit-any
note: string | null;
note: string | TranslateResult | null;
signed: boolean;
type: string;
sort: null | number;

View File

@@ -0,0 +1,4 @@
import parseChoices from './parse-choices';
export { parseChoices };
export default parseChoices;

View File

@@ -0,0 +1,42 @@
import parseChoices from './parse-choices';
describe('Utils / Parse Choices', () => {
it('Filters out empty rows', () => {
const choices = `
test
above is gone
`;
const result = parseChoices(choices);
expect(result.length).toBe(2);
expect(result[0]).toEqual({ text: 'test', value: 'test' });
});
it('Filters out whitespace around options', () => {
const choices = ' bunch of whitespace ';
const result = parseChoices(choices);
expect(result.length).toBe(1);
expect(result[0]).toEqual({ text: 'bunch of whitespace', value: 'bunch of whitespace' });
});
it('Separates on double colon to form key/value pairs', () => {
const choices = `
value::Text
`;
const result = parseChoices(choices);
expect(result[0]).toEqual({ text: 'Text', value: 'value' });
});
it('Trims whitespace around colons', () => {
const choices = `
works :: Yes!
`;
const result = parseChoices(choices);
expect(result[0]).toEqual({ text: 'Yes!', value: 'works' });
});
});

View File

@@ -0,0 +1,21 @@
export default function parseChoices(choices: string) {
return choices
.trim()
.split('\n')
.filter((r) => r.length !== 0)
.map((row) => {
const parts = row.split('::').map((part) => part.trim());
if (parts.length > 1) {
return {
value: parts[0],
text: parts[1],
};
}
return {
value: parts[0],
text: parts[0],
};
});
}

View File

@@ -5,7 +5,6 @@
:placeholder="$t('leave_comment')"
v-model="newCommentContent"
expand-on-focus
full-width
>
<template #append>
<v-button

View File

@@ -35,7 +35,7 @@ export const basic = () =>
<drawer-detail icon="forum" title="Comments">
These sections can hold any markup:
<v-input full-width placeholder="I'm an input" />
<v-input placeholder="I'm an input" />
</drawer-detail>
</drawer-detail-group>`,
});

View File

@@ -13,7 +13,7 @@
<v-menu attached>
<template #activator="{ toggle, active }">
<v-input @click="toggle" :class="{ active }" readonly value="Add filter" full-width>
<v-input @click="toggle" :class="{ active }" readonly value="Add filter">
<template #prepend><v-icon name="add" /></template>
<template #append><v-icon name="expand_more" /></template>
</v-input>

View File

@@ -1,12 +1,12 @@
<template>
<div class="filter-input">
<template v-if="['between', 'nbetween'].includes(operator)">
<v-input :type="type" :value="csvValue[0]" @input="setCSV(0, $event)" full-width>
<v-input :type="type" :value="csvValue[0]" @input="setCSV(0, $event)">
<template #append>
<v-icon name="vertical_align_top" />
</template>
</v-input>
<v-input :type="type" :value="csvValue[1]" @input="setCSV(1, $event)" full-width>
<v-input :type="type" :value="csvValue[1]" @input="setCSV(1, $event)">
<template #append>
<v-icon name="vertical_align_bottom" />
</template>
@@ -19,20 +19,19 @@
:value="val"
:type="type"
@input="setCSV(index, $event)"
full-width
>
<template #append>
<v-icon v-if="csvValue.length > 1" name="close" @click="removeCSV(val)" />
</template>
</v-input>
<v-button outlined dashed full-width @click="addCSV">
<v-button outlined dashed @click="addCSV">
<v-icon name="add" />
{{ $t('add_new') }}
</v-button>
</template>
<template v-else-if="['empty', 'nempty'].includes(operator) === false">
<v-checkbox v-if="type === 'checkbox'" :inputValue="_value" />
<v-input v-else v-model="_value" :type="type" full-width />
<v-input v-else v-model="_value" :type="type" />
</template>
</div>
</template>

View File

@@ -1,6 +1,6 @@
<template>
<drawer-detail :icon="currentLayout.icon" :title="$t('layout_type')">
<v-select :items="layouts" item-text="name" item-value="id" v-model="viewType" full-width />
<v-select :items="layouts" item-text="name" item-value="id" v-model="viewType" />
</drawer-detail>
</template>

View File

@@ -13,6 +13,8 @@ import VDialog from '@/components/v-dialog';
import VOverlay from '@/components/v-dialog';
import VCard, { VCardTitle, VCardActions } from '@/components/v-card';
import Tooltip from '@/directives/tooltip';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.use(VueRouter);
@@ -26,6 +28,8 @@ localVue.component('v-card-title', VCardTitle);
localVue.component('v-card-actions', VCardActions);
localVue.component('v-overlay', VOverlay);
localVue.directive('tooltip', Tooltip);
describe('Views / Private / Module Bar Avatar', () => {
let req: any = {};