Modules store and bar (#112)

* Register views globally

* Use global private view in debug route

* Register login route

* Add barebones login form

* Add auth check on routing

* Add tests and extract checkAuth function

* Fix tests of router

* Move stores into subfolders

* Register modules from modules store

* Register name / icon in modules store

* Update module configs

* Render v-button in module sidebar

* Render correct paths in module sidebar

* Add activated style to button

* Use correct color for button in module bar

* Use correct icons for system modules

* Add tests for modules store

* Remove readme in favor of inline comments
This commit is contained in:
Rijk van Zanten
2020-02-19 15:21:54 -05:00
committed by GitHub
parent a16569f45d
commit 031bae4ac8
31 changed files with 270 additions and 96 deletions

View File

@@ -73,12 +73,14 @@ The loading slot is rendered _on top_ of the content that was there before. Make
| `click` | User clicks on button | `MouseEvent` |
## CSS Variables
| Variable | Default |
|-------------------------------------|------------------------------------------------|
| `--v-button-width` | `auto` |
| `--v-button-height` | `44px` |
| `--v-button-color` | `var(--button-primary-foreground-color)` |
| `--v-button-background-color` | `var(--button-primary-background-color)` |
| `--v-button-color-hover` | `var(--button-primary-foreground-color-hover)` |
| `--v-button-background-color-hover` | `var(--button-primary-background-color-hover)` |
| `--v-button-font-size` | `16px` |
| Variable | Default |
|-----------------------------------------|----------------------------------------------------|
| `--v-button-width` | `auto` |
| `--v-button-height` | `44px` |
| `--v-button-color` | `var(--button-primary-foreground-color)` |
| `--v-button-color-hover` | `var(--button-primary-foreground-color-hover)` |
| `--v-button-color-activated` | `var(--button-primary-foreground-color-activated)` |
| `--v-button-background-color` | `var(--button-primary-background-color)` |
| `--v-button-background-color-hover` | `var(--button-primary-background-color-hover)` |
| `--v-button-background-color-activated` | `var(--button-primary-background-color-activated)` |
| `--v-button-font-size` | `16px` |

View File

@@ -1,6 +1,7 @@
<template>
<component
:is="component"
active-class="activated"
class="v-button"
:class="[sizeClass, { block, rounded, icon, outlined, loading }]"
:type="type"
@@ -80,8 +81,10 @@ export default createComponent({
--v-button-height: 44px;
--v-button-color: var(--button-primary-foreground-color);
--v-button-color-hover: var(--button-primary-foreground-color-hover);
--v-button-color-activated: var(--button-primary-foreground-color-activated);
--v-button-background-color: var(--button-primary-background-color);
--v-button-background-color-hover: var(--button-primary-background-color-hover);
--v-button-background-color-activated: var(--button-primary-background-color-activated);
--v-button-font-size: 16px;
position: relative;
@@ -122,7 +125,7 @@ export default createComponent({
}
}
&:not(.loading):not(:disabled):hover {
&:not(.loading):not(:disabled):not(.activated):hover {
color: var(--v-button-color-hover);
background-color: var(--v-button-background-color-hover);
border: var(--button-border-width) solid var(--v-button-background-color-hover);
@@ -165,7 +168,7 @@ export default createComponent({
}
&.x-large {
--v-button-height: 58px;
--v-button-height: 64px;
--v-button-font-size: 18px;
min-width: 120px;
@@ -192,7 +195,6 @@ export default createComponent({
.content {
position: relative;
top: -1px;
&.invisible {
opacity: 0;
@@ -210,5 +212,10 @@ export default createComponent({
--v-progress-circular-background-color: transparent;
}
}
&.activated {
--v-button-color: var(--v-button-color-activated) !important;
--v-button-background-color: var(--v-button-background-color-activated) !important;
}
}
</style>

View File

@@ -4,7 +4,6 @@ import './styles/main.scss';
import './plugins';
import './directives/register';
import './components/register';
import './modules/register';
import './views/register';
import router from './router';

View File

@@ -1,8 +1,10 @@
import { ModuleConfig } from '@/modules/types';
import { ModuleConfig } from '@/types/modules';
import Collections from './collections.vue';
const config: ModuleConfig = {
id: 'collections',
icon: 'box',
name: i18n => i18n.tc('collection', 2),
routes: [
{
path: '/',

View File

@@ -1,8 +1,10 @@
import { ModuleConfig } from '@/modules/types';
import { ModuleConfig } from '@/types/modules';
import Files from './files.vue';
const config: ModuleConfig = {
id: 'files',
icon: 'collections',
name: i18n => i18n.t('collections.directus_files'),
routes: [
{
path: '/',

6
src/modules/index.ts Normal file
View File

@@ -0,0 +1,6 @@
import CollectionsModule from './collections/';
import FilesModule from './files/';
import SettingsModule from './settings/';
import UsersModule from './users/';
export default [CollectionsModule, FilesModule, SettingsModule, UsersModule];

View File

@@ -1,23 +0,0 @@
import Vue from 'vue';
import { registerModule } from './register';
import { ModuleConfig } from './types';
import router from '@/router';
jest.mock('@/router', () => ({
addRoutes: jest.fn()
}));
describe('Modules / Register', () => {
beforeEach(() => {
(router.addRoutes as jest.Mock).mockClear();
});
it('Registers the routes in router', () => {
const mockModule: ModuleConfig = {
id: 'test-module',
routes: []
};
registerModule(mockModule);
expect(router.addRoutes).toBeCalledWith([]);
});
});

View File

@@ -1,26 +0,0 @@
import router from '@/router';
import { ModuleConfig } from '@/modules/types';
import CollectionsModule from './collections/';
import FilesModule from './files/';
import SettingsModule from './settings/';
import UsersModule from './users/';
// The core modules are available regardless of project, so they can be registered immediately
[CollectionsModule, FilesModule, SettingsModule, UsersModule].forEach(registerModule);
export function registerModule(config: ModuleConfig) {
const routes = config.routes.map(route => ({
...route,
path: `/:project/${config.id}${route.path}`
}));
router.addRoutes(routes);
}
/**
* @NOTE
* The system modules that are registered here will most likely have to be re-registered on login
* as reset the router on logout to prevent custom modules from persisting between project switches
* wrongly
*/

View File

@@ -1,8 +1,10 @@
import { ModuleConfig } from '@/modules/types';
import { ModuleConfig } from '@/types/modules';
import Settings from './settings.vue';
const config: ModuleConfig = {
id: 'settings',
icon: 'settings',
name: i18n => i18n.t('settings'),
routes: [
{
path: '/',

View File

@@ -1,6 +0,0 @@
import { RouteConfig } from 'vue-router';
export type ModuleConfig = {
id: string;
routes: RouteConfig[];
};

View File

@@ -1,8 +1,10 @@
import { ModuleConfig } from '@/modules/types';
import { ModuleConfig } from '@/types/modules';
import Users from './users.vue';
const config: ModuleConfig = {
id: 'users',
icon: 'people',
name: i18n => i18n.t('collections.directus_users'),
routes: [
{
path: '/',

View File

@@ -1,6 +1,7 @@
import VueRouter, { NavigationGuard } from 'vue-router';
import Debug from '@/routes/debug.vue';
import { useProjectsStore } from '@/stores/projects';
import { useModulesStore } from '@/stores/modules';
import LoginRoute from '@/routes/login';
import api from '@/api';
@@ -23,10 +24,12 @@ const router = new VueRouter({
export const onBeforeEach: NavigationGuard = async (to, from, next) => {
const projectsStore = useProjectsStore();
const modulesStore = useModulesStore();
// First load
if (from.name === null) {
await projectsStore.getProjects();
modulesStore.registerGlobalModules();
if (projectsStore.state.needsInstall === true) {
return next('/install');

View File

@@ -0,0 +1,4 @@
import { useModulesStore } from './modules';
export { useModulesStore };
export default useModulesStore;

View File

@@ -0,0 +1,104 @@
import Vue from 'vue';
import VueCompositionAPI from '@vue/composition-api';
import router from '@/router';
import { useModulesStore } from './modules';
import { ModuleConfig } from '@/types/modules';
import systemModules from '@/modules/';
jest.mock('@/modules/', () => [
{
id: 'system-test-1',
icon: 'box',
name: 'System Module 1',
routes: []
},
{
id: 'system-test-2',
icon: 'box',
name: 'System Module 2',
routes: []
}
]);
describe('Stores / Modules', () => {
beforeAll(() => {
Vue.use(VueCompositionAPI);
});
describe('registerModule', () => {
it('Prefixes each route path with /:project/:module/', () => {
jest.spyOn(router, 'addRoutes');
const modulesStore = useModulesStore({});
const testModule: ModuleConfig = {
id: 'test-module',
icon: 'box',
name: 'Test Module',
routes: [
{
path: '/test'
}
]
};
modulesStore.registerModule(testModule);
expect(router.addRoutes).toHaveBeenCalledWith([
{
path: '/:project/test-module/test'
}
]);
});
it('Adds the routes when registering the module', () => {
jest.spyOn(router, 'addRoutes');
const modulesStore = useModulesStore({});
const testModule: ModuleConfig = {
id: 'test-module',
icon: 'box',
name: 'Test Module',
routes: []
};
modulesStore.registerModule(testModule);
expect(router.addRoutes).toHaveBeenCalledWith([]);
});
it('Adds the module to the store if registration is completed', () => {
jest.spyOn(router, 'addRoutes');
const modulesStore = useModulesStore({});
const testModule: ModuleConfig = {
id: 'test-module',
icon: 'box',
name: 'Test Module',
routes: []
};
modulesStore.registerModule(testModule);
expect(modulesStore.state.modules).toEqual([
{
id: 'test-module',
icon: 'box',
name: 'Test Module'
}
]);
});
it('Uses the static name, or calls the function if method is given for name', () => {
jest.spyOn(router, 'addRoutes');
const modulesStore = useModulesStore({});
const testModule: ModuleConfig = {
id: 'test-module',
icon: 'box',
name: jest.fn(),
routes: []
};
modulesStore.registerModule(testModule);
expect(testModule.name).toHaveBeenCalled();
});
});
describe('registerGlobalModules', () => {
it('Calls registerModule for each global module', () => {
const modulesStore = useModulesStore({});
modulesStore.registerModule = jest.fn();
modulesStore.registerGlobalModules();
expect(modulesStore.registerModule).toHaveBeenCalledTimes(2);
});
});
});

View File

@@ -0,0 +1,44 @@
import { createStore } from 'pinia';
import { Module, ModuleConfig } from '@/types/modules';
import systemModules from '@/modules/';
import router from '@/router';
import { i18n } from '@/lang';
export const useModulesStore = createStore({
id: 'modules',
state: () => ({
modules: [] as Module[]
}),
actions: {
/**
* Registers all global modules, including the system ones
*/
registerGlobalModules() {
systemModules.forEach(this.registerModule);
},
/**
* Register a single module. Adds the routes to the router, resolves the name, and adds it to
* the modules store state.
* @param config Config object for module
*/
registerModule(config: ModuleConfig) {
const routes = config.routes.map(route => ({
...route,
path: `/:project/${config.id}${route.path}`
}));
router.addRoutes(routes);
const name = typeof config.name === 'function' ? config.name(i18n) : config.name;
this.state.modules = [
...this.state.modules,
{
id: config.id,
icon: config.icon,
name: name
}
];
}
}
});

View File

@@ -0,0 +1,4 @@
import { useProjectsStore } from './projects';
export { useProjectsStore };
export default useProjectsStore;

View File

@@ -1,22 +1,7 @@
import { createStore } from 'pinia';
import { Project } from '@/types/project';
import { Projects, ProjectWithKey, ProjectError } from './types';
import api from '@/api';
export interface ProjectWithKey extends Project {
key: string;
}
export interface ProjectError {
key: string;
status: number;
error: {
code: number;
message: string;
} | null;
}
type Projects = (ProjectWithKey | ProjectError)[];
export const useProjectsStore = createStore({
id: 'projects',
state: () => ({

View File

@@ -0,0 +1,16 @@
import { Project } from '@/types/project';
export interface ProjectWithKey extends Project {
key: string;
}
export interface ProjectError {
key: string;
status: number;
error: {
code: number;
message: string;
} | null;
}
export type Projects = (ProjectWithKey | ProjectError)[];

View File

@@ -0,0 +1,4 @@
import { useRequestsStore } from './requests';
export { useRequestsStore };
export default useRequestsStore;

View File

@@ -3,6 +3,7 @@ body {
--black: #000;
--white: #fff;
--off-white: #f9fafa;
--off-black: #13181a;
--red: #f44336;
--red-50: #ffebee;

View File

@@ -65,9 +65,11 @@ body {
--button-primary-background-color: var(--blue-grey-900);
--button-primary-background-color-hover: var(--blue-grey-800);
--button-primary-background-color-disabled: var(--blue-grey-400);
--button-primary-background-color-activated: var(--off-black);
--button-primary-foreground-color: var(--white);
--button-primary-foreground-color-hover: var(--white);
--button-primary-foreground-color-disabled: var(--blue-grey-300);
--button-primary-foreground-color-activated: var(--white);
--button-secondary-background-color: var(--blue-grey-100);
--button-secondary-background-color-hover: var(--blue-grey-200);

16
src/types/modules.ts Normal file
View File

@@ -0,0 +1,16 @@
import Vuei18n from 'vue-i18n';
import { RouteConfig } from 'vue-router';
import { i18n } from '@/lang/';
export type Module = {
id: string;
icon: string;
name: string | Vuei18n.TranslateResult;
};
export type ModuleConfig = {
id: string;
routes: RouteConfig[];
icon: string;
name: string | ((i18n: Vuei18n) => Vuei18n.TranslateResult);
};

View File

@@ -1,3 +1,3 @@
# Types
TypeScript types for the API system objects.
Shared TypeScript types.

View File

@@ -12,7 +12,8 @@
<script lang="ts">
import { createComponent, ref, computed, watch } from '@vue/composition-api';
import { useProjectsStore, ProjectWithKey, ProjectError } from '@/stores/projects';
import { useProjectsStore } from '@/stores/projects';
import { ProjectWithKey, ProjectError } from '@/stores/projects/types';
import { useRequestsStore } from '@/stores/requests';
export default createComponent({

View File

@@ -1,27 +1,47 @@
<template>
<div class="module-bar">
<module-bar-logo />
<v-button v-for="module in modules" :key="module.id" icon x-large :to="module.to">
<v-icon :name="module.icon" />
</v-button>
</div>
</template>
<script lang="ts">
import { createComponent } from '@vue/composition-api';
import { createComponent, computed } from '@vue/composition-api';
import ModuleBarLogo from './_module-bar-logo.vue';
import { useModulesStore } from '@/stores/modules/';
import { useProjectsStore } from '@/stores/projects';
export default createComponent({
components: {
ModuleBarLogo
},
setup() {}
setup() {
const modulesStore = useModulesStore();
const projectsStore = useProjectsStore();
const { currentProjectKey } = projectsStore.state;
const modules = computed(() =>
modulesStore.state.modules.map(module => ({
...module,
to: `/${currentProjectKey}/${module.id}/`
}))
);
return { modules: modules };
}
});
</script>
<style lang="scss" scoped>
.module-bar {
display: inline-block;
width: 64px;
height: 100%;
font-size: 1rem;
background-color: #263238;
.v-button {
--v-button-color: var(--blue-grey-400);
}
}
</style>

View File

@@ -83,6 +83,7 @@ export default createComponent({
top: 0;
left: 0;
z-index: 50;
display: flex;
height: 100%;
font-size: 0;
transform: translateX(-100%);

View File

@@ -2,7 +2,8 @@ import Vue from 'vue';
import VueCompositionAPI from '@vue/composition-api';
import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
import VIcon from '@/components/v-icon/';
import { useProjectsStore, ProjectWithKey } from '@/stores/projects';
import { useProjectsStore } from '@/stores/projects';
import { ProjectWithKey } from '@/stores/projects/types';
import Tooltip from '@/directives/tooltip/tooltip';
const localVue = createLocalVue();

View File

@@ -17,7 +17,8 @@
import { version } from '../../../package.json';
import { createComponent, computed } from '@vue/composition-api';
import PublicViewLogo from './_logo.vue';
import { useProjectsStore, ProjectWithKey, ProjectError } from '@/stores/projects';
import { useProjectsStore } from '@/stores/projects/';
import { ProjectWithKey, ProjectError } from '@/stores/projects/types';
export default createComponent({
components: {