Restructure state management in setup flow

This commit is contained in:
rijkvanzanten
2020-07-24 12:37:42 -04:00
parent a4e788507b
commit 66f653234d
14 changed files with 439 additions and 448 deletions

View File

@@ -1,11 +0,0 @@
sonar.organization=directus
sonar.projectKey=app-next
sonar.sources=src
sonar.exclusions=src/**/*.story.ts,src/**/*.test.ts
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.testExecutionReportPaths=coverage/sonar.xml
sonar.pullrequest.provider=github
sonar.pullrequest.github.repository=directus/app-next

View File

@@ -1,4 +1,5 @@
import { computed, Ref } from '@vue/composition-api';
import { clone } from 'lodash';
export default function useSync<T, K extends keyof T>(
props: T,
@@ -8,7 +9,7 @@ export default function useSync<T, K extends keyof T>(
): Ref<Readonly<T[K]>> {
return computed<T[K]>({
get() {
return props[key];
return clone(props[key]);
},
set(newVal) {
emit(`update:${key}`, newVal);

View File

@@ -1,6 +1,6 @@
<template>
<div class="actions">
<v-button secondary :to="`/settings/data-model/${collection}`">
<v-button secondary @click="$emit('cancel')">
{{ $t('cancel') }}
</v-button>
<div class="spacer" />

View File

@@ -2,9 +2,9 @@
<div>
<h2 class="type-title">{{ $t('display_setup_title') }}</h2>
<v-fancy-select class="select" :items="selectItems" v-model="_field.system.display" />
<v-fancy-select class="select" :items="selectItems" v-model="fieldData.system.display" />
<template v-if="_field.system.display">
<template v-if="fieldData.system.display">
<v-form
v-if="
selectedDisplay.options &&
@@ -13,7 +13,7 @@
"
:fields="selectedDisplay.options"
primary-key="+"
v-model="_field.system.options"
v-model="fieldData.system.options"
/>
<v-notice v-else>
@@ -26,24 +26,20 @@
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import displays from '@/displays';
import useSync from '@/composables/use-sync';
import { state } from '../store';
export default defineComponent({
props: {
fieldData: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
},
setup(props, { emit }) {
const _field = useSync(props, 'fieldData', emit);
const availabledisplays = computed(() =>
const availableDisplays = computed(() =>
displays.filter((display) => {
const matchesType = display.types.includes(props.fieldData.database?.type || 'alias');
const matchesType = display.types.includes(state.fieldData?.type || 'alias');
const matchesRelation = true;
// if (props.type === 'standard') {
@@ -61,7 +57,7 @@ export default defineComponent({
);
const selectItems = computed(() =>
availabledisplays.value.map((display) => ({
availableDisplays.value.map((display) => ({
text: display.name,
value: display.id,
icon: display.icon,
@@ -69,10 +65,10 @@ export default defineComponent({
);
const selectedDisplay = computed(() => {
return displays.find((display) => display.id === _field.value.system.display);
return displays.find((display) => display.id === state.fieldData.system.display);
});
return { _field, selectItems, selectedDisplay };
return { fieldData: state.fieldData, selectItems, selectedDisplay };
},
});
</script>

View File

@@ -2,9 +2,9 @@
<div>
<h2 class="type-title">{{ $t('interface_setup_title') }}</h2>
<v-fancy-select class="select" :items="selectItems" v-model="_field.system.interface" />
<v-fancy-select class="select" :items="selectItems" v-model="fieldData.system.interface" />
<template v-if="_field.system.interface">
<template v-if="fieldData.system.interface">
<v-form
v-if="
selectedInterface.options &&
@@ -13,7 +13,7 @@
"
:fields="selectedInterface.options"
primary-key="+"
v-model="_field.system.options"
v-model="fieldData.system.options"
/>
<v-notice v-else>
@@ -26,24 +26,20 @@
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import interfaces from '@/interfaces';
import useSync from '@/composables/use-sync';
import { state } from '../store';
export default defineComponent({
props: {
fieldData: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
},
setup(props, { emit }) {
const _field = useSync(props, 'fieldData', emit);
const availableInterfaces = computed(() =>
interfaces.filter((inter) => {
const matchesType = inter.types.includes(props.fieldData.database?.type || 'alias');
const matchesType = inter.types.includes(state.fieldData?.type || 'alias');
let matchesRelation = false;
if (props.type === 'standard') {
@@ -69,10 +65,10 @@ export default defineComponent({
);
const selectedInterface = computed(() => {
return interfaces.find((inter) => inter.id === _field.value.system.interface);
return interfaces.find((inter) => inter.id === state.fieldData.system.interface);
});
return { _field, selectItems, selectedInterface };
return { fieldData: state.fieldData, selectItems, selectedInterface };
},
});
</script>

View File

@@ -12,18 +12,14 @@
</div>
<div class="field">
<div class="type-label">{{ $t('related_collection') }}</div>
<v-select
:disabled="type === 'files'"
:items="collectionItems"
v-model="_relations[1].collection_one"
/>
<v-select :disabled="type === 'files'" :items="collectionItems" v-model="relations[1].collection_one" />
</div>
<v-input disabled :value="_relations[0].primary_one" />
<v-select :disabled="!junctionCollection" :items="junctionFields" v-model="_relations[0].field_many" />
<v-input disabled :value="relations[0].primary_one" />
<v-select :disabled="!junctionCollection" :items="junctionFields" v-model="relations[0].field_many" />
<div class="spacer" />
<div class="spacer" />
<v-select :disabled="!junctionCollection" :items="junctionFields" v-model="_relations[1].field_many" />
<v-input disabled :value="_relations[1].primary_one" />
<v-select :disabled="!junctionCollection" :items="junctionFields" v-model="relations[1].field_many" />
<v-input disabled :value="relations[1].primary_one" />
<v-icon name="arrow_forward" />
<v-icon name="arrow_backward" />
</div>
@@ -36,31 +32,22 @@ import { orderBy } from 'lodash';
import useCollectionsStore from '@/stores/collections';
import useFieldsStore from '@/stores/fields';
import { Relation } from '@/stores/relations/types';
import useSync from '@/composables/use-sync';
import { Field } from '@/stores/fields/types';
import { state } from '../store';
export default defineComponent({
props: {
type: {
type: String,
required: true,
},
relations: {
type: Array as PropType<Relation[]>,
required: true,
},
fieldData: {
type: Object,
required: true,
},
collection: {
type: String,
required: true,
},
},
setup(props, { emit }) {
const _relations = useSync(props, 'relations', emit);
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
@@ -86,11 +73,11 @@ export default defineComponent({
const junctionCollection = computed({
get() {
return _relations.value[0].collection_many;
return state.relations[0].collection_many;
},
set(collection: string) {
_relations.value[0].collection_many = collection;
_relations.value[1].collection_many = collection;
state.relations[0].collection_many = collection;
state.relations[1].collection_many = collection;
},
});
@@ -101,11 +88,11 @@ export default defineComponent({
text: field.field,
value: field.field,
disabled:
_relations.value[0].field_many === field.field || _relations.value[1].field_many === field.field,
state.relations[0].field_many === field.field || state.relations[1].field_many === field.field,
}));
});
return { _relations, collectionItems, junctionCollection, junctionFields };
return { relations: state.relations, collectionItems, junctionCollection, junctionFields };
},
});
</script>

View File

@@ -4,14 +4,14 @@
<div class="grid">
<div class="field">
<div class="type-label">{{ $t('this_collection') }}</div>
<v-input disabled :value="_relations[0].collection_many" />
<v-input disabled :value="relations[0].collection_many" />
</div>
<div class="field">
<div class="type-label">{{ $t('related_collection') }}</div>
<v-select
:placeholder="$t('choose_a_collection')"
:items="items"
v-model="_relations[0].collection_one"
v-model="relations[0].collection_one"
/>
</div>
<v-input disabled :value="fieldData.field" />
@@ -45,33 +45,20 @@ import useCollectionsStore from '@/stores/collections';
import useFieldsStore from '@/stores/fields';
import i18n from '@/lang';
import { state } from '../store';
export default defineComponent({
props: {
type: {
type: String,
required: true,
},
relations: {
type: Array as PropType<Relation[]>,
required: true,
},
newFields: {
type: Array as PropType<DeepPartial<Field>[]>,
required: true,
},
fieldData: {
type: Object,
required: true,
},
collection: {
type: String,
required: true,
},
},
setup(props, { emit }) {
const _relations = useSync(props, 'relations', emit);
const _newFields = useSync(props, 'newFields', emit);
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
@@ -79,13 +66,13 @@ export default defineComponent({
const { hasCorresponding, correspondingField, correspondingLabel } = useCorresponding();
return {
_relations,
_newFields,
relations: state.relations,
items,
relatedPrimary,
hasCorresponding,
correspondingField,
correspondingLabel,
fieldData: state.fieldData,
};
function useRelation() {
@@ -110,8 +97,8 @@ export default defineComponent({
);
const relatedPrimary = computed(() => {
return _relations.value[0].collection_one
? fieldsStore.getPrimaryKeyFieldForCollection(_relations.value[0].collection_one)?.field
return state.relations[0].collection_one
? fieldsStore.getPrimaryKeyFieldForCollection(state.relations[0].collection_one)?.field
: null;
});
@@ -121,14 +108,14 @@ export default defineComponent({
function useCorresponding() {
const hasCorresponding = computed({
get() {
return _newFields.value.length > 0;
return state.newFields.length > 0;
},
set(enabled: boolean) {
if (enabled === true) {
_newFields.value = [
state.newFields = [
{
field: '',
collection: _relations.value[0].collection_one,
collection: state.relations[0].collection_one,
system: {
special: 'o2m',
interface: 'one-to-many',
@@ -136,26 +123,26 @@ export default defineComponent({
},
];
} else {
_newFields.value = [];
state.newFields = [];
}
},
});
const correspondingField = computed({
get() {
return _newFields.value?.[0]?.field || null;
return state.newFields?.[0]?.field || null;
},
set(field: string | null) {
_newFields.value = [
state.newFields = [
{
...(_newFields.value[0] || {}),
...(state.newFields[0] || {}),
field: field || '',
},
];
_relations.value = [
state.relations = [
{
..._relations.value[0],
...state.relations[0],
field_one: field,
},
];
@@ -163,8 +150,8 @@ export default defineComponent({
});
const correspondingLabel = computed(() => {
if (_relations.value[0].collection_one) {
return i18n.t('add_o2m_to_collection', { collection: _relations.value[0].collection_one });
if (state.relations[0].collection_one) {
return i18n.t('add_o2m_to_collection', { collection: state.relations[0].collection_one });
}
return i18n.t('add_field_related');

View File

@@ -12,10 +12,10 @@
</div>
<v-input disabled :value="currentCollectionPrimaryKey.field" />
<v-select
v-model="_relations[0].field_many"
:disabled="!_relations[0].collection_many"
v-model="relations[0].field_many"
:disabled="!relations[0].collection_many"
:items="fields"
:placeholder="!_relations[0].collection_many ? $t('choose_a_collection') : $t('choose_a_field')"
:placeholder="!relations[0].collection_many ? $t('choose_a_collection') : $t('choose_a_field')"
/>
<v-icon name="arrow_forward" />
</div>
@@ -32,39 +32,26 @@ import useFieldsStore from '@/stores/fields';
import { orderBy } from 'lodash';
import i18n from '@/lang';
import { state } from '../store';
export default defineComponent({
props: {
type: {
type: String,
required: true,
},
relations: {
type: Array as PropType<Relation[]>,
required: true,
},
newFields: {
type: Array as PropType<DeepPartial<Field>[]>,
required: true,
},
fieldData: {
type: Object,
required: true,
},
collection: {
type: String,
required: true,
},
},
setup(props, { emit }) {
const _relations = useSync(props, 'relations', emit);
const _newFields = useSync(props, 'newFields', emit);
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const { items, fields, currentCollectionPrimaryKey, collectionMany } = useRelation();
return { _relations, items, fields, currentCollectionPrimaryKey, collectionMany };
return { relations: state.relations, items, fields, currentCollectionPrimaryKey, collectionMany };
function useRelation() {
const availableCollections = computed(() => {
@@ -92,16 +79,15 @@ export default defineComponent({
);
const fields = computed(() => {
if (!_relations.value[0].collection_many) return [];
if (!state.relations[0].collection_many) return [];
return fieldsStore.state.fields
.filter((field) => {
if (field.collection !== _relations.value[0].collection_many) return false;
if (field.collection !== state.relations[0].collection_many) 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.database || field.database.type !== currentCollectionPrimaryKey.value.database.type)
return false;
if (!field.database || field.type !== currentCollectionPrimaryKey.value.type) return false;
return true;
})
@@ -110,49 +96,16 @@ export default defineComponent({
const collectionMany = computed({
get() {
return _relations.value[0].collection_many;
return state.relations[0].collection_many!;
},
set(collection: string) {
_relations.value[0].collection_many = collection;
_relations.value[0].field_many = '';
state.relations[0].collection_many = collection;
state.relations[0].field_many = '';
},
});
return { availableCollections, items, fields, currentCollectionPrimaryKey, collectionMany };
}
function useCorresponding() {
const hasCorresponding = computed({
get() {
return _newFields.value.length > 0;
},
set(enabled: boolean) {
if (enabled === true) {
_newFields.value = [
{
field: _relations.value[0].field_many,
collection: _relations.value[0].collection_many,
system: {
interface: 'many-to-one',
},
},
];
} else {
_newFields.value = [];
}
},
});
const correspondingLabel = computed(() => {
if (_relations.value[0].collection_many) {
return i18n.t('add_m2o_to_collection', { collection: _relations.value[0].collection_many });
}
return i18n.t('add_field_related');
});
return { hasCorresponding, correspondingLabel };
}
},
});
</script>

View File

@@ -1,35 +1,13 @@
<template>
<relationship-m2o
:collection="collection"
:field-data="fieldData"
:relations.sync="_relations"
:new-fields.sync="_newFields"
:type="type"
v-if="type === 'm2o' || type === 'file'"
/>
<relationship-o2m
:collection="collection"
:field-data="fieldData"
:relations.sync="_relations"
:new-fields.sync="_newFields"
:type="type"
v-else-if="type === 'o2m'"
/>
<relationship-m2m
:collection="collection"
:field-data="fieldData"
:relations.sync="_relations"
:new-fields.sync="_newFields"
:type="type"
v-else-if="type === 'm2m' || type === 'files'"
/>
<relationship-m2o :collection="collection" :type="type" v-if="type === 'm2o' || type === 'file'" />
<relationship-o2m :collection="collection" :type="type" v-else-if="type === 'o2m'" />
<relationship-m2m :collection="collection" :type="type" v-else-if="type === 'm2m' || type === 'files'" />
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { Relation } from '@/stores/relations/types';
import { Field } from '@/stores/fields/types';
import useSync from '@/composables/use-sync';
import RelationshipM2o from './relationship-m2o.vue';
import RelationshipO2m from './relationship-o2m.vue';
@@ -46,28 +24,10 @@ export default defineComponent({
type: String,
required: true,
},
relations: {
type: Array as PropType<Relation[]>,
required: true,
},
newFields: {
type: Array as PropType<DeepPartial<Field>[]>,
required: true,
},
fieldData: {
type: Object,
required: true,
},
collection: {
type: String,
required: true,
},
},
setup(props, { emit }) {
const _relations = useSync(props, 'relations', emit);
const _newFields = useSync(props, 'newFields', emit);
return { _relations, _newFields };
},
});
</script>

View File

@@ -5,16 +5,16 @@
<div class="form">
<div class="field">
<div class="label type-label">{{ $t('key') }}</div>
<v-input autofocus class="monospace" v-model="_field.field" db-safe />
<v-input :disabled="isExisting" autofocus class="monospace" v-model="fieldData.field" db-safe />
</div>
<div class="field">
<div class="label type-label">{{ $t('type') }}</div>
<v-input v-if="!_field.database" :value="$t('alias')" disabled />
<v-input v-if="!fieldData.database" :value="$t('alias')" disabled />
<v-select
v-else
:disabled="typeDisabled"
:value="_field.database.type"
:disabled="typeDisabled || isExisting"
:value="fieldData.type"
@input="setType"
:items="typesWithLabels"
:placeholder="typePlaceholder"
@@ -23,32 +23,32 @@
<div class="field full">
<div class="label type-label">{{ $t('note') }}</div>
<v-input v-model="_field.system.comment" :placeholder="$t('add_note')" />
<v-input v-model="fieldData.system.comment" :placeholder="$t('add_note')" />
</div>
<!-- @todo base default value field type on selected type -->
<div class="field" v-if="_field.database">
<div class="field" v-if="fieldData.database">
<div class="label type-label">{{ $t('default_value') }}</div>
<v-input
class="monospace"
v-model="_field.database.default_value"
v-model="fieldData.database.default_value"
:placeholder="$t('add_a_default_value')"
/>
</div>
<div class="field" v-if="_field.database">
<div class="field" v-if="fieldData.database">
<div class="label type-label">{{ $t('length') }}</div>
<v-input
type="number"
:placeholder="_field.database.type !== 'string' ? $t('not_available_for_type') : '255'"
:disabled="_field.database.type !== 'string'"
v-model="_field.database.max_length"
:placeholder="fieldData.type !== 'string' ? $t('not_available_for_type') : '255'"
:disabled="isExisting || fieldData.type !== 'string'"
v-model="fieldData.database.max_length"
/>
</div>
<div class="field" v-if="_field.database">
<div class="field" v-if="fieldData.database">
<div class="label type-label">{{ $t('allow_null') }}</div>
<v-checkbox v-model="_field.database.is_nullable" :label="$t('allow_null_label')" block />
<v-checkbox v-model="fieldData.database.is_nullable" :label="$t('allow_null_label')" block />
</div>
<!--
@@ -56,7 +56,7 @@
<div class="field">
<div class="label type-label">{{ $t('unique') }}</div>
<v-input v-model="_field.database.unique" />
<v-input v-model="fieldData.database.unique" />
</div> -->
</div>
</div>
@@ -67,11 +67,12 @@ import { defineComponent, computed } from '@vue/composition-api';
import useSync from '@/composables/use-sync';
import { types } from '@/stores/fields/types';
import i18n from '@/lang';
import { state } from '../store';
export default defineComponent({
props: {
fieldData: {
type: Object,
isExisting: {
type: Boolean,
required: true,
},
type: {
@@ -80,8 +81,6 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const _field = useSync(props, 'fieldData', emit);
const typesWithLabels = computed(() =>
types
.filter((type) => {
@@ -113,23 +112,22 @@ export default defineComponent({
return i18n.t('choose_a_type');
});
return { _field, typesWithLabels, setType, typeDisabled, typePlaceholder };
return { fieldData: state.fieldData, typesWithLabels, setType, typeDisabled, typePlaceholder };
function setType(value: typeof types[number]) {
if (value === 'uuid') {
_field.value.system.special = 'uuid';
state.fieldData.system.special = 'uuid';
} else {
_field.value.system.special = null;
state.fieldData.system.special = null;
}
// We'll reset the interface/display as they most likely won't work for the newly selected
// type
_field.value.system.interface = null;
_field.value.system.options = null;
_field.value.system.display = null;
_field.value.system.display_options = null;
_field.value.database.type = value;
state.fieldData.system.interface = null;
state.fieldData.system.options = null;
state.fieldData.system.display = null;
state.fieldData.system.display_options = null;
state.fieldData.type = value;
}
},
});

View File

@@ -1,37 +1,35 @@
<template>
<v-modal :active="active" title="Test" persistent>
<template #sidebar>
<setup-tabs :current.sync="currentTab" :tabs="tabs" :type="type" />
<setup-tabs :current.sync="currentTab" :tabs="tabs" :type="localType" />
</template>
<setup-schema
:collection="collection"
v-if="currentTab[0] === 'schema'"
:field-data.sync="fieldData"
:type="type"
:is-existing="field !== '+'"
:collection="collection"
:type="localType"
/>
<setup-relationship
:collection="collection"
v-if="currentTab[0] === 'relationship'"
:field-data.sync="fieldData"
:relations.sync="relations"
:new-fields.sync="newFields"
:type="type"
:is-existing="field !== '+'"
:collection="collection"
:type="localType"
/>
<setup-interface
:collection="collection"
v-if="currentTab[0] === 'interface'"
:field-data.sync="fieldData"
:type="type"
:is-existing="field !== '+'"
:collection="collection"
:type="localType"
/>
<setup-display
:collection="collection"
v-if="currentTab[0] === 'display'"
:field-data.sync="fieldData"
:type="type"
:is-existing="field !== '+'"
:collection="collection"
:type="localType"
/>
<template #footer>
@@ -41,6 +39,7 @@
:current.sync="currentTab"
:tabs="tabs"
@save="saveField"
@cancel="cancelField"
/>
</template>
</v-modal>
@@ -59,9 +58,12 @@ import { isEmpty } from 'lodash';
import api from '@/api';
import { Relation } from '@/stores/relations/types';
import { useFieldsStore } from '@/stores/fields';
import { useRelationsStore } from '@/stores/relations';
import { Field } from '@/stores/fields/types';
import router from '@/router';
import { initLocalStore, state, clearLocalStore } from './store';
export default defineComponent({
components: {
SetupTabs,
@@ -86,20 +88,45 @@ export default defineComponent({
},
},
setup(props) {
const fieldsStore = useFieldsStore();
const relationsStore = useRelationsStore();
const localType = computed(() => {
if (props.field === '+') return props.type;
let type: 'standard' | 'file' | 'files' | 'o2m' | 'm2m' | 'm2o' = 'standard';
const existingField = fieldsStore.getField(props.collection, props.field);
type = getLocalTypeForField(props.collection, props.field);
return type;
});
// This makes sure we still see the enter animation
/** @todo fix this in the transition */
const active = ref(false);
onMounted(() => {
active.value = true;
});
const fieldsStore = useFieldsStore();
initLocalStore(props.collection, props.field, localType.value);
const { tabs, currentTab } = useTabs();
const { fieldData, relations, newFields } = useData();
const saving = ref(false);
return { active, tabs, currentTab, fieldData, saveField, saving, relations, newFields };
return {
active,
tabs,
currentTab,
fieldData: state.fieldData,
saveField,
saving,
relations: state.relations,
newFields: state.newFields,
cancelField,
localType,
};
function useTabs() {
const tabs = computed(() => {
@@ -121,7 +148,7 @@ export default defineComponent({
},
];
if (['o2m', 'm2o', 'm2m', 'files'].includes(props.type)) {
if (['o2m', 'm2o', 'm2m', 'files'].includes(localType.value)) {
tabs.splice(1, 0, {
text: i18n.t('relationship'),
value: 'relationship',
@@ -138,241 +165,99 @@ export default defineComponent({
function relationshipDisabled() {
return (
isEmpty(fieldData.field) ||
(['o2m', 'm2m', 'files', 'm2o'].includes(props.type) === false && isEmpty(fieldData.database.type))
isEmpty(state.fieldData.field) ||
(['o2m', 'm2m', 'files', 'm2o'].includes(localType.value) === false &&
isEmpty(state.fieldData.type))
);
}
function interfaceDisplayDisabled() {
if (['o2m', 'm2o', 'file'].includes(props.type)) {
if (['o2m', 'm2o', 'file'].includes(localType.value)) {
return (
relations.value.length === 0 ||
isEmpty(relations.value[0].collection_many) ||
isEmpty(relations.value[0].field_many) ||
isEmpty(relations.value[0].collection_one)
state.relations.length === 0 ||
isEmpty(state.relations[0].collection_many) ||
isEmpty(state.relations[0].field_many) ||
isEmpty(state.relations[0].collection_one)
);
}
if (['m2m', 'files'].includes(props.type)) {
if (['m2m', 'files'].includes(localType.value)) {
return (
relations.value.length !== 2 ||
isEmpty(relations.value[0].collection_many) ||
isEmpty(relations.value[0].field_many) ||
isEmpty(relations.value[0].field_one) ||
isEmpty(relations.value[1].collection_many) ||
isEmpty(relations.value[1].field_many) ||
isEmpty(relations.value[1].collection_one)
state.relations.length !== 2 ||
isEmpty(state.relations[0].collection_many) ||
isEmpty(state.relations[0].field_many) ||
isEmpty(state.relations[0].field_one) ||
isEmpty(state.relations[1].collection_many) ||
isEmpty(state.relations[1].field_many) ||
isEmpty(state.relations[1].collection_one)
);
}
return isEmpty(fieldData.field) || isEmpty(fieldData.database.type);
return isEmpty(state.fieldData.field) || isEmpty(state.fieldData.type);
}
}
function useData() {
/** @todo this should technically be a DeepPartial<Field>, but that's a bit annoying to deal with rn */
const fieldData = reactive<any>({
field: '',
database: {
type: undefined,
default_value: undefined,
max_length: undefined,
is_nullable: true,
},
system: {
hidden: false,
interface: undefined,
options: undefined,
display: undefined,
display_options: undefined,
readonly: false,
special: undefined,
note: undefined,
},
});
const relations = ref<Partial<Relation>[]>([]);
// Allow the panes to create additional fields outside of this one. This is used to
// auto generated related o2m columns / junction collections etc
const newFields = ref<DeepPartial<Field>[]>([]);
if (props.type === 'file') {
fieldData.database.type = 'uuid';
relations.value = [
{
collection_many: props.collection,
field_many: '',
primary_many: fieldsStore.getPrimaryKeyFieldForCollection(props.collection)?.field,
collection_one: 'directus_files',
primary_one: fieldsStore.getPrimaryKeyFieldForCollection('directus_files')?.field,
},
];
watch(
() => fieldData.field,
() => {
relations.value[0].field_many = fieldData.field;
}
);
}
if (props.type === 'm2o') {
relations.value = [
{
collection_many: props.collection,
field_many: '',
primary_many: fieldsStore.getPrimaryKeyFieldForCollection(props.collection)?.field,
collection_one: '',
primary_one: fieldsStore.getPrimaryKeyFieldForCollection('directus_files')?.field,
},
];
watch(
() => fieldData.field,
() => {
relations.value[0].field_many = fieldData.field;
}
);
// Make sure to keep the current m2o field type in sync with the primary key of the
// selected related collection
watch(
() => relations.value[0].collection_one,
() => {
const field = fieldsStore.getPrimaryKeyFieldForCollection(relations.value[0].collection_one);
fieldData.database.type = field.database.type;
}
);
watch(
() => relations.value[0].collection_one,
() => {
if (newFields.value.length > 0) {
newFields.value[0].collection = relations.value[0].collection_one;
}
}
);
}
if (props.type === 'o2m') {
delete fieldData.database;
fieldData.system.special = 'o2m';
relations.value = [
{
collection_many: '',
field_many: '',
primary_many: '',
collection_one: props.collection,
field_one: fieldData.field,
primary_one: fieldsStore.getPrimaryKeyFieldForCollection(props.collection)?.field,
},
];
watch(
() => fieldData.field,
() => {
relations.value[0].field_one = fieldData.field;
}
);
watch(
() => relations.value[0].collection_many,
() => {
relations.value[0].primary_many = fieldsStore.getPrimaryKeyFieldForCollection(
relations.value[0].collection_many
).field;
}
);
}
if (props.type === 'm2m' || props.type === 'files') {
delete fieldData.database;
fieldData.system.special = 'm2m';
relations.value = [
{
collection_many: '',
field_many: '',
primary_many: '',
collection_one: props.collection,
field_one: fieldData.field,
primary_one: fieldsStore.getPrimaryKeyFieldForCollection(props.collection)?.field,
},
{
collection_many: '',
field_many: '',
primary_many: '',
collection_one: props.type === 'files' ? 'directus_files' : '',
field_one: null,
primary_one:
props.type === 'files'
? fieldsStore.getPrimaryKeyFieldForCollection('directus_files')?.field
: '',
},
];
watch(
() => fieldData.field,
() => {
relations.value[0].field_one = fieldData.field;
}
);
watch(
() => relations.value[0].collection_many,
() => {
const pkField = fieldsStore.getPrimaryKeyFieldForCollection(relations.value[0].collection_many)
?.field;
relations.value[0].primary_many = pkField;
relations.value[1].primary_many = pkField;
}
);
watch(
() => relations.value[0].field_many,
() => {
relations.value[1].junction_field = relations.value[0].field_many;
}
);
watch(
() => relations.value[1].field_many,
() => {
relations.value[0].junction_field = relations.value[1].field_many;
}
);
}
return { fieldData, relations, newFields };
}
async function saveField() {
saving.value = true;
try {
await api.post(`/fields/${props.collection}`, fieldData);
if (props.field !== '+') {
await api.patch(`/fields/${props.collection}/${props.field}`, state.fieldData);
} else {
await api.post(`/fields/${props.collection}`, state.fieldData);
}
await Promise.all(
newFields.value.map((newField) => {
state.newFields.map((newField: Partial<Field>) => {
return api.post(`/fields/${newField.collection}`, newField);
})
);
await api.post(`/relations`, relations.value);
await api.post(`/relations`, state.relations);
router.push(`/settings/data-model/${props.collection}`);
clearLocalStore();
} catch (error) {
console.error(error);
} finally {
saving.value = false;
}
}
function cancelField() {
router.push(`/settings/data-model/${props.collection}`);
clearLocalStore();
}
function getLocalTypeForField(
collection: string,
field: string
): 'standard' | 'file' | 'files' | 'o2m' | 'm2m' | 'm2o' {
const fieldInfo = fieldsStore.getField(collection, field);
const relations = relationsStore.getRelationsForField(collection, field);
if (relations.length === 0) return 'standard';
if (relations.length === 1) {
const relation = relations[0];
if (relation.collection_one === 'directus_files') return 'file';
if (relation.collection_many === collection) return 'm2o';
return 'o2m';
}
if (relations.length === 2) {
if (
relations[0].collection_one === 'directus_files' ||
relations[1].collection_one === 'directus_files'
) {
return 'files';
} else {
return 'm2m';
}
}
return 'standard';
}
},
});
</script>

View File

@@ -0,0 +1,240 @@
/**
* This is a "local store" meant to make the field data shareable between the different panes
* and components within the field setup modal flow.
*
* It's reset every time the modal opens and shouldn't be used outside of the field-detail flow.
*/
import useFieldsStore from '@/stores/fields';
import useRelationsStore from '@/stores/relations';
import { reactive, watch } from '@vue/composition-api';
const fieldsStore = useFieldsStore();
const relationsStore = useRelationsStore();
const state = reactive<any>({
fieldData: {
field: '',
type: '',
database: {
default_value: undefined,
max_length: undefined,
is_nullable: true,
},
system: {
hidden: false,
interface: undefined,
options: undefined,
display: undefined,
display_options: undefined,
readonly: false,
special: undefined,
note: undefined,
},
},
relations: [],
newFields: [],
});
export { state, initLocalStore, clearLocalStore };
function initLocalStore(
collection: string,
field: string,
type: 'standard' | 'file' | 'files' | 'm2o' | 'o2m' | 'm2m'
) {
const isExisting = field !== '+';
if (isExisting) {
const existingField = fieldsStore.getField(collection, field);
state.fieldData.field = existingField.field;
state.fieldData.type = existingField.type;
state.fieldData.database = existingField.database;
state.fieldData.system = existingField.system;
state.relations = relationsStore.getRelationsForField(collection, field);
}
if (type === 'file') {
if (!isExisting) {
state.fieldData.type = 'uuid';
state.relations = [
{
collection_many: collection,
field_many: '',
primary_many: fieldsStore.getPrimaryKeyFieldForCollection(collection)?.field,
collection_one: 'directus_files',
primary_one: fieldsStore.getPrimaryKeyFieldForCollection('directus_files')?.field,
},
];
}
watch(
() => state.fieldData.field,
() => {
state.relations[0].field_many = state.fieldData.field;
}
);
}
if (type === 'm2o') {
if (!isExisting) {
state.relations = [
{
collection_many: collection,
field_many: '',
primary_many: fieldsStore.getPrimaryKeyFieldForCollection(collection)?.field,
collection_one: '',
primary_one: fieldsStore.getPrimaryKeyFieldForCollection('directus_files')?.field,
},
];
}
watch(
() => state.fieldData.field,
() => {
state.relations[0].field_many = state.fieldData.field;
}
);
// Make sure to keep the current m2o field type in sync with the primary key of the
// selected related collection
watch(
() => state.relations[0].collection_one,
() => {
const field = fieldsStore.getPrimaryKeyFieldForCollection(state.relations[0].collection_one);
state.fieldData.type = field.type;
}
);
watch(
() => state.relations[0].collection_one,
() => {
if (state.newFields.length > 0) {
state.newFields[0].collection = state.relations[0].collection_one;
}
}
);
}
if (type === 'o2m') {
delete state.fieldData.database;
if (!isExisting) {
state.fieldData.system.special = 'o2m';
state.relations.push = [
{
collection_many: '',
field_many: '',
primary_many: '',
collection_one: collection,
field_one: state.fieldData.field,
primary_one: fieldsStore.getPrimaryKeyFieldForCollection(collection)?.field,
},
];
}
watch(
() => state.fieldData.field,
() => {
state.relations[0].field_one = state.fieldData.field;
}
);
watch(
() => state.relations[0].collection_many,
() => {
state.relations[0].primary_many = fieldsStore.getPrimaryKeyFieldForCollection(
state.relations[0].collection_many
).field;
}
);
}
if (type === 'm2m' || type === 'files') {
delete state.fieldData.database;
if (!isExisting) {
state.fieldData.system.special = 'm2m';
state.relations = [
{
collection_many: '',
field_many: '',
primary_many: '',
collection_one: collection,
field_one: state.fieldData.field,
primary_one: fieldsStore.getPrimaryKeyFieldForCollection(collection)?.field,
},
{
collection_many: '',
field_many: '',
primary_many: '',
collection_one: type === 'files' ? 'directus_files' : '',
field_one: null,
primary_one:
type === 'files' ? fieldsStore.getPrimaryKeyFieldForCollection('directus_files')?.field : '',
},
];
}
watch(
() => state.fieldData.field,
() => {
state.relations[0].field_one = state.fieldData.field;
}
);
watch(
() => state.relations[0].collection_many,
() => {
const pkField = fieldsStore.getPrimaryKeyFieldForCollection(state.relations[0].collection_many)?.field;
state.relations[0].primary_many = pkField;
state.relations[1].primary_many = pkField;
}
);
watch(
() => state.relations[0].field_many,
() => {
state.relations[1].junction_field = state.relations[0].field_many;
}
);
watch(
() => state.relations[1].field_many,
() => {
state.relations[0].junction_field = state.relations[1].field_many;
}
);
}
}
function clearLocalStore() {
state.fieldData = {
field: '',
type: '',
database: {
default_value: undefined,
max_length: undefined,
is_nullable: true,
},
system: {
hidden: false,
interface: undefined,
options: undefined,
display: undefined,
display_options: undefined,
readonly: false,
special: undefined,
note: undefined,
},
};
state.relations = [];
state.newFields = [];
}

View File

@@ -88,7 +88,6 @@ export const useFieldsStore = createStore({
parseField(field: FieldRaw): Field {
let name: string | VueI18n.TranslateResult;
const type = field.database === null ? 'alias' : getLocalType(field.database.type);
const system = field.system === null ? getSystemDefault(field.collection, field.field) : field.system;
if (notEmpty(system.translation) && system.translation.length > 0) {
@@ -110,7 +109,6 @@ export const useFieldsStore = createStore({
return {
...field,
name,
type,
system,
};
},

View File

@@ -68,6 +68,7 @@ export type SystemField = {
export interface FieldRaw {
collection: string;
field: string;
type: typeof types[number];
database: DatabaseColumn | null;
system: SystemField | null;