Document and structure utils / compositions (#168)

* Document and structure utils / compositions

* Fix tests

* Ignore tests in sonar cloud?

* Please sonar don't use my test files
This commit is contained in:
Rijk van Zanten
2020-03-12 12:31:36 -04:00
committed by GitHub
parent 7c732579c8
commit 68c625ec79
42 changed files with 260 additions and 119 deletions

View File

@@ -2,9 +2,7 @@ sonar.organization=directus
sonar.projectKey=app-next
sonar.sources=src
sonar.exclusions=src/**/*.story.ts
sonar.test.sources=src/**/*.test.ts
sonar.test.exclusions=src/**/*.test.ts
sonar.exclusions=src/**/*.story.ts,src/**/*.test.ts
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.testExecutionReportPaths=coverage/sonar.xml

View File

@@ -1,7 +1,7 @@
import { mount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import VItemGroup from './v-item-group.vue';
import * as composition from '@/compositions/groupable';
import * as composition from '@/compositions/groupable/groupable';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);

View File

@@ -1,7 +1,7 @@
import { mount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import VItem from './v-item.vue';
import * as composition from '@/compositions/groupable';
import * as composition from '@/compositions/groupable/groupable';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);

View File

@@ -1,6 +1,6 @@
import Vue from 'vue';
import { provide, inject, ref } from '@vue/composition-api';
import mountComposition from '../../.jest/mount-composition';
import mountComposition from '../../../.jest/mount-composition';
import { useGroupable, useGroupableParent } from './groupable';
describe('Groupable', () => {

View File

@@ -1,6 +1,6 @@
import Vue from 'vue';
import { computed, onBeforeUnmount, inject, ref, provide, Ref, watch } from '@vue/composition-api';
import { notEmpty, isEmpty } from '@/utils/is-empty';
import { notEmpty, isEmpty } from '@/utils/is-empty/';
type GroupableInstance = {
active: Ref<boolean>;

View File

@@ -0,0 +1,4 @@
import { useGroupable, useGroupableParent } from './groupable';
export { useGroupable, useGroupableParent };
export default { useGroupable, useGroupableParent };

View File

@@ -0,0 +1,63 @@
# `useGroupable` / `useGroupableParent`
Can be used to make a selectable group of items within a parent container. This is used to facilitate
the functionality of the `v-item-group` base component, and other groupable components like `detail-group`,
`button-group`.
## Usage
Use the `useGroupableParent` function in a parent component that will contain one or more components
in it's slots (deeply nested or not) that use the `useGroupable` compositions.
### `useGroupableParent(state: GroupableParentState, options: GroupableParentOptions): void`
The `useGroupableParent` composition accepts two paremeters: state and options.
State includes a `selection` key that can be used to pass an array of selected items, so you can
manage this active state from the parent context. The `onSelectionChange` property of state is a
callback function that fires whenever the selection changes from a child groupable item.
The `options` parameter can be used to set some behavioral options for the selection logic. These
options include `multiple: boolean`, `max: number`, `mandatory: boolean`.
```js
import { defineComponent } from '@vue/composition-api';
import { useGroupableParent } from '@/compositions/size-class/';
export default defineComponent({
props: {
...sizeProps
},
setup(props) {
useGroupableParent(
{
selection: selection,
onSelectionChange: newSelectionValues => emit('input', newSelectionValues)
},
{
multiple: multiple,
max: max,
mandatory: mandatory
}
);
}
});
```
### `useGroupable(value: string | number): { active: Ref<boolean>; toggle: () => void; }`
Registers this component as a child of the first parent component that uses the `useGroupableParent`
component.
The `useGroupable` composition accepts a single parameter (`value`) that will be used in the selection
state of the groupable parent. The composition returns an object with the active state, and a function
that will toggle this component's active state in the parent's selection.
```js
import { defineComponent } from '@vue/composition-api';
import { useGroupable } from '@/compositions/groupable';
export default defineComponent({
setup(props) {
const { active, toggle } = useGroupable('unique-value-for-this-item');
return { active, toggle };
}
});
```

View File

@@ -4,95 +4,8 @@ Compositions are reusable pieces of logic that can be used inside Vue components
## Table of Contents
* [Event Listener](#event-listener)
* [Size Class](#size-class)
* [Time from Now](#time-from-now)
* [Window Size](#window-size)
## Event Listener
The event listener composition allows you to bound event listeners to global elements.
**Note:** You should rely on Vue's event handlers like `@click` whenever possible. This composition acts as an escape hatch for triggering things based on out-of-component events.
The composition removes the event handler whenever the component is unmounted.
### Usage
```js
import { defineComponent } from '@vue/composition-api';
import useEventListener from '@/compositions/use-event-listener';
export default defineComponent({
setup() {
useEventListener(window, 'scroll', onScroll);
function onScroll(event) {
console.log(event);
}
}
});
```
---
## Size Class
Shared size class prop handler for base components. Adds `x-small`, `small`, `large`, and `x-large` props to the component, and converts the prop into a string that can be added to classes.
### Usage
```js
import { defineComponent } from '@vue/composition-api';
import useSizeClass, { sizeProps } from '@/compositions/size-class';
export default defineComponent({
props: {
...sizeProps
},
setup(props) {
const sizeClass = useSizeClass(props);
}
});
```
---
## Time from Now
Returns ref string time from current datetime based on date-fns formatDistance.
### Usage
```js
import { defineComponent } from '@vue/composition-api';
import useTimeFromNow from '@/compositions/use-time-from-now';
export default defineComponent({
setup() {
const date = new Date('2020-01-01T13:55');
const timeFromNow = useTimeFromNow(date);
}
});
```
The composition accepts an optional second parameter that controls how often the value is update. You can set this to `0` if you don't want the value to update at all.
---
## Window Size
Returns a `ref` of `width` and `height` of the current window size. Updates the value on window resizes.
### Usage
```js
import { defineComponent } from '@vue/composition-api';
import useWindowSize from '@/compositions/window-size';
export default defineComponent({
setup() {
const { width, height } = useWindowSize();
}
});
```
* [`useGroupable` / `useGroupableParent`](./groupable)
* [`useSizeClass`](./size-class)
* [`useEventListener`](./use-event-listener)
* [`useTimeFromNow`](./use-time-from-now)
* [`useWindowSize`](./use-window-size)

View File

@@ -0,0 +1,4 @@
import useSizeClass, { sizeProps } from './size-class';
export { sizeProps, useSizeClass };
export default useSizeClass;

View File

@@ -0,0 +1,28 @@
# Size Class
Set of props and a composition that can inject a standardized sizing prop for components.
A component that uses this composition and the corresponding props will accept the `x-small` through
`x-large` props:
```html
<v-button x-small />
<v-button small />
<v-button large />
<v-button x-large />
```
## Usage
```js
import { defineComponent } from '@vue/composition-api';
import useSizeClass, { sizeProps } from '@/compositions/size-class/';
export default defineComponent({
props: {
...sizeProps
},
setup(props) {
const sizeClass = useSizeClass(props);
return { sizeClass }; // one of x-small, small, large, x-large
}
});
```

View File

@@ -1,5 +1,5 @@
import useSizeClass from './size-class';
import mountComposition from '../../.jest/mount-composition';
import mountComposition from '../../../.jest/mount-composition';
describe('Compositions / Size Class', () => {
it('Extracts the correct class based on given props', () => {

View File

@@ -0,0 +1,4 @@
import useEventListener from './use-event-listener';
export { useEventListener };
export default useEventListener;

View File

@@ -0,0 +1,19 @@
# `useEventListener(target: HTMLElement, type: string, handler: (event: Event) => void, options: AddEventListenerOptions): void`
Can be used to attach an event listener to any DOM element that will automatically be attached /
cleaned up whenever the component mounts / unmounts.
## Usage
```js
import { defineComponent } from '@vue/composition-api';
import { useEventListener } from '@/compositions/use-event-listener';
export default defineComponent({
setup(props) {
useEventListener(document.querySelector('#example'), 'click', onExampleClick);
function onExampleClick(event) {
// ...
}
}
});
```

View File

@@ -1,6 +1,6 @@
import { ref } from '@vue/composition-api';
import useEventListener from './use-event-listener';
import mountComposition from '../../.jest/mount-composition';
import mountComposition from '../../../.jest/mount-composition';
describe('Compositions / Event Listener', () => {
it('Adds passed event listener onMounted', () => {

View File

@@ -0,0 +1,16 @@
# `useTimeFromNow(date: Date | number, autoUpdate: number = 60000): Ref<string>
Composition that can be used to create a relative time format that is auto updated every `autoUpdate`
milliseconds.
## Usage
```js
import { defineComponent } from '@vue/composition-api';
import { useTimeFromNow } from '@/compositions/use-time-from-now';
export default defineComponent({
setup(props) {
const timeFromNow = useTimeFromNow(Date.now());
return { timeFromNow };
}
});
```

View File

@@ -1,6 +1,6 @@
import { Ref } from '@vue/composition-api';
import useTimeFromNow from './use-time-from-now';
import mountComposition from '../../.jest/mount-composition';
import mountComposition from '../../../.jest/mount-composition';
import mockdate from 'mockdate';
describe('Compositions / Event Listener', () => {

View File

@@ -0,0 +1,16 @@
# useWindowSize(options: WindowSizeOptions = { throttle: 100 }): { width: Ref<number>; height: Ref<number>; }
Returns the window's width and height in an object. These values are reactive.
The optional `options` parameter allows you to set the throttling speed.
## Usage
```js
import { defineComponent } from '@vue/composition-api';
import { useWindowSize } from '@/compositions/use-window-size/';
export default defineComponent({
setup(props) {
const { width, height } = useWindowSize();
}
});
```

View File

@@ -1,6 +1,6 @@
import { watch } from '@vue/composition-api';
import useWindowSize from './use-window-size';
import mountComposition from '../../.jest/mount-composition';
import mountComposition from '../../../.jest/mount-composition';
describe('Compositions / Window Size', () => {
it('Adds passed event listener onMounted', async () => {

View File

@@ -1,4 +1,4 @@
import registerComponent from '@/utils/register-component';
import registerComponent from '@/utils/register-component/';
import interfaces from './index';
// inter, cause interface is reserved keyword in JS... o_o

View File

@@ -1,4 +1,4 @@
import registerComponent from '@/utils/register-component';
import registerComponent from '@/utils/register-component/';
import layouts from './index';
layouts.forEach(layout => {

View File

@@ -3,7 +3,7 @@ import api from '@/api';
import { Collection, CollectionRaw } from './types';
import { useProjectsStore } from '@/stores/projects';
import i18n from '@/lang/';
import { notEmpty } from '@/utils/is-empty';
import { notEmpty } from '@/utils/is-empty/';
import VueI18n from 'vue-i18n';
import formatTitle from '@directus/format-title';

View File

@@ -3,7 +3,7 @@ import { FieldRaw, Field } from './types';
import api from '@/api';
import { useProjectsStore } from '@/stores/projects';
import VueI18n from 'vue-i18n';
import { notEmpty } from '@/utils/is-empty';
import { notEmpty } from '@/utils/is-empty/';
import { i18n } from '@/lang';
import formatTitle from '@directus/format-title';

View File

@@ -1,5 +1,8 @@
# Styles
The global styles of the application. Even though everything is based around scoped styles in the components, there's still a need to have a certain set of global styles. Most importantly the global reset ([_base.scss](./_base.scss)) and CSS Custom Properties (variables) ([_variables.scss](./_variables.scss)).
The global styles of the application. Even though everything is based around scoped styles in the
components, there's still a need to have a certain set of global styles. Most importantly the global
reset ([_base.scss](./_base.scss)) and CSS Custom Properties (variables).
There are a couple of plugins (v-tooltip / codemirror / etc) that expect their styles to be global. This folder can be used for that as well.
There are a couple of third party plugins (codemirror / TinyMCE etc) that expect their styles to be
global. This folder can be used for that as well.

View File

@@ -1,9 +1,3 @@
/**
* Compares two given arrays for equality. Only works for primitive values (no nested objects)
* @param a1 First array
* @param a2 Second array
* @see https://stackoverflow.com/a/55614659/4859211
*/
export default function arraysAreEqual(
a1: readonly (string | number)[],
a2: readonly (string | number)[]

View File

@@ -0,0 +1,4 @@
import arraysAreEqual from './arrays-are-equal';
export { arraysAreEqual };
export default arraysAreEqual;

View File

@@ -0,0 +1,15 @@
# `arraysAreEqual`
Tests if two given arrays contain the same primitive values. **Note**: this _does not_ check object
equality in arrays.
Based on https://stackoverflow.com/a/55614659/4859211
## Usage
```js
const arr1 = ['a', 'b', 'c'];
const arr2 = [1, 2, 3];
const arr3 = ['a', 'b', 'c'];
arraysAreEqual(arr1, arr2); // false
arraysAreEqual(arr1, arr3); // true
```

View File

@@ -0,0 +1,4 @@
import { isEmpty, notEmpty } from './is-empty';
export { isEmpty, notEmpty };
export default { isEmpty, notEmpty };

View File

@@ -0,0 +1,30 @@
# `isEmpty` / `notEmpty`
Checks if the given value is `null` or `undefined`. Can be used in place of a simple "truthy" check:
Before: `if (value) { ... }`
After: `if (notEmpty(value)) { ... }
## Usage
```js
const a = undefined;
const b = null;
const c = 'test';
const d = 42;
const e = [];
const f = {};
isEmpty(a); // true
isEmpty(b); // true
isEmpty(c); // false
isEmpty(d); // false
isEmpty(e); // false
isEmpty(f); // false
notEmpty(a); // false
notEmpty(b); // false
notEmpty(c); // true
notEmpty(d); // true
notEmpty(e); // true
notEmpty(f); // true
```

View File

@@ -2,6 +2,8 @@
Various utility functions that can be reused across components.
## Testing
## Table of Contents
Please make sure that new utils have unit tests where appropriate.
* [arraysAreEqual](./arrays-are-equal)
* [isEmpty / notEmpty](./isEmpty)
* [registerComponent](./register-component)

View File

@@ -0,0 +1,4 @@
import registerComponent from './register-component';
export { registerComponent };
export default registerComponent;

View File

@@ -0,0 +1,13 @@
# `registerComponent`
Registers a component into the global Vue context.
## Usage
```js
registerComponent('v-button', VButton);
```
## Vue.component() vs registerComponent()
`registerComponent` internally calls `Vue.component()` directly, and doesn't do anything else. The
function is purely to extend the accepted TypeScript type for the second parameter. Vue accepts the
second parameter to be of type `Component`, yet it's `Vue.component()` function doesn't actually have
that type in it's definition.

View File

@@ -1,7 +1,7 @@
import { mount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI, { ref } from '@vue/composition-api';
import DrawerDetail from './drawer-detail.vue';
import * as GroupableComposition from '@/compositions/groupable';
import * as GroupableComposition from '@/compositions/groupable/groupable';
import VIcon from '@/components/v-icon';
const localVue = createLocalVue();

View File

@@ -1,3 +1,10 @@
# Views
Views are the top-level parent component that are used in all modules. Directus will only have two Views for the foreseeable future: `public` and `private` for non-authenticated and authenticated routes respectively
Views are the top-level parent component that are used in all modules. Directus will only have two
Views for the foreseeable future: `public` and `private` for non-authenticated and authenticated
routes respectively.
## Table of Contents
* [Private View](./private-view)
* [Public View](./public)