mirror of
https://github.com/directus/directus.git
synced 2026-01-29 02:48:19 -05:00
Merge branch 'main' into suggest-m2o
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<template>
|
||||
<draggable v-model="selectedFields" draggable=".draggable" :set-data="hideDragImage" class="v-field-select">
|
||||
<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.stop="removeField(field.field)"
|
||||
@click="removeField(field.field)"
|
||||
>
|
||||
{{ field.name }}
|
||||
</v-chip>
|
||||
<v-menu
|
||||
show-arrow
|
||||
v-model="menuActive"
|
||||
v-show="selectableFields.length > 0"
|
||||
slot="footer"
|
||||
class="add"
|
||||
placement="bottom-end"
|
||||
>
|
||||
<template #activator="{ toggle }">
|
||||
<v-chip @click="toggle">
|
||||
<v-icon name="add" />
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<v-list dense>
|
||||
<field-list-item
|
||||
@add="addField"
|
||||
v-for="field in selectableFields"
|
||||
:key="field.field"
|
||||
:field="field"
|
||||
:depth="depth"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<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>
|
||||
|
||||
@@ -42,7 +42,7 @@ import FieldListItem from '../v-field-template/field-list-item.vue';
|
||||
import { useFieldsStore } from '@/stores';
|
||||
import { Field } from '@/types/';
|
||||
import Draggable from 'vuedraggable';
|
||||
import useFieldTree, { findTree, filterTree } from '@/composables/use-field-tree';
|
||||
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';
|
||||
@@ -87,7 +87,7 @@ export default defineComponent({
|
||||
|
||||
const selectedFields = computed({
|
||||
get() {
|
||||
return props.value.map((field) => ({
|
||||
return _value.value.map((field) => ({
|
||||
field,
|
||||
name: findTree(tree.value, field.split('.'))?.name as string,
|
||||
}));
|
||||
@@ -97,11 +97,45 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
|
||||
const selectableFields = computed(() => {
|
||||
return filterTree(tree.value, (field, prefix) => props.value.includes(prefix + field.field) === false);
|
||||
const availableFields = computed(() => {
|
||||
return filterTree(tree.value);
|
||||
});
|
||||
|
||||
return { menuActive, addField, removeField, selectableFields, selectedFields, hideDragImage, tree };
|
||||
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);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<v-list-item
|
||||
v-if="field.children === undefined || depth === 0"
|
||||
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>
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
@@ -3,36 +3,6 @@ import { FieldTree } from './types';
|
||||
import { useFieldsStore, useRelationsStore } from '@/stores/';
|
||||
import { Field, Relation } from '@/types';
|
||||
|
||||
export 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));
|
||||
}
|
||||
|
||||
export function filterTree(
|
||||
tree: FieldTree[] | undefined,
|
||||
f: (field: FieldTree, prefix: string) => boolean,
|
||||
prefix = ''
|
||||
) {
|
||||
if (tree === undefined) return undefined;
|
||||
|
||||
const newTree: FieldTree[] = [];
|
||||
tree.forEach((field) => {
|
||||
if (f(field, prefix)) {
|
||||
newTree.push({
|
||||
field: field.field,
|
||||
name: field.name,
|
||||
children: filterTree(field.children, f, prefix + field.field + '.'),
|
||||
});
|
||||
}
|
||||
});
|
||||
return newTree.length === 0 ? undefined : newTree;
|
||||
}
|
||||
|
||||
export default function useFieldTree(collection: Ref<string>, showHidden = false) {
|
||||
const fieldsStore = useFieldsStore();
|
||||
const relationsStore = useRelationsStore();
|
||||
@@ -40,11 +10,15 @@ export default function useFieldTree(collection: Ref<string>, showHidden = false
|
||||
const tree = computed<FieldTree[]>(() => {
|
||||
return fieldsStore
|
||||
.getFieldsForCollection(collection.value)
|
||||
.filter(
|
||||
(field: Field) =>
|
||||
showHidden ||
|
||||
(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[]) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<v-notice type="warning" v-if="collection == null">
|
||||
<v-notice type="warning" v-if="junctionCollection === null">
|
||||
{{ $t('interfaces.one-to-many.no_collection') }}
|
||||
</v-notice>
|
||||
<div v-else>
|
||||
<p class="type-label">{{ $t('select_fields') }}</p>
|
||||
<v-field-select :collection="collection" v-model="fields"></v-field-select>
|
||||
<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>
|
||||
|
||||
@@ -16,10 +18,18 @@ 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,
|
||||
@@ -40,27 +50,24 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
|
||||
const collection = computed(() => {
|
||||
if (props.fieldData.field == null || props.fieldData.meta?.collection == null) return null;
|
||||
|
||||
const collection = props.fieldData.meta.collection;
|
||||
const field = props.fieldData.field;
|
||||
|
||||
const relations: Relation[] = relationsStore.getRelationsForField(collection, field);
|
||||
|
||||
const junction = relations.find((r) => r.one_collection === collection && r.one_field === field);
|
||||
if (junction === undefined) return null;
|
||||
|
||||
return junction.many_collection;
|
||||
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, collection };
|
||||
return { fields, junctionCollection };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.type-label {
|
||||
margin-bottom: 4px;
|
||||
@import '@/styles/mixins/form-grid';
|
||||
|
||||
.form-grid {
|
||||
@include form-grid;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<v-notice type="warning" v-if="collection == null">
|
||||
<v-notice type="warning" v-if="relatedCollection === null">
|
||||
{{ $t('interfaces.one-to-many.no_collection') }}
|
||||
</v-notice>
|
||||
<div v-else class="grid">
|
||||
<div class="full">
|
||||
<div v-else class="form-grid">
|
||||
<div class="field full">
|
||||
<p class="type-label">{{ $t('select_fields') }}</p>
|
||||
<v-field-select :collection="collection" v-model="fields"></v-field-select>
|
||||
<v-field-select :collection="relatedCollection" v-model="fields" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,10 +17,18 @@ 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,
|
||||
@@ -33,26 +41,24 @@ export default defineComponent({
|
||||
get() {
|
||||
return props.value?.fields;
|
||||
},
|
||||
set(newTemplate: string) {
|
||||
set(newFields: string) {
|
||||
emit('input', {
|
||||
...(props.value || {}),
|
||||
fields: newTemplate,
|
||||
fields: newFields,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const collection = computed(() => {
|
||||
const collection = props.fieldData.meta?.collection;
|
||||
const field = props.fieldData.field;
|
||||
|
||||
if (collection == null || field == null) return null;
|
||||
|
||||
const relationData: Relation[] = relationsStore.getRelationsForField(collection, field);
|
||||
|
||||
return relationData.find((r) => r.one_collection === collection && r.one_field === field)?.many_collection;
|
||||
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, collection };
|
||||
return { fields, relatedCollection };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -60,7 +66,7 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixins/form-grid.scss';
|
||||
|
||||
.grid {
|
||||
.form-grid {
|
||||
@include form-grid;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
"one-to-many": "One to Many",
|
||||
"description": "Select multiple related items",
|
||||
"readable_fields_copy": "Select the fields that the user can view",
|
||||
"no_collection": "No matching relation could be found"
|
||||
"no_collection": "The collection could not be found"
|
||||
},
|
||||
"radio-buttons": {
|
||||
"radio-buttons": "Radio Buttons",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user