mirror of
https://github.com/directus/directus.git
synced 2026-04-03 03:00:39 -04:00
Various relational updates
This commit is contained in:
@@ -105,7 +105,7 @@ router.patch(
|
||||
let results: any = [];
|
||||
|
||||
for (const field of req.body) {
|
||||
await service.updateField(req.params.collection, field, req.accountability);
|
||||
await service.updateField(req.params.collection, field);
|
||||
|
||||
const updatedField = await service.readOne(req.params.collection, field.field);
|
||||
|
||||
@@ -120,17 +120,13 @@ router.patch(
|
||||
'/:collection/:field',
|
||||
validateCollection,
|
||||
useCollection('directus_fields'),
|
||||
// @todo: validate field
|
||||
asyncHandler(async (req, res) => {
|
||||
const exists = await schemaInspector.hasColumn(req.collection, req.params.field);
|
||||
if (exists === false) throw new ForbiddenException();
|
||||
|
||||
const service = new FieldsService({ accountability: req.accountability });
|
||||
const fieldData: Partial<Field> & { field: string; type: typeof types[number] } = req.body;
|
||||
|
||||
if (!fieldData.field) fieldData.field = req.params.field;
|
||||
|
||||
await service.updateField(req.params.collection, fieldData, req.accountability);
|
||||
await service.updateField(req.params.collection, fieldData);
|
||||
|
||||
const updatedField = await service.readOne(req.params.collection, req.params.field);
|
||||
|
||||
@@ -143,11 +139,7 @@ router.delete(
|
||||
validateCollection,
|
||||
useCollection('directus_fields'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const exists = await schemaInspector.hasColumn(req.collection, req.params.field);
|
||||
if (exists === false) throw new ForbiddenException();
|
||||
|
||||
const service = new FieldsService({ accountability: req.accountability });
|
||||
|
||||
await service.deleteField(req.params.collection, req.params.field, req.accountability);
|
||||
|
||||
res.status(200).end();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import database, { schemaInspector } from '../database';
|
||||
import { Field } from '../types/field';
|
||||
import { uniq } from 'lodash';
|
||||
import { Accountability, AbstractServiceOptions, FieldMeta } from '../types';
|
||||
import { Accountability, AbstractServiceOptions, FieldMeta, Relation } from '../types';
|
||||
import ItemsService from '../services/items';
|
||||
import { ColumnBuilder } from 'knex';
|
||||
import getLocalType from '../utils/get-local-type';
|
||||
@@ -172,6 +171,10 @@ export default class FieldsService {
|
||||
field: Partial<Field> & { field: string; type: typeof types[number] },
|
||||
table?: CreateTableBuilder // allows collection creation to
|
||||
) {
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
throw new ForbiddenException('Only admins can perform this action.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo
|
||||
* Check if table / directus_fields row already exists
|
||||
@@ -198,7 +201,11 @@ export default class FieldsService {
|
||||
|
||||
/** @todo research how to make this happen in SQLite / Redshift */
|
||||
|
||||
async updateField(collection: string, field: RawField, accountability?: Accountability) {
|
||||
async updateField(collection: string, field: RawField) {
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
throw new ForbiddenException('Only admins can perform this action.');
|
||||
}
|
||||
|
||||
if (field.schema) {
|
||||
await this.knex.schema.alterTable(collection, (table) => {
|
||||
let column: ColumnBuilder;
|
||||
@@ -251,12 +258,35 @@ export default class FieldsService {
|
||||
}
|
||||
|
||||
/** @todo save accountability */
|
||||
async deleteField(collection: string, field: string, accountability?: Accountability) {
|
||||
await database('directus_fields').delete().where({ collection, field });
|
||||
async deleteField(collection: string, field: string) {
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
throw new ForbiddenException('Only admins can perform this action.');
|
||||
}
|
||||
|
||||
await database.schema.table(collection, (table) => {
|
||||
table.dropColumn(field);
|
||||
});
|
||||
await this.knex('directus_fields').delete().where({ collection, field });
|
||||
|
||||
if (await schemaInspector.hasColumn(collection, field)) {
|
||||
await this.knex.schema.table(collection, (table) => {
|
||||
table.dropColumn(field);
|
||||
});
|
||||
}
|
||||
|
||||
const relations = await this.knex
|
||||
.select<Relation[]>('*')
|
||||
.from('directus_relations')
|
||||
.where({ many_collection: collection, many_field: field })
|
||||
.orWhere({ one_collection: collection, one_field: field });
|
||||
|
||||
for (const relation of relations) {
|
||||
const isM2O = relation.many_collection === collection && relation.many_field === field;
|
||||
|
||||
if (isM2O) {
|
||||
await this.knex('directus_relations').delete().where({ many_collection: collection, many_field: field });
|
||||
await this.deleteField(relation.one_collection, relation.one_field);
|
||||
} else {
|
||||
await this.knex('directus_relations').update({ one_field: null }).where({ one_collection: collection, one_field: field });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public addColumnToTable(table: CreateTableBuilder, field: Field) {
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<v-list-item-content>
|
||||
<render-template :template="template" :item="item" :collection="relatedCollection" />
|
||||
</v-list-item-content>
|
||||
<v-list-item-icon>
|
||||
<v-icon name="launch" small />
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
@@ -8,5 +8,18 @@ export default defineInterface(({ i18n }) => ({
|
||||
component: InterfaceOneToMany,
|
||||
types: ['alias'],
|
||||
relationship: 'o2m',
|
||||
options: [],
|
||||
options: [
|
||||
{
|
||||
field: 'fields',
|
||||
type: 'json',
|
||||
name: i18n.tc('field', 0),
|
||||
meta: {
|
||||
interface: 'tags',
|
||||
width: 'full',
|
||||
options: {
|
||||
placeholder: i18n.t('readable_fields_copy'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -67,7 +67,7 @@ import useCollection from '@/composables/use-collection';
|
||||
import { useCollectionsStore, useRelationsStore, useFieldsStore } from '@/stores/';
|
||||
import ModalDetail from '@/views/private/components/modal-detail';
|
||||
import ModalBrowse from '@/views/private/components/modal-browse';
|
||||
import { Filter } from '@/types';
|
||||
import { Filter, Field } from '@/types';
|
||||
import { Header } from '@/components/v-table/types';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -91,7 +91,7 @@ export default defineComponent({
|
||||
},
|
||||
fields: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
@@ -194,7 +194,7 @@ export default defineComponent({
|
||||
async function fetchCurrent() {
|
||||
loading.value = true;
|
||||
|
||||
let fields = [...props.fields];
|
||||
let fields = [...(props.fields.length > 0 ? props.fields : getDefaultFields())];
|
||||
|
||||
if (fields.includes(relatedPrimaryKeyField.value.field) === false) {
|
||||
fields.push(relatedPrimaryKeyField.value.field);
|
||||
@@ -306,7 +306,7 @@ export default defineComponent({
|
||||
watch(
|
||||
() => props.fields,
|
||||
() => {
|
||||
tableHeaders.value = props.fields
|
||||
tableHeaders.value = (props.fields.length > 0 ? props.fields : getDefaultFields())
|
||||
.map((fieldKey) => {
|
||||
const field = fieldsStore.getField(relatedCollection.value.collection, fieldKey);
|
||||
|
||||
@@ -517,6 +517,11 @@ export default defineComponent({
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function getDefaultFields(): string[] {
|
||||
const fields = fieldsStore.getFieldsForCollection(relatedCollection.value.collection);
|
||||
return fields.slice(0, 3).map((field: Field) => field.field);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -226,7 +226,7 @@ import BookmarkAdd from '@/views/private/components/bookmark-add';
|
||||
import BookmarkEdit from '@/views/private/components/bookmark-edit';
|
||||
import router from '@/router';
|
||||
import marked from 'marked';
|
||||
import { usePermissionsStore } from '@/stores';
|
||||
import { usePermissionsStore, useUserStore } from '@/stores';
|
||||
|
||||
type Item = {
|
||||
[field: string]: any;
|
||||
@@ -253,6 +253,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const userStore = useUserStore();
|
||||
const permissionsStore = usePermissionsStore();
|
||||
const layout = ref<LayoutComponent | null>(null);
|
||||
|
||||
@@ -476,6 +477,9 @@ export default defineComponent({
|
||||
|
||||
function usePermissions() {
|
||||
const batchEditAllowed = computed(() => {
|
||||
const admin = userStore.state?.currentUser?.role.admin === true;
|
||||
if (admin) return true;
|
||||
|
||||
const updatePermissions = permissionsStore.state.permissions.find(
|
||||
(permission) => permission.action === 'update' && permission.collection === collection.value
|
||||
);
|
||||
@@ -484,6 +488,8 @@ export default defineComponent({
|
||||
|
||||
const batchSoftDeleteAllowed = computed(() => {
|
||||
if (!currentCollection.value?.meta?.soft_delete_field) return false;
|
||||
const admin = userStore.state?.currentUser?.role.admin === true;
|
||||
if (admin) return true;
|
||||
|
||||
const updatePermissions = permissionsStore.state.permissions.find(
|
||||
(permission) => permission.action === 'update' && permission.collection === collection.value
|
||||
@@ -495,6 +501,9 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const batchDeleteAllowed = computed(() => {
|
||||
const admin = userStore.state?.currentUser?.role.admin === true;
|
||||
if (admin) return true;
|
||||
|
||||
const deletePermissions = permissionsStore.state.permissions.find(
|
||||
(permission) => permission.action === 'delete' && permission.collection === collection.value
|
||||
);
|
||||
@@ -502,6 +511,9 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const createAllowed = computed(() => {
|
||||
const admin = userStore.state?.currentUser?.role.admin === true;
|
||||
if (admin) return true;
|
||||
|
||||
const createPermissions = permissionsStore.state.permissions.find(
|
||||
(permission) => permission.action === 'create' && permission.collection === collection.value
|
||||
);
|
||||
|
||||
@@ -8,11 +8,7 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('related_collection') }}</div>
|
||||
<v-select
|
||||
:placeholder="$t('select_one')"
|
||||
:items="items"
|
||||
v-model="relations[0].one_collection"
|
||||
/>
|
||||
<v-select :placeholder="$t('select_one')" :items="items" v-model="relations[0].one_collection" />
|
||||
</div>
|
||||
<v-input disabled :value="fieldData.field" />
|
||||
<v-input disabled :value="relatedPrimary" />
|
||||
@@ -78,10 +74,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']
|
||||
|
||||
@@ -80,16 +80,15 @@ export default defineComponent({
|
||||
if (!state.relations[0].many_collection) return [];
|
||||
|
||||
return fieldsStore.state.fields
|
||||
.filter((field) => {
|
||||
if (field.collection !== state.relations[0].many_collection) return false;
|
||||
|
||||
// Make sure the selected field matches the type of primary key of the current
|
||||
// collection. Otherwise you aren't able to properly save the primary key
|
||||
if (!field.schema || field.type !== currentCollectionPrimaryKey.value.type) return false;
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((field) => field.field);
|
||||
.filter((field) => field.collection === state.relations[0].many_collection)
|
||||
.map((field) => ({
|
||||
text: field.field,
|
||||
value: field.field,
|
||||
disabled:
|
||||
!field.schema ||
|
||||
field.schema?.is_primary_key ||
|
||||
field.type !== currentCollectionPrimaryKey.value.type,
|
||||
}));
|
||||
});
|
||||
|
||||
const collectionMany = computed({
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { useFieldsStore, useRelationsStore } from '@/stores/';
|
||||
import { reactive, watch } from '@vue/composition-api';
|
||||
import { clone } from 'lodash';
|
||||
|
||||
const fieldsStore = useFieldsStore();
|
||||
const relationsStore = useRelationsStore();
|
||||
@@ -47,7 +48,7 @@ function initLocalStore(
|
||||
const isExisting = field !== '+';
|
||||
|
||||
if (isExisting) {
|
||||
const existingField = fieldsStore.getField(collection, field);
|
||||
const existingField = clone(fieldsStore.getField(collection, field));
|
||||
|
||||
state.fieldData.field = existingField.field;
|
||||
state.fieldData.type = existingField.type;
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function getRelatedCollection(collection: string, field: string)
|
||||
const type = fieldInfo.type.toLowerCase();
|
||||
|
||||
// o2m | m2m
|
||||
if (type === 'alias') {
|
||||
if (['o2m', 'm2m', 'alias'].includes(type)) {
|
||||
return relations[0].many_collection;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user