mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Register interfaces dynamically, prevent recursive import
This commit is contained in:
@@ -1,69 +1,12 @@
|
||||
import InterfaceCheckboxes from './checkboxes';
|
||||
import InterfaceCode from './code';
|
||||
import InterfaceCollections from './collections';
|
||||
import InterfaceColor from './color';
|
||||
import InterfaceDateTime from './datetime';
|
||||
import InterfaceDivider from './divider/';
|
||||
import InterfaceDropdown from './dropdown/';
|
||||
import InterfaceDropdownMultiselect from './dropdown-multiselect/';
|
||||
import InterfaceFile from './file';
|
||||
import InterfaceFiles from './files';
|
||||
import InterfaceHash from './hash';
|
||||
import InterfaceIcon from './icon';
|
||||
import InterfaceImage from './image';
|
||||
import InterfaceManyToMany from './many-to-many';
|
||||
import InterfaceManyToOne from './many-to-one';
|
||||
import InterfaceMarkdown from './markdown';
|
||||
import InterfaceNotice from './notice';
|
||||
import InterfaceNumeric from './numeric/';
|
||||
import InterfaceOneToMany from './one-to-many';
|
||||
import InterfaceRadioButtons from './radio-buttons';
|
||||
import InterfaceRepeater from './repeater';
|
||||
import InterfaceSlider from './slider/';
|
||||
import InterfaceSlug from './slug';
|
||||
import InterfaceStatus from './status';
|
||||
import InterfaceTags from './tags';
|
||||
import InterfaceTFASetup from './tfa-setup';
|
||||
import InterfaceTextarea from './textarea/';
|
||||
import InterfaceTextInput from './text-input/';
|
||||
import InterfaceToggle from './toggle/';
|
||||
import InterfaceTranslations from './translations';
|
||||
import InterfaceUser from './user';
|
||||
import InterfaceWYSIWYG from './wysiwyg/';
|
||||
import { ref, Ref } from '@vue/composition-api';
|
||||
import { InterfaceConfig } from './types';
|
||||
|
||||
export const interfaces = [
|
||||
InterfaceCheckboxes,
|
||||
InterfaceCode,
|
||||
InterfaceCollections,
|
||||
InterfaceColor,
|
||||
InterfaceDateTime,
|
||||
InterfaceDivider,
|
||||
InterfaceDropdown,
|
||||
InterfaceDropdownMultiselect,
|
||||
InterfaceFile,
|
||||
InterfaceFiles,
|
||||
InterfaceHash,
|
||||
InterfaceIcon,
|
||||
InterfaceImage,
|
||||
InterfaceManyToMany,
|
||||
InterfaceManyToOne,
|
||||
InterfaceMarkdown,
|
||||
InterfaceNotice,
|
||||
InterfaceNumeric,
|
||||
InterfaceOneToMany,
|
||||
InterfaceRadioButtons,
|
||||
InterfaceRepeater,
|
||||
InterfaceSlider,
|
||||
InterfaceSlug,
|
||||
InterfaceStatus,
|
||||
InterfaceTags,
|
||||
InterfaceTFASetup,
|
||||
InterfaceTextarea,
|
||||
InterfaceTextInput,
|
||||
InterfaceToggle,
|
||||
InterfaceTranslations,
|
||||
InterfaceUser,
|
||||
InterfaceWYSIWYG,
|
||||
];
|
||||
let interfaces: Ref<InterfaceConfig[]>;
|
||||
|
||||
export default interfaces;
|
||||
export function getInterfaces() {
|
||||
if (!interfaces) {
|
||||
interfaces = ref([]);
|
||||
}
|
||||
|
||||
return interfaces;
|
||||
}
|
||||
|
||||
11
app/src/interfaces/interface-options/index.ts
Normal file
11
app/src/interfaces/interface-options/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineInterface } from '../define';
|
||||
import InterfaceOptions from './interface-options.vue';
|
||||
|
||||
export default defineInterface(({ i18n }) => ({
|
||||
id: 'interface-options',
|
||||
name: 'Interface Options',
|
||||
icon: 'box',
|
||||
component: InterfaceOptions,
|
||||
types: ['string'],
|
||||
options: [],
|
||||
}));
|
||||
66
app/src/interfaces/interface-options/interface-options.vue
Normal file
66
app/src/interfaces/interface-options/interface-options.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<v-notice v-if="!selectedInterface">
|
||||
{{ $t('select_interface') }}
|
||||
</v-notice>
|
||||
|
||||
<v-notice v-else-if="!selectedInterface.options">
|
||||
{{ $t('no_options_available') }}
|
||||
</v-notice>
|
||||
|
||||
<div class="inset" v-else>
|
||||
<v-form
|
||||
v-if="Array.isArray(selectedInterface.options)"
|
||||
:fields="selectedInterface.options"
|
||||
primary-key="+"
|
||||
:edits="value"
|
||||
@input="$listeners.input"
|
||||
/>
|
||||
|
||||
<component
|
||||
:value="value"
|
||||
@input="$listeners.input"
|
||||
:field-data="fieldData"
|
||||
:is="`interface-options-${selectedInterface.id}`"
|
||||
v-else
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, inject, ref } from '@vue/composition-api';
|
||||
import { getInterfaces } from '@/interfaces';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
interfaceField: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { parent }) {
|
||||
const interfaces = getInterfaces();
|
||||
|
||||
const values = inject('form-values', ref<Record<string, any>>({}));
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
if (!values.value[props.interfaceField]) return;
|
||||
|
||||
return interfaces.value.find((inter) => inter.id === values.value[props.interfaceField]);
|
||||
});
|
||||
|
||||
return { selectedInterface, values };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.inset {
|
||||
padding: 8px;
|
||||
border: var(--border-width) solid var(--border-normal);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
</style>
|
||||
11
app/src/interfaces/interface/index.ts
Normal file
11
app/src/interfaces/interface/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineInterface } from '../define';
|
||||
import InterfaceInterface from './interface.vue';
|
||||
|
||||
export default defineInterface(({ i18n }) => ({
|
||||
id: 'interface',
|
||||
name: 'Interface',
|
||||
icon: 'box',
|
||||
component: InterfaceInterface,
|
||||
types: ['string'],
|
||||
options: [],
|
||||
}));
|
||||
34
app/src/interfaces/interface/interface.vue
Normal file
34
app/src/interfaces/interface/interface.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<v-select :items="items" @input="$listeners.input" :value="value" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import i18n from '@/lang';
|
||||
import { getInterfaces } from '@/interfaces';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const interfaces = getInterfaces();
|
||||
|
||||
const items = computed(() => {
|
||||
return interfaces.value
|
||||
.filter((inter) => inter.relationship === undefined && inter.system !== true)
|
||||
.map((inter) => {
|
||||
return {
|
||||
text: inter.name,
|
||||
value: inter.id,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return { items };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,10 +1,22 @@
|
||||
import registerComponent from '@/utils/register-component/';
|
||||
import interfaces from './index';
|
||||
import { getInterfaces } from './index';
|
||||
import { Component } from 'vue';
|
||||
|
||||
interfaces.forEach((inter) => {
|
||||
const interfaces = getInterfaces();
|
||||
|
||||
const context = require.context('.', true, /^.*index\.ts$/);
|
||||
const modules = context
|
||||
.keys()
|
||||
.map((key) => context(key))
|
||||
.map((mod) => mod.default)
|
||||
.filter((m) => m);
|
||||
|
||||
interfaces.value = modules;
|
||||
|
||||
interfaces.value.forEach((inter) => {
|
||||
registerComponent('interface-' + inter.id, inter.component);
|
||||
|
||||
if (typeof inter.options === 'function') {
|
||||
registerComponent(`interface-options-${inter.id}`, inter.options);
|
||||
if (typeof inter.options !== 'function' && Array.isArray(inter.options) === false) {
|
||||
registerComponent(`interface-options-${inter.id}`, inter.options as Component);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineInterface } from '../define';
|
||||
import InterfaceRepeater from './repeater.vue';
|
||||
import RepeaterOptions from './options.vue';
|
||||
|
||||
export default defineInterface(({ i18n }) => ({
|
||||
id: 'repeater',
|
||||
@@ -7,5 +8,5 @@ export default defineInterface(({ i18n }) => ({
|
||||
icon: 'replay',
|
||||
component: InterfaceRepeater,
|
||||
types: ['json'],
|
||||
options: [],
|
||||
options: RepeaterOptions,
|
||||
}));
|
||||
|
||||
143
app/src/interfaces/repeater/options.vue
Normal file
143
app/src/interfaces/repeater/options.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="type-label">Template</p>
|
||||
<v-input class="input" v-model="template" :placeholder="`{{ field }}`" />
|
||||
|
||||
<p class="type-label">Fields</p>
|
||||
<repeater v-model="repeaterValue" :template="`{{ field }} — {{ interface }}`" :fields="repeaterFields" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import Repeater from './repeater.vue';
|
||||
import { Field, FieldMeta } from '@/types';
|
||||
import i18n from '@/lang';
|
||||
|
||||
export default defineComponent({
|
||||
components: { Repeater },
|
||||
props: {
|
||||
value: {
|
||||
type: Object as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const repeaterValue = computed({
|
||||
get() {
|
||||
return props.value?.fields?.map((field: Field) => field.meta);
|
||||
},
|
||||
set(newVal: FieldMeta[]) {
|
||||
const fields = newVal.map((meta) => ({
|
||||
field: meta.field,
|
||||
meta,
|
||||
}));
|
||||
|
||||
emit('input', {
|
||||
...(props.value || {}),
|
||||
fields: fields,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const repeaterFields: DeepPartial<Field>[] = [
|
||||
{
|
||||
name: i18n.tc('field', 1),
|
||||
field: 'field',
|
||||
type: 'string',
|
||||
meta: {
|
||||
interface: 'text-input',
|
||||
width: 'half',
|
||||
sort: 1,
|
||||
options: {
|
||||
font: 'monospace',
|
||||
},
|
||||
},
|
||||
schema: null,
|
||||
},
|
||||
{
|
||||
name: i18n.t('field_width'),
|
||||
field: 'width',
|
||||
type: 'string',
|
||||
meta: {
|
||||
interface: 'dropdown',
|
||||
width: 'half',
|
||||
sort: 2,
|
||||
options: {
|
||||
choices: [
|
||||
{
|
||||
value: 'half',
|
||||
text: i18n.t('half_width'),
|
||||
},
|
||||
{
|
||||
value: 'full',
|
||||
text: i18n.t('full_width'),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
schema: null,
|
||||
},
|
||||
{
|
||||
name: i18n.t('interface'),
|
||||
field: 'interface',
|
||||
type: 'string',
|
||||
meta: {
|
||||
interface: 'interface',
|
||||
width: 'half',
|
||||
sort: 3,
|
||||
},
|
||||
schema: null,
|
||||
},
|
||||
{
|
||||
name: i18n.t('note'),
|
||||
field: 'note',
|
||||
type: 'string',
|
||||
meta: {
|
||||
interface: 'text-input',
|
||||
width: 'half',
|
||||
sort: 4,
|
||||
},
|
||||
schema: null,
|
||||
},
|
||||
{
|
||||
name: i18n.t('options'),
|
||||
field: 'options',
|
||||
type: 'string',
|
||||
meta: {
|
||||
interface: 'interface-options',
|
||||
width: 'full',
|
||||
sort: 5,
|
||||
options: {
|
||||
interfaceField: 'interface',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const template = computed({
|
||||
get() {
|
||||
return props.value?.template;
|
||||
},
|
||||
set(newTemplate: string) {
|
||||
emit('input', {
|
||||
...(props.value || {}),
|
||||
template: newTemplate,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return { repeaterValue, repeaterFields, template };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.type-label {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
</style>
|
||||
@@ -16,7 +16,5 @@ export type InterfaceConfig = {
|
||||
};
|
||||
|
||||
export type InterfaceContext = { i18n: VueI18n };
|
||||
|
||||
export type InterfaceDefineParam = InterfaceDefineParamGeneric<InterfaceConfig>;
|
||||
|
||||
export type InterfaceDefineParamGeneric<T> = T | ((context: InterfaceContext) => T);
|
||||
|
||||
@@ -22,14 +22,19 @@
|
||||
v-model="fieldData.meta.options"
|
||||
/>
|
||||
|
||||
<component v-model="fieldData" :is="`interface-options-${selectedInterface.id}`" v-else />
|
||||
<component
|
||||
v-model="fieldData.meta.options"
|
||||
:field-data="fieldData"
|
||||
:is="`interface-options-${selectedInterface.id}`"
|
||||
v-else
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import interfaces from '@/interfaces';
|
||||
import { getInterfaces } from '@/interfaces';
|
||||
|
||||
import { state } from '../store';
|
||||
|
||||
@@ -41,8 +46,10 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const interfaces = getInterfaces();
|
||||
|
||||
const availableInterfaces = computed(() =>
|
||||
interfaces.filter((inter) => {
|
||||
interfaces.value.filter((inter) => {
|
||||
// Filter out all system interfaces
|
||||
if (inter.system !== undefined && inter.system === true) return false;
|
||||
|
||||
@@ -72,7 +79,7 @@ export default defineComponent({
|
||||
);
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
return interfaces.find((inter) => inter.id === state.fieldData.meta.interface);
|
||||
return interfaces.value.find((inter) => inter.id === state.fieldData.meta.interface);
|
||||
});
|
||||
|
||||
return { fieldData: state.fieldData, selectItems, selectedInterface };
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
import { defineComponent, PropType, ref, computed } from '@vue/composition-api';
|
||||
import { Field } from '@/types';
|
||||
import { useCollectionsStore, useFieldsStore } from '@/stores/';
|
||||
import interfaces from '@/interfaces';
|
||||
import { getInterfaces } from '@/interfaces';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -119,6 +119,8 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const interfaces = getInterfaces();
|
||||
|
||||
const editActive = ref(false);
|
||||
const fieldsStore = useFieldsStore();
|
||||
const collectionsStore = useCollectionsStore();
|
||||
@@ -127,7 +129,7 @@ export default defineComponent({
|
||||
const { duplicateActive, duplicateName, collections, duplicateTo, saveDuplicate, duplicating } = useDuplicate();
|
||||
|
||||
const interfaceName = computed(() => {
|
||||
return interfaces.find((inter) => inter.id === props.field.meta.interface)?.name;
|
||||
return interfaces.value.find((inter) => inter.id === props.field.meta.interface)?.name;
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user