Setup field setup modal as nested route

This commit is contained in:
rijkvanzanten
2020-07-22 13:49:05 -04:00
parent 325502a7b1
commit 26f673011c
33 changed files with 202 additions and 1846 deletions

View File

@@ -216,10 +216,6 @@ body {
justify-content: flex-end;
}
&:active {
transform: scale(0.98);
}
&:focus {
outline: 0;
}
@@ -229,10 +225,6 @@ body {
background-color: var(--v-button-background-color-disabled);
border: var(--border-width) solid var(--v-button-background-color-disabled);
cursor: not-allowed;
&:active {
transform: scale(1);
}
}
&.rounded {

View File

@@ -14,7 +14,7 @@ export default function useFieldTree(collection: Ref<string>) {
.getFieldsForCollection(collection.value)
.filter(
(field: Field) =>
field.system?.hidden_browse === false && field.system?.special?.toLowerCase() !== 'alias'
field.system?.hidden === false && field.system?.special?.toLowerCase() !== 'alias'
)
.map((field: Field) => parseField(field, []));
@@ -44,7 +44,7 @@ export default function useFieldTree(collection: Ref<string>) {
.getFieldsForCollection(relatedCollection)
.filter(
(field: Field) =>
field.system?.hidden_browse === false &&
field.system?.hidden === false &&
field.system?.special?.toLowerCase() !== 'alias'
);
})

View File

@@ -18,7 +18,7 @@ export default function useFormFields(fields: Ref<Field[]>) {
*/
// Filter out the fields that are marked hidden on detail
formFields = formFields.filter((field) => {
const hiddenDetail = field.system?.hidden_detail;
const hiddenDetail = field.system?.hidden;
if (isEmpty(hiddenDetail)) return true;
return hiddenDetail === false;
});

View File

@@ -33,10 +33,14 @@
"schema_options_title": "All set! Below are some optional configuration options...",
"creating_field": "Creating New Field",
"enter_field_name": "Enter a field name...",
"standard_field": "Standard Field",
"relational_field": "Relational Field",
"single_file": "Single File",
"multiple_files": "Multiple Files",
"m2o_relationship": "Many to One Relationship",
"o2m_relationship": "One to Many Relationship",
"m2m_relationship": "Many to Many Relationship",
"next": "Next",
"previous": "Previous",
"add_field": "Add Field",

View File

@@ -222,7 +222,7 @@ export default defineComponent({
const { info, primaryKeyField, fields: fieldsInCollection } = useCollection(collection);
const availableFields = computed(() =>
fieldsInCollection.value.filter((field) => field.system.hidden_browse !== true)
fieldsInCollection.value.filter((field) => field.system.hidden !== true)
);
const fileFields = computed(() => {

View File

@@ -235,7 +235,7 @@ export default defineComponent({
const { info, primaryKeyField, fields: fieldsInCollection, sortField } = useCollection(collection);
const availableFields = computed(() =>
fieldsInCollection.value.filter((field) => field.system?.hidden_browse === false)
fieldsInCollection.value.filter((field) => field.system?.hidden === false)
);
const { sort, limit, page, fields, fieldsWithRelational } = useItemOptions();

View File

@@ -1,6 +1,6 @@
import { defineModule } from '@/modules/define';
import SettingsProject from './routes/project';
import { SettingsCollections, SettingsFields } from './routes/data-model/';
import { SettingsCollections, SettingsFields, SettingsFieldDetail } from './routes/data-model/';
import { SettingsRolesBrowse, SettingsRolesDetail } from './routes/roles';
import { SettingsWebhooksBrowse, SettingsWebhooksDetail } from './routes/webhooks';
import { SettingsPresetsBrowse, SettingsPresetsDetail } from './routes/presets';
@@ -30,7 +30,20 @@ export default defineModule(({ i18n }) => ({
name: 'settings-fields',
path: '/data-model/:collection',
component: SettingsFields,
props: true,
props: (route) => ({
collection: route.params.collection,
field: route.params.field,
type: route.query.type,
}),
children: [
{
path: ':field',
name: 'settings-fields-field',
components: {
field: SettingsFieldDetail,
},
},
],
},
{
name: 'settings-roles-browse',

View File

@@ -147,9 +147,7 @@ export default defineComponent({
return sortBy(
collectionsStore.state.collections.filter(
(collection) =>
collection.collection.startsWith('directus_') === false &&
collection.managed === true &&
collection.hidden === false
collection.collection.startsWith('directus_') === false && collection.hidden === false
),
'collection'
);
@@ -160,9 +158,7 @@ export default defineComponent({
collectionsStore.state.collections
.filter(
(collection) =>
collection.collection.startsWith('directus_') === false &&
collection.managed === true &&
collection.hidden === true
collection.collection.startsWith('directus_') === false && collection.hidden === true
)
.map((collection) => ({ ...collection, icon: 'visibility_off' })),
'collection'

View File

@@ -220,8 +220,7 @@ export default defineComponent({
const field: DeepPartial<Field> = {
field: primaryKeyFieldName.value,
system: {
hidden_browse: false,
hidden_detail: false,
hidden: false,
interface: 'numeric',
readonly: true,
},
@@ -318,8 +317,7 @@ export default defineComponent({
field: systemFields[1].name,
system: {
interface: 'sort',
hidden_detail: true,
hidden_browse: true,
hidden: true,
width: 'full',
special: 'sort',
},
@@ -340,8 +338,7 @@ export default defineComponent({
display: 'both',
},
readonly: true,
hidden_detail: true,
hidden_browse: true,
hidden: true,
width: 'full',
},
database: {
@@ -357,8 +354,7 @@ export default defineComponent({
special: 'datetime_created',
interface: 'datetime-created',
readonly: true,
hidden_detail: true,
hidden_browse: true,
hidden: true,
width: 'full',
},
database: {
@@ -378,8 +374,7 @@ export default defineComponent({
display: 'both',
},
readonly: true,
hidden_detail: true,
hidden_browse: true,
hidden: true,
width: 'full',
},
database: {
@@ -395,8 +390,7 @@ export default defineComponent({
special: 'datetime_updated',
interface: 'datetime-updated',
readonly: true,
hidden_detail: true,
hidden_browse: true,
hidden: true,
width: 'full',
},
database: {

View File

@@ -0,0 +1,41 @@
<template>
<v-modal :active="active" title="Test">
{{ field }} {{ type }}
<router-link to="/settings/data-model/customers">Back</router-link>
</v-modal>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from '@vue/composition-api';
export default defineComponent({
props: {
collection: {
type: String,
required: true,
},
field: {
type: String,
required: true,
},
type: {
type: String,
default: null,
},
},
setup() {
const active = ref(false);
// This makes sure we still see the enter animation
onMounted(() => {
active.value = true;
});
return { active };
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,4 @@
import SettingsFieldDetail from './field-detail.vue';
export { SettingsFieldDetail };
export default SettingsFieldDetail;

View File

@@ -21,7 +21,7 @@
</template>
<v-list dense>
<v-list-item @click="$emit('edit')">
<v-list-item :to="`/settings/data-model/${field.collection}/${field.field}`">
<v-list-item-icon><v-icon name="edit" /></v-list-item-icon>
<v-list-item-content>
{{ $t('edit_field') }}
@@ -49,7 +49,7 @@
</v-list-item>
<v-divider />
<v-list-item @click="$emit('toggle-visibility', field)">
<template v-if="field.hidden_detail === false">
<template v-if="field.hidden === false">
<v-list-item-icon><v-icon name="visibility_off" /></v-list-item-icon>
<v-list-item-content>{{ $t('hide_field_on_detail') }}</v-list-item-content>
</template>

View File

@@ -1,91 +0,0 @@
<template>
<div>
<h2 class="type-title" v-if="isNew">{{ $t('display_setup_title') }}</h2>
<v-fancy-select :items="items" :value="value.display" @input="emitValue('display', $event)" />
<template v-if="selectedDisplay">
<v-form
v-if="
selectedDisplay.options &&
Array.isArray(selectedDisplay.options) &&
selectedDisplay.options.length > 0
"
:fields="selectedDisplay.options"
primary-key="+"
:edits="value.options"
@input="emitValue('options', $event)"
/>
<v-notice v-else>
{{ $t('no_options_available') }}
</v-notice>
</template>
</div>
</template>
<script lang="ts">
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: {
isNew: {
type: Boolean,
default: false,
},
value: {
type: Object as PropType<Field>,
required: true,
},
localType: {
type: String as PropType<LocalType>,
required: true,
},
},
setup(props, { emit }) {
const items = computed<FancySelectItem[]>(() => {
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(() => {
return displays.find((inter) => inter.id === props.value.system.display) || null;
});
return { emitValue, items, selectedDisplay };
function emitValue(key: string, value: any) {
emit('input', {
...props.value,
[key]: value,
});
}
},
});
</script>
<style lang="scss" scoped>
.v-fancy-select {
margin-bottom: 48px;
}
</style>

View File

@@ -1,100 +0,0 @@
<template>
<div>
<h2 class="type-title" v-if="isNew">{{ $t('field_setup_title') }}</h2>
<div class="type-label">
{{ $t('name') }}
<v-icon class="required" sup name="star" />
</div>
<v-input
class="field"
:value="value.field"
@input="emitValue('field', $event)"
db-safe
:disabled="isNew === false"
/>
<v-fancy-select :disabled="isNew === false" :items="items" :value="localType" @input="setLocalType" />
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from '@vue/composition-api';
import { Field } from '@/stores/fields/types';
import { LocalType } from './types';
import i18n from '@/lang';
import { FancySelectItem } from '@/components/v-fancy-select/types';
export default defineComponent({
props: {
value: {
type: Object as PropType<Field>,
required: true,
},
localType: {
type: String as PropType<LocalType>,
default: null,
},
isNew: {
type: Boolean,
default: true,
},
},
setup(props, { emit }) {
const items = computed<FancySelectItem[]>(() => [
{
text: i18n.t('standard_field'),
value: 'standard',
icon: 'create',
},
{
text: i18n.t('relational_field'),
value: 'relational',
icon: 'call_merge',
},
{
text: i18n.t('single_file'),
value: 'file',
icon: 'photo',
},
{
text: i18n.t('multiple_files'),
value: 'files',
icon: 'collections',
},
]);
return { emitValue, items, setLocalType };
function emitValue(key: string, value: any) {
emit('input', {
...props.value,
[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>
<style lang="scss" scoped>
.field {
--v-input-font-family: var(--family-monospace);
margin-bottom: 48px;
}
.required {
color: var(--primary);
}
</style>

View File

@@ -1,113 +0,0 @@
<template>
<div>
<h2 class="type-title" v-if="isNew">{{ $t('interface_setup_title') }}</h2>
<v-fancy-select :items="items" :value="value.interface" @input="setInterface" />
<template v-if="selectedInterface">
<v-form
v-if="
selectedInterface.options &&
Array.isArray(selectedInterface.options) &&
selectedInterface.options.length > 0
"
:fields="selectedInterface.options"
primary-key="+"
:edits="value.options"
@input="emitValue('options', $event)"
/>
<v-notice v-else>
{{ $t('no_options_available') }}
</v-notice>
</template>
</div>
</template>
<script lang="ts">
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: {
isNew: {
type: Boolean,
default: false,
},
value: {
type: Object as PropType<Field>,
required: true,
},
localType: {
type: String as PropType<LocalType>,
required: true,
},
},
setup(props, { emit }) {
const items = computed<FancySelectItem[]>(() => {
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.system.interface) || null;
});
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', {
...props.value,
[key]: value,
});
}
},
});
</script>
<style lang="scss" scoped>
.v-fancy-select {
margin-bottom: 48px;
}
</style>

View File

@@ -1,210 +0,0 @@
<template>
<div class="grid">
<div class="field">
<div class="type-label">{{ $t('this_collection') }}</div>
<v-input disabled :value="collection" />
</div>
<div class="field">
<div class="type-label">{{ $t('junction_collection') }}</div>
<v-select :items="collectionItems" v-model="junctionCollection" />
</div>
<div class="field">
<div class="type-label">{{ $t('related_collection') }}</div>
<v-select :items="collectionItems" v-model="relatedCollection" />
</div>
<v-input disabled :value="field.field" />
<v-select :disabled="!junctionCollection" :items="junctionFields" v-model="junctionFieldCurrent" />
<div class="spacer" />
<div class="spacer" />
<v-select :disabled="!junctionCollection" :items="junctionFields" v-model="junctionFieldRelated" />
<v-input disabled value="id" />
<v-icon name="arrow_forward" />
<v-icon name="arrow_backward" />
</div>
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from '@vue/composition-api';
import useCollectionsStore from '@/stores/collections';
import orderBy from 'lodash/orderBy';
import { Field } from '@/stores/fields/types';
import useFieldsStore from '@/stores/fields/';
import { Relation } from '@/stores/relations/types';
import useSync from '@/composables/use-sync';
export default defineComponent({
props: {
collection: {
type: String,
required: true,
},
field: {
type: Object as PropType<Field>,
required: true,
},
isNew: {
type: Boolean,
required: true,
},
newRelations: {
type: Array as PropType<Partial<Relation>[]>,
required: true,
},
existingRelations: {
type: Array as PropType<Relation[]>,
required: true,
},
},
setup(props, { emit }) {
const _newRelations = useSync(props, 'newRelations', emit);
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const availableCollections = computed(() => {
return orderBy(
collectionsStore.state.collections.filter((collection) => {
return (
collection.collection.startsWith('directus_') === false &&
collection.collection !== props.collection
);
}),
['collection'],
['asc']
);
});
const collectionItems = computed(() =>
availableCollections.value.map((collection) => ({
text: collection.name,
value: collection.collection,
}))
);
const defaultNewRelations = computed<Partial<Relation>[]>(() => [
{
collection_many: undefined,
field_many: undefined,
collection_one: props.field.collection,
field_one: props.field.field,
junction_field: undefined,
},
{
collection_many: undefined,
field_many: undefined,
collection_one: undefined,
field_one: undefined,
junction_field: undefined,
},
]);
const junctionCollection = computed({
get() {
return props.newRelations[0]?.collection_many || null;
},
set(newJunctionCollection: string | null) {
let relations: readonly Partial<Relation>[];
if (_newRelations.value.length === 0) relations = [...defaultNewRelations.value];
else relations = [..._newRelations.value];
relations[0].collection_many = newJunctionCollection || undefined;
relations[1].collection_many = newJunctionCollection || undefined;
_newRelations.value = relations;
},
});
const junctionFields = computed(() => {
if (!junctionCollection.value) return [];
return fieldsStore.getFieldsForCollection(junctionCollection.value).map((field: Field) => ({
text: field.name,
value: field.field,
}));
});
const relatedCollection = computed({
get() {
return props.newRelations[1]?.collection_one || null;
},
set(newRelatedCollection: string | null) {
let relations: readonly Partial<Relation>[];
if (_newRelations.value.length === 0) relations = [...defaultNewRelations.value];
else relations = [..._newRelations.value];
relations[1].collection_one = newRelatedCollection || undefined;
_newRelations.value = relations;
},
});
const junctionFieldCurrent = computed({
get() {
return props.newRelations[0]?.field_many || null;
},
set(newJunctionField: string | null) {
let relations: readonly Partial<Relation>[];
if (_newRelations.value.length === 0) relations = [...defaultNewRelations.value];
else relations = [..._newRelations.value];
relations[0].field_many = newJunctionField || undefined;
relations[1].junction_field = newJunctionField || undefined;
_newRelations.value = relations;
},
});
const junctionFieldRelated = computed({
get() {
return props.newRelations[1]?.field_many || null;
},
set(newJunctionField: string | null) {
let relations: readonly Partial<Relation>[];
if (_newRelations.value.length === 0) relations = [...defaultNewRelations.value];
else relations = [..._newRelations.value];
relations[1].field_many = newJunctionField || undefined;
relations[0].junction_field = newJunctionField || undefined;
_newRelations.value = relations;
},
});
return {
availableCollections,
collectionItems,
junctionCollection,
junctionFields,
relatedCollection,
junctionFieldCurrent,
junctionFieldRelated,
};
},
});
</script>
<style lang="scss" scoped>
.grid {
position: relative;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-top: 48px;
.v-icon {
--v-icon-color: var(--foreground-subdued);
position: absolute;
transform: translateX(-50%);
&:first-of-type {
bottom: 85px;
left: 32.8%;
}
&:last-of-type {
bottom: 14px;
left: 67%;
}
}
}
</style>

View File

@@ -1,141 +0,0 @@
<template>
<div>
<div class="grid">
<div class="field">
<div class="type-label">{{ $t('this_collection') }}</div>
<v-input disabled :value="collection" />
</div>
<div class="field">
<div class="type-label">{{ $t('related_collection') }}</div>
<v-select
v-if="isNew"
:placeholder="$t('choose_a_collection')"
:items="items"
v-model="collectionOne"
/>
<v-input disabled v-else :value="existingRelation.collection_many" />
</div>
<v-input disabled :value="field.field" />
<v-input disabled value="id" />
<v-icon name="arrow_back" />
</div>
<v-divider />
<div class="grid">
<div class="field">
<div class="type-label">{{ $t('create_corresponding_field') }}</div>
<v-checkbox block :disabled="isNew === false" :label="$t('add_field_related')" />
</div>
<div class="field">
<div class="type-label">{{ $t('corresponding_field_name') }}</div>
<v-input :disabled="isNew === false" />
</div>
<v-icon name="arrow_forward" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from '@vue/composition-api';
import useCollectionsStore from '@/stores/collections';
import orderBy from 'lodash/orderBy';
import { Field } from '@/stores/fields/types';
import { Relation } from '@/stores/relations/types';
import useSync from '@/composables/use-sync';
export default defineComponent({
props: {
collection: {
type: String,
required: true,
},
field: {
type: Object as PropType<Field>,
required: true,
},
existingRelations: {
type: Array as PropType<Relation[]>,
required: true,
},
newRelations: {
type: Array as PropType<Partial<Relation>[]>,
required: true,
},
isNew: {
type: Boolean,
required: true,
},
},
setup(props, { emit }) {
const _newRelations = useSync(props, 'newRelations', emit);
const collectionsStore = useCollectionsStore();
const availableCollections = computed(() => {
return orderBy(
collectionsStore.state.collections.filter((collection) => {
return (
collection.collection.startsWith('directus_') === false &&
collection.collection !== props.collection
);
}),
['collection'],
['asc']
);
});
const items = computed(() =>
availableCollections.value.map((collection) => ({
text: collection.name,
value: collection.collection,
}))
);
const existingRelation = computed(() => {
return props.existingRelations.find((relation) => {
return relation.field_many === props.field.field && relation.collection_many === props.field.collection;
});
});
const collectionOne = computed({
get() {
return _newRelations.value[0]?.collection_one || null;
},
set(newCollectionOne: string | null) {
_newRelations.value = [
{
field_many: props.field.field,
collection_many: props.field.collection,
collection_one: newCollectionOne || undefined,
},
];
},
});
return { availableCollections, items, existingRelation, collectionOne };
},
});
</script>
<style lang="scss" scoped>
.grid {
position: relative;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px 32px;
margin-top: 48px;
.v-icon {
--v-icon-color: var(--foreground-subdued);
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
}
}
.v-divider {
margin: 48px 0;
}
</style>

View File

@@ -1,163 +0,0 @@
<template>
<div class="grid">
<div class="field">
<div class="type-label">{{ $t('this_collection') }}</div>
<v-input disabled :value="collection" />
</div>
<div class="field">
<div class="type-label">{{ $t('related_collection') }}</div>
<v-select
v-if="isNew"
:placeholder="$t('choose_a_collection')"
:items="collectionItems"
v-model="collectionMany"
/>
<v-input disabled v-else :value="existingRelation.collection_many" />
</div>
<v-input disabled :value="field.field" />
<v-select
v-if="isNew"
:disabled="!collectionMany"
:items="collectionFields"
v-model="fieldMany"
:placeholder="!collectionMany ? $t('choose_a_collection') : $t('choose_a_field')"
/>
<v-input disabled v-else :value="existingRelation.field_many" />
<v-icon name="arrow_forward" />
</div>
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from '@vue/composition-api';
import useCollectionsStore from '@/stores/collections';
import orderBy from 'lodash/orderBy';
import { Field } from '@/stores/fields/types';
import { Relation } from '@/stores/relations/types';
import useSync from '@/composables/use-sync';
import useFieldsStore from '@/stores/fields';
export default defineComponent({
props: {
collection: {
type: String,
required: true,
},
field: {
type: Object as PropType<Field>,
required: true,
},
existingRelations: {
type: Array as PropType<Relation[]>,
required: true,
},
newRelations: {
type: Array as PropType<Partial<Relation>[]>,
required: true,
},
isNew: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const _newRelations = useSync(props, 'newRelations', emit);
const availableCollections = computed(() => {
return orderBy(
collectionsStore.state.collections.filter((collection) => {
return (
collection.collection.startsWith('directus_') === false &&
collection.collection !== props.collection
);
}),
['collection'],
['asc']
);
});
const collectionItems = computed(() =>
availableCollections.value.map((collection) => ({
text: collection.name,
value: collection.collection,
}))
);
const existingRelation = computed(() => {
return props.existingRelations.find((relation) => {
return relation.field_one === props.field.field && relation.collection_one === props.field.collection;
});
});
const defaultNewRelation = computed(() => ({
collection_one: props.field.collection,
field_one: props.field.field,
}));
const collectionMany = computed({
get() {
return props.newRelations[0]?.collection_many || null;
},
set(newCollectionOne: string | null) {
_newRelations.value = [
{
...(props.newRelations[0] || defaultNewRelation.value),
collection_many: newCollectionOne || undefined,
},
];
},
});
const fieldMany = computed({
get() {
return props.newRelations[0]?.field_many || null;
},
set(newCollectionOne: string | null) {
_newRelations.value = [
{
...(props.newRelations[0] || defaultNewRelation),
field_many: newCollectionOne || undefined,
},
];
},
});
const collectionFields = computed(() => {
if (!collectionMany.value) return [];
return fieldsStore.getFieldsForCollection(collectionMany.value).map((field: Field) => ({
text: field.name,
value: field.field,
}));
});
return {
availableCollections,
collectionItems,
collectionMany,
fieldMany,
existingRelation,
collectionFields,
};
},
});
</script>
<style lang="scss" scoped>
.grid {
position: relative;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px 32px;
margin-top: 48px;
.v-icon {
--v-icon-color: var(--foreground-subdued);
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
}
}
</style>

View File

@@ -1,119 +0,0 @@
<template>
<div>
<h2 class="type-title" v-if="isNew">{{ $t('relationship_setup_title') }}</h2>
<v-fancy-select
:items="items"
:value="value.type && value.type.toLowerCase()"
@input="emitValue('type', $event)"
:disabled="isNew === false"
/>
<many-to-one
v-if="value.type && value.type.toLowerCase() === 'm2o'"
:collection="value.collection"
:field="value"
@update:field="emit('input', $event)"
:existing-relations="existingRelations"
:new-relations.sync="_newRelations"
:is-new="isNew"
/>
<one-to-many
v-else-if="value.type && value.type.toLowerCase() === 'o2m'"
:collection="value.collection"
:field="value"
@update:field="emit('input', $event)"
:existing-relations="existingRelations"
:new-relations.sync="_newRelations"
:is-new="isNew"
/>
<many-to-many
v-else-if="value.type && value.type.toLowerCase() === 'm2m'"
:collection="value.collection"
:field="value"
@update:field="emit('input', $event)"
:existing-relations="existingRelations"
:new-relations.sync="_newRelations"
:is-new="isNew"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed, watch } from '@vue/composition-api';
import { FancySelectItem } from '@/components/v-fancy-select/types';
import { Field } from '@/stores/fields/types';
import i18n from '@/lang';
import ManyToOne from './field-setup-relationship-m2o.vue';
import OneToMany from './field-setup-relationship-o2m.vue';
import ManyToMany from './field-setup-relationship-m2m.vue';
import useRelationsStore from '@/stores/relations';
import { Relation } from '@/stores/relations/types';
import useSync from '@/composables/use-sync';
export default defineComponent({
components: {
ManyToOne,
OneToMany,
ManyToMany,
},
props: {
isNew: {
type: Boolean,
default: false,
},
value: {
type: Object as PropType<Field>,
required: true,
},
newRelations: {
type: Array as PropType<Partial<Relation>[]>,
required: true,
},
},
setup(props, { emit }) {
const relationsStore = useRelationsStore();
const _newRelations = useSync(props, 'newRelations', emit);
watch(
() => props.value.type,
() => {
_newRelations.value = [];
}
);
const items = computed<FancySelectItem[]>(() => {
return [
{
text: i18n.t('many_to_one'),
value: 'm2o',
icon: 'call_merge',
},
{
text: i18n.t('one_to_many'),
value: 'o2m',
icon: 'call_split',
},
{
text: i18n.t('many_to_many'),
value: 'm2m',
icon: 'compare_arrows',
},
];
});
const existingRelations = computed(() => {
if (props.isNew) return [];
return relationsStore.getRelationsForField(props.value.collection, props.value.field);
});
return { emitValue, items, existingRelations, _newRelations };
function emitValue(key: string, value: any) {
emit('input', {
...props.value,
[key]: value,
});
}
},
});
</script>

View File

@@ -1,196 +0,0 @@
<template>
<div>
<h2 class="type-title" v-if="isNew">{{ $t('schema_options_title') }}</h2>
<v-form :edits="value" @input="$emit('input', $event)" :fields="fields" primary-key="+" />
</div>
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from '@vue/composition-api';
import i18n from '@/lang';
import { FormField } from '@/components/v-form/types';
import { Field, types } from '@/stores/fields/types';
import interfaces from '@/interfaces';
export default defineComponent({
props: {
isNew: {
type: Boolean,
default: false,
},
value: {
type: Object as PropType<Field>,
required: true,
},
},
setup(props) {
const selectedInterface = computed(() => interfaces.find((inter) => inter.id === props.value.system.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[] = [
{
field: 'field',
name: i18n.t('database_column_name'),
system: {
interface: 'slug',
width: 'half',
options: null,
readonly: props.isNew === false,
},
},
{
field: 'note',
name: i18n.t('note'),
system: {
interface: 'text-input',
width: 'full',
options: {
placeholder: i18n.t('add_helpful_note'),
},
},
},
{
field: 'translation',
name: i18n.t('translations'),
system: {
interface: 'key-value',
width: 'full',
options: null,
},
},
{
field: 'default_value',
name: i18n.t('default_value'),
system: {
interface: 'text-input' /** @TODO base on selected datatype */,
width: 'half',
options: {
placeholder: i18n.t('enter_value'),
},
},
},
{
field: 'length',
name: i18n.t('length'),
system: {
interface: 'numeric',
width: 'half',
options: null,
},
},
{
field: 'required',
name: i18n.t('required'),
system: {
interface: 'toggle',
width: 'half',
options: null,
},
},
{
field: 'readonly',
name: i18n.t('readonly'),
system: {
interface: 'toggle',
width: 'half',
options: null,
},
},
{
field: 'hidden_detail',
name: i18n.t('hide_on_detail'),
system: {
interface: 'toggle',
width: 'half',
options: null,
},
},
{
field: 'hidden_browse',
name: i18n.t('hide_on_browse'),
system: {
interface: 'toggle',
width: 'half',
options: null,
},
},
{
field: 'unique',
name: i18n.t('unique'),
system: {
interface: 'toggle',
width: 'half',
options: null,
},
},
{
field: 'primary_key',
name: i18n.t('primary_key'),
system: {
interface: 'toggle',
width: 'half',
options: null,
},
},
{
field: 'validation',
name: i18n.t('validation_regex'),
system: {
interface: 'text-input',
width: 'half',
options: {
font: 'monospace',
placeholder: 'eg: /^[A-Z]+$/',
},
},
},
{
field: 'validation_message',
name: i18n.t('validation_message'),
system: {
interface: 'text-input',
width: 'half',
},
},
{
field: 'type',
name: i18n.t('directus_type'),
system: {
interface: 'dropdown',
width: 'half',
options: {
choices: typeChoices.value,
},
},
},
{
field: 'datatype',
name: i18n.t('database_type'),
system: {
interface: 'text-input',
width: 'half',
},
},
];
return fields;
});
return { fields };
},
});
</script>

View File

@@ -1,305 +0,0 @@
<template>
<v-modal
:active="active"
:title="title"
:subtitle="$t('within_collection', { collection: collectionInfo.name })"
persistent
>
<template #sidebar>
<setup-tabs
:current-tab.sync="currentTab"
:tabs="tabs"
:field="field"
:local-type="localType"
:is-new="isNew"
/>
</template>
<div class="content">
<field-setup-field
v-if="currentTab[0] === 'field'"
v-model="field"
:local-type.sync="localType"
:is-new="isNew"
/>
<field-setup-relationship
v-if="currentTab[0] === 'relationship'"
v-model="field"
:local-type.sync="localType"
:is-new="isNew"
:new-relations.sync="newRelations"
/>
<field-setup-interface
v-if="currentTab[0] === 'interface'"
v-model="field"
:local-type.sync="localType"
:is-new="isNew"
/>
<field-setup-display
v-if="currentTab[0] === 'display'"
v-model="field"
:local-type.sync="localType"
:is-new="isNew"
/>
<field-setup-schema
v-if="currentTab[0] === 'schema'"
v-model="field"
:local-type.sync="localType"
:is-new="isNew"
/>
</div>
<template #footer>
<setup-actions
:current-tab.sync="currentTab"
:tabs="tabs"
:field="field"
:local-type="localType"
:is-new="existingField === null"
:saving="saving"
@cancel="$emit('toggle', false)"
@save="save"
/>
</template>
</v-modal>
</template>
<script lang="ts">
import { defineComponent, PropType, watch, ref, computed, toRefs } from '@vue/composition-api';
import { Field } from '@/stores/fields/types';
import i18n from '@/lang';
import FieldSetupField from './field-setup-field.vue';
import FieldSetupRelationship from './field-setup-relationship.vue';
import FieldSetupInterface from './field-setup-interface.vue';
import FieldSetupDisplay from './field-setup-display.vue';
import FieldSetupSchema from './field-setup-schema.vue';
import SetupTabs from './setup-tabs.vue';
import SetupActions from './setup-actions.vue';
import useFieldsStore from '@/stores/fields/';
import { Relation } from '@/stores/relations/types';
import api from '@/api';
import { LocalType } from './types';
import { localTypeGroups } from './index';
import { Type } from '@/stores/fields/types';
import useCollection from '@/composables/use-collection';
export default defineComponent({
components: {
FieldSetupField,
FieldSetupRelationship,
FieldSetupInterface,
FieldSetupDisplay,
FieldSetupSchema,
SetupTabs,
SetupActions,
},
model: {
prop: 'active',
event: 'toggle',
},
props: {
existingField: {
type: Object as PropType<Field>,
default: null,
},
collection: {
type: String,
required: true,
},
active: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
const fieldsStore = useFieldsStore();
const { collection } = toRefs(props);
const { info: collectionInfo } = useCollection(collection);
const { field, localType } = useField();
const { tabs, currentTab } = useTabs();
const { save, saving } = useSave();
const newRelations = ref<Partial<Relation>[]>([]);
const isNew = computed(() => props.existingField === null);
const title = computed(() =>
isNew.value
? i18n.t('creating_new_field')
: i18n.t('updating_field_field', { field: props.existingField.name })
);
return { field, tabs, currentTab, localType, save, saving, newRelations, isNew, title, collectionInfo };
function useField() {
const defaults = {
id: null,
collection: props.collection,
field: null,
datatype: null,
unique: false,
primary_key: false,
auto_increment: false,
default_value: null,
note: null,
signed: false,
type: null,
sort: null,
interface: null,
options: null,
display: null,
display_options: null,
hidden_detail: false,
hidden_browse: false,
required: false,
locked: false,
translation: null,
readonly: false,
width: 'full',
validation: null,
group: null,
length: null,
};
const field = ref<any>({ ...defaults });
const localType = ref<LocalType | null>(null);
watch(
() => props.active,
() => {
if (!props.existingField) {
field.value = { ...defaults };
localType.value = null;
}
}
);
watch(
() => props.existingField,
(existingField: Field) => {
if (existingField) {
field.value = existingField;
const type: Type = existingField.type;
for (const [group, types] of Object.entries(localTypeGroups)) {
if (types.includes(type)) {
localType.value = group as LocalType;
break;
}
}
} else {
field.value = { ...defaults };
localType.value = null;
}
}
);
return { field, localType };
}
function useTabs() {
const currentTab = ref(['field']);
watch(
() => props.active,
() => {
currentTab.value = ['field'];
}
);
const tabs = computed(() => {
const tabs = [
{
text: i18n.t('field_type'),
value: 'field',
},
{
text: i18n.t('interface'),
value: 'interface',
},
{
text: i18n.t('display'),
value: 'display',
},
{
text: i18n.t('schema'),
value: 'schema',
},
];
if (localType.value === 'relational') {
tabs.splice(1, 0, {
text: i18n.t('relationship'),
value: 'relationship',
});
}
return tabs;
});
return { currentTab, tabs };
}
function useSave() {
const saving = ref(false);
const saveError = ref(null);
return { save, saving, saveError };
async function save() {
saving.value = true;
try {
if (field.value.id === null) {
await fieldsStore.createField(props.collection, field.value);
for (const relation of newRelations.value) {
await createRelation(relation);
}
} else {
if (field.value.hasOwnProperty('name')) {
delete field.value.name;
}
await fieldsStore.updateField(
props.existingField.collection,
props.existingField.field,
field.value
);
}
emit('toggle', false);
} catch (error) {
saveError.value = error;
} finally {
saving.value = false;
}
}
async function createRelation(relation: Partial<Relation>) {
await api.post(`/relations`, relation);
}
}
},
});
</script>
<style lang="scss" scoped>
.spacer {
flex-grow: 1;
}
.content {
::v-deep {
.type-title {
margin-bottom: 48px;
}
.type-label {
margin-bottom: 8px;
}
}
}
</style>

View File

@@ -1,24 +0,0 @@
import FieldSetup from './field-setup.vue';
import { types, Type } from '@/stores/fields/types';
import { LocalType } from './types';
/**
* @todo fix local type groups in settings
*/
const localTypeGroups: Record<LocalType, string[]> = {
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

@@ -1,130 +0,0 @@
<template>
<div class="setup-actions">
<template v-if="isNew">
<v-button secondary @click="$emit('cancel')">
{{ $t('cancel') }}
</v-button>
<div class="spacer" />
<v-button @click="previous" secondary :disabled="previousDisabled">
{{ $t('previous') }}
</v-button>
<v-button v-if="currentTabIndex < tabs.length - 1" @click="next" :disabled="nextDisabled">
{{ $t('next') }}
</v-button>
<v-button v-else :disabled="saveDisabled" @click="$emit('save')" :loading="saving">
{{ $t('save') }}
</v-button>
</template>
<template v-else>
<v-button secondary @click="$emit('cancel')">
{{ $t('cancel') }}
</v-button>
<div class="spacer" />
<v-button @click="$emit('save')" :loading="saving">
{{ $t('save') }}
</v-button>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed, toRefs } from '@vue/composition-api';
import { LocalType, Tab } from './types';
import useSync from '@/composables/use-sync';
import useValidation from './use-validation';
import { Field } from '@/stores/fields/types';
export default defineComponent({
props: {
tabs: {
type: Array as PropType<Tab[]>,
required: true,
},
currentTab: {
type: Array as PropType<string[]>,
required: true,
},
saving: {
type: Boolean,
default: false,
},
localType: {
type: String as PropType<LocalType>,
default: null,
},
field: {
type: Object as PropType<Field>,
required: true,
},
isNew: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
const _currentTab = useSync(props, 'currentTab', emit);
const { field, localType } = toRefs(props);
const { fieldComplete, relationComplete, interfaceComplete, displayComplete, schemaComplete } = useValidation(
field,
localType
);
const currentTabIndex = computed(() => props.tabs.findIndex((tab) => tab.value === props.currentTab[0]));
const previousDisabled = computed(() => {
return currentTabIndex.value === 0;
});
const nextDisabled = computed(() => {
if (props.isNew === false) return false;
switch (props.currentTab[0]) {
case 'field':
return fieldComplete.value === false;
case 'relationship':
return relationComplete.value === false;
case 'interface':
return interfaceComplete.value === false;
case 'display':
return displayComplete.value === false;
default:
return false;
}
});
const saveDisabled = computed(() => {
return schemaComplete.value === false;
});
return { previous, next, currentTabIndex, previousDisabled, nextDisabled, saveDisabled };
function previous() {
const previousTabValue = props.tabs[currentTabIndex.value - 1].value;
_currentTab.value = [previousTabValue];
}
function next() {
const nextTabValue = props.tabs[currentTabIndex.value + 1].value;
_currentTab.value = [nextTabValue];
}
},
});
</script>
<style lang="scss" scoped>
.setup-actions {
display: flex;
align-items: center;
width: 100%;
.v-button:not(:last-child) {
margin-right: 8px;
}
}
.spacer {
flex-grow: 1;
}
</style>

View File

@@ -1,73 +0,0 @@
<template>
<v-tabs vertical v-model="_currentTab">
<v-tab v-for="tab in tabs" :key="tab.value" :value="tab.value" :disabled="tabEnabled(tab) === false">
{{ tab.text }}
</v-tab>
</v-tabs>
</template>
<script lang="ts">
import { defineComponent, PropType, toRefs, computed } from '@vue/composition-api';
import useSync from '@/composables/use-sync';
import { LocalType, Tab } from './types';
import useValidation from './use-validation';
import { Field } from '@/stores/fields/types';
export default defineComponent({
props: {
tabs: {
type: Array as PropType<Tab[]>,
required: true,
},
currentTab: {
type: Array as PropType<string[]>,
default: null,
},
field: {
type: Object as PropType<Field>,
required: true,
},
localType: {
type: String as PropType<LocalType>,
default: null,
},
isNew: {
type: Boolean,
required: true,
},
},
setup(props, { emit }) {
const _currentTab = useSync(props, 'currentTab', emit);
const { field, localType } = toRefs(props);
const { fieldComplete, relationComplete, interfaceComplete, displayComplete } = useValidation(field, localType);
const hasRelationshipTab = computed(() => {
const relationshipTab = props.tabs.find((tab) => tab.value === 'relationship');
return relationshipTab !== undefined;
});
return { _currentTab, fieldComplete, tabEnabled };
function tabEnabled(tab: Tab) {
if (props.isNew === false) return true;
switch (tab.value) {
case 'field':
return true;
case 'relationship':
return fieldComplete.value === true;
case 'interface':
return hasRelationshipTab.value ? relationComplete.value === true : fieldComplete.value === true;
case 'display':
return interfaceComplete.value === true;
case 'schema':
return displayComplete.value === true;
default:
return true;
}
}
},
});
</script>

View File

@@ -1,8 +0,0 @@
import { TranslateResult } from 'vue-i18n';
export type LocalType = 'standard' | 'relational' | 'file' | 'files';
export type Tab = {
text: string | TranslateResult;
value: string;
};

View File

@@ -1,34 +0,0 @@
import { computed, Ref } from '@vue/composition-api';
import { notEmpty } from '@/utils/is-empty';
import { LocalType } from './types';
import { Field } from '@/stores/fields/types';
export default function useValidation(field: Ref<Field>, localType: Ref<LocalType>) {
const fieldComplete = computed<boolean>(() => {
return notEmpty(field.value.field) && notEmpty(localType.value);
});
const relationComplete = computed<boolean>(() => {
return true;
});
const interfaceComplete = computed<boolean>(() => {
return notEmpty(field.value.system.interface);
});
const displayComplete = computed<boolean>(() => {
return notEmpty(field.value.system.display);
});
const schemaComplete = computed<boolean>(() => {
return true;
});
return {
fieldComplete,
relationComplete,
interfaceComplete,
displayComplete,
schemaComplete,
};
}

View File

@@ -8,39 +8,55 @@
@change="($event) => handleChange($event, 'visible')"
:set-data="hideDragImage"
>
<template #header>
<div class="group-name">Visible Fields</div>
</template>
<field-select
v-for="field in sortedVisibleFields"
:key="field.field"
:field="field"
@toggle-visibility="toggleVisibility($event, 'visible')"
@edit="openFieldSetup(field)"
/>
</draggable>
<template #footer>
<v-button class="add-field" align="left" dashed outlined large @click="openFieldSetup()">
<v-menu attached>
<template #activator="{ toggle, active }">
<v-button
@click="toggle"
class="add-field"
align="left"
:dashed="!active"
:class="{ active }"
outlined
large
full-width
>
<v-icon name="add" />
{{ $t('add_field') }}
</v-button>
</template>
</draggable>
<v-list dense>
<v-list-item
v-for="option in addOptions"
:key="option.type"
:to="`/settings/data-model/${collection}/+?type=${option.type}`"
>
<v-list-item-icon>
<v-icon :name="option.icon" />
</v-list-item-icon>
<v-list-item-content>
{{ option.text }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
<draggable
class="field-grid hidden"
class="hidden"
:value="sortedHiddenFields"
handle=".drag-handle"
group="fields"
:set-data="hideDragImage"
@change="($event) => handleChange($event, 'hidden')"
>
<template #header>
<div class="group-name">Hidden Fields</div>
</template>
<field-select
v-for="field in sortedHiddenFields"
:key="field.field"
@@ -48,35 +64,20 @@
@toggle-visibility="toggleVisibility($event, 'hidden')"
@edit="openFieldSetup(field)"
/>
<template #footer>
<v-button class="add-field" align="left" dashed outlined large @click="openFieldSetup()">
<v-icon name="add" />
{{ $t('add_field') }}
</v-button>
</template>
</draggable>
<field-setup
:collection="collection"
:active="fieldSetupActive"
:existing-field="editingField"
@toggle="closeFieldSetup"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref, toRefs } from '@vue/composition-api';
import { defineComponent, computed, toRefs } from '@vue/composition-api';
import useCollection from '@/composables/use-collection/';
import Draggable from 'vuedraggable';
import { Field } from '@/stores/fields/types';
import useFieldsStore from '@/stores/fields/';
import FieldSelect from '../field-select/';
import FieldSetup from '../field-setup/';
import { sortBy } from 'lodash';
import hideDragImage from '@/utils/hide-drag-image';
import { i18n } from '@/lang';
type DraggableEvent = {
moved?: {
@@ -91,7 +92,7 @@ type DraggableEvent = {
};
export default defineComponent({
components: { Draggable, FieldSelect, FieldSetup },
components: { Draggable, FieldSelect },
props: {
collection: {
type: String,
@@ -105,30 +106,58 @@ export default defineComponent({
const sortedVisibleFields = computed(() =>
sortBy(
[...fields.value].filter((field) => field.system.hidden_detail === false),
[...fields.value].filter((field) => field.system.hidden === false),
(field) => field.system.sort || Infinity
)
);
const sortedHiddenFields = computed(() =>
sortBy(
[...fields.value].filter((field) => field.system.hidden_detail === true),
[...fields.value].filter((field) => field.system.hidden === true),
(field) => field.system.sort || Infinity
)
);
const { fieldSetupActive, editingField, openFieldSetup, closeFieldSetup } = useFieldSetup();
const addOptions = computed(() => [
{
type: 'standard',
icon: 'create',
text: i18n.t('standard_field'),
},
{
type: 'file',
icon: 'photo',
text: i18n.t('single_file'),
},
{
type: 'files',
icon: 'collections',
text: i18n.t('multiple_files'),
},
{
type: 'm2o',
icon: 'call_merge',
text: i18n.t('m2o_relationship'),
},
{
type: 'o2m',
icon: 'call_split',
text: i18n.t('o2m_relationship'),
},
{
type: 'm2m',
icon: 'import_export',
text: i18n.t('m2m_relationship'),
},
]);
return {
sortedVisibleFields,
sortedHiddenFields,
handleChange,
toggleVisibility,
fieldSetupActive,
editingField,
openFieldSetup,
closeFieldSetup,
hideDragImage,
addOptions,
};
function handleChange(event: DraggableEvent, location: 'visible' | 'hidden') {
@@ -184,7 +213,7 @@ export default defineComponent({
updates.push({
field: element.field,
system: {
hidden_detail: location === 'hidden',
hidden: location === 'hidden',
sort: newSortValue,
},
});
@@ -208,7 +237,9 @@ export default defineComponent({
return {
field: field.field,
sort: move === 'down' ? sortValue - 1 : sortValue + 1,
system: {
sort: move === 'down' ? sortValue - 1 : sortValue + 1,
},
};
});
@@ -222,26 +253,6 @@ export default defineComponent({
fieldsStore.updateFields(element.collection, updates);
}
function useFieldSetup() {
const fieldSetupActive = ref(false);
const editingField = ref<Field | null>(null);
return { fieldSetupActive, editingField, openFieldSetup, closeFieldSetup };
function openFieldSetup(field: Field | null) {
if (field) {
editingField.value = field;
}
fieldSetupActive.value = true;
}
function closeFieldSetup() {
editingField.value = null;
fieldSetupActive.value = false;
}
}
},
});
</script>
@@ -257,32 +268,24 @@ export default defineComponent({
grid-gap: 12px;
grid-template-columns: 1fr 1fr;
margin-bottom: 24px;
padding: 32px 12px 76px 12px;
padding: 12px;
background-color: var(--background-subdued);
border-radius: var(--border-radius);
}
.group-name {
position: absolute;
top: 6px;
left: 12px;
margin-bottom: 8px;
color: var(--foreground-subdued);
.add-field {
--v-button-font-size: 14px;
--v-button-background-color: var(--foreground-subdued);
--v-button-background-color-hover: var(--primary);
max-width: 50%;
.v-icon {
margin-right: 8px;
}
.add-field {
--v-button-width: 100%;
--v-button-font-size: 14px;
--v-button-background-color: var(--foreground-subdued);
--v-button-background-color-hover: var(--primary);
position: absolute;
bottom: 12px;
left: 12px;
width: calc(100% - 24px);
.v-icon {
margin-right: 8px;
}
&.active {
--v-button-background-color: var(--primary);
}
}

View File

@@ -47,6 +47,8 @@
<fields-management :collection="collection" />
</div>
<router-view name="field" :collection="collection" :field="field" :type="type" />
<v-form
collection="directus_collections"
:loading="loading"
@@ -86,6 +88,16 @@ export default defineComponent({
type: String,
required: true,
},
// Field detail modal only
field: {
type: String,
default: null,
},
type: {
type: String,
default: null,
},
},
setup(props) {
const { collection } = toRefs(props);

View File

@@ -1,4 +1,3 @@
import { SettingsCollections } from './collections';
import { SettingsFields } from './fields';
export { SettingsCollections, SettingsFields };
export * from './collections';
export * from './fields';
export * from './field-detail';

View File

@@ -26,8 +26,7 @@ const fakeFilesField: Field = {
options: null,
display: 'file',
display_options: null,
hidden_detail: true,
hidden_browse: false,
hidden: true,
locked: true,
required: false,
translation: null,
@@ -43,8 +42,7 @@ function getSystemDefault(collection: string, field: string): Field['system'] {
collection,
field,
group: null,
hidden_browse: false,
hidden_detail: false,
hidden: false,
interface: null,
display: null,
display_options: null,

View File

@@ -9,25 +9,26 @@ export type Width = 'half' | 'half-left' | 'half-right' | 'full' | 'fill';
export type Type =
| 'alias'
| 'integer'
| 'bigInteger'
| 'binary'
| 'text'
| 'string'
| 'float'
| 'decimal'
| 'boolean'
| 'date'
| 'datetime'
| 'decimal'
| 'float'
| 'integer'
| 'json'
| 'string'
| 'text'
| 'time'
| 'timestamp'
| 'enum'
| 'json'
| 'uuid'
| 'binary'
| 'unknown';
export const types: Type[] = [
'alias',
'bigInteger',
'binary',
'boolean',
'date',
'datetime',
@@ -39,6 +40,7 @@ export const types: Type[] = [
'text',
'time',
'timestamp',
'binary',
'unknown',
];
@@ -66,8 +68,7 @@ export type SystemField = {
collection: string;
field: string;
group: number | null;
hidden_browse: boolean;
hidden_detail: boolean;
hidden: boolean;
locked: boolean;
interface: string | null;
display: string | null;

View File

@@ -1,6 +1,6 @@
import { Type } from '@/stores/fields/types';
const defaultInterfaceMap = {
const defaultInterfaceMap: Record<Type, string> = {
alias: 'text-input',
bigInteger: 'numeric',
binary: 'text-input',
@@ -15,9 +15,15 @@ const defaultInterfaceMap = {
text: 'textarea',
time: 'datetime',
timestamp: 'datetime',
enum: 'text-input',
uuid: 'text-input',
unknown: 'text-input',
};
/**
* @todo default to correct interfaces for uuid / enum
*/
export default function getDefaultInterfaceForType(type: Type) {
return defaultInterfaceMap[type] || 'text-input';
}

View File

@@ -74,7 +74,7 @@ export default defineComponent({
.getFieldsForCollection(props.collection)
.filter(
(field: Field) =>
field.system?.hidden_browse !== true && field.system?.special?.toLowerCase() !== 'alias'
field.system?.hidden !== true && field.system?.special?.toLowerCase() !== 'alias'
)
.map((field: Field) => parseField(field, []));
@@ -104,7 +104,7 @@ export default defineComponent({
.getFieldsForCollection(relatedCollection)
.filter(
(field: Field) =>
field.system?.hidden_browse !== true &&
field.system?.hidden !== true &&
field.system?.special?.toLowerCase() !== 'alias'
);
})