mirror of
https://github.com/directus/directus.git
synced 2026-01-29 16:28:02 -05:00
Setup field setup modal as nested route
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
import SettingsFieldDetail from './field-detail.vue';
|
||||
|
||||
export { SettingsFieldDetail };
|
||||
export default SettingsFieldDetail;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,8 +0,0 @@
|
||||
import { TranslateResult } from 'vue-i18n';
|
||||
|
||||
export type LocalType = 'standard' | 'relational' | 'file' | 'files';
|
||||
|
||||
export type Tab = {
|
||||
text: string | TranslateResult;
|
||||
value: string;
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user