mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
[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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -193,6 +193,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.v-menu-content {
|
||||
max-height: 50vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: var(--highlight);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user