Status display (#453)

* Convert render function into component

* Allow types definition in display registration

* Add status-dot interface

* Fix render display rendering in tabular view

* Add types to other displays

* Export tooltip from index

* Start on readme

* Add tests for status dot
This commit is contained in:
Rijk van Zanten
2020-04-22 15:41:00 -04:00
committed by GitHub
parent 273f1bafd0
commit 96db381fbf
14 changed files with 210 additions and 44 deletions

View File

@@ -0,0 +1,4 @@
import Tooltip from './tooltip';
export { Tooltip };
export default Tooltip;

View File

@@ -7,4 +7,5 @@ export default defineDisplay({
icon: 'text_format',
handler: handler,
options: null,
types: ['string'],
});

View File

@@ -14,4 +14,5 @@ export default defineDisplay(({ i18n }) => ({
width: 'half',
},
],
types: ['string'],
}));

View File

@@ -1,5 +1,6 @@
import DisplayIcon from './icon/';
import DisplayFormatTitle from './format-title/';
import DisplayStatusDot from './status-dot/';
export const displays = [DisplayIcon, DisplayFormatTitle];
export const displays = [DisplayIcon, DisplayFormatTitle, DisplayStatusDot];
export default displays;

View File

@@ -0,0 +1,11 @@
import { defineDisplay } from '@/displays/define';
import DisplayStatusDot from './status-dot.vue';
export default defineDisplay(({ i18n }) => ({
id: 'status-dot',
name: i18n.t('status_dot'),
types: ['status'],
icon: 'box',
handler: DisplayStatusDot,
options: null,
}));

View File

@@ -0,0 +1,4 @@
# Status Dot
Renders the background color as set in the status mapping of the status type interface as a small dot.

View File

@@ -0,0 +1,72 @@
import DisplayStatusDot from './status-dot.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VIcon from '@/components/v-icon';
import VueCompositionAPI from '@vue/composition-api';
import Tooltip from '@/directives/tooltip';
const localVue = createLocalVue();
localVue.component('v-icon', VIcon);
localVue.use(VueCompositionAPI);
localVue.directive('tooltip', Tooltip);
describe('Displays / Status Dot', () => {
it('Renders an empty span if no value is passed', () => {
const component = shallowMount(DisplayStatusDot, {
localVue,
propsData: {
value: null,
},
});
expect(component.find('span').exists()).toBe(true);
expect(component.find('span').text()).toBe('');
});
it('Renders a question mark icon is status is unknown in interface options', () => {
const component = shallowMount(DisplayStatusDot, {
localVue,
propsData: {
value: 'draft',
interfaceOptions: {
status_mapping: {
published: {},
},
},
},
});
expect(component.find(VIcon).exists()).toBe(true);
expect(component.attributes('name')).toBe('help_outline');
});
it('Renders the dot with the correct color', () => {
const component = shallowMount(DisplayStatusDot, {
localVue,
propsData: {
value: 'draft',
interfaceOptions: {
status_mapping: {
draft: {
background_color: 'rgb(171, 202, 188)',
},
},
},
},
});
expect(component.exists()).toBe(true);
expect(component.attributes('style')).toBe('background-color: rgb(171, 202, 188);');
});
it('Sets status to null if interface options are missing', () => {
const component = shallowMount(DisplayStatusDot, {
localVue,
propsData: {
value: 'draft',
interfaceOptions: null,
},
});
expect((component.vm as any).status).toBe(null);
});
});

View File

@@ -0,0 +1,47 @@
<template>
<span v-if="!value" />
<v-icon name="help_outline" v-else-if="!status" />
<div
v-else
class="dot"
v-tooltip="status.name"
:style="{
backgroundColor: status.background_color,
}"
/>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
export default defineComponent({
props: {
value: {
type: String,
default: null,
},
interfaceOptions: {
type: Object,
default: null,
},
},
setup(props) {
const status = computed(() => {
if (props.interfaceOptions === null) return null;
return props.interfaceOptions.status_mapping?.[props.value];
});
return { status };
},
});
</script>
<style lang="scss" scoped>
.dot {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 6px;
}
</style>

View File

@@ -12,6 +12,7 @@ export type DisplayConfig = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handler: DisplayHandlerFunction | Component;
options: null | Partial<Field>[] | Component;
types: string[];
};
export type DisplayContext = { i18n: VueI18n };

View File

@@ -272,6 +272,8 @@
"select_statuses": "Select Statuses",
"status_dot": "Status (Dot)",
"about_directus": "About Directus",
"activity_log": "Activity Log",
"add_field_filter": "Add a field filter",

View File

@@ -69,13 +69,17 @@
@update:sort="onSortChange"
>
<template v-for="header in tableHeaders" v-slot:[`item.${header.value}`]="{ item }">
<span :key="header.value" v-if="!header.display">{{ item[header.value] }}</span>
<span :key="header.value" v-if="!header.field.display">
{{ item[header.value] }}
</span>
<render-display
v-else
:key="header.value"
:options="header.display_options"
:value="header.value"
:display="header.display"
:value="item[header.value]"
:display="header.field.display"
:options="header.field.displayOptions"
:interface="header.field.interface"
:interface-options="header.field.interfaceOptions"
/>
</template>
@@ -331,8 +335,12 @@ export default defineComponent({
localWidths.value[field.field] ||
_viewOptions.value?.widths?.[field.field] ||
null,
display: field.display,
display_options: field.display_options,
field: {
display: field.display,
displayOptions: field.display_options,
interface: field.interface,
interfaceOptions: field.options,
},
}));
},
set(val) {

View File

@@ -1,38 +1,4 @@
import { defineComponent } from '@vue/composition-api';
import displays from '@/displays';
import { DisplayHandlerFunction } from '@/displays/types';
import RenderDisplay from './render-display.vue';
export default defineComponent({
props: {
display: {
type: String,
default: null,
},
options: {
type: Object,
default: () => ({}),
},
value: {
type: [String, Number, Object, Array],
default: null,
},
},
render(createElement, { props }) {
const display = displays.find((display) => display.id === props.display);
if (!display) {
return props.value;
}
if (typeof display.handler === 'function') {
return (display.handler as DisplayHandlerFunction)(props.value, props.options);
}
return createElement(`display-${props.display}`, {
props: {
...props.options,
value: props.value,
},
});
},
});
export { RenderDisplay };
export default RenderDisplay;

View File

@@ -0,0 +1,48 @@
<template>
<span v-if="!displayInfo">{{ value }}</span>
<span v-else-if="typeof displayInfo.handler === 'function'">
{{ display.handler(value, options) }}
</span>
<component
v-else
:is="`display-${display}`"
v-bind="options"
:interface="$props.interface"
:interface-options="interfaceOptions"
:value="value"
/>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import displays from '@/displays';
export default defineComponent({
props: {
display: {
type: String,
default: null,
},
options: {
type: Object,
default: () => ({}),
},
interface: {
type: String,
default: null,
},
interfaceOptions: {
type: Object,
default: null,
},
value: {
type: [String, Number, Object, Array],
default: null,
},
},
setup(props) {
const displayInfo = displays.find((display) => display.id === props.display) || null;
return { displayInfo };
},
});
</script>