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:
Rijk van Zanten
2020-03-11 11:25:33 -04:00
committed by GitHub
parent a2ba2c8783
commit 971876d018
16 changed files with 251 additions and 12 deletions

View File

@@ -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
View 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
View File

@@ -0,0 +1,4 @@
import InterfaceTextInput from './text-input/';
export const interfaces = [InterfaceTextInput];
export default interfaces;

62
src/interfaces/readme.md Normal file
View 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.

View 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);
}
});

View 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);
}
})
});

View File

@@ -0,0 +1 @@
# Text Input

View 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"
/>
`
});

View 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);
});
});

View 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>

View File

@@ -0,0 +1,6 @@
export type Options = {
monospace: boolean;
trim: boolean;
showCharacterCount: boolean;
placeholder: string;
};

21
src/interfaces/types.ts Normal file
View 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 };

View File

@@ -11,6 +11,7 @@ import './components/register';
import './views/register';
import './modules/register';
import './layouts/register';
import './interfaces/register';
Vue.config.productionTip = false;

View File

@@ -15,6 +15,7 @@ import { hydrate } from '@/hydrate';
jest.mock('@/auth');
jest.mock('@/hydrate');
jest.mock('@/api');
const route: Route = {
name: undefined,

View File

@@ -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
}
]);
});

View File

@@ -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: {