mirror of
https://github.com/directus/directus.git
synced 2026-01-27 02:48:02 -05:00
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:
@@ -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` |
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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: '/',
|
||||
|
||||
@@ -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
6
src/modules/index.ts
Normal 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];
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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: '/',
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { RouteConfig } from 'vue-router';
|
||||
|
||||
export type ModuleConfig = {
|
||||
id: string;
|
||||
routes: RouteConfig[];
|
||||
};
|
||||
@@ -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: '/',
|
||||
|
||||
@@ -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');
|
||||
|
||||
4
src/stores/modules/index.ts
Normal file
4
src/stores/modules/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useModulesStore } from './modules';
|
||||
|
||||
export { useModulesStore };
|
||||
export default useModulesStore;
|
||||
104
src/stores/modules/modules.test.ts
Normal file
104
src/stores/modules/modules.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
44
src/stores/modules/modules.ts
Normal file
44
src/stores/modules/modules.ts
Normal 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
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
4
src/stores/projects/index.ts
Normal file
4
src/stores/projects/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useProjectsStore } from './projects';
|
||||
|
||||
export { useProjectsStore };
|
||||
export default useProjectsStore;
|
||||
@@ -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: () => ({
|
||||
16
src/stores/projects/types.ts
Normal file
16
src/stores/projects/types.ts
Normal 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)[];
|
||||
4
src/stores/requests/index.ts
Normal file
4
src/stores/requests/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useRequestsStore } from './requests';
|
||||
|
||||
export { useRequestsStore };
|
||||
export default useRequestsStore;
|
||||
@@ -3,6 +3,7 @@ body {
|
||||
--black: #000;
|
||||
--white: #fff;
|
||||
--off-white: #f9fafa;
|
||||
--off-black: #13181a;
|
||||
|
||||
--red: #f44336;
|
||||
--red-50: #ffebee;
|
||||
|
||||
@@ -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
16
src/types/modules.ts
Normal 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);
|
||||
};
|
||||
@@ -1,3 +1,3 @@
|
||||
# Types
|
||||
|
||||
TypeScript types for the API system objects.
|
||||
Shared TypeScript types.
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -83,6 +83,7 @@ export default createComponent({
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
font-size: 0;
|
||||
transform: translateX(-100%);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user