mirror of
https://github.com/directus/directus.git
synced 2026-02-16 02:08:12 -05:00
* fix translations string being null * clean up watcher a bit * add sort prop to list interface * use sort for list interface in translation strings
357 lines
8.3 KiB
Vue
357 lines
8.3 KiB
Vue
<template>
|
|
<div class="repeater">
|
|
<v-notice v-if="(Array.isArray(internalValue) && internalValue.length === 0) || internalValue == null">
|
|
{{ placeholder }}
|
|
</v-notice>
|
|
<v-notice v-else-if="!Array.isArray(internalValue)" type="warning">
|
|
<p>{{ t('interfaces.list.incompatible_data') }}</p>
|
|
</v-notice>
|
|
|
|
<v-list v-if="Array.isArray(internalValue) && internalValue.length > 0">
|
|
<draggable
|
|
:disabled="disabled"
|
|
:force-fallback="true"
|
|
:model-value="internalValue"
|
|
item-key="id"
|
|
handle=".drag-handle"
|
|
@update:model-value="$emit('input', $event)"
|
|
>
|
|
<template #item="{ element, index }">
|
|
<v-list-item :dense="internalValue.length > 4" block @click="openItem(index)">
|
|
<v-icon v-if="!disabled && !sort" name="drag_handle" class="drag-handle" left @click.stop="() => {}" />
|
|
<render-template :fields="fields" :item="{ ...defaults, ...element }" :template="templateWithDefaults" />
|
|
<div class="spacer" />
|
|
<v-icon v-if="!disabled" name="close" @click.stop="removeItem(element)" />
|
|
</v-list-item>
|
|
</template>
|
|
</draggable>
|
|
</v-list>
|
|
<v-button v-if="showAddNew" class="add-new" @click="addNew">
|
|
{{ addLabel }}
|
|
</v-button>
|
|
|
|
<v-drawer
|
|
:model-value="drawerOpen"
|
|
:title="displayValue || headerPlaceholder"
|
|
persistent
|
|
@update:model-value="checkDiscard()"
|
|
@cancel="checkDiscard()"
|
|
>
|
|
<template #title>
|
|
<h1 class="type-title">
|
|
<render-template :fields="fields" :item="activeItem" :template="templateWithDefaults" />
|
|
</h1>
|
|
</template>
|
|
|
|
<template #actions>
|
|
<v-button v-tooltip.bottom="t('save')" icon rounded @click="saveItem(active!)">
|
|
<v-icon name="check" />
|
|
</v-button>
|
|
</template>
|
|
|
|
<div class="drawer-item-content">
|
|
<v-form
|
|
:disabled="disabled"
|
|
:fields="fieldsWithNames"
|
|
:model-value="activeItem"
|
|
autofocus
|
|
primary-key="+"
|
|
@update:model-value="trackEdits($event)"
|
|
/>
|
|
</div>
|
|
</v-drawer>
|
|
|
|
<v-dialog v-model="confirmDiscard" @esc="confirmDiscard = false">
|
|
<v-card>
|
|
<v-card-title>{{ t('unsaved_changes') }}</v-card-title>
|
|
<v-card-text>{{ t('unsaved_changes_copy') }}</v-card-text>
|
|
<v-card-actions>
|
|
<v-button secondary @click="discardAndLeave()">
|
|
{{ t('discard_changes') }}
|
|
</v-button>
|
|
<v-button @click="confirmDiscard = false">{{ t('keep_editing') }}</v-button>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { useI18n } from 'vue-i18n';
|
|
import { defineComponent, PropType, computed, ref, toRefs } from 'vue';
|
|
import { Field } from '@directus/shared/types';
|
|
import Draggable from 'vuedraggable';
|
|
import { i18n } from '@/lang';
|
|
import { renderStringTemplate } from '@/utils/render-string-template';
|
|
import hideDragImage from '@/utils/hide-drag-image';
|
|
import formatTitle from '@directus/format-title';
|
|
import { isEqual, sortBy } from 'lodash';
|
|
|
|
export default defineComponent({
|
|
components: { Draggable },
|
|
props: {
|
|
value: {
|
|
type: Array as PropType<Record<string, any>[]>,
|
|
default: null,
|
|
},
|
|
fields: {
|
|
type: Array as PropType<Partial<Field>[]>,
|
|
default: () => [],
|
|
},
|
|
template: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
addLabel: {
|
|
type: String,
|
|
default: () => i18n.global.t('create_new'),
|
|
},
|
|
sort: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
limit: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
headerPlaceholder: {
|
|
type: String,
|
|
default: () => i18n.global.t('empty_item'),
|
|
},
|
|
collection: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
placeholder: {
|
|
type: String,
|
|
default: () => i18n.global.t('no_items'),
|
|
},
|
|
},
|
|
emits: ['input'],
|
|
setup(props, { emit }) {
|
|
const { t } = useI18n();
|
|
|
|
const active = ref<number | null>(null);
|
|
const drawerOpen = computed(() => active.value !== null);
|
|
const { value } = toRefs(props);
|
|
|
|
const templateWithDefaults = computed(() =>
|
|
props.fields?.[0]?.field ? props.template || `{{${props.fields[0].field}}}` : ''
|
|
);
|
|
|
|
const showAddNew = computed(() => {
|
|
if (props.disabled) return false;
|
|
if (props.value === null) return true;
|
|
if (props.limit === null) return true;
|
|
if (Array.isArray(props.value) && props.value.length < props.limit) return true;
|
|
return false;
|
|
});
|
|
|
|
const activeItem = computed(() => (active.value !== null ? edits.value : null));
|
|
|
|
const { displayValue } = renderStringTemplate(templateWithDefaults, activeItem);
|
|
|
|
const defaults = computed(() => {
|
|
const values: Record<string, any> = {};
|
|
|
|
for (const field of props.fields) {
|
|
if (field.schema?.default_value !== undefined && field.schema?.default_value !== null) {
|
|
values[field.field!] = field.schema.default_value;
|
|
}
|
|
}
|
|
|
|
return values;
|
|
});
|
|
|
|
const fieldsWithNames = computed(() =>
|
|
props.fields?.map((field) => {
|
|
return {
|
|
...field,
|
|
name: formatTitle(field.name ?? field.field!),
|
|
};
|
|
})
|
|
);
|
|
|
|
const internalValue = computed({
|
|
get: () => {
|
|
if (props.fields && props.sort) return sortBy(value.value, props.sort);
|
|
return value.value;
|
|
},
|
|
set: (newVal) => {
|
|
value.value = props.fields && props.sort ? sortBy(value.value, props.sort) : newVal;
|
|
},
|
|
});
|
|
|
|
const isNewItem = ref(false);
|
|
const edits = ref({});
|
|
const confirmDiscard = ref(false);
|
|
|
|
return {
|
|
t,
|
|
internalValue,
|
|
updateValues,
|
|
removeItem,
|
|
addNew,
|
|
showAddNew,
|
|
hideDragImage,
|
|
active,
|
|
drawerOpen,
|
|
displayValue,
|
|
activeItem,
|
|
closeDrawer,
|
|
onSort,
|
|
templateWithDefaults,
|
|
defaults,
|
|
fieldsWithNames,
|
|
isNewItem,
|
|
edits,
|
|
confirmDiscard,
|
|
openItem,
|
|
saveItem,
|
|
trackEdits,
|
|
checkDiscard,
|
|
discardAndLeave,
|
|
};
|
|
|
|
function openItem(index: number) {
|
|
isNewItem.value = false;
|
|
|
|
edits.value = { ...internalValue.value[index] };
|
|
active.value = index;
|
|
}
|
|
|
|
function saveItem(index: number) {
|
|
isNewItem.value = false;
|
|
|
|
updateValues(index, edits.value);
|
|
closeDrawer();
|
|
}
|
|
|
|
function trackEdits(updatedValues: any) {
|
|
const combinedValues = Object.assign({}, defaults.value, updatedValues);
|
|
Object.assign(edits.value, combinedValues);
|
|
}
|
|
|
|
function checkDiscard() {
|
|
if (active.value !== null && !isEqual(edits.value, internalValue.value[active.value])) {
|
|
confirmDiscard.value = true;
|
|
} else {
|
|
closeDrawer();
|
|
}
|
|
}
|
|
|
|
function discardAndLeave() {
|
|
closeDrawer();
|
|
confirmDiscard.value = false;
|
|
}
|
|
|
|
function updateValues(index: number, updatedValues: any) {
|
|
const newValue = internalValue.value.map((item: any, i: number) => {
|
|
if (i === index) {
|
|
return updatedValues;
|
|
}
|
|
|
|
return item;
|
|
});
|
|
|
|
if (props.fields && props.sort) {
|
|
emitValue(sortBy(newValue, props.sort));
|
|
} else {
|
|
emitValue(newValue);
|
|
}
|
|
}
|
|
|
|
function removeItem(item: Record<string, any>) {
|
|
if (value.value) {
|
|
emitValue(internalValue.value.filter((i) => i !== item));
|
|
} else {
|
|
emitValue(null);
|
|
}
|
|
}
|
|
|
|
function addNew() {
|
|
isNewItem.value = true;
|
|
|
|
const newDefaults: any = {};
|
|
|
|
props.fields.forEach((field) => {
|
|
newDefaults[field.field!] = field.schema?.default_value;
|
|
});
|
|
|
|
if (Array.isArray(internalValue.value)) {
|
|
emitValue([...internalValue.value, newDefaults]);
|
|
} else {
|
|
if (internalValue.value != null) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn(
|
|
'The repeater interface expects an array as value, but the given value is no array. Overriding given value.'
|
|
);
|
|
}
|
|
|
|
emitValue([newDefaults]);
|
|
}
|
|
|
|
edits.value = { ...newDefaults };
|
|
active.value = (internalValue.value || []).length;
|
|
}
|
|
|
|
function emitValue(value: null | any[]) {
|
|
if (!value || value.length === 0) {
|
|
return emit('input', null);
|
|
}
|
|
|
|
return emit('input', value);
|
|
}
|
|
|
|
function onSort(sortedItems: any[]) {
|
|
if (sortedItems === null || sortedItems.length === 0) {
|
|
return emit('input', null);
|
|
}
|
|
|
|
return emit('input', sortedItems);
|
|
}
|
|
|
|
function closeDrawer() {
|
|
if (isNewItem.value) {
|
|
emitValue(internalValue.value.slice(0, -1));
|
|
}
|
|
|
|
edits.value = {};
|
|
active.value = null;
|
|
}
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.v-notice {
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.v-list {
|
|
--v-list-padding: 0 0 4px;
|
|
}
|
|
|
|
.v-list-item {
|
|
display: flex;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.drag-handle {
|
|
cursor: grap;
|
|
}
|
|
|
|
.drawer-item-content {
|
|
padding: var(--content-padding);
|
|
padding-bottom: var(--content-padding-bottom);
|
|
}
|
|
|
|
.add-new {
|
|
margin-top: 8px;
|
|
}
|
|
</style>
|