Display template display template display (#551)

* Add util to find related collection

* Allow display.fields to be function

* Return fixed relation for user types

* Pass type / collection / field to displays

* Add template display

* Finish display template display template display
This commit is contained in:
Rijk van Zanten
2020-05-11 17:05:11 -04:00
committed by GitHub
parent 3aec11c42a
commit 5c00ddddb1
11 changed files with 279 additions and 33 deletions

View File

@@ -8,6 +8,7 @@ import DisplayImage from './image';
import DisplayUser from './user';
import DisplayRating from './rating';
import DisplayDateTime from './datetime';
import DisplayTemplate from './template';
export const displays = [
DisplayIcon,
@@ -20,5 +21,6 @@ export const displays = [
DisplayUser,
DisplayRating,
DisplayDateTime,
DisplayTemplate,
];
export default displays;

View File

@@ -0,0 +1,44 @@
import { defineDisplay } from '@/displays/define';
import DisplayTemplate from './template.vue';
import getFieldsFromTemplate from '@/utils/get-fields-from-template';
import adjustFieldsForDisplays from '@/utils/adjust-fields-for-displays';
import getRelatedCollection from '@/utils/get-related-collection';
import useCollection from '@/composables/use-collection';
import { ref } from '@vue/composition-api';
type Options = {
template: string;
};
export default defineDisplay(({ i18n }) => ({
id: 'template',
name: i18n.t('template'),
icon: 'text_fields',
handler: DisplayTemplate,
options: [
{
field: 'template',
name: i18n.t('template'),
interface: 'text-input',
width: 'full',
},
],
types: ['string'],
fields: (options: Options, { field, collection }) => {
const relatedCollection = getRelatedCollection(collection, field);
const { primaryKeyField } = useCollection(ref(relatedCollection));
if (!relatedCollection) return [];
const fields = adjustFieldsForDisplays(
getFieldsFromTemplate(options.template),
relatedCollection
);
if (fields.includes(primaryKeyField.value.field) === false) {
fields.push(primaryKeyField.value.field);
}
return fields;
},
}));

View File

@@ -0,0 +1,121 @@
<template>
<value-null v-if="!relatedCollection" />
<v-menu v-else-if="type.toLowerCase() === 'o2m'" show-arrow :disabled="value.length === 0">
<template #activator="{ toggle }">
<span @click.stop="toggle" class="toggle" :class="{ subdued: value.length === 0 }">
<span class="label">{{ $tc('item_count', value.length) }}</span>
</span>
</template>
<v-list dense>
<v-list-item
v-for="item in value"
:key="item[primaryKeyField]"
:to="getLinkForItem(item)"
>
<v-list-item-content>
<render-template
:template="template"
:item="item"
:collection="relatedCollection"
/>
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
<render-template v-else :template="template" :item="value" :collection="relatedCollection" />
</template>
<script lang="ts">
import { defineComponent, computed, PropType, Ref } from '@vue/composition-api';
import getRelatedCollection from '@/utils/get-related-collection';
import useProjectsStore from '@/stores/projects';
import useCollection from '@/composables/use-collection';
import ValueNull from '@/views/private/components/value-null';
export default defineComponent({
components: { ValueNull },
props: {
collection: {
type: String,
required: true,
},
field: {
type: String,
required: true,
},
value: {
type: [Array, Object] as PropType<any | any[]>,
default: null,
},
template: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
},
setup(props) {
const projectsStore = useProjectsStore();
const relatedCollection = computed(() => {
return getRelatedCollection(props.collection, props.field);
});
const primaryKeyField = computed(() => {
if (relatedCollection.value !== null) {
return useCollection(relatedCollection as Ref<string>).primaryKeyField.value;
}
});
return { relatedCollection, primaryKeyField, getLinkForItem };
function getLinkForItem(item: any) {
if (!relatedCollection.value || !primaryKeyField.value) return null;
const { currentProjectKey } = projectsStore.state;
const primaryKey = item[primaryKeyField.value.field];
return `/${currentProjectKey}/collections/${relatedCollection.value}/${primaryKey}`;
}
},
});
</script>
<style lang="scss" scoped>
.toggle {
position: relative;
&::before {
position: absolute;
top: -6px;
left: -6px;
z-index: 1;
width: calc(100% + 12px);
height: calc(100% + 12px);
background-color: var(--background-normal);
border-radius: var(--border-radius);
opacity: 0;
transition: opacity var(--fast) var(--transition);
content: '';
}
.label {
position: relative;
z-index: 2;
}
&:not(.subdued):hover::before {
opacity: 1;
}
&:not(.subdued):active::before {
background-color: var(--background-normal-alt);
}
}
.subdued {
color: var(--foreground-subdued);
}
</style>

View File

@@ -12,6 +12,15 @@ export type DisplayHandlerFunction = (
context: DisplayHandlerFunctionContext
) => string | null;
export type DisplayFieldsFunction = (
options: any,
context: {
collection: string;
field: string;
type: string;
}
) => string[];
export type DisplayConfig = {
id: string;
icon: string;
@@ -20,7 +29,7 @@ export type DisplayConfig = {
handler: DisplayHandlerFunction | Component;
options: null | Partial<Field>[] | Component;
types: string[];
fields?: string[];
fields?: string[] | DisplayFieldsFunction;
};
export type DisplayContext = { i18n: VueI18n };

View File

@@ -94,6 +94,8 @@
:interface="header.field.interface"
:interface-options="header.field.interfaceOptions"
:type="header.field.type"
:collection="collection"
:field="header.field.field"
/>
</template>
@@ -429,6 +431,7 @@ export default defineComponent({
interface: field.interface,
interfaceOptions: field.options,
type: field.type,
field: field.field,
},
}));
},

