script[setup]: interfaces/list (#18423)

This commit is contained in:
Rijk van Zanten
2023-05-03 10:47:18 -04:00
committed by GitHub
parent 294596b9ad
commit bb4fd307cd
2 changed files with 364 additions and 434 deletions

View File

@@ -82,274 +82,211 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, computed, ref, toRefs } from 'vue';
import { computed, ref, toRefs } from 'vue';
import { Field } from '@directus/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'),
},
direction: {
type: String,
default: undefined,
},
},
emits: ['input'],
setup(props, { emit }) {
const { t } = useI18n();
const props = withDefaults(
defineProps<{
value: Record<string, unknown>[] | null;
fields?: Partial<Field>[];
template?: string;
addLabel?: string;
sort?: string;
limit?: number;
disabled?: boolean;
headerPlaceholder?: string;
collection?: string;
placeholder?: string;
direction?: string;
}>(),
{
fields: () => [],
addLabel: () => i18n.global.t('create_new'),
headerPlaceholder: () => i18n.global.t('empty_item'),
placeholder: () => i18n.global.t('no_items'),
}
);
const active = ref<number | null>(null);
const drawerOpen = computed(() => active.value !== null);
const { value } = toRefs(props);
const emit = defineEmits<{
(e: 'input', value: Record<string, unknown>[] | null): void;
}>();
const templateWithDefaults = computed(() =>
props.fields?.[0]?.field ? props.template || `{{${props.fields[0].field}}}` : ''
);
const { t } = useI18n();
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 active = ref<number | null>(null);
const drawerOpen = computed(() => active.value !== null);
const { value } = toRefs(props);
const activeItem = computed(() => (active.value !== null ? edits.value : null));
const templateWithDefaults = computed(() =>
props.fields?.[0]?.field ? props.template || `{{${props.fields[0].field}}}` : ''
);
const isSaveDisabled = computed(() => {
for (const field of props.fields) {
if (
field.meta?.required &&
field.field &&
(edits.value[field.field] === null || edits.value[field.field] === undefined)
) {
return true;
}
}
const showAddNew = computed(() => {
if (props.disabled) return false;
if (props.value === null) return true;
if (props.limit === undefined) return true;
if (Array.isArray(props.value) && props.value.length < props.limit) return true;
return false;
});
return false;
});
const activeItem = computed(() => (active.value !== null ? edits.value : null));
const { displayValue } = renderStringTemplate(templateWithDefaults, activeItem);
const isSaveDisabled = computed(() => {
for (const field of props.fields) {
if (
field.meta?.required &&
field.field &&
(edits.value[field.field] === null || edits.value[field.field] === undefined)
) {
return true;
}
}
const defaults = computed(() => {
const values: Record<string, any> = {};
return false;
});
for (const field of props.fields) {
if (field.schema?.default_value !== undefined && field.schema?.default_value !== null) {
values[field.field!] = field.schema.default_value;
}
}
const { displayValue } = renderStringTemplate(templateWithDefaults, activeItem);
return values;
});
const defaults = computed(() => {
const values: Record<string, any> = {};
const fieldsWithNames = computed(() =>
props.fields?.map((field) => {
return {
...field,
name: formatTitle(field.name ?? field.field!),
};
})
);
for (const field of props.fields) {
if (field.schema?.default_value !== undefined && field.schema?.default_value !== null) {
values[field.field!] = field.schema.default_value;
}
}
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({} as Record<string, any>);
const confirmDiscard = ref(false);
return values;
});
const fieldsWithNames = computed(() =>
props.fields?.map((field) => {
return {
t,
internalValue,
updateValues,
removeItem,
addNew,
showAddNew,
hideDragImage,
active,
drawerOpen,
displayValue,
activeItem,
isSaveDisabled,
closeDrawer,
onSort,
templateWithDefaults,
defaults,
fieldsWithNames,
isNewItem,
edits,
confirmDiscard,
openItem,
saveItem,
trackEdits,
checkDiscard,
discardAndLeave,
...field,
name: formatTitle(field.name ?? field.field!),
};
})
);
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;
}
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({} as Record<string, any>);
const confirmDiscard = ref(false);
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();
}
}
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?: Record<string, unknown>[]) {
if (!value || value.length === 0) {
return emit('input', null);
}
return emit('input', value);
}
function closeDrawer() {
if (isNewItem.value) {
emitValue(internalValue.value?.slice(0, -1));
}
edits.value = {};
active.value = null;
}
</script>
<style lang="scss" scoped>
@@ -367,7 +304,7 @@ export default defineComponent({
}
.drag-handle {
cursor: grap;
cursor: grab;
}
.drawer-item-content {

View File

@@ -29,7 +29,7 @@
<p class="type-label">{{ t('interfaces.list.edit_fields') }}</p>
<repeater
:value="repeaterValue"
:template="`{{ field }} — {{ interface }}`"
template="{{ field }} — {{ interface }}"
:fields="repeaterFields"
@input="repeaterValue = $event"
/>
@@ -37,203 +37,196 @@
</div>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, computed } from 'vue';
import Repeater from './list.vue';
import { Field, FieldMeta } from '@directus/types';
<script setup lang="ts">
import { FIELD_TYPES_SELECT } from '@/constants';
import { DeepPartial } from '@directus/types';
import { translate } from '@/utils/translate-object-values';
import { DeepPartial, Field, FieldMeta } from '@directus/types';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import Repeater from './list.vue';
export default defineComponent({
components: { Repeater },
props: {
value: {
type: Object as PropType<Record<string, any>>,
default: null,
const props = defineProps<{
value: Record<string, any> | null;
}>();
const emit = defineEmits<{
(e: 'input', value: Record<string, any>[] | null): void;
}>();
const { t } = useI18n();
const repeaterValue = computed({
get() {
return props.value?.fields?.map((field: Field) => field.meta);
},
set(newVal: FieldMeta[] | null) {
const fields = (newVal || []).map((meta: Record<string, any>) => ({
field: meta.field,
name: meta.name || meta.field,
type: meta.type,
meta,
}));
emit('input', {
...(props.value || {}),
fields,
});
},
});
const repeaterFields: DeepPartial<Field>[] = [
{
name: t('field', 1),
field: 'field',
type: 'string',
meta: {
interface: 'input',
width: 'half',
sort: 2,
required: true,
options: {
dbSafe: true,
font: 'monospace',
placeholder: t('field_name_placeholder'),
},
},
schema: null,
},
{
name: t('field_width'),
field: 'width',
type: 'string',
meta: {
interface: 'select-dropdown',
width: 'half',
sort: 3,
options: {
choices: [
{
value: 'half',
text: t('half_width'),
},
{
value: 'full',
text: t('full_width'),
},
],
},
},
schema: null,
},
{
name: t('type'),
field: 'type',
type: 'string',
meta: {
interface: 'select-dropdown',
width: 'half',
sort: 4,
options: {
choices: translate(FIELD_TYPES_SELECT),
},
},
schema: null,
},
{
name: t('interface_label'),
field: 'interface',
type: 'string',
meta: {
interface: 'system-interface',
width: 'half',
sort: 5,
options: {
typeField: 'type',
},
},
schema: null,
},
{
name: t('note'),
field: 'note',
type: 'string',
meta: {
interface: 'system-input-translated-string',
width: 'full',
sort: 6,
options: {
placeholder: t('interfaces.list.field_note_placeholder'),
},
},
schema: null,
},
{
name: t('required'),
field: 'required',
type: 'boolean',
meta: {
interface: 'boolean',
sort: 7,
options: {
label: t('requires_value'),
},
width: 'half',
},
},
emits: ['input'],
setup(props, { emit }) {
const { t } = useI18n();
const repeaterValue = computed({
get() {
return props.value?.fields?.map((field: Field) => field.meta);
{
name: t('options'),
field: 'options',
type: 'string',
meta: {
interface: 'system-interface-options',
width: 'full',
sort: 8,
options: {
interfaceField: 'interface',
},
set(newVal: FieldMeta[] | null) {
const fields = (newVal || []).map((meta: Record<string, any>) => ({
field: meta.field,
name: meta.name || meta.field,
type: meta.type,
meta,
}));
emit('input', {
...(props.value || {}),
fields: fields,
});
},
});
const repeaterFields: DeepPartial<Field>[] = [
{
name: t('field', 1),
field: 'field',
type: 'string',
meta: {
interface: 'input',
width: 'half',
sort: 2,
required: true,
options: {
dbSafe: true,
font: 'monospace',
placeholder: t('field_name_placeholder'),
},
},
schema: null,
},
{
name: t('field_width'),
field: 'width',
type: 'string',
meta: {
interface: 'select-dropdown',
width: 'half',
sort: 3,
options: {
choices: [
{
value: 'half',
text: t('half_width'),
},
{
value: 'full',
text: t('full_width'),
},
],
},
},
schema: null,
},
{
name: t('type'),
field: 'type',
type: 'string',
meta: {
interface: 'select-dropdown',
width: 'half',
sort: 4,
options: {
choices: translate(FIELD_TYPES_SELECT),
},
},
schema: null,
},
{
name: t('interface_label'),
field: 'interface',
type: 'string',
meta: {
interface: 'system-interface',
width: 'half',
sort: 5,
options: {
typeField: 'type',
},
},
schema: null,
},
{
name: t('note'),
field: 'note',
type: 'string',
meta: {
interface: 'system-input-translated-string',
width: 'full',
sort: 6,
options: {
placeholder: t('interfaces.list.field_note_placeholder'),
},
},
schema: null,
},
{
name: t('required'),
field: 'required',
type: 'boolean',
meta: {
interface: 'boolean',
sort: 7,
options: {
label: t('requires_value'),
},
width: 'half',
},
},
{
name: t('options'),
field: 'options',
type: 'string',
meta: {
interface: 'system-interface-options',
width: 'full',
sort: 8,
options: {
interfaceField: 'interface',
},
},
},
];
const template = computed({
get() {
return props.value?.template;
},
set(newTemplate: string) {
emit('input', {
...(props.value || {}),
template: newTemplate,
});
},
});
const addLabel = computed({
get() {
return props.value?.addLabel;
},
set(newAddLabel: string) {
emit('input', {
...(props.value || {}),
addLabel: newAddLabel,
});
},
});
const sort = computed({
get() {
return props.value?.sort;
},
set(newSort: string) {
emit('input', {
...(props.value || {}),
sort: newSort,
});
},
});
const sortFields = computed(() => {
if (!repeaterValue.value) return [];
return repeaterValue.value.map((val) => {
return { text: val.field, value: val.field };
});
});
return { t, repeaterValue, repeaterFields, template, addLabel, sort, sortFields };
},
},
];
const template = computed({
get() {
return props.value?.template;
},
set(newTemplate: string) {
emit('input', {
...(props.value || {}),
template: newTemplate,
});
},
});
const addLabel = computed({
get() {
return props.value?.addLabel;
},
set(newAddLabel: string) {
emit('input', {
...(props.value || {}),
addLabel: newAddLabel,
});
},
});
const sort = computed({
get() {
return props.value?.sort;
},
set(newSort: string) {
emit('input', {
...(props.value || {}),
sort: newSort,
});
},
});
const sortFields = computed(() => {
if (!repeaterValue.value) return [];
return repeaterValue.value.map((val) => {
return { text: val.field, value: val.field };
});
});
</script>