Merge pull request #282 from directus/v-field-select

Adds a v-field-select component
This commit is contained in:
Rijk van Zanten
2020-09-24 16:09:18 -04:00
committed by GitHub
15 changed files with 355 additions and 39 deletions

View File

@@ -13,6 +13,7 @@ import VDivider from './v-divider';
import VError from './v-error';
import VFancySelect from './v-fancy-select';
import VFieldTemplate from './v-field-template';
import VFieldSelect from './v-field-select';
import VForm from './v-form';
import VHover from './v-hover/';
import VIcon from './v-icon/';
@@ -64,6 +65,7 @@ Vue.component('v-divider', VDivider);
Vue.component('v-error', VError);
Vue.component('v-fancy-select', VFancySelect);
Vue.component('v-field-template', VFieldTemplate);
Vue.component('v-field-select', VFieldSelect);
Vue.component('v-form', VForm);
Vue.component('v-hover', VHover);
Vue.component('v-icon', VIcon);

View File

@@ -75,8 +75,8 @@ export default defineComponent({
<style>
body {
--v-chip-color: var(--white);
--v-chip-background-color: var(--primary);
--v-chip-color: var(--black);
--v-chip-background-color: var(--background-normal-alt);
--v-chip-color-hover: var(--white);
--v-chip-background-color-hover: var(--primary-125);
--v-chip-close-color: var(--danger);
@@ -89,7 +89,7 @@ body {
.v-chip {
display: inline-flex;
align-items: center;
height: 32px;
height: 36px;
padding: 0 8px;
color: var(--v-chip-color);
font-weight: var(--weight-normal);

View File

@@ -0,0 +1,4 @@
import VFieldSelect from './v-field-select.vue';
export default VFieldSelect;
export { VFieldSelect };

View File

@@ -0,0 +1 @@
# Field Select

View File

@@ -0,0 +1,167 @@
<template>
<v-notice v-if="!availableFields || availableFields.length === 0">
{{ $t('no_fields_in_collection', { collection: (collectionInfo && collectionInfo.name) || collection }) }}
</v-notice>
<draggable v-else v-model="selectedFields" draggable=".draggable" :set-data="hideDragImage" class="v-field-select">
<v-chip
v-for="(field, index) in selectedFields"
:key="index"
class="field draggable"
v-tooltip="field.field"
@click="removeField(field.field)"
>
{{ field.name }}
</v-chip>
<template #footer>
<v-menu show-arrow v-model="menuActive" class="add" placement="bottom">
<template #activator="{ toggle }">
<v-button @click="toggle" small>
{{ $t('add_field') }}
<v-icon small name="add" />
</v-button>
</template>
<v-list dense>
<field-list-item
v-for="field in availableFields"
:key="field.field"
:field="field"
:depth="depth"
@add="addField"
/>
</v-list>
</v-menu>
</template>
</draggable>
</template>
<script lang="ts">
import { defineComponent, toRefs, ref, watch, onMounted, onUnmounted, PropType, computed } from '@vue/composition-api';
import FieldListItem from '../v-field-template/field-list-item.vue';
import { useFieldsStore } from '@/stores';
import { Field } from '@/types/';
import Draggable from 'vuedraggable';
import useFieldTree from '@/composables/use-field-tree';
import useCollection from '@/composables/use-collection';
import { FieldTree } from '../v-field-template/types';
import hideDragImage from '@/utils/hide-drag-image';
export default defineComponent({
components: { FieldListItem, Draggable },
props: {
disabled: {
type: Boolean,
default: false,
},
value: {
type: Array as PropType<string[]>,
default: null,
},
collection: {
type: String,
required: true,
},
depth: {
type: Number,
default: 1,
},
},
setup(props, { emit }) {
const fieldsStore = useFieldsStore();
const menuActive = ref(false);
const { collection } = toRefs(props);
const { info, primaryKeyField, fields: fieldsInCollection, sortField } = useCollection(collection);
const { tree } = useFieldTree(collection, true);
const _value = computed({
get() {
return props.value || [];
},
set(newVal: string[]) {
emit('input', newVal);
},
});
const selectedFields = computed({
get() {
return _value.value.map((field) => ({
field,
name: findTree(tree.value, field.split('.'))?.name as string,
}));
},
set(newVal: { field: string; name: string }[]) {
_value.value = newVal.map((field) => field.field);
},
});
const availableFields = computed(() => {
return filterTree(tree.value);
});
return {
menuActive,
addField,
removeField,
availableFields,
selectedFields,
hideDragImage,
tree,
collectionInfo: info,
};
function findTree(tree: FieldTree[] | undefined, fieldSections: string[]): FieldTree | undefined {
if (tree === undefined) return undefined;
const fieldObject = tree.find((f) => f.field === fieldSections[0]);
if (fieldObject === undefined) return undefined;
if (fieldSections.length === 1) return fieldObject;
return findTree(fieldObject.children, fieldSections.slice(1));
}
function filterTree(tree: FieldTree[] | undefined, prefix = '') {
if (tree === undefined) return undefined;
const newTree: FieldTree[] = tree.map((field) => {
return {
name: field.name,
field: field.field,
disabled: _value.value.includes(prefix + field.field),
children: filterTree(field.children, prefix + field.field + '.'),
};
});
return newTree.length === 0 ? undefined : newTree;
}
function removeField(field: string) {
_value.value = _value.value.filter((f) => f !== field);
}
function addField(field: string) {
const newArray = _value.value;
newArray.push(field);
_value.value = [...new Set(newArray)];
}
},
});
</script>
<style lang="scss" scoped>
.v-field-select {
display: flex;
flex-wrap: wrap;
}
.v-chip.field {
margin-right: 5px;
&:hover {
background-color: var(--danger);
border-color: var(--danger);
}
}
</style>

View File

@@ -1,6 +1,7 @@
<template>
<v-list-item
v-if="field.children === undefined"
:disabled="field.disabled"
@click="$emit('add', `${parent ? parent + '.' : ''}${field.field}`)"
>
<v-list-item-content>{{ field.name }}</v-list-item-content>

View File

@@ -1,7 +1,9 @@
import { Field } from '@/types';
import { TranslateResult } from 'vue-i18n';
export type FieldTree = {
field: string;
name: string | TranslateResult;
disabled?: boolean;
children?: FieldTree[];
};

View File

@@ -3,17 +3,22 @@ import { FieldTree } from './types';
import { useFieldsStore, useRelationsStore } from '@/stores/';
import { Field, Relation } from '@/types';
export default function useFieldTree(collection: Ref<string>) {
export default function useFieldTree(collection: Ref<string>, showHidden = false) {
const fieldsStore = useFieldsStore();
const relationsStore = useRelationsStore();
const tree = computed<FieldTree[]>(() => {
return fieldsStore
.getFieldsForCollection(collection.value)
.filter(
(field: Field) =>
field.meta?.hidden === false && (field.meta?.special || []).includes('alias') === false
)
.filter((field: Field) => {
let shown = (field.meta?.special || []).includes('alias') === false;
if (showHidden === false && field.meta?.hidden === false) {
shown = false;
}
return shown;
})
.map((field: Field) => parseField(field, []));
function parseField(field: Field, parents: Field[]) {

View File

@@ -1,5 +1,6 @@
import { defineInterface } from '../define';
import InterfaceManyToMany from './many-to-many.vue';
import Options from './options.vue';
export default defineInterface(({ i18n }) => ({
id: 'many-to-many',
@@ -9,19 +10,6 @@ export default defineInterface(({ i18n }) => ({
component: InterfaceManyToMany,
relationship: 'm2m',
types: ['alias'],
options: [
{
field: 'fields',
type: 'json',
name: i18n.tc('field', 0),
meta: {
interface: 'tags',
width: 'full',
options: {
placeholder: i18n.t('readable_fields_copy'),
},
},
},
],
options: Options,
recommendedDisplays: ['related-values'],
}));

View File

@@ -0,0 +1,73 @@
<template>
<v-notice type="warning" v-if="junctionCollection === null">
{{ $t('interfaces.one-to-many.no_collection') }}
</v-notice>
<div v-else class="form-grid">
<div class="field full">
<p class="type-label">{{ $t('select_fields') }}</p>
<v-field-select :collection="junctionCollection" v-model="fields" />
</div>
</div>
</template>
<script lang="ts">
import { Field } from '@/types';
import { defineComponent, PropType, computed } from '@vue/composition-api';
import { useRelationsStore } from '@/stores/';
import { Relation } from '@/types';
export default defineComponent({
props: {
collection: {
type: String,
required: true,
},
fieldData: {
type: Object as PropType<Field>,
default: null,
},
relations: {
type: Array as PropType<Relation[]>,
default: () => [],
},
value: {
type: Object as PropType<any>,
default: null,
},
},
setup(props, { emit }) {
const relationsStore = useRelationsStore();
const fields = computed({
get() {
return props.value?.fields;
},
set(newFields: string) {
emit('input', {
...(props.value || {}),
fields: newFields,
});
},
});
const junctionCollection = computed(() => {
if (!props.fieldData || !props.relations || props.relations.length === 0) return null;
const { field } = props.fieldData;
const junctionRelation = props.relations.find(
(relation) => relation.one_collection === props.collection && relation.one_field === field
);
return junctionRelation?.many_collection || null;
});
return { fields, junctionCollection };
},
});
</script>
<style lang="scss" scoped>
@import '@/styles/mixins/form-grid';
.form-grid {
@include form-grid;
}
</style>

View File

@@ -1,5 +1,6 @@
import { defineInterface } from '../define';
import InterfaceOneToMany from './one-to-many.vue';
import Options from './options.vue';
export default defineInterface(({ i18n }) => ({
id: 'one-to-many',
@@ -9,19 +10,6 @@ export default defineInterface(({ i18n }) => ({
component: InterfaceOneToMany,
types: ['alias'],
relationship: 'o2m',
options: [
{
field: 'fields',
type: 'json',
name: i18n.tc('field', 0),
meta: {
interface: 'tags',
width: 'full',
options: {
placeholder: i18n.t('interfaces.one-to-many.readable_fields_copy'),
},
},
},
],
options: Options,
recommendedDisplays: ['related-values'],
}));

View File

@@ -0,0 +1,72 @@
<template>
<v-notice type="warning" v-if="relatedCollection === null">
{{ $t('interfaces.one-to-many.no_collection') }}
</v-notice>
<div v-else class="form-grid">
<div class="field full">
<p class="type-label">{{ $t('select_fields') }}</p>
<v-field-select :collection="relatedCollection" v-model="fields" />
</div>
</div>
</template>
<script lang="ts">
import { Field, Relation } from '@/types';
import { defineComponent, PropType, computed } from '@vue/composition-api';
import { useRelationsStore } from '@/stores/';
export default defineComponent({
props: {
collection: {
type: String,
required: true,
},
fieldData: {
type: Object as PropType<Field>,
default: null,
},
relations: {
type: Array as PropType<Relation[]>,
default: () => [],
},
value: {
type: Object as PropType<any>,
default: null,
},
},
setup(props, { emit }) {
const relationsStore = useRelationsStore();
const fields = computed({
get() {
return props.value?.fields;
},
set(newFields: string) {
emit('input', {
...(props.value || {}),
fields: newFields,
});
},
});
const relatedCollection = computed(() => {
if (!props.fieldData || !props.relations || props.relations.length === 0) return null;
const { field } = props.fieldData;
const relatedRelation = props.relations.find(
(relation) => relation.one_collection === props.collection && relation.one_field === field
);
return relatedRelation?.many_collection || null;
});
return { fields, relatedCollection };
},
});
</script>
<style lang="scss" scoped>
@import '@/styles/mixins/form-grid.scss';
.form-grid {
@include form-grid;
}
</style>

View File

@@ -10,6 +10,8 @@
"only_show_the_file_extension": "Only show the file extension",
"textarea": "Textarea",
"add_field": "Add Field",
"role_name": "Role Name",
"db_only_click_to_configure": "Database Only: Click to Configure ",
@@ -1036,6 +1038,8 @@
}
},
"no_fields_in_collection": "There are no fields in \"{collection}\" yet",
"do_nothing": "Do Nothing",
"generate_and_save_uuid": "Generate and Save UUID",
"save_current_user_id": "Save Current User ID",

View File

@@ -131,7 +131,8 @@
"one-to-many": {
"one-to-many": "One to Many",
"description": "Select multiple related items",
"readable_fields_copy": "Select the fields that the user can view"
"readable_fields_copy": "Select the fields that the user can view",
"no_collection": "The collection could not be found"
},
"radio-buttons": {
"radio-buttons": "Radio Buttons",

View File

@@ -24,7 +24,9 @@
<component
v-model="fieldData.meta.options"
:collection="collection"
:field-data="fieldData"
:relations="relations"
:is="`interface-options-${selectedInterface.id}`"
v-else
/>
@@ -33,7 +35,7 @@
</template>
<script lang="ts">
import { defineComponent, computed, watch } from '@vue/composition-api';
import { defineComponent, computed, watch, toRefs } from '@vue/composition-api';
import { getInterfaces } from '@/interfaces';
import { FancySelectItem } from '@/components/v-fancy-select/types';
@@ -45,6 +47,10 @@ export default defineComponent({
type: String,
required: true,
},
collection: {
type: String,
required: true,
},
},
setup(props) {
const interfaces = getInterfaces();
@@ -109,7 +115,9 @@ export default defineComponent({
return interfaces.value.find((inter) => inter.id === state.fieldData.meta.interface);
});
return { fieldData: state.fieldData, selectItems, selectedInterface };
const { fieldData, relations } = toRefs(state);
return { fieldData, relations, selectItems, selectedInterface };
},
});
</script>