Relational consistency (#4093)

Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
Nitwel
2021-04-22 01:31:12 +02:00
committed by GitHub
parent 070df173d7
commit 7d64c8ab47
14 changed files with 357 additions and 506 deletions

View File

@@ -7,11 +7,12 @@
:to="to"
:class="{
active,
large,
dense,
link: isClickable,
disabled,
dashed,
block,
large,
}"
:href="href"
:download="download"
@@ -29,7 +30,7 @@ import { useGroupable } from '@/composables/groupable';
export default defineComponent({
props: {
large: {
block: {
type: Boolean,
default: false,
},
@@ -69,6 +70,10 @@ export default defineComponent({
type: [String, Number],
default: undefined,
},
large: {
type: Boolean,
default: false,
},
},
setup(props, { listeners }) {
const component = computed<string>(() => {
@@ -77,7 +82,7 @@ export default defineComponent({
return 'li';
});
const { active: groupActive, toggle, activate, deactivate } = useGroupable({
useGroupable({
value: props.value,
});
@@ -90,15 +95,14 @@ export default defineComponent({
<style>
body {
--v-list-item-min-height-large: 40px;
--v-list-item-min-height: 32px;
--v-list-item-padding-large: 0 8px;
--v-list-item-padding: 0 8px 0 calc(8px + var(--v-list-item-indent, 0px));
--v-list-item-margin-large: 4px 0;
--v-list-item-margin: 2px 0;
--v-list-item-min-width: none;
--v-list-item-max-width: none;
--v-list-item-min-height: var(--v-list-item-min-height);
--v-list-item-min-height-large: 40px;
--v-list-item-min-height: 32px;
--v-list-item-max-height: auto;
--v-list-item-border-radius: var(--border-radius);
--v-list-item-color: var(--v-list-color, var(--foreground-normal));
@@ -150,12 +154,12 @@ body {
transition-property: background-color, color;
user-select: none;
&:not(.disabled):not(.dense):hover {
&:not(.disabled):not(.dense):not(.block):hover {
color: var(--v-list-item-color-hover);
background-color: var(--v-list-item-background-color-hover);
}
&:not(.disabled):not(.dense):active {
&:not(.disabled):not(.dense):not(.block):active {
color: var(--v-list-item-color-active);
background-color: var(--v-list-item-background-color-active);
}
@@ -185,13 +189,54 @@ body {
}
}
@at-root {
.v-list,
#{$this},
.v-list #{$this} {
--v-list-item-min-height: var(--v-list-item-min-height);
&.block {
--v-list-item-min-height: 44px;
display: flex;
background-color: var(--background-subdued);
border: 2px solid var(--border-subdued);
border-radius: var(--border-radius);
transition: border-color var(--fast) var(--transition);
.v-icon {
color: var(--foreground-subdued);
&:hover {
color: var(--foreground-normal);
}
}
.drag-handle {
cursor: grab;
}
.drag-handle:active {
cursor: grabbing;
}
.spacer {
flex-grow: 1;
}
&:hover {
background-color: var(--background-subdued);
border: 2px solid var(--border-normal);
}
& + & {
margin-top: 8px;
}
&.dense {
--v-list-item-min-height: 34px;
& + & {
margin-top: 4px;
}
}
}
@at-root {
.v-list.large {
#{$this}:not(.dense) {
--v-list-item-min-height: var(--v-list-item-min-height-large);

View File

@@ -4,54 +4,58 @@
<v-skeleton-loader v-for="n in (value || []).length" :key="n" />
</div>
<draggable
v-else
:force-fallback="true"
:value="previewValues"
handle=".drag-handle"
@input="onSort"
:set-data="hideDragImage"
:disabled="!o2mRelation.sort_field"
>
<template v-for="item of previewValues">
<div
:key="item.$index"
v-if="allowedCollections.includes(item[anyRelation.one_collection_field])"
class="m2a-row"
@click="editExisting((value || [])[item.$index])"
>
<v-icon class="drag-handle" name="drag_handle" @click.stop v-if="o2mRelation.sort_field" />
<span class="collection">{{ collections[item[anyRelation.one_collection_field]].name }}:</span>
<span
v-if="typeof item[anyRelation.many_field] === 'number' || typeof item[anyRelation.many_field] === 'string'"
<v-list v-else>
<draggable
:force-fallback="true"
:value="previewValues"
handle=".drag-handle"
@input="onSort"
:set-data="hideDragImage"
:disabled="!o2mRelation.sort_field"
>
<template v-for="item of previewValues">
<v-list-item
:key="item.$index"
v-if="allowedCollections.includes(item[anyRelation.one_collection_field])"
block
@click="editExisting((value || [])[item.$index])"
>
{{ item[anyRelation.many_field] }}
</span>
<render-template
v-else
:collection="item[anyRelation.one_collection_field]"
:template="templates[item[anyRelation.one_collection_field]]"
:item="item[anyRelation.many_field]"
/>
<div class="spacer" />
<v-icon class="clear-icon" name="clear" @click.stop="deselect((value || [])[item.$index])" />
<v-icon class="launch-icon" name="launch" />
</div>
<v-icon class="drag-handle" left name="drag_handle" @click.stop v-if="o2mRelation.sort_field" />
<span class="collection">{{ collections[item[anyRelation.one_collection_field]].name }}:</span>
<span
v-if="
typeof item[anyRelation.many_field] === 'number' || typeof item[anyRelation.many_field] === 'string'
"
>
{{ item[anyRelation.many_field] }}
</span>
<render-template
v-else
:collection="item[anyRelation.one_collection_field]"
:template="templates[item[anyRelation.one_collection_field]]"
:item="item[anyRelation.many_field]"
/>
<div class="spacer" />
<v-icon class="clear-icon" name="clear" @click.stop="deselect((value || [])[item.$index])" />
<v-icon class="launch-icon" name="launch" />
</v-list-item>
<div v-else class="m2a-row invalid" :key="item.$index">
<v-icon class="invalid-icon" name="warning" left />
<span>{{ $t('invalid_item') }}</span>
<div class="spacer" />
<v-icon class="clear-icon" name="clear" @click.stop="deselect((value || [])[item.$index])" />
</div>
</template>
</draggable>
<v-list-item v-else :key="item.$index" block>
<v-icon class="invalid-icon" name="warning" left />
<span>{{ $t('invalid_item') }}</span>
<div class="spacer" />
<v-icon class="clear-icon" name="clear" @click.stop="deselect((value || [])[item.$index])" />
</v-list-item>
</template>
</draggable>
</v-list>
<div class="buttons">
<v-menu attached>
<v-menu show-arrow>
<template #activator="{ toggle }">
<v-button dashed outlined full-width @click="toggle">
<v-button @click="toggle">
{{ $t('create_new') }}
<v-icon name="arrow_drop_down" right />
</v-button>
</template>
@@ -67,10 +71,11 @@
</v-list>
</v-menu>
<v-menu attached>
<v-menu show-arrow>
<template #activator="{ toggle }">
<v-button dashed outlined full-width @click="toggle">
<v-button @click="toggle" class="existing">
{{ $t('add_existing') }}
<v-icon name="arrow_drop_down" right />
</v-button>
</template>
@@ -633,19 +638,7 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.m2a-row {
display: flex;
align-items: center;
padding: 12px;
background-color: var(--background-subdued);
border: 2px solid var(--border-subdued);
border-radius: var(--border-radius);
cursor: pointer;
& + .m2a-row {
margin-top: 12px;
}
.v-list-item {
.collection {
margin-right: 1ch;
color: var(--primary);
@@ -663,19 +656,15 @@ export default defineComponent({
}
.buttons {
display: grid;
grid-gap: var(--form-horizontal-gap);
grid-template-columns: 1fr 1fr;
margin-top: 12px;
margin-top: 8px;
}
.spacer {
flex-grow: 1;
.existing {
margin-left: 8px;
}
.drag-handle {
margin-right: 8px;
cursor: grab !important;
cursor: grab;
}
.invalid {

View File

@@ -3,43 +3,22 @@
{{ $t('relationship_not_setup') }}
</v-notice>
<div class="many-to-many" v-else>
<v-table
:loading="loading"
:items="sortedItems || items"
:headers.sync="tableHeaders"
show-resize
inline
:sort.sync="sort"
@update:items="sortItems($event)"
@click:row="editItem"
:disabled="disabled"
:show-manual-sort="relationInfo.sortField !== null"
:manual-sort-key="relationInfo.sortField"
>
<template v-for="header in tableHeaders" v-slot:[`item.${header.value}`]="{ item }">
<render-display
:key="header.value"
:value="get(item, header.value)"
:display="header.field.display"
:options="header.field.displayOptions"
:interface="header.field.interface"
:interface-options="header.field.interfaceOptions"
:type="header.field.type"
:collection="relationInfo.junctionCollection"
:field="header.field.field"
/>
</template>
<template #item-append="{ item }">
<v-icon
v-show="!disabled"
name="close"
v-tooltip="$t('deselect')"
class="deselect"
@click.stop="deleteItem(item)"
/>
</template>
</v-table>
<v-list>
<draggable
:force-fallback="true"
:value="sortedItems || items"
@input="sortItems($event)"
handler=".drag-handle"
:disabled="!relation.sort_field"
>
<v-list-item v-for="item in sortedItems || items" :key="item.id" block @click="editItem(item)">
<v-icon v-if="relation.sort_field" name="drag_handle" class="drag-handle" left @click.stop="() => {}" />
<render-template :item="item" :template="templateWithDefaults" />
<div class="spacer" />
<v-icon name="close" @click.stop="deleteItem(item)" />
</v-list-item>
</draggable>
</v-list>
<div class="actions" v-if="!disabled">
<v-button class="new" @click="editModalActive = true">{{ $t('create_new') }}</v-button>
@@ -78,6 +57,7 @@ import { defineComponent, ref, computed, watch, PropType, toRefs } from '@vue/co
import DrawerItem from '@/views/private/components/drawer-item';
import DrawerCollection from '@/views/private/components/drawer-collection';
import { get } from 'lodash';
import Draggable from 'vuedraggable';
import useActions from './use-actions';
import useRelation from './use-relation';
@@ -85,9 +65,10 @@ import usePreview from './use-preview';
import useEdit from './use-edit';
import useSelection from './use-selection';
import useSort from './use-sort';
import { getFieldsFromTemplate } from '@/utils/get-fields-from-template';
export default defineComponent({
components: { DrawerItem, DrawerCollection },
components: { DrawerItem, DrawerCollection, Draggable },
props: {
value: {
type: Array as PropType<(number | string | Record<string, any>)[] | null>,
@@ -105,9 +86,9 @@ export default defineComponent({
type: String,
required: true,
},
fields: {
type: Array as PropType<string[]>,
default: () => [],
template: {
type: String,
default: null,
},
disabled: {
type: Boolean,
@@ -115,14 +96,16 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { value, collection, field, fields } = toRefs(props);
function emitter(newVal: any[] | null) {
emit('input', newVal);
}
const { value, collection, field } = toRefs(props);
const { junction, junctionCollection, relation, relationCollection, relationInfo } = useRelation(collection, field);
const templateWithDefaults = computed(
() => props.template || junctionCollection.value.meta?.display_template || `{{${junction.value.many_primary}}}`
);
const fields = computed(() => getFieldsFromTemplate(templateWithDefaults.value));
const { deleteItem, getUpdatedItems, getNewItems, getPrimaryKeys, getNewSelectedItems } = useActions(
value,
relationInfo,
@@ -177,7 +160,12 @@ export default defineComponent({
sort,
sortItems,
sortedItems,
templateWithDefaults,
};
function emitter(newVal: any[] | null) {
emit('input', newVal);
}
},
});
</script>

View File

@@ -5,9 +5,9 @@
<div v-else class="form-grid">
<div class="field full">
<p class="type-label">{{ $t('select_fields') }}</p>
<v-field-select
<v-field-template
:collection="junctionCollection"
v-model="fields"
v-model="template"
:inject="junctionCollectionExists ? null : { fields: newFields, collections: newCollections, relations }"
/>
</div>
@@ -50,14 +50,14 @@ export default defineComponent({
setup(props, { emit }) {
const collectionsStore = useCollectionsStore();
const fields = computed({
const template = computed({
get() {
return props.value?.fields;
return props.value?.template;
},
set(newFields: string) {
set(newTemplate: string) {
emit('input', {
...(props.value || {}),
fields: newFields,
template: newTemplate,
});
},
});
@@ -77,7 +77,7 @@ export default defineComponent({
);
});
return { fields, junctionCollection, junctionCollectionExists };
return { template, junctionCollection, junctionCollectionExists };
},
});
</script>

View File

@@ -3,43 +3,22 @@
{{ $t('relationship_not_setup') }}
</v-notice>
<div class="one-to-many" v-else>
<v-table
:loading="loading"
:items="sortedItems || items"
:headers.sync="tableHeaders"
show-resize
inline
:sort.sync="sort"
@update:items="sortItems($event)"
@click:row="editItem"
:disabled="disabled"
:show-manual-sort="relation.sort_field !== null"
:manual-sort-key="relation.sort_field"
>
<template v-for="header in tableHeaders" v-slot:[`item.${header.value}`]="{ item }">
<render-display
:key="header.value"
:value="get(item, header.value)"
:display="header.field.display"
:options="header.field.displayOptions"
:interface="header.field.interface"
:interface-options="header.field.interfaceOptions"
:type="header.field.type"
:collection="relatedCollection.collection"
:field="header.field.field"
/>
</template>
<template #item-append="{ item }">
<v-icon
v-if="!disabled"
name="close"
v-tooltip="$t('deselect')"
class="deselect"
@click.stop="deleteItem(item)"
/>
</template>
</v-table>
<v-list>
<draggable
:force-fallback="true"
:value="sortedItems"
@input="sortItems($event)"
handler=".drag-handle"
:disabled="!relation.sort_field"
>
<v-list-item v-for="item in sortedItems" :key="item.id" block @click="editItem(item)">
<v-icon v-if="relation.sort_field" name="drag_handle" class="drag-handle" left @click.stop="() => {}" />
<render-template :collection="relation.many_collection" :item="item" :template="templateWithDefaults" />
<div class="spacer" />
<v-icon name="close" @click.stop="deleteItem(item)" />
</v-list-item>
</draggable>
</v-list>
<div class="actions" v-if="!disabled">
<v-button class="new" @click="currentlyEditing = '+'">{{ $t('create_new') }}</v-button>
@@ -83,9 +62,11 @@ import { Header, Sort } from '@/components/v-table/types';
import { isEqual, sortBy } from 'lodash';
import { get } from 'lodash';
import { unexpectedError } from '@/utils/unexpected-error';
import { getFieldsFromTemplate } from '@/utils/get-fields-from-template';
import Draggable from 'vuedraggable';
export default defineComponent({
components: { DrawerItem, DrawerCollection },
components: { DrawerItem, DrawerCollection, Draggable },
props: {
value: {
type: Array as PropType<(number | string | Record<string, any>)[] | null>,
@@ -103,9 +84,9 @@ export default defineComponent({
type: String,
required: true,
},
fields: {
type: Array as PropType<string[]>,
default: () => [],
template: {
type: String,
default: null,
},
disabled: {
type: Boolean,
@@ -123,6 +104,11 @@ export default defineComponent({
const { stageSelection, selectModalActive, selectionFilters } = useSelection();
const { sort, sortItems, sortedItems } = useSort();
const templateWithDefaults = computed(
() => props.template || relatedCollection.value.meta?.display_template || `{{${relation.value.many_primary}}}`
);
const fields = computed(() => getFieldsFromTemplate(templateWithDefaults.value));
return {
relation,
tableHeaders,
@@ -142,8 +128,14 @@ export default defineComponent({
sort,
sortedItems,
get,
getItemFromIndex,
templateWithDefaults,
};
function getItemFromIndex(index: number) {
return (sortedItems.value || items.value)[index];
}
function getNewItems() {
const pkField = relatedPrimaryKeyField.value.field;
if (props.value === null) return [];
@@ -203,7 +195,7 @@ export default defineComponent({
}
function useSort() {
const sort = ref<Sort>({ by: relation.value.sort_field || props.fields[0], desc: false });
const sort = ref<Sort>({ by: relation.value.sort_field || fields.value[0], desc: false });
function sortItems(newItems: Record<string, any>[]) {
if (relation.value.sort_field === null) return;
@@ -217,7 +209,7 @@ export default defineComponent({
}
const sortedItems = computed(() => {
if (relation.value.sort_field === null || sort.value.by !== relation.value.sort_field) return null;
if (relation.value.sort_field === null || sort.value.by !== relation.value.sort_field) return items.value;
const desc = sort.value.desc;
const sorted = sortBy(items.value, [relation.value.sort_field]);
@@ -258,14 +250,14 @@ export default defineComponent({
loading.value = true;
const pkField = relatedPrimaryKeyField.value.field;
const fields = [...(props.fields.length > 0 ? props.fields : getDefaultFields())];
const fieldsList = [...(fields.value.length > 0 ? fields.value : getDefaultFields())];
if (fields.includes(pkField) === false) {
fields.push(pkField);
if (fieldsList.includes(pkField) === false) {
fieldsList.push(pkField);
}
if (relation.value.sort_field !== null && fields.includes(relation.value.sort_field) === false)
fields.push(relation.value.sort_field);
if (relation.value.sort_field !== null && fieldsList.includes(relation.value.sort_field) === false)
fieldsList.push(relation.value.sort_field);
try {
const endpoint = relatedCollection.value.collection.startsWith('directus_')
@@ -279,7 +271,7 @@ export default defineComponent({
if (primaryKeys && primaryKeys.length > 0) {
const response = await api.get(endpoint, {
params: {
fields: fields,
fields: fieldsList,
[`filter[${pkField}][_in]`]: primaryKeys.join(','),
},
});
@@ -316,9 +308,9 @@ export default defineComponent({
// Seeing we don't care about saving those tableHeaders, we can reset it whenever the
// fields prop changes (most likely when we're navigating to a different o2m context)
watch(
() => props.fields,
() => fields.value,
() => {
tableHeaders.value = (props.fields.length > 0 ? props.fields : getDefaultFields())
tableHeaders.value = (fields.value.length > 0 ? fields.value : getDefaultFields())
.map((fieldKey) => {
const field = fieldsStore.getField(relatedCollection.value.collection, fieldKey);
@@ -456,11 +448,11 @@ export default defineComponent({
<style lang="scss" scoped>
.actions {
margin-top: 12px;
margin-top: 8px;
}
.existing {
margin-left: 12px;
margin-left: 8px;
}
.deselect {

View File

@@ -3,14 +3,19 @@
{{ $t('interfaces.one-to-many.no_collection') }}
</v-notice>
<div v-else class="form-grid">
<div class="field full">
<div class="field half-left">
<p class="type-label">{{ $t('select_fields') }}</p>
<v-field-select
<v-field-template
:collection="relatedCollection"
v-model="fields"
v-model="template"
:inject="relatedCollectionExists ? null : { fields: newFields, collections: newCollections, relations }"
/>
</div>
<div class="field half-right">
<p class="type-label">{{ $t('order') }}</p>
<v-field-select v-model="order" :collection="relatedCollection" />
</div>
</div>
</template>
@@ -49,14 +54,26 @@ export default defineComponent({
setup(props, { emit }) {
const collectionsStore = useCollectionsStore();
const fields = computed({
const template = computed({
get() {
return props.value?.fields;
return props.value?.template;
},
set(newFields: string) {
set(newTemplate: string) {
emit('input', {
...(props.value || {}),
fields: newFields,
template: newTemplate,
});
},
});
const order = computed({
get() {
return props.value?.order;
},
set(newTemplate: string) {
emit('input', {
...(props.value || {}),
order: newTemplate,
});
},
});
@@ -76,7 +93,7 @@ export default defineComponent({
);
});
return { fields, relatedCollection, relatedCollectionExists };
return { template, order, relatedCollection, relatedCollectionExists };
},
});
</script>

View File

@@ -1,49 +0,0 @@
<template>
<div class="form">
<v-divider />
<v-form :disabled="disabled" :fields="fields" :edits="value" primary-key="+" @input="$emit('input', $event)" />
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { Field } from '@/types';
export default defineComponent({
props: {
value: {
type: Object,
default: null,
},
fields: {
type: Array as PropType<Partial<Field>[]>,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
},
});
</script>
<style lang="scss" scoped>
@import '@/styles/mixins/type-styles.scss';
.form {
--form-vertical-gap: 24px;
--form-horizontal-gap: 24px;
padding: 12px;
padding-top: 0;
::v-deep .type-label {
font-weight: 600;
@include type-text;
}
}
.v-divider {
margin-bottom: 12px;
}
</style>

View File

@@ -1,92 +0,0 @@
<template>
<div :class="{ subdued: !displayValue }" class="header type-text" @click="toggle">
<v-icon v-if="disabled === false" name="drag_handle" class="drag-handle" />
{{ displayValue ? displayValue : placeholder }}
<span class="spacer" />
<v-icon v-if="disabled === false" name="close" class="delete" @click.stop.prevent="$emit('delete')" />
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { render } from 'micromustache';
import { getFieldsFromTemplate } from '@/utils/get-fields-from-template';
export default defineComponent({
props: {
value: {
type: Object,
default: null,
},
template: {
type: String,
required: true,
},
placeholder: {
type: String,
default: null,
},
toggle: {
type: Function,
required: true,
},
active: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
setup(props) {
const fieldsInTemplate = computed(() => getFieldsFromTemplate(props.template));
const displayValue = computed(() => {
if (!props.value) return;
for (const [key, value] of Object.entries(props.value)) {
if (fieldsInTemplate.value.includes(key) === false) continue;
if (value === undefined) return null;
}
return render(props.template, props.value);
});
return { displayValue };
},
});
</script>
<style lang="scss" scoped>
.header {
display: flex;
align-items: center;
padding: 12px;
cursor: pointer;
}
.spacer {
flex-grow: 1;
}
.subdued {
color: var(--foreground-subdued);
}
.v-icon {
--v-icon-color: var(--foreground-subdued);
}
.drag-handle {
margin-right: 8px;
&:hover {
--v-icon-color: var(--foreground-normal);
}
}
.delete:hover {
--v-icon-color: var(--danger);
}
</style>

View File

@@ -1,76 +0,0 @@
<template>
<v-item class="row" v-slot:default="{ active, toggle }" :active="initialActive" :watch="false">
<repeater-row-header
:template="template"
:value="value"
:active="active"
:toggle="toggle"
@delete="$emit('delete')"
:disabled="disabled"
:placeholder="headerPlaceholder"
/>
<transition-expand>
<div v-if="active">
<repeater-row-form :disabled="disabled" :fields="fields" :value="value" @input="$emit('input', $event)" />
</div>
</transition-expand>
</v-item>
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { Field } from '@/types';
import RepeaterRowHeader from './repeater-row-header.vue';
import RepeaterRowForm from './repeater-row-form.vue';
export default defineComponent({
components: { RepeaterRowHeader, RepeaterRowForm },
props: {
value: {
type: Object,
default: null,
},
fields: {
type: Array as PropType<Partial<Field>[]>,
default: () => [],
},
initialActive: {
type: Boolean,
default: false,
},
template: {
type: String,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
headerPlaceholder: {
type: String,
default: null,
},
},
});
</script>
<style lang="scss" scoped>
.row {
--background-page: var(--background-subdued);
background-color: var(--card-face-color);
box-shadow: var(--card-shadow);
border-radius: var(--border-radius);
& + .row {
margin-top: 8px;
}
.repeater {
.row {
background-color: var(--background-page);
border-color: var(--border-normal);
}
}
}
</style>

View File

@@ -1,37 +1,56 @@
<template>
<v-item-group class="repeater">
<draggable :value="value" handle=".drag-handle" @input="onSort" :set-data="hideDragImage">
<repeater-row
v-for="(row, index) in value"
:force-fallback="true"
:key="index"
:value="row"
:template="_template"
:fields="fields"
@input="updateValues(index, $event)"
@delete="removeItem(row)"
:disabled="disabled"
:headerPlaceholder="headerPlaceholder"
:initialActive="addedIndex === index"
/>
</draggable>
<button @click="addNew" class="add-new" v-if="showAddNew">
<div class="repeater">
<v-list>
<draggable :force-fallback="true" :value="value" @input="$emit('input', $event)" handler=".drag-handle">
<v-list-item v-for="(item, index) in value" :key="item.id" block @click="active = index">
<v-icon name="drag_handle" class="drag-handle" left @click.stop="() => {}" />
<render-template :fields="fields" :item="item" :template="templateWithDefaults" />
<div class="spacer" />
<v-icon name="close" @click.stop="removeItem(item)" />
</v-list-item>
</draggable>
</v-list>
<v-button @click="addNew" class="add-new" v-if="showAddNew">
<v-icon name="add" />
{{ addLabel }}
</button>
</v-item-group>
</v-button>
<v-drawer
:active="drawerOpen"
@toggle="closeDrawer()"
:title="displayValue || headerPlaceholder"
persistent
@cancel="closeDrawer()"
>
<template #actions>
<v-button @click="closeDrawer()" icon rounded v-tooltip.bottom="$t('save')">
<v-icon name="check" />
</v-button>
</template>
<div class="drawer-item-content">
<v-form
:disabled="disabled"
:fields="fields"
:edits="activeItem"
primary-key="+"
@input="updateValues(active, $event)"
/>
</div>
</v-drawer>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed, ref } from '@vue/composition-api';
import RepeaterRow from './repeater-row.vue';
import { defineComponent, PropType, computed, ref, toRefs } from '@vue/composition-api';
import { Field } from '@/types';
import Draggable from 'vuedraggable';
import i18n from '@/lang';
import { renderStringTemplate } from '@/utils/render-string-template';
import hideDragImage from '@/utils/hide-drag-image';
export default defineComponent({
components: { RepeaterRow, Draggable },
components: { Draggable },
props: {
value: {
type: Array as PropType<Record<string, any>[]>,
@@ -61,14 +80,17 @@ export default defineComponent({
type: String,
default: i18n.t('empty_item'),
},
collection: {
type: String,
default: null,
},
},
setup(props, { emit }) {
const addedIndex = ref<number | null>(null);
const active = ref<number | null>(null);
const drawerOpen = computed(() => active.value !== null);
const { value } = toRefs(props);
const _template = computed(() => {
if (props.template === null) return props.fields.length > 0 ? `{{${props.fields[0].field}}}` : '';
return props.template;
});
const templateWithDefaults = computed(() => props.template || `{{${props.fields[0].field}}}`);
const showAddNew = computed(() => {
if (props.disabled) return false;
@@ -78,7 +100,24 @@ export default defineComponent({
return false;
});
return { updateValues, onSort, removeItem, addNew, showAddNew, hideDragImage, addedIndex, _template };
const activeItem = computed(() => (active.value !== null ? value.value[active.value] : null));
const { displayValue } = renderStringTemplate(templateWithDefaults, activeItem);
return {
updateValues,
removeItem,
addNew,
showAddNew,
hideDragImage,
active,
drawerOpen,
displayValue,
activeItem,
closeDrawer,
onSort,
templateWithDefaults,
};
function updateValues(index: number, updatedValues: any) {
emitValue(
@@ -92,14 +131,9 @@ export default defineComponent({
);
}
function onSort(sortedItems: any[]) {
emitValue(sortedItems);
}
function removeItem(row: any) {
addedIndex.value = null;
if (props.value) {
emitValue(props.value.filter((existingItem) => existingItem !== row));
function removeItem(item: Record<string, any>) {
if (value.value) {
emitValue(props.value.filter((i) => i !== item));
} else {
emitValue(null);
}
@@ -113,13 +147,13 @@ export default defineComponent({
newDefaults[field.field!] = field.schema?.default_value;
});
addedIndex.value = props.value === null ? 0 : props.value.length;
if (props.value !== null) {
emitValue([...props.value, newDefaults]);
} else {
emitValue([newDefaults]);
}
active.value = (props.value || []).length;
}
function emitValue(value: null | any[]) {
@@ -129,31 +163,39 @@ export default defineComponent({
return emit('input', value);
}
function onSort(sortedItems: any[]) {
if (sortedItems === null || sortedItems.length === 0) {
return emit('input', null);
}
return emit('input', sortedItems);
}
function closeDrawer() {
active.value = null;
}
},
});
</script>
<style lang="scss" scoped>
.add-new {
.v-list-item {
display: flex;
align-items: center;
width: 100%;
height: 48px;
color: var(--foreground-normal);
cursor: pointer;
}
.drag-handle {
cursor: grap;
}
.drawer-item-content {
padding: var(--content-padding);
padding-bottom: var(--content-padding-bottom);
}
.add-new {
margin-top: 8px;
padding: 10px; // 10 not 12, offset for border
color: var(--foreground-subdued);
border: 2px dashed var(--border-normal);
border-radius: var(--border-radius);
transition: var(--fast) var(--transition);
transition-property: color, border-color;
.v-icon {
margin-right: 8px;
}
&:hover {
color: var(--primary);
border-color: var(--primary);
}
}
</style>

View File

@@ -3,18 +3,19 @@
<v-skeleton-loader v-for="n in 5" :key="n" />
</div>
<div class="translations" v-else>
<button
<v-list class="translations" v-else>
<v-list-item
v-for="languageItem in languages"
:key="languageItem[languagesPrimaryKeyField]"
@click="startEditing(languageItem[languagesPrimaryKeyField])"
class="language-row"
block
>
<v-icon class="translate" name="translate" />
<v-icon class="translate" name="translate" left />
<render-template :template="languagesTemplate" :collection="languagesCollection" :item="languageItem" />
<div class="spacer" />
<v-icon class="launch" name="launch" />
</button>
</v-list-item>
<drawer-item
v-if="editing"
@@ -26,7 +27,7 @@
@input="stageEdits"
@update:active="cancelEdit"
/>
</div>
</v-list>
</template>
<script lang="ts">
@@ -334,28 +335,6 @@ export default defineComponent({
<style lang="scss" scoped>
.language-row {
--v-icon-color: var(--foreground-subdued);
display: flex;
align-items: center;
width: 100%;
padding: 12px;
text-align: left;
background-color: var(--background-subdued);
border-radius: var(--border-radius);
& + & {
margin-top: 8px;
}
.translate {
margin-right: 12px;
}
.spacer {
flex-grow: 1;
}
.launch {
transition: color var(--fast) var(--transition);
}

View File

@@ -12,6 +12,7 @@
@end="drag = false"
:set-data="hideDragImage"
:disabled="disabled"
:force-fallback="true"
@change="$emit('change', $event)"
>
<li class="row" v-for="(item, index) in tree" :key="item.id">

View File

@@ -1,6 +1,5 @@
<template>
<div>
<div class="form">
<div class="field half-left" v-if="fieldData.meta">
<div class="label type-label">{{ $t('readonly') }}</div>
@@ -21,7 +20,7 @@
<div class="label type-label">{{ $t('field_name_translations') }}</div>
<interface-repeater
v-model="fieldData.meta.translations"
:template="'{{ translation }} ({{ language }})'"
:template="'[{{ language }}] {{ translation }}'"
:fields="[
{
field: 'language',
@@ -30,6 +29,11 @@
meta: {
interface: 'system-language',
width: 'half',
display: 'formatted-value',
display_options: {
font: 'monospace',
color: 'var(--foreground-subdued)',
},
},
schema: {
default_value: 'en-US',
@@ -55,10 +59,7 @@
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import useSync from '@/composables/use-sync';
import { types } from '@/types';
import i18n from '@/lang';
import { defineComponent } from '@vue/composition-api';
import { state } from '../store';
export default defineComponent({

View File

@@ -30,7 +30,11 @@ export default defineComponent({
props: {
collection: {
type: String,
required: true,
default: null,
},
fields: {
type: Array as PropType<Field[]>,
default: null,
},
item: {
type: Object as PropType<Record<string, any>>,
@@ -47,6 +51,20 @@ export default defineComponent({
const regex = /({{.*?}})/g;
const fields = computed(() => {
const fields: Field[] = [];
if (props.collection) {
fields.push(...fieldsStore.getFieldsForCollection(props.collection));
}
if (props.fields) {
fields.push(...props.fields);
}
return fields;
});
const parts = computed(() =>
props.template
.split(regex)
@@ -55,18 +73,14 @@ export default defineComponent({
if (part.startsWith('{{') === false) return part;
const fieldKey = part.replace(/{{/g, '').replace(/}}/g, '').trim();
const field: Field | null = fieldsStore.getField(props.collection, fieldKey);
// Instead of crashing when the field doesn't exist, we'll render a couple question
// marks to indicate it's absence
if (!field) return null;
const field: Field | undefined = fields.value.find((field) => field.field === fieldKey);
// Try getting the value from the item, return some question marks if it doesn't exist
const value = get(props.item, fieldKey);
if (value === undefined) return null;
// If no display is configured, we can render the raw value
if (field.meta?.display === null) return value;
if (!field || field.meta?.display === null) return value;
const displayInfo = displays.value.find((display) => display.id === field.meta?.display);