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:
@@ -7,6 +7,7 @@
|
||||
</transition>
|
||||
<router-view v-if="!hydrating" />
|
||||
<portal-target name="dialog-outlet" multiple />
|
||||
<portal-target name="popper-outlet" multiple />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -111,6 +111,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
dbSafe: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit, listeners }) {
|
||||
const input = ref<HTMLInputElement>(null);
|
||||
@@ -133,6 +137,13 @@ export default defineComponent({
|
||||
value = slugify(value, { separator: props.slugSeparator });
|
||||
}
|
||||
|
||||
if (props.dbSafe === true) {
|
||||
value = value.toLowerCase();
|
||||
value = value.replace(/\s/g, '_');
|
||||
// Replace é -> e etc
|
||||
value = value.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
}
|
||||
|
||||
emit('input', value);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,9 @@ export function usePopper(
|
||||
popperInstance.value = createPopper(reference.value!, popper.value!, {
|
||||
placement: options.value.attached ? 'bottom-start' : options.value.placement,
|
||||
modifiers: getModifiers(resolve),
|
||||
strategy: 'fixed',
|
||||
});
|
||||
popperInstance.value.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,7 +60,12 @@ export function usePopper(
|
||||
offset: options.value.attached ? [0, 0] : [0, 8],
|
||||
},
|
||||
},
|
||||
preventOverflow,
|
||||
{
|
||||
...preventOverflow,
|
||||
options: {
|
||||
padding: 8,
|
||||
},
|
||||
},
|
||||
computeStyles,
|
||||
flip,
|
||||
eventListeners,
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
class="v-menu"
|
||||
v-click-outside="{
|
||||
handler: deactivate,
|
||||
disabled: isActive === false || closeOnClick === false,
|
||||
}"
|
||||
>
|
||||
<div class="v-menu">
|
||||
<div ref="activator" class="v-menu-activator" :class="{ attached }">
|
||||
<slot
|
||||
name="activator"
|
||||
@@ -18,30 +12,33 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="popper"
|
||||
class="v-menu-popper"
|
||||
:class="{ active: isActive, attached }"
|
||||
:data-placement="popperPlacement"
|
||||
:style="styles"
|
||||
>
|
||||
<div
|
||||
class="arrow"
|
||||
:class="{ active: showArrow && isActive }"
|
||||
:style="arrowStyles"
|
||||
data-popper-arrow
|
||||
/>
|
||||
<transition-expand :to="popperPlacement">
|
||||
<portal to="popper-outlet">
|
||||
<transition name="bounce">
|
||||
<div
|
||||
class="v-menu-popper"
|
||||
:id="id"
|
||||
:class="{ active: isActive, attached }"
|
||||
:data-placement="popperPlacement"
|
||||
:style="styles"
|
||||
v-if="isActive"
|
||||
class="v-menu-content"
|
||||
:class="{ 'overflow-scroll': overflowScroll }"
|
||||
@click="onContentClick"
|
||||
v-click-outside="{
|
||||
handler: deactivate,
|
||||
disabled: isActive === false || closeOnClick === false,
|
||||
events: ['click'],
|
||||
}"
|
||||
>
|
||||
<slot :active="isActive" />
|
||||
<div
|
||||
class="arrow"
|
||||
:class="{ active: showArrow && isActive }"
|
||||
:style="arrowStyles"
|
||||
data-popper-arrow
|
||||
/>
|
||||
<div class="v-menu-content" @click.stop="onContentClick">
|
||||
<slot :active="isActive" />
|
||||
</div>
|
||||
</div>
|
||||
</transition-expand>
|
||||
</div>
|
||||
</transition>
|
||||
</portal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -49,6 +46,20 @@
|
||||
import { defineComponent, ref, PropType, computed, watch } from '@vue/composition-api';
|
||||
import { usePopper } from './use-popper';
|
||||
import { Placement } from '@popperjs/core';
|
||||
import { nanoid } from 'nanoid';
|
||||
import Vue from 'vue';
|
||||
|
||||
/**
|
||||
* @NOTE
|
||||
*
|
||||
* The framerate takes a little hit when opening menus, as we're rendering the content of it while
|
||||
* we're opening it. We could potentially optimize this by rendering the menu content ahead of time,
|
||||
* so all the processing power is freed up for the popper calculations and transition logic.
|
||||
*
|
||||
* However, we _can not_ render all menu content at all times, as that greatly decreases perf in the
|
||||
* app. I'm thinking we might be able to pre-render the menu content on hover of the activator, so
|
||||
* the actual act of opening the menu is quicker.
|
||||
*/
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -80,19 +91,25 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
overflowScroll: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const activator = ref<HTMLElement>(null);
|
||||
const popper = ref<HTMLElement>(null);
|
||||
|
||||
const reference = computed<HTMLElement | null>(() => {
|
||||
return (activator.value as HTMLElement)?.childNodes[0] as HTMLElement;
|
||||
});
|
||||
|
||||
const id = computed(() => nanoid());
|
||||
const popper = ref<HTMLElement>(null);
|
||||
|
||||
const { isActive, activate, deactivate, toggle } = useActiveState();
|
||||
|
||||
watch(isActive, () => {
|
||||
Vue.nextTick(() => {
|
||||
popper.value = document.getElementById(id.value);
|
||||
});
|
||||
});
|
||||
|
||||
const { start, stop, styles, arrowStyles, placement: popperPlacement } = usePopper(
|
||||
reference,
|
||||
popper,
|
||||
@@ -103,12 +120,11 @@ export default defineComponent({
|
||||
}))
|
||||
);
|
||||
|
||||
const { isActive, activate, deactivate, toggle } = useActiveState();
|
||||
|
||||
return {
|
||||
id,
|
||||
activator,
|
||||
reference,
|
||||
popper,
|
||||
reference,
|
||||
isActive,
|
||||
toggle,
|
||||
deactivate,
|
||||
@@ -136,10 +152,10 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
|
||||
watch(isActive, async (newActive) => {
|
||||
if (newActive !== null && newActive === true) {
|
||||
watch(popper, async () => {
|
||||
if (popper.value !== null) {
|
||||
await start();
|
||||
} else {
|
||||
} else if (stop) {
|
||||
stop();
|
||||
}
|
||||
});
|
||||
@@ -185,9 +201,9 @@ body {
|
||||
}
|
||||
|
||||
.v-menu-popper {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
left: -999px;
|
||||
z-index: 5;
|
||||
z-index: 500;
|
||||
min-width: var(--v-menu-min-width);
|
||||
transform: translateY(2px);
|
||||
pointer-events: none;
|
||||
@@ -195,107 +211,272 @@ body {
|
||||
&.active {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow,
|
||||
.arrow::before,
|
||||
.arrow::after {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
.arrow,
|
||||
.arrow::before,
|
||||
.arrow::after {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
&::before,
|
||||
&::after {
|
||||
background: var(--border-normal);
|
||||
transform: rotate(45deg) scale(0);
|
||||
transition: transform var(--fast) var(--transition-out);
|
||||
transition-delay: 0;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.arrow {
|
||||
&::before,
|
||||
&::after {
|
||||
background: var(--border-normal);
|
||||
transform: rotate(45deg) scale(0);
|
||||
transition: transform var(--fast) var(--transition-out);
|
||||
transition-delay: 0;
|
||||
content: '';
|
||||
}
|
||||
&.active::before,
|
||||
&.active::after {
|
||||
transform: rotate(45deg) scale(1);
|
||||
transition: transform var(--medium) var(--transition-in);
|
||||
}
|
||||
|
||||
&.active::before,
|
||||
&.active::after {
|
||||
transform: rotate(45deg) scale(1);
|
||||
transition: transform var(--medium) var(--transition-in);
|
||||
}
|
||||
&::after {
|
||||
background: var(--background-subdued);
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
background: var(--background-subdued);
|
||||
[data-placement^='top'] .arrow {
|
||||
bottom: -4px;
|
||||
|
||||
&::after {
|
||||
bottom: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-placement^='bottom'] .arrow {
|
||||
top: -4px;
|
||||
|
||||
&::after {
|
||||
top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-placement^='right'] .arrow {
|
||||
left: -4px;
|
||||
|
||||
&::after {
|
||||
left: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-placement^='left'] .arrow {
|
||||
right: -4px;
|
||||
|
||||
&::after {
|
||||
right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.v-menu-content {
|
||||
max-height: 30vh;
|
||||
padding: 0 4px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: var(--background-subdued);
|
||||
border: 2px solid var(--border-normal);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0px 4px 12px rgba(38, 50, 56, 0.1);
|
||||
transition-timing-function: var(--transition-out);
|
||||
transition-duration: var(--fast);
|
||||
transition-property: opacity, transform;
|
||||
contain: content;
|
||||
|
||||
.v-list {
|
||||
--v-list-background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
[data-placement='top'] > .v-menu-content {
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
[data-placement='top-start'] > .v-menu-content {
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
|
||||
[data-placement='top-end'] > .v-menu-content {
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
|
||||
[data-placement='right'] > .v-menu-content {
|
||||
transform-origin: center left;
|
||||
}
|
||||
|
||||
[data-placement='right-start'] > .v-menu-content {
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
[data-placement='right-end'] > .v-menu-content {
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
|
||||
[data-placement='bottom'] > .v-menu-content {
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
[data-placement='bottom-start'] > .v-menu-content {
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
[data-placement='bottom-end'] > .v-menu-content {
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
[data-placement='left'] > .v-menu-content {
|
||||
transform-origin: center right;
|
||||
}
|
||||
|
||||
[data-placement='left-start'] > .v-menu-content {
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
[data-placement='left-end'] > .v-menu-content {
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
|
||||
.bounce-enter-active,
|
||||
.bounce-leave-active {
|
||||
transition: opacity var(--fast) var(--transition);
|
||||
|
||||
& > .v-menu-content {
|
||||
transition: transform var(--fast) cubic-bezier(0, 0, 0.2, 1.5);
|
||||
}
|
||||
}
|
||||
|
||||
.bounce-enter,
|
||||
.bounce-leave-to {
|
||||
opacity: 0;
|
||||
|
||||
&[data-placement='top'] > .v-menu-content {
|
||||
transform: scaleY(0.8);
|
||||
}
|
||||
|
||||
&[data-placement='top-start'] > .v-menu-content {
|
||||
transform: scaleY(0.8) scaleX(0.8);
|
||||
}
|
||||
|
||||
&[data-placement='top-end'] > .v-menu-content {
|
||||
transform: scaleY(0.8) scaleX(0.8);
|
||||
}
|
||||
|
||||
&[data-placement='right'] > .v-menu-content {
|
||||
transform: scaleX(0.8);
|
||||
}
|
||||
|
||||
&[data-placement='right-start'] > .v-menu-content {
|
||||
transform: scaleY(0.8) scaleX(0.8);
|
||||
}
|
||||
|
||||
&[data-placement='right-end'] > .v-menu-content {
|
||||
transform: scaleY(0.8) scaleX(0.8);
|
||||
}
|
||||
|
||||
&[data-placement='bottom'] > .v-menu-content {
|
||||
transform: scaleY(0.8);
|
||||
}
|
||||
|
||||
&[data-placement='bottom-start'] > .v-menu-content {
|
||||
transform: scaleY(0.8);
|
||||
}
|
||||
|
||||
&[data-placement='bottom-end'] > .v-menu-content {
|
||||
transform: scaleY(0.8);
|
||||
}
|
||||
|
||||
&[data-placement='left'] > .v-menu-content {
|
||||
transform: scaleX(0.8);
|
||||
}
|
||||
|
||||
&[data-placement='left-start'] > .v-menu-content {
|
||||
transform: scaleY(0.8) scaleX(0.8);
|
||||
}
|
||||
|
||||
&[data-placement='left-end'] > .v-menu-content {
|
||||
transform: scaleY(0.8) scaleX(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
// .bounce-enter,
|
||||
// .bounce-leave-to {
|
||||
// & [data-placement='top'] > .v-menu-content {
|
||||
// transform: scaleY(0.8);
|
||||
// }
|
||||
|
||||
// & [data-placement='top-start'] > .v-menu-content {
|
||||
// transform: scaleY(0.8) scaleX(0.8);
|
||||
// }
|
||||
|
||||
// & [data-placement='top-end'] > .v-menu-content {
|
||||
// transform: scaleY(0.8) scaleX(0.8);
|
||||
// }
|
||||
|
||||
// & [data-placement='right'] > .v-menu-content {
|
||||
// transform: scaleX(0.8);
|
||||
// }
|
||||
|
||||
// & [data-placement='right-start'] > .v-menu-content {
|
||||
// transform: scaleY(0.8) scaleX(0.8);
|
||||
// }
|
||||
|
||||
// & [data-placement='right-end'] > .v-menu-content {
|
||||
// transform: scaleY(0.8) scaleX(0.8);
|
||||
// }
|
||||
|
||||
// & [data-placement='bottom'] > .v-menu-content {
|
||||
// transform: scaleY(0.8);
|
||||
// }
|
||||
|
||||
// & [data-placement='bottom-start'] > .v-menu-content {
|
||||
// transform: scaleY(0.8);
|
||||
// }
|
||||
|
||||
// & [data-placement='bottom-end'] > .v-menu-content {
|
||||
// transform: scaleY(0.8);
|
||||
// }
|
||||
|
||||
// & [data-placement='left'] > .v-menu-content {
|
||||
// transform: scaleX(0.8);
|
||||
// }
|
||||
|
||||
// & [data-placement='left-start'] > .v-menu-content {
|
||||
// transform: scaleY(0.8) scaleX(0.8);
|
||||
// }
|
||||
|
||||
// & [data-placement='left-end'] > .v-menu-content {
|
||||
// transform: scaleY(0.8) scaleX(0.8);
|
||||
// }
|
||||
// }
|
||||
|
||||
// .bounce-enter-active > .v-menu-content,
|
||||
// .bounce-leave-active > .v-menu-content {
|
||||
// transform: scaleY(1) scaleX(1);
|
||||
// transition-timing-function: cubic-bezier(0, 0, 0.2, 1.5);
|
||||
// transition-duration: var(--fast);
|
||||
// }
|
||||
|
||||
.attached {
|
||||
&[data-placement^='top'] {
|
||||
> .v-menu-content {
|
||||
border-bottom: none;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-placement^='top'] .arrow {
|
||||
bottom: -4px;
|
||||
|
||||
&::after {
|
||||
bottom: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-placement^='bottom'] .arrow {
|
||||
top: -4px;
|
||||
|
||||
&::after {
|
||||
top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-placement^='right'] .arrow {
|
||||
left: -4px;
|
||||
|
||||
&::after {
|
||||
left: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-placement^='left'] .arrow {
|
||||
right: -4px;
|
||||
|
||||
&::after {
|
||||
right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.v-menu-content {
|
||||
max-height: 50vh;
|
||||
padding: 0 4px;
|
||||
background-color: var(--background-subdued);
|
||||
border: 2px solid var(--border-normal);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0px 4px 12px rgba(38, 50, 56, 0.1);
|
||||
transition-timing-function: var(--transition-out);
|
||||
transition-duration: var(--fast);
|
||||
transition-property: opacity, transform;
|
||||
|
||||
&.overflow-scroll {
|
||||
contain: content;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.v-list {
|
||||
--v-list-background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.attached {
|
||||
&[data-placement^='top'] {
|
||||
> .v-menu-content {
|
||||
border-bottom: none;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-placement^='bottom'] {
|
||||
> .v-menu-content {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
&[data-placement^='bottom'] {
|
||||
> .v-menu-content {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ export function onEvent({
|
||||
handler: Handler;
|
||||
middleware: Middleware;
|
||||
}) {
|
||||
event.stopPropagation();
|
||||
const path = event.composedPath();
|
||||
const isClickOutside = path
|
||||
? path.indexOf(el) < 0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-menu attached :disabled="disabled" :overflow-scroll="false">
|
||||
<v-menu attached :disabled="disabled">
|
||||
<template #activator="{ toggle, active }">
|
||||
<v-input
|
||||
:active="active"
|
||||
|
||||
@@ -29,7 +29,7 @@ export default defineInterface(({ i18n }) => ({
|
||||
field: 'trim',
|
||||
name: 'Trim',
|
||||
width: 'half',
|
||||
interface: 'switch',
|
||||
interface: 'toggle',
|
||||
},
|
||||
{
|
||||
field: 'font',
|
||||
|
||||
@@ -89,9 +89,15 @@
|
||||
"item_delete_success": "Item Deleted | Items Deleted",
|
||||
"item_delete_failed": "Couldn't Delete Item | Couldn't Delete Items",
|
||||
"item_in": "Item {primaryKey} in {collection} | Items {primaryKey} in {collection}",
|
||||
"this_collection": "This Collection",
|
||||
"related_collection": "Related Collection",
|
||||
|
||||
"submit": "Submit",
|
||||
|
||||
"add_field_related": "Add Field to Related Collection",
|
||||
"create_corresponding_field": "Create Corresponding Field",
|
||||
"corresponding_field_name": "Corresponding Field Name",
|
||||
|
||||
"datetime": "DateTime",
|
||||
"date-fns_date": "PPP",
|
||||
"date-fns_time": "K:mm a",
|
||||
@@ -114,6 +120,10 @@
|
||||
"december": "December"
|
||||
},
|
||||
|
||||
"many_to_many": "Many to Many (M2M)",
|
||||
"one_to_many": "One to Many (O2M)",
|
||||
"many_to_one": "Many to One (M2O)",
|
||||
|
||||
"set_to_now": "Set to Now",
|
||||
|
||||
"name": "Name",
|
||||
@@ -277,7 +287,7 @@
|
||||
"max": "Max",
|
||||
"minimum_value": "Minimum Value",
|
||||
"maximum_value": "Maximum Value",
|
||||
"placeholder": "Placerholder",
|
||||
"placeholder": "Placeholder",
|
||||
"step_interval": "Step Interval",
|
||||
"icon_left": "Icon (Left)",
|
||||
"icon_right": "Icon (Right)",
|
||||
@@ -805,7 +815,6 @@
|
||||
"project_not_configured": "Project Not Configured",
|
||||
"readable_fields_copy": "Select the fields that the user can view",
|
||||
"regex": "RegEx",
|
||||
"related_collection": "Related Collection",
|
||||
"related_entries": "Has related entries",
|
||||
"relational": "Relational",
|
||||
"relationship": "Relationship",
|
||||
@@ -859,7 +868,6 @@
|
||||
"translated_field_name": "Translated field name...",
|
||||
"not_translated_in_language": "Not Translated in {language}",
|
||||
"this_item_is_not_available": "This item is not available.",
|
||||
"this_collection": "This Collection",
|
||||
"to": "To",
|
||||
"turn_all_on": "Turn all on",
|
||||
"turn_all_off": "Turn all off",
|
||||
|
||||
@@ -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