mirror of
https://github.com/directus/directus.git
synced 2026-01-23 19:18:07 -05:00
Add Tree-View Interface (#4602)
* Fix local type extraction * Render basic tree in tree-view * Render drawer-item in tree-view-group * Retrieve nested draggable changes * Default sort query to configured sortField * Store nested group / sort to API * Allow updating item values * Figure out a dropzone approach on preview * Finish editable tree view * Set sort value based on sort field in relation * Add create-new / add-existing * Respect previously made nested edits * Add description in setup * Fix fetching level of o2m sort field * Remove min height on empty root * Remove unused types * Add notice for invalid relationship type * Allow recursive o2m in setup * Styling tweak * Revert changes in v-list * Revert changes in groupable
This commit is contained in:
@@ -155,8 +155,6 @@ function getDBQuery(
|
||||
delete queryCopy.limit;
|
||||
}
|
||||
|
||||
query.sort = query.sort || [{ column: primaryKeyField, order: 'asc' }];
|
||||
|
||||
applyQuery(table, dbQuery, queryCopy, schema);
|
||||
|
||||
return dbQuery;
|
||||
@@ -187,6 +185,10 @@ function applyParentFilters(nestedCollectionNodes: NestedCollectionNode[], paren
|
||||
nestedNode.children.push({ type: 'field', name: nestedNode.relation.many_field });
|
||||
}
|
||||
|
||||
if (nestedNode.relation.sort_field) {
|
||||
nestedNode.children.push({ type: 'field', name: nestedNode.relation.sort_field });
|
||||
}
|
||||
|
||||
nestedNode.query = {
|
||||
...nestedNode.query,
|
||||
filter: {
|
||||
@@ -245,16 +247,30 @@ function mergeWithParentItems(
|
||||
}
|
||||
} else if (nestedNode.type === 'o2m') {
|
||||
for (const parentItem of parentItems) {
|
||||
let itemChildren = nestedItems.filter((nestedItem) => {
|
||||
if (nestedItem === null) return false;
|
||||
if (Array.isArray(nestedItem[nestedNode.relation.many_field])) return true;
|
||||
let itemChildren = nestedItems
|
||||
.filter((nestedItem) => {
|
||||
if (nestedItem === null) return false;
|
||||
if (Array.isArray(nestedItem[nestedNode.relation.many_field])) return true;
|
||||
|
||||
return (
|
||||
nestedItem[nestedNode.relation.many_field] == parentItem[nestedNode.relation.one_primary!] ||
|
||||
nestedItem[nestedNode.relation.many_field]?.[nestedNode.relation.one_primary!] ==
|
||||
parentItem[nestedNode.relation.one_primary!]
|
||||
);
|
||||
});
|
||||
return (
|
||||
nestedItem[nestedNode.relation.many_field] == parentItem[nestedNode.relation.one_primary!] ||
|
||||
nestedItem[nestedNode.relation.many_field]?.[nestedNode.relation.one_primary!] ==
|
||||
parentItem[nestedNode.relation.one_primary!]
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// This is pre-filled in get-ast-from-query
|
||||
const { column, order } = nestedNode.query.sort![0]!;
|
||||
|
||||
if (a[column] === b[column]) return 0;
|
||||
if (a[column] === null) return 1;
|
||||
if (b[column] === null) return -1;
|
||||
if (order === 'asc') {
|
||||
return a[column] < b[column] ? -1 : 1;
|
||||
} else {
|
||||
return a[column] < b[column] ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
// We re-apply the requested limit here. This forces the _n_ nested items per parent concept
|
||||
if (nested) {
|
||||
|
||||
@@ -445,7 +445,9 @@ export class PayloadService {
|
||||
const relatedRecords: Partial<Item>[] = [];
|
||||
|
||||
if (Array.isArray(payload[relation.one_field!])) {
|
||||
for (const relatedRecord of payload[relation.one_field!] || []) {
|
||||
for (let i = 0; i < (payload[relation.one_field!] || []).length; i++) {
|
||||
const relatedRecord = (payload[relation.one_field!] || [])[i];
|
||||
|
||||
let record = cloneDeep(relatedRecord);
|
||||
|
||||
if (typeof relatedRecord === 'string' || typeof relatedRecord === 'number') {
|
||||
@@ -467,6 +469,15 @@ export class PayloadService {
|
||||
};
|
||||
}
|
||||
|
||||
if (relation.sort_field) {
|
||||
record = {
|
||||
...record,
|
||||
[relation.sort_field]: i + 1,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(record);
|
||||
|
||||
relatedRecords.push({
|
||||
...record,
|
||||
[relation.many_field]: parent || payload[relation.one_primary!],
|
||||
|
||||
@@ -5,6 +5,10 @@ import { Permission } from './permissions';
|
||||
export type SchemaOverview = {
|
||||
tables: SO;
|
||||
relations: Relation[];
|
||||
collections: {
|
||||
collection: string;
|
||||
sort_field: string | null;
|
||||
}[];
|
||||
fields: {
|
||||
id: number;
|
||||
collection: string;
|
||||
|
||||
@@ -61,6 +61,11 @@ export default async function getASTFromQuery(
|
||||
delete query.fields;
|
||||
delete query.deep;
|
||||
|
||||
if (!query.sort) {
|
||||
const sortField = schema.collections.find((collectionInfo) => collectionInfo.collection === collection)?.sort_field;
|
||||
query.sort = [{ column: sortField || schema.tables[collection].primary, order: 'asc' }];
|
||||
}
|
||||
|
||||
ast.children = await parseFields(collection, fields, deep);
|
||||
|
||||
return ast;
|
||||
@@ -180,6 +185,10 @@ export default async function getASTFromQuery(
|
||||
query: getDeepQuery(deep?.[relationalField] || {}),
|
||||
children: await parseFields(relatedCollection, nestedFields as string[], deep?.[relationalField] || {}),
|
||||
};
|
||||
|
||||
if (relationType === 'o2m' && !child!.query.sort) {
|
||||
child!.query.sort = [{ column: relation.sort_field || relation.many_primary, order: 'asc' }];
|
||||
}
|
||||
}
|
||||
|
||||
if (child) {
|
||||
|
||||
@@ -24,6 +24,10 @@ export async function getSchema(options?: {
|
||||
|
||||
const relations = await database.select('*').from('directus_relations');
|
||||
|
||||
const collections = await database
|
||||
.select<{ collection: string; sort_field: string | null }[]>('collection', 'sort_field')
|
||||
.from('directus_collections');
|
||||
|
||||
const fields = await database
|
||||
.select<{ id: number; collection: string; field: string; special: string }[]>(
|
||||
'id',
|
||||
@@ -72,6 +76,7 @@ export async function getSchema(options?: {
|
||||
return {
|
||||
tables: schemaOverview,
|
||||
relations: relations,
|
||||
collections,
|
||||
fields: fields.map((transform) => ({
|
||||
...transform,
|
||||
special: transform.special?.split(','),
|
||||
|
||||
14
app/src/interfaces/tree-view/index.ts
Normal file
14
app/src/interfaces/tree-view/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineInterface } from '../define';
|
||||
import InterfaceTreeView from './tree-view.vue';
|
||||
|
||||
export default defineInterface(({ i18n }) => ({
|
||||
id: 'tree-view',
|
||||
name: i18n.t('tree_view'),
|
||||
description: i18n.t('interfaces.tree-view.description'),
|
||||
icon: 'account_tree',
|
||||
types: ['alias'],
|
||||
groups: ['o2m'],
|
||||
relational: true,
|
||||
component: InterfaceTreeView,
|
||||
options: [],
|
||||
}));
|
||||
72
app/src/interfaces/tree-view/item-preview.vue
Normal file
72
app/src/interfaces/tree-view/item-preview.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="preview">
|
||||
<render-template :collection="collection" :template="template" :item="item" />
|
||||
<div class="spacer" />
|
||||
<div class="actions" v-if="!disabled">
|
||||
<v-icon v-tooltip="$t('edit')" name="launch" @click="editActive = true" />
|
||||
<v-icon v-tooltip="$t('deselect')" name="clear" @click="$emit('deselect')" />
|
||||
</div>
|
||||
|
||||
<drawer-item
|
||||
:active.sync="editActive"
|
||||
:collection="collection"
|
||||
:primary-key="item[primaryKeyField] || '+'"
|
||||
:edits="item"
|
||||
@input="$emit('input', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import DrawerItem from '@/views/private/components/drawer-item';
|
||||
|
||||
export default defineComponent({
|
||||
components: { DrawerItem },
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
template: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
primaryKeyField: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const editActive = ref(false);
|
||||
return { editActive };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.preview {
|
||||
display: flex;
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
--v-icon-color: var(--foreground-subdued);
|
||||
--v-icon-color-hover: var(--foreground-normal);
|
||||
|
||||
.v-icon + .v-icon {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
180
app/src/interfaces/tree-view/nested-draggable.vue
Normal file
180
app/src/interfaces/tree-view/nested-draggable.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<draggable
|
||||
class="drag-area"
|
||||
:class="{ root, drag }"
|
||||
tag="ul"
|
||||
:list="tree"
|
||||
:group="{ name: 'g1' }"
|
||||
item-key="id"
|
||||
draggable=".row"
|
||||
v-bind="dragOptions"
|
||||
@start="drag = true"
|
||||
@end="drag = false"
|
||||
:set-data="hideDragImage"
|
||||
:disabled="disabled"
|
||||
@change="$emit('change', $event)"
|
||||
>
|
||||
<li class="row" v-for="(item, index) in tree" :key="item.id">
|
||||
<item-preview
|
||||
:item="item"
|
||||
:template="template"
|
||||
:collection="collection"
|
||||
:primary-key-field="primaryKeyField"
|
||||
:disabled="disabled"
|
||||
@input="replaceItem(index, $event)"
|
||||
@deselect="removeItem(index)"
|
||||
/>
|
||||
<nested-draggable
|
||||
:tree="item[childrenField] || []"
|
||||
:template="template"
|
||||
:collection="collection"
|
||||
:primary-key-field="primaryKeyField"
|
||||
:children-field="childrenField"
|
||||
:disabled="disabled"
|
||||
@change="$emit('change', $event)"
|
||||
@input="replaceChildren(index, $event)"
|
||||
/>
|
||||
</li>
|
||||
</draggable>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import draggable from 'vuedraggable';
|
||||
import { defineComponent, ref, PropType } from '@vue/composition-api';
|
||||
import hideDragImage from '@/utils/hide-drag-image';
|
||||
import ItemPreview from './item-preview.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'nested-draggable',
|
||||
props: {
|
||||
tree: {
|
||||
required: true,
|
||||
type: Array as PropType<Record<string, any>[]>,
|
||||
default: () => [],
|
||||
},
|
||||
root: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
template: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
primaryKeyField: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
childrenField: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
draggable,
|
||||
ItemPreview,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const drag = ref(false);
|
||||
|
||||
const editActive = ref(false);
|
||||
|
||||
return {
|
||||
drag,
|
||||
hideDragImage,
|
||||
editActive,
|
||||
dragOptions: {
|
||||
animation: 150,
|
||||
group: 'description',
|
||||
disabled: false,
|
||||
ghostClass: 'ghost',
|
||||
},
|
||||
replaceItem,
|
||||
removeItem,
|
||||
replaceChildren,
|
||||
};
|
||||
|
||||
function replaceItem(index: number, item: Record<string, any>) {
|
||||
emit(
|
||||
'input',
|
||||
props.tree.map((child, childIndex) => {
|
||||
if (childIndex === index) {
|
||||
return item;
|
||||
}
|
||||
return child;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
emit(
|
||||
'input',
|
||||
props.tree.filter((child, childIndex) => childIndex !== index)
|
||||
);
|
||||
}
|
||||
|
||||
function replaceChildren(index: number, tree: Record<string, any>[]) {
|
||||
emit(
|
||||
'input',
|
||||
props.tree.map((child, childIndex) => {
|
||||
if (childIndex === index) {
|
||||
return {
|
||||
...child,
|
||||
[props.childrenField]: tree,
|
||||
};
|
||||
}
|
||||
|
||||
return child;
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.drag-area {
|
||||
min-height: 12px;
|
||||
|
||||
&.root {
|
||||
margin-left: 0;
|
||||
padding: 0;
|
||||
|
||||
&:empty {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
.preview {
|
||||
padding: 12px 12px;
|
||||
background-color: var(--card-face-color);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0px 0px 6px 0px rgba(var(--card-shadow-color), 0.2);
|
||||
cursor: grab;
|
||||
transition: var(--fast) var(--transition);
|
||||
transition-property: box-shadow, background-color;
|
||||
|
||||
& + .drag-area:not(:empty) {
|
||||
padding-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flip-list-move {
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
|
||||
.ghost .preview {
|
||||
background-color: var(--primary-alt);
|
||||
box-shadow: 0 !important;
|
||||
}
|
||||
</style>
|
||||
336
app/src/interfaces/tree-view/tree-view.vue
Normal file
336
app/src/interfaces/tree-view/tree-view.vue
Normal file
@@ -0,0 +1,336 @@
|
||||
<template>
|
||||
<v-notice type="warning" v-if="relation.many_collection !== relation.one_collection">
|
||||
{{ $t('interfaces.tree-view.recursive_only') }}
|
||||
</v-notice>
|
||||
|
||||
<div v-else class="tree-view">
|
||||
<nested-draggable
|
||||
:template="template"
|
||||
:collection="collection"
|
||||
:tree="stagedValues || []"
|
||||
:primary-key-field="primaryKeyField.field"
|
||||
:children-field="relation.one_field"
|
||||
:disabled="disabled"
|
||||
root
|
||||
@change="onDraggableChange"
|
||||
@input="emitValue"
|
||||
/>
|
||||
|
||||
<div class="actions" v-if="!disabled">
|
||||
<v-button class="new" @click="addNewActive = true">{{ $t('create_new') }}</v-button>
|
||||
<v-button class="existing" @click="selectDrawer = true">
|
||||
{{ $t('add_existing') }}
|
||||
</v-button>
|
||||
</div>
|
||||
|
||||
<drawer-item
|
||||
v-if="!disabled"
|
||||
:active="addNewActive"
|
||||
:collection="collection"
|
||||
:primary-key="'+'"
|
||||
:edits="{}"
|
||||
@input="addNew"
|
||||
@update:active="addNewActive = false"
|
||||
/>
|
||||
|
||||
<drawer-collection
|
||||
v-if="!disabled"
|
||||
:active.sync="selectDrawer"
|
||||
:collection="collection"
|
||||
:selection="[]"
|
||||
:filters="selectionFilters"
|
||||
@input="stageSelection"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, PropType, onMounted, watch } from '@vue/composition-api';
|
||||
import { useCollection } from '@/composables/use-collection';
|
||||
import { useRelationsStore } from '@/stores';
|
||||
import api from '@/api';
|
||||
import getFieldsFromTemplate from '@/utils/get-fields-from-template';
|
||||
import draggable from 'vuedraggable';
|
||||
import hideDragImage from '@/utils/hide-drag-image';
|
||||
import NestedDraggable from './nested-draggable.vue';
|
||||
import { Filter } from '@/types';
|
||||
import { Relation } from '@/types';
|
||||
import DrawerCollection from '@/views/private/components/drawer-collection';
|
||||
import DrawerItem from '@/views/private/components/drawer-item';
|
||||
|
||||
export default defineComponent({
|
||||
components: { draggable, NestedDraggable, DrawerCollection, DrawerItem },
|
||||
props: {
|
||||
value: {
|
||||
type: Array as PropType<(number | string | Record<string, any>)[]>,
|
||||
default: null,
|
||||
},
|
||||
displayTemplate: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
field: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
primaryKey: {
|
||||
type: [String, Number],
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const relationsStore = useRelationsStore();
|
||||
const openItems = ref([]);
|
||||
|
||||
const { relation } = useRelation();
|
||||
const { info, primaryKeyField } = useCollection(relation.value.one_collection);
|
||||
const { loading, error, stagedValues, fetchValues, emitValue } = useValues();
|
||||
|
||||
const { stageSelection, selectDrawer, selectionFilters } = useSelection();
|
||||
const { addNewActive, addNew } = useAddNew();
|
||||
|
||||
const template = computed(() => {
|
||||
return props.displayTemplate || info.value?.meta?.display_template || `{{${primaryKeyField.value.field}}}`;
|
||||
});
|
||||
|
||||
onMounted(fetchValues);
|
||||
watch(() => props.primaryKey, fetchValues, { immediate: true });
|
||||
|
||||
const dragging = ref(false);
|
||||
|
||||
return {
|
||||
relation,
|
||||
openItems,
|
||||
template,
|
||||
loading,
|
||||
error,
|
||||
stagedValues,
|
||||
fetchValues,
|
||||
primaryKeyField,
|
||||
onDraggableChange,
|
||||
hideDragImage,
|
||||
dragging,
|
||||
emitValue,
|
||||
stageSelection,
|
||||
selectDrawer,
|
||||
selectionFilters,
|
||||
addNewActive,
|
||||
addNew,
|
||||
};
|
||||
|
||||
function useValues() {
|
||||
const loading = ref(false);
|
||||
const error = ref<any>(null);
|
||||
|
||||
const stagedValues = ref<Record<string, any>[]>([]);
|
||||
|
||||
return { loading, error, stagedValues, fetchValues, emitValue, getFieldsToFetch };
|
||||
|
||||
async function fetchValues() {
|
||||
if (!props.primaryKey || !relation.value || props.primaryKey === '+') return;
|
||||
|
||||
// In case props.value is already an array of edited objects
|
||||
if (props.value?.length > 0 && props.value.every((item) => typeof item === 'object')) {
|
||||
stagedValues.value = props.value as Record<string, any>[];
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(`/items/${props.collection}/${props.primaryKey}`, {
|
||||
params: {
|
||||
fields: getFieldsToFetch(),
|
||||
},
|
||||
});
|
||||
|
||||
stagedValues.value = response.data.data?.[relation.value.one_field!] ?? [];
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getFieldsToFetch() {
|
||||
const fields = [
|
||||
...new Set([primaryKeyField.value.field, relation.value.one_field, ...getFieldsFromTemplate(template.value)]),
|
||||
];
|
||||
|
||||
const result: string[] = [];
|
||||
|
||||
const prefix = `${relation.value.one_field}.`;
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
for (const field of fields) {
|
||||
result.push(`${prefix.repeat(i)}${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function emitValue(value: Record<string, any>[]) {
|
||||
stagedValues.value = value;
|
||||
|
||||
if (relation.value.sort_field) {
|
||||
return emit('input', addSort(value));
|
||||
}
|
||||
|
||||
emit('input', value);
|
||||
|
||||
function addSort(value: Record<string, any>[]): Record<string, any>[] {
|
||||
return (value || []).map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
[relation.value.sort_field!]: index,
|
||||
[relation.value.one_field!]: addSort(item[relation.value.one_field!]),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useRelation() {
|
||||
const relation = computed<Relation>(() => {
|
||||
return relationsStore.getRelationsForField(props.collection, props.field)?.[0];
|
||||
});
|
||||
|
||||
return { relation };
|
||||
}
|
||||
|
||||
function onDraggableChange() {
|
||||
emit('input', stagedValues.value);
|
||||
}
|
||||
|
||||
function useSelection() {
|
||||
const selectDrawer = ref(false);
|
||||
|
||||
const selectedPrimaryKeys = computed<(number | string)[]>(() => {
|
||||
if (stagedValues.value === null) return [];
|
||||
|
||||
const pkField = primaryKeyField.value.field;
|
||||
|
||||
return [props.primaryKey, ...getPKs(stagedValues.value)];
|
||||
|
||||
function getPKs(values: Record<string, any>[]): (string | number)[] {
|
||||
const pks = [];
|
||||
|
||||
for (const value of values) {
|
||||
if (!value[pkField]) continue;
|
||||
pks.push(value[pkField]);
|
||||
const childPKs = getPKs(value[relation.value.one_field!]);
|
||||
pks.push(...childPKs);
|
||||
}
|
||||
|
||||
return pks;
|
||||
}
|
||||
});
|
||||
|
||||
const selectionFilters = computed<Filter[]>(() => {
|
||||
const pkField = primaryKeyField.value.field;
|
||||
|
||||
if (selectedPrimaryKeys.value.length === 0) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'selection',
|
||||
field: pkField,
|
||||
operator: 'nin',
|
||||
value: selectedPrimaryKeys.value.join(','),
|
||||
locked: true,
|
||||
},
|
||||
{
|
||||
key: 'parent',
|
||||
field: relation.value.many_field,
|
||||
operator: 'null',
|
||||
value: true,
|
||||
locked: true,
|
||||
},
|
||||
] as Filter[];
|
||||
});
|
||||
|
||||
return { stageSelection, selectDrawer, selectionFilters };
|
||||
|
||||
async function stageSelection(newSelection: (number | string)[]) {
|
||||
loading.value = true;
|
||||
|
||||
const selection = newSelection.filter((item) => selectedPrimaryKeys.value.includes(item) === false);
|
||||
|
||||
const fields = [
|
||||
...new Set([primaryKeyField.value.field, relation.value.one_field, ...getFieldsFromTemplate(template.value)]),
|
||||
];
|
||||
|
||||
const result: string[] = [];
|
||||
|
||||
const prefix = `${relation.value.one_field}.`;
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
for (const field of fields) {
|
||||
result.push(`${prefix.repeat(i)}${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await api.get(`/items/${props.collection}`, {
|
||||
params: {
|
||||
fields: [...fields, ...result],
|
||||
filter: {
|
||||
[primaryKeyField.value.field]: {
|
||||
_in: selection,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newVal = [...response.data.data, ...stagedValues.value];
|
||||
|
||||
if (newVal.length === 0) emitValue([]);
|
||||
else emitValue(newVal);
|
||||
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function useAddNew() {
|
||||
const addNewActive = ref(false);
|
||||
|
||||
return { addNewActive, addNew };
|
||||
|
||||
function addNew(item: Record<string, any>) {
|
||||
emitValue([...stagedValues.value, item]);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep {
|
||||
ul,
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 24px;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.existing {
|
||||
margin-left: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -583,6 +583,7 @@ settings_webhooks: Webhooks
|
||||
settings_presets: Presets & Bookmarks
|
||||
scope: Scope
|
||||
layout: Layout
|
||||
tree_view: Tree View
|
||||
changes_are_permanent: Changes are permanent
|
||||
preset_name_placeholder: Name of bookmark...
|
||||
preset_search_placeholder: Search query...
|
||||
@@ -984,6 +985,9 @@ interfaces:
|
||||
translations:
|
||||
display_template: Display Template
|
||||
no_collection: No Collection
|
||||
tree-view:
|
||||
description: Tree view for nested recursive one-to-many items
|
||||
recursive_only: The tree view interface only works for recursive relationships.
|
||||
user:
|
||||
user: User
|
||||
description: Select an existing directus user
|
||||
|
||||
@@ -253,9 +253,7 @@ export default defineComponent({
|
||||
const availableCollections = computed(() => {
|
||||
return orderBy(
|
||||
collectionsStore.state.collections.filter((collection) => {
|
||||
return (
|
||||
collection.collection.startsWith('directus_') === false && collection.collection !== props.collection
|
||||
);
|
||||
return collection.collection.startsWith('directus_') === false;
|
||||
}),
|
||||
['collection'],
|
||||
['asc']
|
||||
@@ -265,7 +263,7 @@ export default defineComponent({
|
||||
const systemCollections = computed(() => {
|
||||
return orderBy(
|
||||
collectionsStore.state.collections.filter((collection) => {
|
||||
return collection.collection.startsWith('directus_') === true && collection.collection !== props.collection;
|
||||
return collection.collection.startsWith('directus_') === true;
|
||||
}),
|
||||
['collection'],
|
||||
['asc']
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref, computed, reactive, PropType, watch, toRefs } from '@vue/composition-api';
|
||||
import { defineComponent, ref, computed, PropType, toRefs } from '@vue/composition-api';
|
||||
import SetupTabs from './components/tabs.vue';
|
||||
import SetupActions from './components/actions.vue';
|
||||
import SetupSchema from './components/schema.vue';
|
||||
|
||||
Reference in New Issue
Block a user