mirror of
https://github.com/directus/directus.git
synced 2026-04-03 03:00:39 -04:00
Interfaces registration (#155)
* Add interface registration logic * Register interfaces in main * Add basic text input example * Add storybook knob for placeholder * Add test for text-input * Fix tests
This commit is contained in:
@@ -13,13 +13,14 @@ The HTML `<input>` element supports a huge amount of attributes and events. In o
|
||||
You can add any custom (text) prefix/suffix to the value in the input using the `prefix` and `suffix` slots.
|
||||
|
||||
## Props
|
||||
| Prop | Description | Default |
|
||||
|-------------|------------------------------------------------|---------|
|
||||
| `autofocus` | Autofocusses the input on render | `false` |
|
||||
| `disabled` | Set the disabled state for the input | `false` |
|
||||
| `monospace` | Render the entered value in the monospace font | `false` |
|
||||
| `prefix` | Prefix the users value with a value | -- |
|
||||
| `suffix` | Show a value at the end of the input | -- |
|
||||
| Prop | Description | Default |
|
||||
|--------------|------------------------------------------------|---------|
|
||||
| `autofocus` | Autofocusses the input on render | `false` |
|
||||
| `disabled` | Set the disabled state for the input | `false` |
|
||||
| `monospace` | Render the entered value in the monospace font | `false` |
|
||||
| `full-width` | Render the input with 100% width | `false` |
|
||||
| `prefix` | Prefix the users value with a value | -- |
|
||||
| `suffix` | Show a value at the end of the input | -- |
|
||||
|
||||
Note: all other attached attributes are bound to the input HTMLELement in the component. This allows you to attach any of the standard HTML attributes like `min`, `length`, or `pattern`.
|
||||
|
||||
|
||||
13
src/interfaces/define.ts
Normal file
13
src/interfaces/define.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { i18n } from '@/lang';
|
||||
import { Interface, InterfaceOptions, InterfaceContext } from './types';
|
||||
|
||||
export function defineInterface(options: InterfaceOptions): Interface {
|
||||
const context: InterfaceContext = { i18n };
|
||||
|
||||
const config = {
|
||||
id: options.id,
|
||||
...options.register(context)
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
4
src/interfaces/index.ts
Normal file
4
src/interfaces/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import InterfaceTextInput from './text-input/';
|
||||
|
||||
export const interfaces = [InterfaceTextInput];
|
||||
export default interfaces;
|
||||
62
src/interfaces/readme.md
Normal file
62
src/interfaces/readme.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Interfaces
|
||||
Interfaces are the individual blocks that allow editing and viewing individual pieces of data. They
|
||||
can be seen as the individual fields in a form, where the field is a single column in a table.
|
||||
|
||||
## Defining interfaces
|
||||
Interfaces need to be defined through the `defineInterface` function. This allows the interface to
|
||||
register things like it's name, options, and the way it displays data across the platform.
|
||||
|
||||
```js
|
||||
export default defineInterface({
|
||||
id: 'text-input',
|
||||
register: ({ i18n }) => ({
|
||||
name: i18n.t('interfaces.text-input.text-input'),
|
||||
icon: 'box',
|
||||
component: InterfaceTextInput,
|
||||
display: value => {
|
||||
return formatTitle(value);
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### `id`
|
||||
Unique ID for the interface within the platform. This is not shown to the end user, but is used
|
||||
internally to build up forms and layouts.
|
||||
|
||||
### `register`
|
||||
Callback function that allows the interface to register it's options and other user-facing parameters.
|
||||
|
||||
The one parameter that the register function gets is `context`. Context holds the following properties:
|
||||
|
||||
| Property | Description |
|
||||
|----------|---------------------------------------------------------------------------------------------------------|
|
||||
| `i18n` | The internal vue-i18n instance. Can be used to return a translated name or translated interface options |
|
||||
|
||||
#### `name`
|
||||
The user-facing name of the interface. By using the `i18n` handler from context, you can make this
|
||||
localized.
|
||||
|
||||
#### `icon`
|
||||
The icon that's shown when refering to this interface. It's most prominent usage is in the field-setup
|
||||
wizard.
|
||||
|
||||
#### `component`
|
||||
The Vue component that makes up the input of the interface. This is the component that will be rendered
|
||||
in the edit form.
|
||||
|
||||
#### `display`
|
||||
Next to the actual input, interfaces have the ability to define the way it's value it's shown elsewhere
|
||||
in the platform. This can be useful if the data needs to be manipulated before it can shown to the
|
||||
end user in a way that makes sense. For example, one might want to convert a hex value into a color
|
||||
swatch when the value is being shown in the platform.
|
||||
|
||||
This value can either be:
|
||||
|
||||
* `null`
|
||||
render the raw value as stored in the database
|
||||
* `(val) => string | number`
|
||||
a callback function that converts the value before being shown
|
||||
* Vue Component
|
||||
a custom Vue component that will be rendered inline. Ideally this is a functional component, to
|
||||
ensure performance when looking at large datasets.
|
||||
11
src/interfaces/register.ts
Normal file
11
src/interfaces/register.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import registerComponent from '@/utils/register-component';
|
||||
import interfaces from './index';
|
||||
|
||||
// inter, cause interface is reserved keyword in JS... o_o
|
||||
interfaces.forEach(inter => {
|
||||
registerComponent('interface-' + inter.id, inter.component);
|
||||
|
||||
if (inter.display && typeof inter.display === 'object') {
|
||||
registerComponent('display-' + inter.id, inter.display);
|
||||
}
|
||||
});
|
||||
15
src/interfaces/text-input/index.ts
Normal file
15
src/interfaces/text-input/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import InterfaceTextInput from './text-input.vue';
|
||||
import { defineInterface } from '@/interfaces/define';
|
||||
import formatTitle from '@directus/format-title';
|
||||
|
||||
export default defineInterface({
|
||||
id: 'text-input',
|
||||
register: ({ i18n }) => ({
|
||||
name: i18n.t('interfaces.text-input.text-input'),
|
||||
icon: 'box',
|
||||
component: InterfaceTextInput,
|
||||
display: value => {
|
||||
return formatTitle(value);
|
||||
}
|
||||
})
|
||||
});
|
||||
1
src/interfaces/text-input/readme.md
Normal file
1
src/interfaces/text-input/readme.md
Normal file
@@ -0,0 +1 @@
|
||||
# Text Input
|
||||
45
src/interfaces/text-input/text-input.story.ts
Normal file
45
src/interfaces/text-input/text-input.story.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { withKnobs, boolean, text } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import Vue from 'vue';
|
||||
import InterfaceTextInput from './text-input.vue';
|
||||
import markdown from './readme.md';
|
||||
import withPadding from '../../../.storybook/decorators/with-padding';
|
||||
import { createComponent } from '@vue/composition-api';
|
||||
|
||||
Vue.component('interface-text-input', InterfaceTextInput);
|
||||
|
||||
export default {
|
||||
title: 'Interfaces / Text Input',
|
||||
decorators: [withKnobs, withPadding],
|
||||
parameters: {
|
||||
notes: markdown
|
||||
}
|
||||
};
|
||||
|
||||
export const basic = () =>
|
||||
createComponent({
|
||||
props: {
|
||||
monospace: {
|
||||
default: boolean('Monospace', false, 'Options')
|
||||
},
|
||||
trim: {
|
||||
default: boolean('Trim', false, 'Options')
|
||||
},
|
||||
showCharacterCount: {
|
||||
default: boolean('Show Character Count', false, 'Options')
|
||||
},
|
||||
placeholder: {
|
||||
default: text('Placeholder', 'Enter a value...', 'Options')
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const onInput = action('input');
|
||||
return { onInput };
|
||||
},
|
||||
template: `
|
||||
<interface-text-input
|
||||
:options="{ monospace, trim, showCharacterCount, placeholder }"
|
||||
@input="onInput"
|
||||
/>
|
||||
`
|
||||
});
|
||||
29
src/interfaces/text-input/text-input.test.ts
Normal file
29
src/interfaces/text-input/text-input.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import InterfaceTextInput from './text-input.vue';
|
||||
|
||||
import VInput from '@/components/v-input';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
localVue.component('v-input', VInput);
|
||||
|
||||
describe('Interfaces / Text Input', () => {
|
||||
it('Renders a v-input', () => {
|
||||
const component = shallowMount(InterfaceTextInput, {
|
||||
localVue,
|
||||
propsData: {
|
||||
options: {
|
||||
monospace: false,
|
||||
trim: false,
|
||||
showCharacterCount: false,
|
||||
placeholder: 'Enter value...'
|
||||
}
|
||||
},
|
||||
listeners: {
|
||||
input: () => {}
|
||||
}
|
||||
});
|
||||
expect(component.find(VInput).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
27
src/interfaces/text-input/text-input.vue
Normal file
27
src/interfaces/text-input/text-input.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<v-input
|
||||
:monospace="options.monospace"
|
||||
:value="value"
|
||||
@input="$listeners.input"
|
||||
:placeholder="options.placeholder"
|
||||
full-width
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from '@vue/composition-api';
|
||||
import { Options } from './types';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
options: {
|
||||
type: Object as PropType<Options>,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
6
src/interfaces/text-input/types.ts
Normal file
6
src/interfaces/text-input/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type Options = {
|
||||
monospace: boolean;
|
||||
trim: boolean;
|
||||
showCharacterCount: boolean;
|
||||
placeholder: string;
|
||||
};
|
||||
21
src/interfaces/types.ts
Normal file
21
src/interfaces/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import VueI18n from 'vue-i18n';
|
||||
import { Component } from 'vue';
|
||||
|
||||
export type InterfaceOptions = {
|
||||
id: string;
|
||||
register: (context: InterfaceContext) => InterfaceConfig;
|
||||
};
|
||||
|
||||
export interface InterfaceConfig {
|
||||
icon: string;
|
||||
name: string | VueI18n.TranslateResult;
|
||||
component: Component;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
display: null | ((value: any) => string) | Component;
|
||||
}
|
||||
|
||||
export interface Interface extends InterfaceConfig {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type InterfaceContext = { i18n: VueI18n };
|
||||
@@ -11,6 +11,7 @@ import './components/register';
|
||||
import './views/register';
|
||||
import './modules/register';
|
||||
import './layouts/register';
|
||||
import './interfaces/register';
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { hydrate } from '@/hydrate';
|
||||
|
||||
jest.mock('@/auth');
|
||||
jest.mock('@/hydrate');
|
||||
jest.mock('@/api');
|
||||
|
||||
const route: Route = {
|
||||
name: undefined,
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('Stores / collections', () => {
|
||||
}
|
||||
] as any;
|
||||
|
||||
expect(collectionsStore.visibleCollections).toEqual([
|
||||
expect(collectionsStore.visibleCollections.value).toEqual([
|
||||
{
|
||||
collection: 'test-1'
|
||||
},
|
||||
@@ -72,12 +72,14 @@ describe('Stores / collections', () => {
|
||||
}
|
||||
] as any;
|
||||
|
||||
expect(collectionsStore.visibleCollections).toEqual([
|
||||
expect(collectionsStore.visibleCollections.value).toEqual([
|
||||
{
|
||||
collection: 'test-2'
|
||||
collection: 'test-2',
|
||||
hidden: false
|
||||
},
|
||||
{
|
||||
collection: 'test-3'
|
||||
collection: 'test-3',
|
||||
hidden: null
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ export const useCollectionsStore = createStore({
|
||||
visibleCollections: state => {
|
||||
return state.collections
|
||||
.filter(({ collection }) => collection.startsWith('directus_') === false)
|
||||
.filter(({ hidden }) => hidden === false);
|
||||
.filter(({ hidden }) => hidden !== true);
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
||||
Reference in New Issue
Block a user