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:
Rijk van Zanten
2020-06-12 16:51:12 -04:00
committed by GitHub
parent 4e8a7de004
commit 5a745b0c4d
15 changed files with 214 additions and 85 deletions

View File

@@ -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 };
}

View File

@@ -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: [
{

View File

@@ -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));

View File

@@ -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;

View File

@@ -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;

View File

@@ -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",

View File

@@ -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',

View File

@@ -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',

View File

@@ -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(() => {

View File

@@ -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>

View File

@@ -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', {

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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,

View File

@@ -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