mirror of
https://github.com/directus/directus.git
synced 2026-01-28 03:38:07 -05:00
Interface grouping (#717)
* Manage types * Fix typing * Unset interface on local type change * Add note * Limit available displays based on localType * Filter displays based on localtype * Only show displays that fit the type * Limit type options for interface selection * Dont import unused type
This commit is contained in:
@@ -21,8 +21,8 @@ export function useCollection(collection: Ref<string>) {
|
||||
return fields.value?.find((field) => field.collection === collection.value && field.primary_key === true)!;
|
||||
});
|
||||
|
||||
const ownerField = computed(() => {
|
||||
return fields.value?.find((field) => field.type === 'owner') || null;
|
||||
const userCreatedField = computed(() => {
|
||||
return fields.value?.find((field) => field.type === 'user_created') || null;
|
||||
});
|
||||
|
||||
const statusField = computed(() => {
|
||||
@@ -54,5 +54,5 @@ export function useCollection(collection: Ref<string>) {
|
||||
);
|
||||
});
|
||||
|
||||
return { info, fields, primaryKeyField, ownerField, statusField, softDeleteStatus, sortField };
|
||||
return { info, fields, primaryKeyField, userCreatedField, statusField, softDeleteStatus, sortField };
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { defineDisplay } from '@/displays/define';
|
||||
|
||||
export default defineDisplay(({ i18n }) => ({
|
||||
id: 'mime-type',
|
||||
name: i18n.t('mime-type'),
|
||||
name: i18n.t('mime_type'),
|
||||
icon: 'picture_as_pdf',
|
||||
options: [
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ export default defineDisplay(({ i18n }) => ({
|
||||
width: 'full',
|
||||
},
|
||||
],
|
||||
types: ['string'],
|
||||
types: ['m2o', 'o2m', 'm2m'],
|
||||
fields: (options: Options, { field, collection }) => {
|
||||
const relatedCollection = getRelatedCollection(collection, field);
|
||||
const { primaryKeyField } = useCollection(ref(relatedCollection as string));
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import VueI18n from 'vue-i18n';
|
||||
import { Component } from 'vue';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import { Type } from '@/interfaces/types';
|
||||
import { Field, Type } from '@/stores/fields/types';
|
||||
|
||||
export type DisplayHandlerFunctionContext = {
|
||||
type: string;
|
||||
|
||||
@@ -1,40 +1,6 @@
|
||||
import VueI18n from 'vue-i18n';
|
||||
import { Component } from 'vue';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
|
||||
const types = [
|
||||
'alias',
|
||||
'array',
|
||||
'boolean',
|
||||
'binary',
|
||||
'datetime',
|
||||
'date',
|
||||
'time',
|
||||
'file',
|
||||
'files',
|
||||
'hash',
|
||||
'group',
|
||||
'integer',
|
||||
'decimal',
|
||||
'json',
|
||||
'lang',
|
||||
'm2o',
|
||||
'o2m',
|
||||
'm2m',
|
||||
'slug',
|
||||
'sort',
|
||||
'status',
|
||||
'string',
|
||||
'translation',
|
||||
'uuid',
|
||||
'datetime_created',
|
||||
'datetime_updated',
|
||||
'user_created',
|
||||
'user_updated',
|
||||
'user',
|
||||
] as const;
|
||||
|
||||
export type Type = typeof types[number];
|
||||
import { Field, Type } from '@/stores/fields/types';
|
||||
|
||||
export type InterfaceConfig = {
|
||||
id: string;
|
||||
|
||||
@@ -76,6 +76,9 @@
|
||||
"confirm_revert": "Confirm Revert",
|
||||
"confirm_revert_body": "This will revert the item to the selected state.",
|
||||
|
||||
"mime_type": "MIME Type",
|
||||
"filesize": "Filesize",
|
||||
|
||||
"editing_role": "{role} Role",
|
||||
"adding_webhook": "Adding Webhook",
|
||||
|
||||
|
||||
@@ -325,7 +325,7 @@ export default defineComponent({
|
||||
|
||||
if (systemFields[2].enabled === true) {
|
||||
fields.push({
|
||||
type: 'owner',
|
||||
type: 'user_created',
|
||||
datatype: 'INT',
|
||||
field: systemFields[2].name,
|
||||
interface: 'owner',
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
import { defineComponent, computed, PropType } from '@vue/composition-api';
|
||||
import i18n from '@/lang';
|
||||
import { FormField } from '@/components/v-form/types';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import { Field, types } from '@/stores/fields/types';
|
||||
import interfaces from '@/interfaces';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -24,6 +25,21 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const selectedInterface = computed(() => interfaces.find((inter) => inter.id === props.value.interface));
|
||||
|
||||
const typeChoices = computed(() => {
|
||||
let availableTypes = types;
|
||||
|
||||
if (selectedInterface.value) {
|
||||
availableTypes = selectedInterface.value.types;
|
||||
}
|
||||
|
||||
return availableTypes.map((type) => ({
|
||||
text: i18n.t(type),
|
||||
value: type,
|
||||
}));
|
||||
});
|
||||
|
||||
const fields = computed(() => {
|
||||
const fields: FormField[] = [
|
||||
{
|
||||
@@ -127,8 +143,11 @@ export default defineComponent({
|
||||
{
|
||||
field: 'type',
|
||||
name: i18n.t('directus_type'),
|
||||
interface: 'text-input',
|
||||
interface: 'dropdown',
|
||||
width: 'half',
|
||||
options: {
|
||||
choices: typeChoices.value,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'datatype',
|
||||
|
||||
@@ -29,6 +29,8 @@ import { defineComponent, computed, PropType } from '@vue/composition-api';
|
||||
import displays from '@/displays/';
|
||||
import { FancySelectItem } from '@/components/v-fancy-select/types';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import { localTypeGroups } from './index';
|
||||
import { LocalType } from './types';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -40,14 +42,30 @@ export default defineComponent({
|
||||
type: Object as PropType<Field>,
|
||||
required: true,
|
||||
},
|
||||
localType: {
|
||||
type: String as PropType<LocalType>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const items = computed<FancySelectItem[]>(() => {
|
||||
return displays.map((inter) => ({
|
||||
text: inter.name,
|
||||
value: inter.id,
|
||||
icon: inter.icon,
|
||||
}));
|
||||
return (
|
||||
displays
|
||||
// Filter interfaces based on the localType that was selected
|
||||
.filter((display) => {
|
||||
return display.types.some((type) => localTypeGroups[props.localType].includes(type));
|
||||
})
|
||||
// When choosing an interface, the type is preset. We can safely assume that a
|
||||
// type has been set when you reach the display pane
|
||||
.filter((display) => {
|
||||
return display.types.includes(props.value.type);
|
||||
})
|
||||
.map((inter) => ({
|
||||
text: inter.name,
|
||||
value: inter.id,
|
||||
icon: inter.icon,
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
const selectedDisplay = computed(() => {
|
||||
|
||||
@@ -14,12 +14,7 @@
|
||||
:disabled="isNew === false"
|
||||
/>
|
||||
|
||||
<v-fancy-select
|
||||
:disabled="isNew === false"
|
||||
:items="items"
|
||||
:value="localType"
|
||||
@input="$emit('update:localType', $event)"
|
||||
/>
|
||||
<v-fancy-select :disabled="isNew === false" :items="items" :value="localType" @input="setLocalType" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -69,7 +64,7 @@ export default defineComponent({
|
||||
},
|
||||
]);
|
||||
|
||||
return { emitValue, items };
|
||||
return { emitValue, items, setLocalType };
|
||||
|
||||
function emitValue(key: string, value: any) {
|
||||
emit('input', {
|
||||
@@ -77,6 +72,17 @@ export default defineComponent({
|
||||
[key]: value,
|
||||
});
|
||||
}
|
||||
|
||||
function setLocalType(newType: string) {
|
||||
emit('update:localType', newType);
|
||||
|
||||
// Reset the interface when changing the localtype. If you change localType, the previously
|
||||
// selected interface most likely doesn't exist in the new selection anyways
|
||||
emit('input', {
|
||||
...props.value,
|
||||
interface: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<h2 class="type-title" v-if="isNew">{{ $t('interface_setup_title') }}</h2>
|
||||
|
||||
<v-fancy-select :items="items" :value="value.interface" @input="emitValue('interface', $event)" />
|
||||
<v-fancy-select :items="items" :value="value.interface" @input="setInterface" />
|
||||
|
||||
<template v-if="selectedInterface">
|
||||
<v-form
|
||||
@@ -29,6 +29,8 @@ import { defineComponent, computed, PropType } from '@vue/composition-api';
|
||||
import interfaces from '@/interfaces/';
|
||||
import { FancySelectItem } from '@/components/v-fancy-select/types';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import { LocalType } from './types';
|
||||
import { localTypeGroups } from './index';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -40,21 +42,59 @@ export default defineComponent({
|
||||
type: Object as PropType<Field>,
|
||||
required: true,
|
||||
},
|
||||
localType: {
|
||||
type: String as PropType<LocalType>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const items = computed<FancySelectItem[]>(() => {
|
||||
return interfaces.map((inter) => ({
|
||||
text: inter.name,
|
||||
value: inter.id,
|
||||
icon: inter.icon,
|
||||
}));
|
||||
return (
|
||||
interfaces
|
||||
// Filter interfaces based on the localType that was selected
|
||||
.filter((inter) => {
|
||||
return inter.types.some((type) => localTypeGroups[props.localType].includes(type));
|
||||
})
|
||||
.filter((inter) => {
|
||||
if (props.value.type && props.isNew === false) {
|
||||
return inter.types.includes(props.value.type);
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((inter) => ({
|
||||
text: inter.name,
|
||||
value: inter.id,
|
||||
icon: inter.icon,
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
return interfaces.find((inter) => inter.id === props.value.interface) || null;
|
||||
});
|
||||
|
||||
return { emitValue, items, selectedInterface };
|
||||
return { emitValue, items, selectedInterface, setInterface };
|
||||
|
||||
function setInterface(value: string | null) {
|
||||
if (value === null) {
|
||||
return emit('input', {
|
||||
...props.value,
|
||||
interface: null,
|
||||
});
|
||||
}
|
||||
|
||||
const chosenInterface = interfaces.find((inter) => inter.id === value);
|
||||
|
||||
if (!chosenInterface) return;
|
||||
|
||||
// This also presets the field type
|
||||
emit('input', {
|
||||
...props.value,
|
||||
interface: value,
|
||||
type: chosenInterface.types[0],
|
||||
});
|
||||
}
|
||||
|
||||
function emitValue(key: string, value: any) {
|
||||
emit('input', {
|
||||
|
||||
@@ -74,8 +74,9 @@ import useFieldsStore from '@/stores/fields/';
|
||||
import { Relation } from '@/stores/relations/types';
|
||||
import api from '@/api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
|
||||
import { LocalType } from './types';
|
||||
import { localTypeGroups } from './index';
|
||||
import { Type } from '@/stores/fields/types';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -109,7 +110,7 @@ export default defineComponent({
|
||||
const fieldsStore = useFieldsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const { field, localType } = usefield();
|
||||
const { field, localType } = useField();
|
||||
const { tabs, currentTab } = useTabs();
|
||||
const { save, saving } = useSave();
|
||||
|
||||
@@ -117,7 +118,7 @@ export default defineComponent({
|
||||
|
||||
return { field, tabs, currentTab, localType, save, saving, newRelations };
|
||||
|
||||
function usefield() {
|
||||
function useField() {
|
||||
const defaults = {
|
||||
id: null,
|
||||
collection: props.collection,
|
||||
@@ -166,16 +167,13 @@ export default defineComponent({
|
||||
if (existingField) {
|
||||
field.value = existingField;
|
||||
|
||||
const type = existingField.type.toLowerCase();
|
||||
const type: Type = existingField.type;
|
||||
|
||||
if (type === 'file') {
|
||||
localType.value = 'file';
|
||||
} else if (type === 'files') {
|
||||
localType.value = 'files';
|
||||
} else if (['o2m', 'm2o', 'm2m'].includes(type)) {
|
||||
localType.value = 'relational';
|
||||
} else {
|
||||
localType.value = 'standard';
|
||||
for (const [group, types] of Object.entries(localTypeGroups)) {
|
||||
if (types.includes(type)) {
|
||||
localType.value = group as LocalType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
field.value = { ...defaults };
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
import FieldSetup from './field-setup.vue';
|
||||
import { types, Type } from '@/stores/fields/types';
|
||||
import { LocalType } from './types';
|
||||
|
||||
const localTypeGroups: Record<LocalType, Type[]> = {
|
||||
relational: ['m2o', 'o2m', 'm2m', 'translation'],
|
||||
file: ['file'],
|
||||
files: ['files'],
|
||||
standard: [],
|
||||
};
|
||||
|
||||
localTypeGroups.standard = types.filter((typeName: Type) => {
|
||||
return (
|
||||
[...localTypeGroups.relational, ...localTypeGroups.file, ...localTypeGroups.files].includes(typeName) === false
|
||||
);
|
||||
});
|
||||
|
||||
export { localTypeGroups };
|
||||
|
||||
export { FieldSetup };
|
||||
export default FieldSetup;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
/>
|
||||
<permissions-toggle
|
||||
type="read"
|
||||
:options="ownerField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:options="userCreatedField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:value="getCombinedPermission('read')"
|
||||
:save-permission="saveForAllStatuses"
|
||||
:collection="collection"
|
||||
@@ -21,7 +21,7 @@
|
||||
/>
|
||||
<permissions-toggle
|
||||
type="update"
|
||||
:options="ownerField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:options="userCreatedField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:value="getCombinedPermission('update')"
|
||||
:save-permission="saveForAllStatuses"
|
||||
:collection="collection"
|
||||
@@ -29,7 +29,7 @@
|
||||
/>
|
||||
<permissions-toggle
|
||||
type="delete"
|
||||
:options="ownerField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:options="userCreatedField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:value="getCombinedPermission('delete')"
|
||||
:save-permission="saveForAllStatuses"
|
||||
:collection="collection"
|
||||
@@ -117,7 +117,7 @@
|
||||
/>
|
||||
<permissions-toggle
|
||||
type="read"
|
||||
:options="ownerField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:options="userCreatedField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:value="getPermissionValue('read', status.value)"
|
||||
:status="status.value"
|
||||
:save-permission="savePermission"
|
||||
@@ -127,7 +127,7 @@
|
||||
/>
|
||||
<permissions-toggle
|
||||
type="update"
|
||||
:options="ownerField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:options="userCreatedField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:value="getPermissionValue('update', status.value)"
|
||||
:status="status.value"
|
||||
:save-permission="savePermission"
|
||||
@@ -137,7 +137,7 @@
|
||||
/>
|
||||
<permissions-toggle
|
||||
type="delete"
|
||||
:options="ownerField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:options="userCreatedField ? ['none', 'mine', 'role', 'full'] : ['none', 'full']"
|
||||
:value="getPermissionValue('delete', status.value)"
|
||||
:status="status.value"
|
||||
:save-permission="savePermission"
|
||||
@@ -241,7 +241,7 @@ export default defineComponent({
|
||||
},
|
||||
setup(props) {
|
||||
const { collection } = toRefs(props);
|
||||
const { fields, info, statusField, ownerField } = useCollection(collection);
|
||||
const { fields, info, statusField, userCreatedField } = useCollection(collection);
|
||||
|
||||
const detailsOpen = ref(false);
|
||||
|
||||
@@ -289,7 +289,7 @@ export default defineComponent({
|
||||
statuses,
|
||||
detailsOpen,
|
||||
permissions,
|
||||
ownerField,
|
||||
userCreatedField,
|
||||
getPermissionValue,
|
||||
getCombinedPermission,
|
||||
saveForAllStatuses,
|
||||
|
||||
@@ -7,6 +7,69 @@ type Translation = {
|
||||
|
||||
export type Width = 'half' | 'half-left' | 'half-right' | 'full' | 'fill';
|
||||
|
||||
export type Type =
|
||||
| 'alias'
|
||||
| 'array'
|
||||
| 'boolean'
|
||||
| 'binary'
|
||||
| 'datetime'
|
||||
| 'date'
|
||||
| 'time'
|
||||
| 'file'
|
||||
| 'files'
|
||||
| 'hash'
|
||||
| 'group'
|
||||
| 'integer'
|
||||
| 'decimal'
|
||||
| 'json'
|
||||
| 'lang'
|
||||
| 'm2o'
|
||||
| 'o2m'
|
||||
| 'm2m'
|
||||
| 'slug'
|
||||
| 'sort'
|
||||
| 'status'
|
||||
| 'string'
|
||||
| 'translation'
|
||||
| 'uuid'
|
||||
| 'datetime_created'
|
||||
| 'datetime_updated'
|
||||
| 'user_created'
|
||||
| 'user_updated'
|
||||
| 'user';
|
||||
|
||||
export const types: Type[] = [
|
||||
'alias',
|
||||
'array',
|
||||
'boolean',
|
||||
'binary',
|
||||
'datetime',
|
||||
'date',
|
||||
'time',
|
||||
'file',
|
||||
'files',
|
||||
'hash',
|
||||
'group',
|
||||
'integer',
|
||||
'decimal',
|
||||
'json',
|
||||
'lang',
|
||||
'm2o',
|
||||
'o2m',
|
||||
'm2m',
|
||||
'slug',
|
||||
'sort',
|
||||
'status',
|
||||
'string',
|
||||
'translation',
|
||||
'uuid',
|
||||
'datetime_created',
|
||||
'datetime_updated',
|
||||
'user_created',
|
||||
'user_updated',
|
||||
'user',
|
||||
];
|
||||
|
||||
export interface FieldRaw {
|
||||
id: number;
|
||||
collection: string;
|
||||
@@ -18,7 +81,7 @@ export interface FieldRaw {
|
||||
default_value: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
note: string | TranslateResult | null;
|
||||
signed: boolean;
|
||||
type: string;
|
||||
type: Type;
|
||||
sort: null | number;
|
||||
interface: string | null;
|
||||
options: null | { [key: string]: any }; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
Reference in New Issue
Block a user