mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Field setup (#536)
* Use the correct table header widths for settings / colections * [WIP] Rework field setup flow * Finish setup actions logic * Remove old field-setup * Use fixed strategy in popper, render to portal * Stop relying on no-overflow mode * Stop propagation of outside click event * Finish field setup, start on relational setup * Finish up v-menu * Add relational m2o markup * Add relational m2o markup * Remove empty source, fix codesmell
This commit is contained in:
@@ -100,14 +100,16 @@ export default defineComponent({
|
||||
{
|
||||
text: '',
|
||||
value: 'icon',
|
||||
width: 42,
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: i18n.t('name'),
|
||||
value: 'collection',
|
||||
value: 'name',
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
text: i18n.t('description'),
|
||||
text: i18n.t('note'),
|
||||
value: 'note',
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,105 +1,108 @@
|
||||
<template>
|
||||
<v-menu attached :class="hidden ? 'half' : field.width" close-on-content-click>
|
||||
<template #activator="{ toggle, active }">
|
||||
<v-input
|
||||
class="field"
|
||||
:class="{ hidden, active }"
|
||||
readonly
|
||||
@click="toggle"
|
||||
:value="field.name"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon class="drag-handle" name="drag_indicator" @click.stop />
|
||||
</template>
|
||||
<div :class="hidden ? 'half' : field.width">
|
||||
<v-menu attached close-on-content-click>
|
||||
<template #activator="{ toggle, active }">
|
||||
<v-input
|
||||
class="field"
|
||||
:class="{ hidden, active }"
|
||||
readonly
|
||||
@click="toggle"
|
||||
:value="field.name"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon class="drag-handle" name="drag_indicator" @click.stop />
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<v-icon name="expand_more" />
|
||||
</template>
|
||||
</v-input>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-icon name="expand_more" />
|
||||
</template>
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
<v-list dense>
|
||||
<v-list-item @click="$emit('edit')">
|
||||
<v-list-item-icon><v-icon name="edit" /></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
{{ $t('edit_field') }}
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list dense>
|
||||
<v-list-item @click="$emit('edit')">
|
||||
<v-list-item-icon><v-icon name="edit" /></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
{{ $t('edit_field') }}
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-dialog v-model="duplicateActive">
|
||||
<template #activator="{ on }">
|
||||
<v-list-item @click="on">
|
||||
<v-list-item-icon>
|
||||
<v-icon name="control_point_duplicate" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('duplicate_field') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<v-dialog v-model="duplicateActive">
|
||||
<template #activator="{ on }">
|
||||
<v-list-item @click="on">
|
||||
<v-list-item-icon>
|
||||
<v-icon name="control_point_duplicate" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('duplicate_field') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('duplicate_where_to') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<span class="label">{{ $tc('collection', 0) }}</span>
|
||||
<v-select class="monospace" :items="collections" v-model="duplicateTo" />
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('duplicate_where_to') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<span class="label">{{ $tc('collection', 0) }}</span>
|
||||
<v-select
|
||||
class="monospace"
|
||||
:items="collections"
|
||||
v-model="duplicateTo"
|
||||
/>
|
||||
|
||||
<span class="label">{{ $tc('field', 0) }}</span>
|
||||
<v-input class="monospace" v-model="duplicateName" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-button secondary @click="duplicateActive = false">
|
||||
{{ $t('cancel') }}
|
||||
</v-button>
|
||||
<v-button @click="saveDuplicate" :loading="duplicating">
|
||||
{{ $t('duplicate') }}
|
||||
</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-divider />
|
||||
<v-list-item @click="setWidth('half')" :disabled="hidden || field.width === 'half'">
|
||||
<v-list-item-icon><v-icon name="border_vertical" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('half_width') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item @click="setWidth('full')" :disabled="hidden || field.width === 'full'">
|
||||
<v-list-item-icon><v-icon name="border_right" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('full_width') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item @click="setWidth('fill')" :disabled="hidden || field.width === 'fill'">
|
||||
<v-list-item-icon><v-icon name="aspect_ratio" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('fill_width') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<v-list-item @click="$emit('toggle-visibility', field)">
|
||||
<template v-if="field.hidden_detail === 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>
|
||||
<template v-else>
|
||||
<v-list-item-icon><v-icon name="visibility" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('show_field_on_detail') }}</v-list-item-content>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<span class="label">{{ $tc('field', 0) }}</span>
|
||||
<v-input class="monospace" v-model="duplicateName" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-button secondary @click="duplicateActive = false">
|
||||
{{ $t('cancel') }}
|
||||
</v-button>
|
||||
<v-button @click="saveDuplicate" :loading="duplicating">
|
||||
{{ $t('duplicate') }}
|
||||
</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-divider />
|
||||
<v-list-item @click="setWidth('half')" :disabled="hidden || field.width === 'half'">
|
||||
<v-list-item-icon><v-icon name="border_vertical" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('half_width') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item @click="setWidth('full')" :disabled="hidden || field.width === 'full'">
|
||||
<v-list-item-icon><v-icon name="border_right" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('full_width') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item @click="setWidth('fill')" :disabled="hidden || field.width === 'fill'">
|
||||
<v-list-item-icon><v-icon name="aspect_ratio" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('fill_width') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<v-list-item @click="$emit('toggle-visibility', field)">
|
||||
<template v-if="field.hidden_detail === 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>
|
||||
<template v-else>
|
||||
<v-list-item-icon><v-icon name="visibility" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('show_field_on_detail') }}</v-list-item-content>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-dialog v-model="deleteActive">
|
||||
<template #activator="{ on }">
|
||||
<v-list-item @click="on">
|
||||
<v-list-item-icon><v-icon name="delete" /></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
{{ $t('delete_field') }}
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title>Are you sure you want to delete this field?</v-card-title>
|
||||
<v-card-actions>
|
||||
<v-button @click="deleteActive = false" secondary>Cancel</v-button>
|
||||
<v-button :loading="deleting" @click="deleteField">Delete</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-list-item @click="deleteActive = true">
|
||||
<v-list-item-icon><v-icon name="delete" /></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
{{ $t('delete_field') }}
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-dialog v-model="deleteActive">
|
||||
<v-card>
|
||||
<v-card-title>Are you sure you want to delete this field?</v-card-title>
|
||||
<v-card-actions>
|
||||
<v-button @click="deleteActive = false" secondary>Cancel</v-button>
|
||||
<v-button :loading="deleting" @click="deleteField">Delete</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
<template>
|
||||
<v-tab-item value="advanced">
|
||||
<h2 class="title" v-if="isNew">{{ $t('advanced_options_title') }}</h2>
|
||||
<div>
|
||||
<h2 class="type-title" v-if="isNew">{{ $t('advanced_options_title') }}</h2>
|
||||
|
||||
<v-form :initial-values="existingField" v-model="_edits" :fields="fields" primary-key="+" />
|
||||
</v-tab-item>
|
||||
<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 { Field } from '@/stores/fields/types';
|
||||
import i18n from '@/lang';
|
||||
import { FormField } from '@/components/v-form/types';
|
||||
import { useSync } from '@/composables/use-sync';
|
||||
import { i18n } from '@/lang';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
existingField: {
|
||||
type: Object as PropType<Field>,
|
||||
default: null,
|
||||
},
|
||||
edits: {
|
||||
type: Object as PropType<Partial<Field>>,
|
||||
default: null,
|
||||
},
|
||||
isNew: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: Object as PropType<Field>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
setup(props) {
|
||||
const fields = computed(() => {
|
||||
const fields: FormField[] = [
|
||||
{
|
||||
@@ -56,7 +51,7 @@ export default defineComponent({
|
||||
options: null,
|
||||
},
|
||||
{
|
||||
field: 'default',
|
||||
field: 'default_value',
|
||||
name: i18n.t('default_value'),
|
||||
interface: 'text-input' /** @TODO base on selected datatype */,
|
||||
width: 'half',
|
||||
@@ -74,42 +69,42 @@ export default defineComponent({
|
||||
{
|
||||
field: 'required',
|
||||
name: i18n.t('required'),
|
||||
interface: 'switch',
|
||||
interface: 'toggle',
|
||||
width: 'half',
|
||||
options: null,
|
||||
},
|
||||
{
|
||||
field: 'readonly',
|
||||
name: i18n.t('readonly'),
|
||||
interface: 'switch',
|
||||
interface: 'toggle',
|
||||
width: 'half',
|
||||
options: null,
|
||||
},
|
||||
{
|
||||
field: 'hidden_detail',
|
||||
name: i18n.t('hide_on_detail'),
|
||||
interface: 'switch',
|
||||
interface: 'toggle',
|
||||
width: 'half',
|
||||
options: null,
|
||||
},
|
||||
{
|
||||
field: 'hidden_browse',
|
||||
name: i18n.t('hide_on_browse'),
|
||||
interface: 'switch',
|
||||
interface: 'toggle',
|
||||
width: 'half',
|
||||
options: null,
|
||||
},
|
||||
{
|
||||
field: 'unique',
|
||||
name: i18n.t('unique'),
|
||||
interface: 'switch',
|
||||
interface: 'toggle',
|
||||
width: 'half',
|
||||
options: null,
|
||||
},
|
||||
{
|
||||
field: 'primary_key',
|
||||
name: i18n.t('primary_key'),
|
||||
interface: 'switch',
|
||||
interface: 'toggle',
|
||||
width: 'half',
|
||||
options: null,
|
||||
},
|
||||
@@ -146,9 +141,7 @@ export default defineComponent({
|
||||
return fields;
|
||||
});
|
||||
|
||||
const _edits = useSync(props, 'edits', emit);
|
||||
|
||||
return { fields, _edits };
|
||||
return { fields };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,60 +1,63 @@
|
||||
<template>
|
||||
<v-tab-item value="display">
|
||||
<h2 class="title" v-if="isNew">{{ $t('display_setup_title') }}</h2>
|
||||
<v-fancy-select :items="items" v-model="_display" />
|
||||
<transition-expand>
|
||||
<v-form
|
||||
v-if="
|
||||
selectedDisplay &&
|
||||
selectedDisplay.options &&
|
||||
Array.isArray(selectedDisplay.options)
|
||||
"
|
||||
:fields="selectedDisplay.options"
|
||||
primary-key="+"
|
||||
v-model="_options"
|
||||
/>
|
||||
</transition-expand>
|
||||
</v-tab-item>
|
||||
<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)"
|
||||
/>
|
||||
|
||||
<v-form
|
||||
v-if="
|
||||
selectedDisplay && selectedDisplay.options && Array.isArray(selectedDisplay.options)
|
||||
"
|
||||
:fields="selectedDisplay.options"
|
||||
primary-key="+"
|
||||
:edits="value.options"
|
||||
@input="emitValue('options', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import useSync from '@/composables/use-sync/';
|
||||
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';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
display: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
options: {
|
||||
type: Object as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
isNew: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: Object as PropType<Field>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const _display = useSync(props, 'display', emit);
|
||||
const _options = useSync(props, 'options', emit);
|
||||
|
||||
const items = computed<FancySelectItem[]>(() => {
|
||||
return displays.map((display) => ({
|
||||
text: display.name,
|
||||
value: display.id,
|
||||
icon: display.icon,
|
||||
return displays.map((inter) => ({
|
||||
text: inter.name,
|
||||
value: inter.id,
|
||||
icon: inter.icon,
|
||||
}));
|
||||
});
|
||||
|
||||
const selectedDisplay = computed(() => {
|
||||
return displays.find((display) => display.id === _display.value) || null;
|
||||
return displays.find((inter) => inter.id === props.value.display) || null;
|
||||
});
|
||||
|
||||
return { _display, _options, items, selectedDisplay };
|
||||
return { emitValue, items, selectedDisplay };
|
||||
|
||||
function emitValue(key: string, value: any) {
|
||||
emit('input', {
|
||||
...props.value,
|
||||
[key]: value,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,46 +1,48 @@
|
||||
<template>
|
||||
<v-tab-item value="field">
|
||||
<h2 class="title" v-if="isNew">{{ $t('field_setup_title') }}</h2>
|
||||
<div>
|
||||
<h2 class="type-title" v-if="isNew">{{ $t('field_setup_title') }}</h2>
|
||||
|
||||
<label for="name" class="label">{{ $t('name') }}</label>
|
||||
<div class="type-label">{{ $t('name') }}</div>
|
||||
<v-input
|
||||
class="field"
|
||||
:value="value.field"
|
||||
@input="emitValue('field', $event)"
|
||||
db-safe
|
||||
:disabled="isNew === false"
|
||||
id="name"
|
||||
v-model="_field"
|
||||
:placeholder="$t('enter_field_name')"
|
||||
/>
|
||||
|
||||
<v-fancy-select :disabled="isNew === false" :items="items" v-model="_type" />
|
||||
</v-tab-item>
|
||||
<v-fancy-select
|
||||
:disabled="isNew === false"
|
||||
:items="items"
|
||||
:value="localType"
|
||||
@input="$emit('update:localType', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import useSync from '@/composables/use-sync/';
|
||||
import { FieldType } from './types';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import { LocalType } from './types';
|
||||
import i18n from '@/lang';
|
||||
import { FancySelectItem } from '@/components/v-fancy-select/types';
|
||||
import { i18n } from '@/lang';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
field: {
|
||||
type: String,
|
||||
default: null,
|
||||
value: {
|
||||
type: Object as PropType<Field>,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<FieldType>,
|
||||
localType: {
|
||||
type: String as PropType<LocalType>,
|
||||
default: null,
|
||||
validator: (val: string) => ['standard', 'relational', 'file', 'files'].includes(val),
|
||||
},
|
||||
isNew: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const _field = useSync(props, 'field', emit);
|
||||
const _type = useSync(props, 'type', emit);
|
||||
|
||||
const items = computed<FancySelectItem[]>(() => [
|
||||
{
|
||||
text: i18n.t('standard_field'),
|
||||
@@ -64,13 +66,22 @@ export default defineComponent({
|
||||
},
|
||||
]);
|
||||
|
||||
return { _field, _type, items };
|
||||
return { emitValue, items };
|
||||
|
||||
function emitValue(key: string, value: any) {
|
||||
emit('input', {
|
||||
...props.value,
|
||||
[key]: value,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-input {
|
||||
.field {
|
||||
--v-input-font-family: var(--family-monospace);
|
||||
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,47 +1,45 @@
|
||||
<template>
|
||||
<v-tab-item value="interface">
|
||||
<h2 class="title" v-if="isNew">{{ $t('interface_setup_title') }}</h2>
|
||||
<v-fancy-select :items="items" v-model="_interface" />
|
||||
<transition-expand>
|
||||
<v-form
|
||||
v-if="
|
||||
selectedInterface &&
|
||||
selectedInterface.options &&
|
||||
Array.isArray(selectedInterface.options)
|
||||
"
|
||||
:fields="selectedInterface.options"
|
||||
primary-key="+"
|
||||
v-model="_options"
|
||||
/>
|
||||
</transition-expand>
|
||||
</v-tab-item>
|
||||
<div>
|
||||
<h2 class="type-title" v-if="isNew">{{ $t('relationship_setup_title') }}</h2>
|
||||
|
||||
<v-fancy-select
|
||||
:items="items"
|
||||
:value="value.interface"
|
||||
@input="emitValue('interface', $event)"
|
||||
/>
|
||||
|
||||
<v-form
|
||||
v-if="
|
||||
selectedInterface &&
|
||||
selectedInterface.options &&
|
||||
Array.isArray(selectedInterface.options)
|
||||
"
|
||||
:fields="selectedInterface.options"
|
||||
primary-key="+"
|
||||
:edits="value.options"
|
||||
@input="emitValue('options', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import useSync from '@/composables/use-sync/';
|
||||
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';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
interface: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
options: {
|
||||
type: Object as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
isNew: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: Object as PropType<Field>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const _interface = useSync(props, 'interface', emit);
|
||||
const _options = useSync(props, 'options', emit);
|
||||
|
||||
const items = computed<FancySelectItem[]>(() => {
|
||||
return interfaces.map((inter) => ({
|
||||
text: inter.name,
|
||||
@@ -51,10 +49,17 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
return interfaces.find((inter) => inter.id === _interface.value) || null;
|
||||
return interfaces.find((inter) => inter.id === props.value.interface) || null;
|
||||
});
|
||||
|
||||
return { _interface, _options, items, selectedInterface };
|
||||
return { emitValue, items, selectedInterface };
|
||||
|
||||
function emitValue(key: string, value: any) {
|
||||
emit('input', {
|
||||
...props.value,
|
||||
[key]: value,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<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" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('related_collection') }}</div>
|
||||
<v-select :items="collectionItems" />
|
||||
</div>
|
||||
<v-input disabled :value="field.field" />
|
||||
<v-select :items="collectionItems" allow-other />
|
||||
<div class="spacer" />
|
||||
<div class="spacer" />
|
||||
<v-select :items="collectionItems" allow-other />
|
||||
<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';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
field: {
|
||||
type: Object as PropType<Field>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
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 collectionItems = computed(() =>
|
||||
availableCollections.value.map((collection) => ({
|
||||
text: collection.name,
|
||||
value: collection.collection,
|
||||
}))
|
||||
);
|
||||
|
||||
return { availableCollections, collectionItems };
|
||||
},
|
||||
});
|
||||
</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>
|
||||
@@ -0,0 +1,99 @@
|
||||
<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 :items="items" />
|
||||
</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 :label="$t('add_field_related')" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('corresponding_field_name') }}</div>
|
||||
<v-input />
|
||||
</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';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
field: {
|
||||
type: Object as PropType<Field>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
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,
|
||||
}))
|
||||
);
|
||||
|
||||
return { availableCollections, items };
|
||||
},
|
||||
});
|
||||
</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>
|
||||
@@ -0,0 +1,79 @@
|
||||
<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 :items="collectionItems" />
|
||||
</div>
|
||||
<v-input disabled :value="field.field" />
|
||||
<v-select :items="collectionItems" allow-other />
|
||||
<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';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
field: {
|
||||
type: Object as PropType<Field>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
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 collectionItems = computed(() =>
|
||||
availableCollections.value.map((collection) => ({
|
||||
text: collection.name,
|
||||
value: collection.collection,
|
||||
}))
|
||||
);
|
||||
|
||||
return { availableCollections, collectionItems };
|
||||
},
|
||||
});
|
||||
</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,32 +1,83 @@
|
||||
<template>
|
||||
<v-tab-item value="relationship">
|
||||
<h2 class="title" v-if="isNew">{{ $t('relationship_setup_title') }}</h2>
|
||||
<!-- <v-fancy-select :items="items" v-model="_interface" /> -->
|
||||
<div>
|
||||
<h2 class="type-title" v-if="isNew">{{ $t('relationship_setup_title') }}</h2>
|
||||
<v-fancy-select :items="items" :value="value.type" @input="emitValue('type', $event)" />
|
||||
|
||||
<transition-expand>
|
||||
relationship
|
||||
</transition-expand>
|
||||
</v-tab-item>
|
||||
<many-to-one
|
||||
v-if="value.type === 'm2o'"
|
||||
:collection="value.collection"
|
||||
:field="value"
|
||||
@update:field="emit('input', $event)"
|
||||
/>
|
||||
<one-to-many
|
||||
v-else-if="value.type === 'o2m'"
|
||||
:collection="value.collection"
|
||||
:field="value"
|
||||
@update:field="emit('input', $event)"
|
||||
/>
|
||||
<many-to-many
|
||||
v-else-if="value.type === 'm2m'"
|
||||
:collection="value.collection"
|
||||
:field="value"
|
||||
@update:field="emit('input', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import { defineComponent, PropType, computed } 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';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
ManyToMany,
|
||||
},
|
||||
props: {
|
||||
isNew: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: Object as PropType<Field>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
setup(props, { emit }) {
|
||||
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',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
return { emitValue, items };
|
||||
|
||||
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,93 +1,77 @@
|
||||
<template>
|
||||
<v-modal :active="active" :title="modalTitle" persistent>
|
||||
<v-dialog :active="saveError !== null" @toggle="saveError = null">
|
||||
<v-card class="selectable">
|
||||
<v-card-title>
|
||||
{{ saveError && saveError.message }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
{{ saveError && saveError.response.data.error.message }}
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-button @click="saveError = null">{{ $t('dismiss') }}</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-modal :active="active" title="Field" persistent>
|
||||
<template #sidebar>
|
||||
<v-tabs vertical v-model="currentTab">
|
||||
<v-tab
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
:disabled="tab.disabled"
|
||||
:value="tab.value"
|
||||
>
|
||||
{{ tab.text }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<setup-tabs
|
||||
:current-tab.sync="currentTab"
|
||||
:tabs="tabs"
|
||||
:field="field"
|
||||
:local-type="localType"
|
||||
:is-new="existingField === null"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<v-tabs-items v-model="currentTab" class="content">
|
||||
<field-setup-field :is-new="isNew" :field.sync="field" :type.sync="localType" />
|
||||
<field-setup-relationship v-if="needsRelationalSetup" :is-new="isNew" />
|
||||
<div class="content">
|
||||
<field-setup-field
|
||||
v-if="currentTab[0] === 'field'"
|
||||
v-model="field"
|
||||
:local-type.sync="localType"
|
||||
:is-new="existingField === null"
|
||||
/>
|
||||
<field-setup-relationship
|
||||
v-if="currentTab[0] === 'relationship'"
|
||||
v-model="field"
|
||||
:local-type.sync="localType"
|
||||
:is-new="existingField === null"
|
||||
/>
|
||||
<field-setup-interface
|
||||
:is-new="isNew"
|
||||
:interface.sync="interfaceKey"
|
||||
:options.sync="interfaceOptions"
|
||||
v-if="currentTab[0] === 'interface'"
|
||||
v-model="field"
|
||||
:local-type.sync="localType"
|
||||
:is-new="existingField === null"
|
||||
/>
|
||||
<field-setup-display
|
||||
:is-new="isNew"
|
||||
:display.sync="displayKey"
|
||||
:options.sync="displayOptions"
|
||||
v-if="currentTab[0] === 'display'"
|
||||
v-model="field"
|
||||
:local-type.sync="localType"
|
||||
:is-new="existingField === null"
|
||||
/>
|
||||
<field-setup-advanced
|
||||
:is-new="isNew"
|
||||
:existing-field="existingField"
|
||||
:edits.sync="edits"
|
||||
v-if="currentTab[0] === 'advanced'"
|
||||
v-model="field"
|
||||
:local-type.sync="localType"
|
||||
:is-new="existingField === null"
|
||||
/>
|
||||
</v-tabs-items>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<v-button secondary outlined @click="$emit('toggle', false)">
|
||||
{{ $t('cancel') }}
|
||||
</v-button>
|
||||
<div class="spacer" />
|
||||
<v-button secondary @click="previous" :disabled="previousDisabled">
|
||||
{{ $t('previous') }}
|
||||
</v-button>
|
||||
<v-button
|
||||
@click="next"
|
||||
:disabled="nextDisabled"
|
||||
v-if="currentTab[0] !== tabs.length - 1"
|
||||
>
|
||||
{{ $t('next') }}
|
||||
</v-button>
|
||||
<v-button
|
||||
@click="save"
|
||||
:disabled="saveDisabled"
|
||||
v-if="currentTab[0] === tabs.length - 1"
|
||||
:loading="saving"
|
||||
>
|
||||
{{ $t('finish_setup') }}
|
||||
</v-button>
|
||||
<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, ref, computed, watch } from '@vue/composition-api';
|
||||
import { defineComponent, PropType, watch, ref, computed } 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 FieldSetupAdvanced from './field-setup-advanced.vue';
|
||||
import { i18n } from '@/lang';
|
||||
import { TranslateResult } from 'vue-i18n';
|
||||
import { isEmpty } from '@/utils/is-empty';
|
||||
import { useFieldsStore } from '@/stores/fields/';
|
||||
import SetupTabs from './setup-tabs.vue';
|
||||
import SetupActions from './setup-actions.vue';
|
||||
import useFieldsStore from '@/stores/fields/';
|
||||
|
||||
import { LocalType } from './types';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -96,6 +80,8 @@ export default defineComponent({
|
||||
FieldSetupInterface,
|
||||
FieldSetupDisplay,
|
||||
FieldSetupAdvanced,
|
||||
SetupTabs,
|
||||
SetupActions,
|
||||
},
|
||||
model: {
|
||||
prop: 'active',
|
||||
@@ -118,166 +104,51 @@ export default defineComponent({
|
||||
setup(props, { emit }) {
|
||||
const fieldsStore = useFieldsStore();
|
||||
|
||||
const isNew = computed(() => props.existingField === null);
|
||||
const { field, localType } = usefield();
|
||||
const { tabs, currentTab } = useTabs();
|
||||
const { save, saving } = useSave();
|
||||
|
||||
const edits = ref<Partial<Field>>({});
|
||||
return { field, tabs, currentTab, localType, save, saving };
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
() => (edits.value = {})
|
||||
);
|
||||
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 modalTitle = computed<TranslateResult>(() => {
|
||||
if (isNew.value === true) {
|
||||
return i18n.t('creating_field');
|
||||
}
|
||||
|
||||
return i18n.t('editing_field', { field: props.existingField.name });
|
||||
});
|
||||
|
||||
const { field, localType } = useFieldSetup();
|
||||
const { needsRelationalSetup, relationships } = useRelationshipSetup();
|
||||
const { interfaceKey, interfaceOptions } = useInterfaceSetup();
|
||||
const { displayKey, displayOptions } = useDisplaySetup();
|
||||
|
||||
const { tabs, currentTab, previousDisabled, nextDisabled, previous, next } = useTabs();
|
||||
const { save, saveDisabled, saveError, saving } = useSave();
|
||||
|
||||
return {
|
||||
currentTab,
|
||||
displayKey,
|
||||
displayOptions,
|
||||
edits,
|
||||
field,
|
||||
interfaceKey,
|
||||
interfaceOptions,
|
||||
isNew,
|
||||
localType,
|
||||
modalTitle,
|
||||
needsRelationalSetup,
|
||||
next,
|
||||
nextDisabled,
|
||||
previous,
|
||||
previousDisabled,
|
||||
relationships,
|
||||
save,
|
||||
saveDisabled,
|
||||
saveError,
|
||||
saving,
|
||||
tabs,
|
||||
};
|
||||
|
||||
function useTabs() {
|
||||
const fieldIncomplete = computed<boolean>(() => {
|
||||
return isEmpty(field.value) || isEmpty(localType.value);
|
||||
});
|
||||
|
||||
const relationshipIncomplete = computed<boolean>(() => {
|
||||
return false;
|
||||
});
|
||||
|
||||
const interfaceIncomplete = computed<boolean>(() => {
|
||||
/** @NOTE this is where we can check for required fields */
|
||||
return isEmpty(interfaceKey.value);
|
||||
});
|
||||
|
||||
const displayIncomplete = computed<boolean>(() => {
|
||||
/** @NOTE this is where we can check for required fields */
|
||||
return isEmpty(displayKey.value);
|
||||
});
|
||||
|
||||
const tabs = computed(() => {
|
||||
const tabs = [
|
||||
{
|
||||
text: i18n.t('field_setup'),
|
||||
value: 'field',
|
||||
},
|
||||
{
|
||||
text: i18n.t('interface_setup'),
|
||||
disabled: isNew.value && fieldIncomplete.value,
|
||||
value: 'interface',
|
||||
},
|
||||
{
|
||||
text: i18n.t('display_setup'),
|
||||
disabled: isNew.value && interfaceIncomplete.value,
|
||||
value: 'display',
|
||||
},
|
||||
{
|
||||
text: i18n.t('advanced_options'),
|
||||
disabled: isNew.value && displayIncomplete.value,
|
||||
value: 'advanced',
|
||||
},
|
||||
];
|
||||
|
||||
if (needsRelationalSetup.value === true) {
|
||||
tabs.splice(1, 0, {
|
||||
text: i18n.t('relationship_setup'),
|
||||
disabled: isNew.value && fieldIncomplete.value,
|
||||
value: 'relationship',
|
||||
});
|
||||
|
||||
tabs[2].disabled = isNew.value && relationshipIncomplete.value;
|
||||
tabs[3].disabled = isNew.value && interfaceIncomplete.value;
|
||||
tabs[4].disabled = isNew.value && displayIncomplete.value;
|
||||
}
|
||||
|
||||
return tabs;
|
||||
});
|
||||
|
||||
const currentTab = ref(['field']);
|
||||
|
||||
const currentTabIndex = computed(() =>
|
||||
tabs.value.findIndex((tab) => tab.value === currentTab.value[0])
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
() => (currentTab.value = ['field'])
|
||||
);
|
||||
|
||||
const previousDisabled = computed(() => {
|
||||
return currentTabIndex.value === 0;
|
||||
});
|
||||
|
||||
const nextDisabled = computed(() => {
|
||||
const nextTabIndex = currentTabIndex.value + 1;
|
||||
const nextTabIsDisabled = tabs.value[nextTabIndex]?.disabled === true;
|
||||
|
||||
return nextTabIsDisabled;
|
||||
});
|
||||
|
||||
return { tabs, currentTab, previous, next, nextDisabled, previousDisabled };
|
||||
|
||||
function previous() {
|
||||
const previousTabValue = tabs.value[currentTabIndex.value - 1].value;
|
||||
currentTab.value = [previousTabValue];
|
||||
}
|
||||
|
||||
function next() {
|
||||
const nextTabValue = tabs.value[currentTabIndex.value + 1].value;
|
||||
currentTab.value = [nextTabValue];
|
||||
}
|
||||
}
|
||||
|
||||
function useFieldSetup() {
|
||||
const field = computed({
|
||||
get() {
|
||||
return edits.value.field || props.existingField?.field;
|
||||
},
|
||||
set(newField: string) {
|
||||
edits.value = {
|
||||
...edits.value,
|
||||
field: newField,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const localType = ref<string>(null);
|
||||
const field = ref<any>({ ...defaults });
|
||||
const localType = ref<LocalType>(null);
|
||||
|
||||
watch(
|
||||
() => props.existingField,
|
||||
(existingField: Field) => {
|
||||
if (existingField) {
|
||||
field.value = existingField;
|
||||
|
||||
if (existingField.type === 'file') {
|
||||
localType.value = 'file';
|
||||
} else if (existingField.type === 'files') {
|
||||
@@ -288,6 +159,7 @@ export default defineComponent({
|
||||
localType.value = 'standard';
|
||||
}
|
||||
} else {
|
||||
field.value = { ...defaults };
|
||||
localType.value = null;
|
||||
}
|
||||
}
|
||||
@@ -296,105 +168,66 @@ export default defineComponent({
|
||||
return { field, localType };
|
||||
}
|
||||
|
||||
function useRelationshipSetup() {
|
||||
const needsRelationalSetup = computed(
|
||||
() => localType.value && ['relational', 'file', 'files'].includes(localType.value)
|
||||
);
|
||||
function useTabs() {
|
||||
const currentTab = ref(['field']);
|
||||
const tabs = computed(() => {
|
||||
const tabs = [
|
||||
{
|
||||
text: i18n.t('field_setup'),
|
||||
value: 'field',
|
||||
},
|
||||
{
|
||||
text: i18n.t('interface_setup'),
|
||||
value: 'interface',
|
||||
},
|
||||
{
|
||||
text: i18n.t('display_setup'),
|
||||
value: 'display',
|
||||
},
|
||||
{
|
||||
text: i18n.t('advanced_options'),
|
||||
value: 'advanced',
|
||||
},
|
||||
];
|
||||
|
||||
const relationships: any[] = [];
|
||||
if (localType.value === 'relational') {
|
||||
tabs.splice(1, 0, {
|
||||
text: i18n.t('relationship_setup'),
|
||||
value: 'relationship',
|
||||
});
|
||||
}
|
||||
|
||||
return { needsRelationalSetup, relationships };
|
||||
}
|
||||
|
||||
function useInterfaceSetup() {
|
||||
const interfaceKey = computed({
|
||||
get() {
|
||||
return edits.value.interface || props.existingField?.interface;
|
||||
},
|
||||
set(newInterface: string | null) {
|
||||
edits.value = {
|
||||
...edits.value,
|
||||
interface: newInterface,
|
||||
};
|
||||
},
|
||||
return tabs;
|
||||
});
|
||||
|
||||
const interfaceOptions = computed({
|
||||
get() {
|
||||
return edits.value.options || props.existingField?.options;
|
||||
},
|
||||
|
||||
set(newOptions: { [key: string]: any } | null) {
|
||||
edits.value = {
|
||||
...edits.value,
|
||||
options: newOptions,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return { interfaceKey, interfaceOptions };
|
||||
}
|
||||
|
||||
function useDisplaySetup() {
|
||||
const displayKey = computed({
|
||||
get() {
|
||||
return edits.value.display || props.existingField?.display;
|
||||
},
|
||||
set(newDisplay: string | null) {
|
||||
edits.value = {
|
||||
...edits.value,
|
||||
display: newDisplay,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const displayOptions = computed({
|
||||
get() {
|
||||
return edits.value.display_options || props.existingField?.display_options;
|
||||
},
|
||||
|
||||
set(newOptions: { [key: string]: any } | null) {
|
||||
edits.value = {
|
||||
...edits.value,
|
||||
display_options: newOptions,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return { displayKey, displayOptions };
|
||||
return { currentTab, tabs };
|
||||
}
|
||||
|
||||
function useSave() {
|
||||
const saveDisabled = computed(() => {
|
||||
return edits.value === null || Object.keys(edits.value).length === 0;
|
||||
});
|
||||
|
||||
const saving = ref(false);
|
||||
const saveError = ref(null);
|
||||
|
||||
return { save, saving, saveDisabled, saveError };
|
||||
return { save, saving, saveError };
|
||||
|
||||
async function save() {
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
if (isNew.value === true) {
|
||||
await fieldsStore.createField(props.collection, edits.value);
|
||||
if (field.value.id === null) {
|
||||
await fieldsStore.createField(props.collection, field.value);
|
||||
} else {
|
||||
await fieldsStore.updateField(
|
||||
props.existingField.collection,
|
||||
props.existingField.field,
|
||||
edits.value
|
||||
field.value
|
||||
);
|
||||
}
|
||||
|
||||
emit('toggle', false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
saveError.value = error;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -408,22 +241,12 @@ export default defineComponent({
|
||||
|
||||
.content {
|
||||
::v-deep {
|
||||
.title {
|
||||
.type-title {
|
||||
margin-bottom: 48px;
|
||||
font-weight: normal;
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
line-height: 29px;
|
||||
letter-spacing: -0.8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
.type-label {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
line-height: 19px;
|
||||
letter-spacing: -0.32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="setup-actions">
|
||||
<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>
|
||||
</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,
|
||||
advancedComplete,
|
||||
} = 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 advancedComplete.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>
|
||||
@@ -0,0 +1,85 @@
|
||||
<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 'advanced':
|
||||
return displayComplete.value === true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1 +1,8 @@
|
||||
export type FieldType = 'field' | 'relationship' | 'file' | 'files';
|
||||
import { TranslateResult } from 'vue-i18n';
|
||||
|
||||
export type LocalType = 'standard' | 'relational' | 'file' | 'files';
|
||||
|
||||
export type Tab = {
|
||||
text: string | TranslateResult;
|
||||
value: string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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.interface);
|
||||
});
|
||||
|
||||
const displayComplete = computed<boolean>(() => {
|
||||
return notEmpty(field.value.display);
|
||||
});
|
||||
|
||||
const advancedComplete = computed<boolean>(() => {
|
||||
return true;
|
||||
});
|
||||
|
||||
return {
|
||||
fieldComplete,
|
||||
relationComplete,
|
||||
interfaceComplete,
|
||||
displayComplete,
|
||||
advancedComplete,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user