mirror of
https://github.com/directus/directus.git
synced 2026-01-27 00:48:16 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -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>;
|
||||
4
src/compositions/groupable/index.ts
Normal file
4
src/compositions/groupable/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useGroupable, useGroupableParent } from './groupable';
|
||||
|
||||
export { useGroupable, useGroupableParent };
|
||||
export default { useGroupable, useGroupableParent };
|
||||
63
src/compositions/groupable/readme.md
Normal file
63
src/compositions/groupable/readme.md
Normal 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 };
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -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)
|
||||
|
||||
4
src/compositions/size-class/index.ts
Normal file
4
src/compositions/size-class/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import useSizeClass, { sizeProps } from './size-class';
|
||||
|
||||
export { sizeProps, useSizeClass };
|
||||
export default useSizeClass;
|
||||
28
src/compositions/size-class/readme.md
Normal file
28
src/compositions/size-class/readme.md
Normal 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
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -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', () => {
|
||||
4
src/compositions/use-event-listener/index.ts
Normal file
4
src/compositions/use-event-listener/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import useEventListener from './use-event-listener';
|
||||
|
||||
export { useEventListener };
|
||||
export default useEventListener;
|
||||
19
src/compositions/use-event-listener/readme.md
Normal file
19
src/compositions/use-event-listener/readme.md
Normal 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) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -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', () => {
|
||||
16
src/compositions/use-time-from-now/readme.md
Normal file
16
src/compositions/use-time-from-now/readme.md
Normal 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 };
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -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', () => {
|
||||
16
src/compositions/use-window-size/readme.md
Normal file
16
src/compositions/use-window-size/readme.md
Normal 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();
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -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 () => {
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import registerComponent from '@/utils/register-component';
|
||||
import registerComponent from '@/utils/register-component/';
|
||||
import layouts from './index';
|
||||
|
||||
layouts.forEach(layout => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)[]
|
||||
4
src/utils/arrays-are-equal/index.ts
Normal file
4
src/utils/arrays-are-equal/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import arraysAreEqual from './arrays-are-equal';
|
||||
|
||||
export { arraysAreEqual };
|
||||
export default arraysAreEqual;
|
||||
15
src/utils/arrays-are-equal/readme.md
Normal file
15
src/utils/arrays-are-equal/readme.md
Normal 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
|
||||
```
|
||||
4
src/utils/is-empty/index.ts
Normal file
4
src/utils/is-empty/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { isEmpty, notEmpty } from './is-empty';
|
||||
|
||||
export { isEmpty, notEmpty };
|
||||
export default { isEmpty, notEmpty };
|
||||
30
src/utils/is-empty/readme.md
Normal file
30
src/utils/is-empty/readme.md
Normal 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
|
||||
```
|
||||
@@ -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)
|
||||
|
||||
4
src/utils/register-component/index.ts
Normal file
4
src/utils/register-component/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import registerComponent from './register-component';
|
||||
|
||||
export { registerComponent };
|
||||
export default registerComponent;
|
||||
13
src/utils/register-component/readme.md
Normal file
13
src/utils/register-component/readme.md
Normal 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.
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user