mirror of
https://github.com/directus/directus.git
synced 2026-01-28 11:07:58 -05:00
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:
@@ -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;
|
||||
|
||||
44
src/displays/template/index.ts
Normal file
44
src/displays/template/index.ts
Normal 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;
|
||||
},
|
||||
}));
|
||||
121
src/displays/template/template.vue
Normal file
121
src/displays/template/template.vue
Normal 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>
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
22
src/utils/get-related-collection/get-related-collection.ts
Normal file
22
src/utils/get-related-collection/get-related-collection.ts
Normal 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;
|
||||
}
|
||||
4
src/utils/get-related-collection/index.ts
Normal file
4
src/utils/get-related-collection/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import getRelatedCollection from './get-related-collection';
|
||||
|
||||
export { getRelatedCollection };
|
||||
export default getRelatedCollection;
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user