mirror of
https://github.com/directus/directus.git
synced 2026-01-27 18:18:09 -05:00
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:
4
src/directives/tooltip/index.ts
Normal file
4
src/directives/tooltip/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import Tooltip from './tooltip';
|
||||
|
||||
export { Tooltip };
|
||||
export default Tooltip;
|
||||
@@ -7,4 +7,5 @@ export default defineDisplay({
|
||||
icon: 'text_format',
|
||||
handler: handler,
|
||||
options: null,
|
||||
types: ['string'],
|
||||
});
|
||||
|
||||
@@ -14,4 +14,5 @@ export default defineDisplay(({ i18n }) => ({
|
||||
width: 'half',
|
||||
},
|
||||
],
|
||||
types: ['string'],
|
||||
}));
|
||||
|
||||
@@ -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;
|
||||
|
||||
11
src/displays/status-dot/index.ts
Normal file
11
src/displays/status-dot/index.ts
Normal 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,
|
||||
}));
|
||||
4
src/displays/status-dot/readme.md
Normal file
4
src/displays/status-dot/readme.md
Normal 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.
|
||||
|
||||
0
src/displays/status-dot/status-dot.story.ts
Normal file
0
src/displays/status-dot/status-dot.story.ts
Normal file
72
src/displays/status-dot/status-dot.test.ts
Normal file
72
src/displays/status-dot/status-dot.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
47
src/displays/status-dot/status-dot.vue
Normal file
47
src/displays/status-dot/status-dot.vue
Normal 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>
|
||||
@@ -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 };
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user