View File

@@ -47,6 +47,18 @@ export const useRelationsStore = createStore({
] as Relation[];
}
if (['user', 'user_created', 'user_updated', 'owner'].includes(fieldInfo.type)) {
return [
{
collection_many: collection,
field_many: field,
collection_one: 'directus_users',
field_one: null,
junction_field: null,
},
] as Relation[];
}
return this.getRelationsForCollection(collection).filter((relation: Relation) => {
return relation.field_many === field || relation.field_one === field;
});

View File

@@ -1,5 +1,6 @@
import useFieldsStore from '@/stores/fields';
import displays from '@/displays';
import { Field } from '@/stores/fields/types';
export default function adjustFieldsForDisplays(
fields: readonly string[],
@@ -7,9 +8,9 @@ export default function adjustFieldsForDisplays(
) {
const fieldsStore = useFieldsStore();
const adjustedFields = fields
const adjustedFields: string[] = fields
.map((fieldKey) => {
const field = fieldsStore.getField(parentCollection, fieldKey);
const field: Field = fieldsStore.getField(parentCollection, fieldKey);
if (!field) return fieldKey;
if (field.display === null) return fieldKey;
@@ -24,6 +25,16 @@ export default function adjustFieldsForDisplays(
);
}
if (typeof display.fields === 'function') {
return display
.fields(field.display_options, {
collection: parentCollection,
field: fieldKey,
type: field.type,
})
.map((relatedFieldKey: string) => `${fieldKey}.${relatedFieldKey}`);
}
return fieldKey;
})
.flat();

View File

@@ -0,0 +1,22 @@
import useRelationsStore from '@/stores/relations';
import useFieldsStore from '@/stores/fields';
export default function getRelatedCollection(collection: string, field: string) {
const relationsStore = useRelationsStore();
const fieldsStore = useFieldsStore();
const relations = relationsStore.getRelationsForField(collection, field);
const fieldInfo = fieldsStore.getField(collection, field);
const type = fieldInfo.type.toLowerCase();
let relatedCollection: string | null = null;
if (['user', 'user_updated', 'owner', 'file', 'm2o'].includes(type)) {
relatedCollection = relations[0].collection_one;
} else if (type === 'o2m') {
relatedCollection = relations[0].collection_many;
}
return relatedCollection;
}

View File

@@ -0,0 +1,4 @@
import getRelatedCollection from './get-related-collection';
export { getRelatedCollection };
export default getRelatedCollection;

View File

@@ -12,6 +12,8 @@
:interface-options="interfaceOptions"
:value="value"
:type="type"
:collection="collection"
:field="field"
/>
</template>
@@ -47,6 +49,14 @@ export default defineComponent({
type: String,
required: true,
},
collection: {
type: String,
required: true,
},
field: {
type: String,
required: true,
},
},
setup(props) {
const displayInfo = computed(

View File

@@ -12,7 +12,7 @@
:type="part.type"
v-bind="part.options"
/>
<template v-else>{{ part }}</template>
<span v-else :key="index" class="raw">{{ part }}</span>
</template>
</div>
</template>
@@ -47,43 +47,47 @@ export default defineComponent({
const regex = /({{.*?}})/g;
const parts = computed(() =>
props.template.split(regex).map((part) => {
if (part.startsWith('{{') === false) return part;
props.template
.split(regex)
.map((part) => {
if (part.startsWith('{{') === false) return part;
const fieldKey = part.replace(/{{/g, '').replace(/}}/g, '').trim();
const field: Field | null = fieldsStore.getField(props.collection, fieldKey);
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 '???';
// Instead of crashing when the field doesn't exist, we'll render a couple question
// marks to indicate it's absence
if (!field) return '???';
// 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 '???';
// 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 '???';
// If no display is configured, we can render the raw value
if (field.display === null) return value;
// If no display is configured, we can render the raw value
if (field.display === null) return value;
const displayInfo = displays.find((display) => display.id === field.display);
const displayInfo = displays.find((display) => display.id === field.display);
// If used display doesn't exist in the current project, return raw value
if (!displayInfo) return value;
// If used display doesn't exist in the current project, return raw value
if (!displayInfo) return value;
// If the display handler is a function, we parse the value and return the result
if (typeof displayInfo.handler === 'function') {
const handler = displayInfo.handler as Function;
return handler(value, field.display_options);
}
// If the display handler is a function, we parse the value and return the result
if (typeof displayInfo.handler === 'function') {
const handler = displayInfo.handler as Function;
return handler(value, field.display_options);
}
return {
component: field.display,
options: field.display_options,
value: value,
interface: field.interface,
interfaceOptions: field.options,
type: field.type,
};
})
return {
component: field.display,
options: field.display_options,
value: value,
interface: field.interface,
interfaceOptions: field.options,
type: field.type,
};
})
.map((p) => (typeof p === 'string' ? p.trim() : p))
.filter((p) => p)
);
return { parts };
@@ -99,4 +103,8 @@ export default defineComponent({
.subdued {
color: var(--foreground-subdued);
}
.raw:not(:last-child) {
margin-right: 4px;
}
</style>