[Settings] Add duplicate field option (#263)

* Add duplicate field option

* Add missing prop to readme of v-select

* Re-enable preventOverflow
This commit is contained in:
Rijk van Zanten
2020-03-30 16:47:56 -04:00
committed by GitHub
parent 6205e6bf94
commit 790e7b2eb7
10 changed files with 180 additions and 21 deletions

View File

@@ -27,6 +27,7 @@ import VOverlay from './v-overlay/';
import VPagination from './v-pagination/';
import VProgressLinear from './v-progress/linear/';
import VProgressCircular from './v-progress/circular/';
import VSelect from './v-select/';
import VSheet from './v-sheet/';
import VSlider from './v-slider/';
import VSwitch from './v-switch/';
@@ -64,6 +65,7 @@ Vue.component('v-overlay', VOverlay);
Vue.component('v-pagination', VPagination);
Vue.component('v-progress-linear', VProgressLinear);
Vue.component('v-progress-circular', VProgressCircular);
Vue.component('v-select', VSelect);
Vue.component('v-sheet', VSheet);
Vue.component('v-slider', VSlider);
Vue.component('v-switch', VSwitch);

View File

@@ -94,7 +94,7 @@ export default defineComponent({
position: relative;
z-index: 2;
max-height: 90%;
transform: translateY(50px);
transform: translateY(-50px);
opacity: 0;
transition: var(--medium) var(--transition-in);
transition-property: opacity, transform;
@@ -105,7 +105,7 @@ export default defineComponent({
pointer-events: all;
.content {
transform: translateY(0);
transform: translateY(-100px);
opacity: 1;
}
}

View File

@@ -52,13 +52,13 @@ export function usePopper(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const modifiers: Partial<Modifier<any>>[] = [
popperOffsets,
preventOverflow,
{
...offset,
options: {
offset: options.value.attached ? [0, -2] : [0, 8],
},
},
preventOverflow,
computeStyles,
flip,
eventListeners,

View File

@@ -193,6 +193,7 @@ export default defineComponent({
}
.v-menu-content {
max-height: 50vh;
overflow-x: hidden;
overflow-y: auto;
background-color: var(--highlight);

View File

@@ -30,6 +30,8 @@ Renders a dropdown input.
| `value` | Currently selected item(s) | |
| `multiple` | Allow multiple items to be selected | `false` |
| `placeholder` | What placeholder to show when no items are selected | |
| `full-width` | Render the select at full width | |
| `monospace` | Render the value and options monospaced | |
## Events

View File

@@ -1,7 +1,14 @@
<template>
<v-menu class="v-select" attached :close-on-content-click="multiple === false">
<template #activator="{ toggle }">
<v-input readonly :value="displayValue" @click="toggle" :placeholder="placeholder">
<v-input
:full-width="fullWidth"
:monospace="monospace"
readonly
:value="displayValue"
@click="toggle"
:placeholder="placeholder"
>
<template #append><v-icon name="expand_more" /></template>
</v-input>
</template>
@@ -16,7 +23,9 @@
@click="multiple ? null : $emit('input', item.value)"
>
<v-list-item-content>
<v-list-item-title v-if="multiple === false">{{ item.text }}</v-list-item-title>
<v-list-item-title v-if="multiple === false">
<span :class="{ monospace }">{{ item.text }}</span>
</v-list-item-title>
<v-checkbox
v-else
:inputValue="value || []"
@@ -68,6 +77,14 @@ export default defineComponent({
type: String,
default: null,
},
fullWidth: {
type: Boolean,
default: false,
},
monospace: {
type: Boolean,
default: false,
},
},
setup(props) {
const _items = computed(() =>
@@ -106,3 +123,9 @@ export default defineComponent({
},
});
</script>
<style lang="scss" scoped>
.monospace {
font-family: var(--family-monospace);
}
</style>

View File

@@ -16,6 +16,7 @@
"field_delete_failure": "Could not delete '{field}'",
"fields_update_success": "Fields updated",
"fields_update_failure": "Could not update fields",
"duplicate_where_to": "Where would you like to duplicate this field to?",
"about_directus": "About Directus",
"activity": "Activity",

View File

@@ -31,10 +31,35 @@
</template>
<field-setup :field="field" />
</v-dialog>
<v-list-item>
<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>
<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 monospace :items="collections" v-model="duplicateTo" full-width />
<span class="label">{{ $tc('field', 0) }}</span>
<v-input monospace v-model="duplicateName" full-width />
</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 inset />
<v-list-item @click="setWidth('half')" :disabled="hidden || field.width === 'half'">
<v-list-item-icon><v-icon name="border_vertical" /></v-list-item-icon>
@@ -83,9 +108,10 @@
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from '@vue/composition-api';
import { defineComponent, PropType, ref, computed } from '@vue/composition-api';
import { Field } from '@/stores/fields/types';
import useFieldsStore from '@/stores/fields/';
import useCollectionsStore from '@/stores/collections/';
import FieldSetup from '../field-setup/';
export default defineComponent({
@@ -103,10 +129,31 @@ export default defineComponent({
setup(props) {
const editActive = ref(false);
const fieldsStore = useFieldsStore();
const collectionsStore = useCollectionsStore();
const { deleteActive, deleting, deleteField } = useDeleteField();
const {
duplicateActive,
duplicateName,
collections,
duplicateTo,
saveDuplicate,
duplicating,
} = useDuplicate();
return { editActive, setWidth, deleteActive, deleting, deleteField };
return {
editActive,
setWidth,
deleteActive,
deleting,
deleteField,
duplicateActive,
collections,
duplicateName,
duplicateTo,
saveDuplicate,
duplicating,
};
function setWidth(width: string) {
fieldsStore.updateField(props.field.collection, props.field.field, { width });
@@ -128,6 +175,50 @@ export default defineComponent({
deleteActive.value = false;
}
}
function useDuplicate() {
const duplicateActive = ref(false);
const duplicateName = ref(props.field.field + '_copy');
const duplicating = ref(false);
const collections = computed(() =>
collectionsStore.state.collections
.map(({ collection }) => collection)
.filter((collection) => collection.startsWith('directus_') === false)
);
const duplicateTo = ref(props.field.collection);
return {
duplicateActive,
duplicateName,
collections,
duplicateTo,
saveDuplicate,
duplicating,
};
async function saveDuplicate() {
const newField = {
...props.field,
field: duplicateName.value,
collection: duplicateTo.value,
};
delete newField.id;
delete newField.sort;
delete newField.name;
duplicating.value = true;
try {
await fieldsStore.createField(duplicateTo.value, newField);
duplicateActive.value = false;
} catch (error) {
console.log(error);
} finally {
duplicating.value = false;
}
}
}
},
});
</script>

View File

@@ -42,6 +42,7 @@ import Draggable from 'vuedraggable';
import { Field } from '@/stores/fields/types';
import useFieldsStore from '@/stores/fields/';
import FieldSelect from '../field-select/';
import { sortBy } from 'lodash';
type DraggableEvent = {
moved?: {
@@ -68,19 +69,17 @@ export default defineComponent({
const fieldsStore = useFieldsStore();
const sortedVisibleFields = computed(() =>
[...fields.value]
.filter(({ hidden_detail }) => hidden_detail === false)
.sort((a, b) => {
return (a.sort || 0) > (b.sort || 0) ? 1 : -1;
})
sortBy(
[...fields.value].filter(({ hidden_detail }) => hidden_detail === false),
(field) => field.sort || Infinity
)
);
const sortedHiddenFields = computed(() =>
[...fields.value]
.filter(({ hidden_detail }) => hidden_detail === true)
.sort((a, b) => {
return (a.sort || -1) > (b.sort || -1) ? 1 : -1;
})
sortBy(
[...fields.value].filter(({ hidden_detail }) => hidden_detail === true),
(field) => field.sort || Infinity
)
);
return { sortedVisibleFields, sortedHiddenFields, handleChange, toggleVisibility };

View File

@@ -50,6 +50,46 @@ export const useFieldsStore = createStore({
async dehydrate() {
this.reset();
},
async createField(collectionKey: string, newField: Field) {
const projectsStore = useProjectsStore();
const currentProjectKey = projectsStore.state.currentProjectKey;
const stateClone = [...this.state.fields];
// Update locally first, so the changes are visible immediately
this.state.fields = [...this.state.fields, newField];
// Save to API, and update local state again to make sure everything is in sync with the
// API
try {
const response = await api.post(
`/${currentProjectKey}/fields/${collectionKey}`,
newField
);
this.state.fields = this.state.fields.map((field) => {
if (field.collection === collectionKey && field.field === newField.field) {
return response.data.data;
}
return field;
});
notify({
title: i18n.t('field_create_success', { field: newField.field }),
type: 'success',
});
} catch (error) {
notify({
title: i18n.t('field_create_failure', { field: newField.field }),
type: 'error',
});
// reset the changes if the api sync failed
this.state.fields = stateClone;
throw error;
}
},
async updateField(
collectionKey: string,
fieldKey: string,