mirror of
https://github.com/directus/directus.git
synced 2026-02-16 14:25:01 -05:00
Private view (#41)
* Add file structure * Add basics of private view * Add composition api in every test * Install nanoid * Add request queue * Register all global components * Make bunny run on api queue * Use private route for debug now * Move request queue to store * Remove unused sleep function in hover test * Use new request queue in private view * Remove jest pre-test config * Finish logo + tests * Add tests for private view * Fix unhandled promise in api test
This commit is contained in:
@@ -18,11 +18,13 @@
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"@types/nanoid": "^2.1.0",
|
||||
"@vue/composition-api": "^0.3.4",
|
||||
"axios": "^0.19.2",
|
||||
"date-fns": "^2.9.0",
|
||||
"debug": "^4.1.1",
|
||||
"lodash": "^4.17.15",
|
||||
"nanoid": "^2.1.11",
|
||||
"pinia": "0.0.5",
|
||||
"v-tooltip": "^2.0.3",
|
||||
"vue": "^2.6.11",
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { getRootPath } from './api';
|
||||
import Vue from 'vue';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import { onRequest, onResponse, onError, getRootPath } from './api';
|
||||
import { useRequestsStore } from '@/stores/requests';
|
||||
|
||||
describe('API', () => {
|
||||
beforeAll(() => {
|
||||
globalThis.window = Object.create(window);
|
||||
Vue.use(VueCompositionAPI);
|
||||
});
|
||||
|
||||
it('Calculates the correct API root URL based on window', () => {
|
||||
@@ -16,4 +20,46 @@ describe('API', () => {
|
||||
const result = getRootPath();
|
||||
expect(result).toBe('/api/nested/');
|
||||
});
|
||||
|
||||
it('Calls startRequest on the store on any request', () => {
|
||||
const store = useRequestsStore({});
|
||||
const spy = jest.spyOn(store, 'startRequest');
|
||||
spy.mockImplementation(() => 'abc');
|
||||
const newRequest = onRequest({});
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(newRequest.id).toBe('abc');
|
||||
});
|
||||
|
||||
it('Calls endRequest on responses', () => {
|
||||
const store = useRequestsStore({});
|
||||
const spy = jest.spyOn(store, 'endRequest');
|
||||
onResponse({
|
||||
data: null,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config: {
|
||||
id: 'abc'
|
||||
}
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith('abc');
|
||||
});
|
||||
|
||||
it('Calls endRequest on errors', async () => {
|
||||
const store = useRequestsStore({});
|
||||
const spy = jest.spyOn(store, 'endRequest');
|
||||
try {
|
||||
await onError({
|
||||
response: {
|
||||
config: {
|
||||
id: 'abc'
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error).toEqual({ response: { config: { id: 'abc' } } });
|
||||
}
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('abc');
|
||||
});
|
||||
});
|
||||
|
||||
40
src/api.ts
40
src/api.ts
@@ -1,9 +1,47 @@
|
||||
import axios from 'axios';
|
||||
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { useRequestsStore } from '@/stores/requests';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: getRootPath()
|
||||
});
|
||||
|
||||
interface RequestConfig extends AxiosRequestConfig {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface Response extends AxiosResponse {
|
||||
config: RequestConfig;
|
||||
}
|
||||
|
||||
export const onRequest = (config: AxiosRequestConfig) => {
|
||||
const requestsStore = useRequestsStore();
|
||||
const id = requestsStore.startRequest();
|
||||
|
||||
const requestConfig: RequestConfig = {
|
||||
id: id,
|
||||
...config
|
||||
};
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export const onResponse = (response: AxiosResponse | Response) => {
|
||||
const requestsStore = useRequestsStore();
|
||||
const id = (response.config as RequestConfig).id;
|
||||
requestsStore.endRequest(id);
|
||||
return response;
|
||||
};
|
||||
|
||||
export const onError = (error: any) => {
|
||||
const requestsStore = useRequestsStore();
|
||||
const id = (error.response.config as RequestConfig).id;
|
||||
requestsStore.endRequest(id);
|
||||
return Promise.reject(error);
|
||||
};
|
||||
|
||||
api.interceptors.request.use(onRequest);
|
||||
api.interceptors.response.use(onResponse, onError);
|
||||
|
||||
export function getRootPath(): string {
|
||||
const path = window.location.pathname;
|
||||
const parts = path.split('/');
|
||||
|
||||
31
src/components/register.ts
Normal file
31
src/components/register.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
import VAvatar from './v-avatar/';
|
||||
import VButton from './v-button/';
|
||||
import VCheckbox from './v-checkbox/';
|
||||
import VChip from './v-chip/';
|
||||
import VHover from './v-hover/';
|
||||
import VIcon from './v-icon/';
|
||||
import VInput from './v-input/';
|
||||
import VOverlay from './v-overlay/';
|
||||
import VProgressLinear from './v-progress/linear/';
|
||||
import VSheet from './v-sheet/';
|
||||
import VSlider from './v-slider/';
|
||||
import VSpinner from './v-spinner/';
|
||||
import VSwitch from './v-switch/';
|
||||
import VTable from './v-table/';
|
||||
|
||||
Vue.component('v-avatar', VAvatar);
|
||||
Vue.component('v-button', VButton);
|
||||
Vue.component('v-checkbox', VCheckbox);
|
||||
Vue.component('v-chip', VChip);
|
||||
Vue.component('v-hover', VHover);
|
||||
Vue.component('v-icon', VIcon);
|
||||
Vue.component('v-input', VInput);
|
||||
Vue.component('v-overlay', VOverlay);
|
||||
Vue.component('v-progress-linear', VProgressLinear);
|
||||
Vue.component('v-sheet', VSheet);
|
||||
Vue.component('v-slider', VSlider);
|
||||
Vue.component('v-spinner', VSpinner);
|
||||
Vue.component('v-switch', VSwitch);
|
||||
Vue.component('v-table', VTable);
|
||||
@@ -5,12 +5,6 @@ import VHover from './v-hover.vue';
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Hover', () => {
|
||||
let component: Wrapper<Vue>;
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import Vue from 'vue';
|
||||
import router from './router';
|
||||
import i18n from './lang/';
|
||||
|
||||
import './styles/main.scss';
|
||||
import './plugins';
|
||||
import './components/register';
|
||||
|
||||
import router from './router';
|
||||
import i18n from './lang/';
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<public-view>
|
||||
<private-view>
|
||||
Hello!
|
||||
</public-view>
|
||||
</private-view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { createComponent } from '@vue/composition-api';
|
||||
import PublicView from '@/views/public/';
|
||||
import PrivateView from '@/views/private/';
|
||||
|
||||
export default createComponent({
|
||||
components: {
|
||||
PublicView
|
||||
PrivateView
|
||||
},
|
||||
props: {},
|
||||
setup() {
|
||||
|
||||
@@ -2,7 +2,6 @@ import Vue from 'vue';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import api from '@/api';
|
||||
import { useProjectsStore } from './projects';
|
||||
import { setActiveReq } from 'pinia';
|
||||
|
||||
describe('Stores / Projects', () => {
|
||||
beforeAll(() => {
|
||||
|
||||
31
src/stores/requests.test.ts
Normal file
31
src/stores/requests.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import Vue from 'vue';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import { useRequestsStore } from './requests';
|
||||
|
||||
describe('Stores / Projects', () => {
|
||||
beforeAll(() => {
|
||||
Vue.use(VueCompositionAPI);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Computes the queueHasItems state', () => {
|
||||
const store = useRequestsStore({});
|
||||
store.state.queue = ['abc', 'def', 'gh'];
|
||||
expect(store.queueHasItems.value).toBe(true);
|
||||
store.state.queue = [];
|
||||
expect(store.queueHasItems.value).toBe(false);
|
||||
});
|
||||
|
||||
test('Queue management', () => {
|
||||
const store = useRequestsStore({});
|
||||
expect(store.state.queue.length).toBe(0);
|
||||
const id = store.startRequest();
|
||||
expect(store.state.queue.length).toBe(1);
|
||||
expect(id).toBe(store.state.queue[0]);
|
||||
store.endRequest(id);
|
||||
expect(store.state.queue.length).toBe(0);
|
||||
});
|
||||
});
|
||||
22
src/stores/requests.ts
Normal file
22
src/stores/requests.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createStore } from 'pinia';
|
||||
import nanoid from 'nanoid';
|
||||
|
||||
export const useRequestsStore = createStore({
|
||||
id: 'requests',
|
||||
state: () => ({
|
||||
queue: [] as string[]
|
||||
}),
|
||||
getters: {
|
||||
queueHasItems: state => state.queue.length > 0
|
||||
},
|
||||
actions: {
|
||||
startRequest() {
|
||||
const id = nanoid();
|
||||
this.state.queue = [...this.state.queue, id];
|
||||
return id;
|
||||
},
|
||||
endRequest(id: string) {
|
||||
this.state.queue = this.state.queue.filter((queueID: string) => queueID !== id);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
$breakpoints: (
|
||||
'small': (min-width: 600px),
|
||||
'medium': (min-width: 960px),
|
||||
'large': (min-width: 1264px),
|
||||
'x-large': (min-width: 1903px)
|
||||
'large': (min-width: 1260px),
|
||||
'x-large': (min-width: 1900px)
|
||||
) !default;
|
||||
|
||||
@mixin breakpoint($breakpoint) {
|
||||
|
||||
104
src/views/private/_module-bar-logo.test.ts
Normal file
104
src/views/private/_module-bar-logo.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import ModuleBarLogo from './_module-bar-logo.vue';
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
import { useRequestsStore } from '@/stores/requests';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
|
||||
describe('Views / Private / Module Bar / Logo', () => {
|
||||
let component: Wrapper<Vue>;
|
||||
const projectsStore = useProjectsStore();
|
||||
const requestsStore = useRequestsStore();
|
||||
|
||||
beforeEach(() => {
|
||||
component = mount(ModuleBarLogo, { localVue });
|
||||
projectsStore.reset();
|
||||
requestsStore.reset();
|
||||
});
|
||||
|
||||
it('Renders the default rabbit when were not in a project', () => {
|
||||
expect((component.vm as any).customLogoPath).toBe(null);
|
||||
});
|
||||
|
||||
it('Renders the default rabbit when the current project errored out', () => {
|
||||
projectsStore.state.projects = [
|
||||
{
|
||||
key: 'my-project',
|
||||
status: 500,
|
||||
error: {
|
||||
code: 400,
|
||||
message: 'Could not connect to the database'
|
||||
}
|
||||
}
|
||||
];
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
expect((component.vm as any).customLogoPath).toBe(null);
|
||||
});
|
||||
|
||||
it('Renders the default rabbit when the current project does not have a custom logo', () => {
|
||||
projectsStore.state.projects = [
|
||||
{
|
||||
key: 'my-project',
|
||||
api: {
|
||||
requires2FA: false,
|
||||
project_foreground: null,
|
||||
project_background: null,
|
||||
project_color: '#abcdef',
|
||||
project_public_note: '',
|
||||
default_locale: 'en-US',
|
||||
telemetry: false,
|
||||
project_name: 'test',
|
||||
project_logo: null
|
||||
}
|
||||
}
|
||||
];
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
expect((component.vm as any).customLogoPath).toBe(null);
|
||||
});
|
||||
|
||||
it('Renders the custom logo if set', async () => {
|
||||
projectsStore.state.projects = [
|
||||
{
|
||||
key: 'my-project',
|
||||
api: {
|
||||
requires2FA: false,
|
||||
project_foreground: null,
|
||||
project_background: null,
|
||||
project_color: '#abcdef',
|
||||
project_public_note: '',
|
||||
default_locale: 'en-US',
|
||||
telemetry: false,
|
||||
project_name: 'test',
|
||||
project_logo: {
|
||||
full_url: 'abc',
|
||||
url: 'abc'
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
projectsStore.state.currentProjectKey = 'my-project';
|
||||
await component.vm.$nextTick();
|
||||
expect((component.vm as any).customLogoPath).toBe('abc');
|
||||
expect(component.find('img').attributes().src).toBe('abc');
|
||||
});
|
||||
|
||||
it('Only stops running if the queue is empty', async () => {
|
||||
requestsStore.state.queue = [];
|
||||
await component.vm.$nextTick();
|
||||
(component.vm as any).isRunning = true;
|
||||
(component.vm as any).stopRunningIfQueueIsEmpty();
|
||||
expect((component.vm as any).isRunning).toBe(false);
|
||||
|
||||
requestsStore.state.queue = ['abc'];
|
||||
await component.vm.$nextTick();
|
||||
expect((component.vm as any).isRunning).toBe(true);
|
||||
(component.vm as any).stopRunningIfQueueIsEmpty();
|
||||
expect((component.vm as any).isRunning).toBe(true);
|
||||
requestsStore.state.queue = [];
|
||||
await component.vm.$nextTick();
|
||||
(component.vm as any).stopRunningIfQueueIsEmpty();
|
||||
expect((component.vm as any).isRunning).toBe(false);
|
||||
});
|
||||
});
|
||||
89
src/views/private/_module-bar-logo.vue
Normal file
89
src/views/private/_module-bar-logo.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="module-bar-logo">
|
||||
<img class="custom-logo" v-if="customLogoPath" :src="customLogoPath" />
|
||||
<div
|
||||
v-else
|
||||
class="logo"
|
||||
:class="{ running: isRunning }"
|
||||
@animationiteration="stopRunningIfQueueIsEmpty"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { createComponent, ref, computed, watch } from '@vue/composition-api';
|
||||
import { useProjectsStore, ProjectWithKey, ProjectError } from '@/stores/projects';
|
||||
import { useRequestsStore } from '@/stores/requests';
|
||||
|
||||
export default createComponent({
|
||||
setup() {
|
||||
const projectsStore = useProjectsStore();
|
||||
const requestsStore = useRequestsStore();
|
||||
|
||||
const customLogoPath = computed<string | null>(() => {
|
||||
if (projectsStore.currentProject.value === null) return null;
|
||||
if ((projectsStore.currentProject.value as ProjectError).error !== undefined) {
|
||||
return null;
|
||||
}
|
||||
const currentProject = projectsStore.currentProject.value as ProjectWithKey;
|
||||
return currentProject.api.project_logo?.full_url || null;
|
||||
});
|
||||
|
||||
const isRunning = ref(false);
|
||||
|
||||
const queueHasItems = requestsStore.queueHasItems;
|
||||
|
||||
watch(
|
||||
() => queueHasItems.value,
|
||||
hasItems => {
|
||||
if (hasItems) isRunning.value = true;
|
||||
}
|
||||
);
|
||||
|
||||
return { customLogoPath, isRunning, stopRunningIfQueueIsEmpty };
|
||||
|
||||
function stopRunningIfQueueIsEmpty() {
|
||||
if (queueHasItems.value === false) isRunning.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.module-bar-logo {
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
padding: 12px;
|
||||
background-color: var(--brand);
|
||||
|
||||
.custom-logo {
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 12px;
|
||||
width: 40px;
|
||||
height: 32px;
|
||||
margin: 0 auto;
|
||||
background-image: url('../../assets/sprite.svg');
|
||||
background-position: 0% 0%;
|
||||
background-size: 600px 32px;
|
||||
}
|
||||
|
||||
.running {
|
||||
animation: 560ms run steps(14) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes run {
|
||||
100% {
|
||||
background-position: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
27
src/views/private/_module-bar.vue
Normal file
27
src/views/private/_module-bar.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="module-bar">
|
||||
<module-bar-logo />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { createComponent } from '@vue/composition-api';
|
||||
import ModuleBarLogo from './_module-bar-logo.vue';
|
||||
|
||||
export default createComponent({
|
||||
components: {
|
||||
ModuleBarLogo
|
||||
},
|
||||
setup() {}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.module-bar {
|
||||
display: inline-block;
|
||||
width: 64px;
|
||||
height: 100%;
|
||||
font-size: 1rem;
|
||||
background-color: #263238;
|
||||
}
|
||||
</style>
|
||||
4
src/views/private/index.ts
Normal file
4
src/views/private/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import PrivateView from './private-view.vue';
|
||||
|
||||
export { PrivateView };
|
||||
export default PrivateView;
|
||||
1
src/views/private/private-view.readme.md
Normal file
1
src/views/private/private-view.readme.md
Normal file
@@ -0,0 +1 @@
|
||||
# Private View
|
||||
19
src/views/private/private-view.story.ts
Normal file
19
src/views/private/private-view.story.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import Vue from 'vue';
|
||||
import PrivateView from './private-view.vue';
|
||||
import markdown from './private-view.readme.md';
|
||||
|
||||
Vue.component('private-view', PrivateView);
|
||||
|
||||
export default {
|
||||
title: 'Views / Private',
|
||||
component: PrivateView,
|
||||
parameters: {
|
||||
notes: markdown
|
||||
}
|
||||
};
|
||||
|
||||
export const basic = () => ({
|
||||
template: `
|
||||
<private-view />
|
||||
`
|
||||
});
|
||||
62
src/views/private/private-view.test.ts
Normal file
62
src/views/private/private-view.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import PrivateView from './private-view.vue';
|
||||
import VOverlay from '@/components/v-overlay';
|
||||
import useWindowSize from '@/compositions/window-size';
|
||||
|
||||
let mockWidth = 50;
|
||||
|
||||
jest.mock('@/compositions/window-size', () =>
|
||||
jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
width: {
|
||||
value: mockWidth
|
||||
},
|
||||
height: {
|
||||
value: mockWidth
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
localVue.component('v-overlay', VOverlay);
|
||||
|
||||
describe('Views / Private', () => {
|
||||
beforeEach(() => {
|
||||
(useWindowSize as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
it('Shows nav with overlay if screen is < 960px', async () => {
|
||||
mockWidth = 600;
|
||||
const component = shallowMount(PrivateView, { localVue });
|
||||
expect((component.vm as any).navWithOverlay).toBe(true);
|
||||
});
|
||||
|
||||
it('Does not render overlay for nav if screen is >= 960px', async () => {
|
||||
mockWidth = 960;
|
||||
let component = shallowMount(PrivateView, { localVue });
|
||||
expect((component.vm as any).navWithOverlay).toBe(false);
|
||||
|
||||
mockWidth = 1000;
|
||||
component = shallowMount(PrivateView, { localVue });
|
||||
expect((component.vm as any).navWithOverlay).toBe(false);
|
||||
});
|
||||
|
||||
it('Shows drawer with overlay if screen is < 1260px', async () => {
|
||||
mockWidth = 600;
|
||||
const component = shallowMount(PrivateView, { localVue });
|
||||
expect((component.vm as any).drawerWithOverlay).toBe(true);
|
||||
});
|
||||
|
||||
it('Does not render overlay for drawer if screen is >= 1260px', async () => {
|
||||
mockWidth = 1260;
|
||||
let component = shallowMount(PrivateView, { localVue });
|
||||
expect((component.vm as any).drawerWithOverlay).toBe(false);
|
||||
|
||||
mockWidth = 1300;
|
||||
component = shallowMount(PrivateView, { localVue });
|
||||
expect((component.vm as any).drawerWithOverlay).toBe(false);
|
||||
});
|
||||
});
|
||||
145
src/views/private/private-view.vue
Normal file
145
src/views/private/private-view.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="private-view">
|
||||
<aside class="navigation" :class="{ 'is-open': navOpen }">
|
||||
<module-bar />
|
||||
<div class="module-nav"></div>
|
||||
</aside>
|
||||
<div class="content">
|
||||
<header>
|
||||
<button @click="navOpen = true">Toggle nav</button>
|
||||
<button @click="drawerOpen = !drawerOpen">Toggle drawer</button>
|
||||
</header>
|
||||
<main></main>
|
||||
</div>
|
||||
<aside class="drawer" :class="{ 'is-open': drawerOpen }"></aside>
|
||||
|
||||
<v-overlay
|
||||
v-if="navWithOverlay"
|
||||
class="nav-overlay"
|
||||
:active="navOpen"
|
||||
@click="navOpen = false"
|
||||
/>
|
||||
<v-overlay
|
||||
v-if="drawerWithOverlay"
|
||||
class="drawer-overlay"
|
||||
:active="drawerOpen"
|
||||
@click="drawerOpen = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { createComponent, ref, computed, watch } from '@vue/composition-api';
|
||||
import useWindowSize from '@/compositions/window-size';
|
||||
import ModuleBar from './_module-bar.vue';
|
||||
import api from '@/api';
|
||||
|
||||
// Breakpoints:
|
||||
// 600, 960, 1260, 1900
|
||||
|
||||
export default createComponent({
|
||||
components: {
|
||||
ModuleBar
|
||||
},
|
||||
props: {},
|
||||
setup() {
|
||||
const navOpen = ref<boolean>(false);
|
||||
const drawerOpen = ref<boolean>(false);
|
||||
|
||||
const { width } = useWindowSize();
|
||||
|
||||
const navWithOverlay = computed<boolean>(() => width.value < 960);
|
||||
const drawerWithOverlay = computed<boolean>(() => width.value < 1260);
|
||||
|
||||
return {
|
||||
navOpen,
|
||||
drawerOpen,
|
||||
navWithOverlay,
|
||||
drawerWithOverlay,
|
||||
width
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixins/breakpoint';
|
||||
|
||||
.private-view {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.nav-overlay {
|
||||
--v-overlay-z-index: 49;
|
||||
}
|
||||
|
||||
.drawer-overlay {
|
||||
--v-overlay-z-index: 29;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
height: 100%;
|
||||
font-size: 0;
|
||||
transform: translateX(-100%);
|
||||
transition: transform var(--slow) var(--transition);
|
||||
|
||||
&.is-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.module-nav {
|
||||
display: inline-block;
|
||||
width: 220px;
|
||||
height: 100%;
|
||||
font-size: 1rem;
|
||||
background-color: #eceff1;
|
||||
}
|
||||
|
||||
@include breakpoint(medium) {
|
||||
position: relative;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 30;
|
||||
width: 284px;
|
||||
height: 100%;
|
||||
background-color: #eceff1;
|
||||
transform: translateX(100%);
|
||||
transition: transform var(--slow) var(--transition);
|
||||
|
||||
&.is-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
@include breakpoint(medium) {
|
||||
transform: translateX(calc(100% - 64px));
|
||||
}
|
||||
|
||||
@include breakpoint(large) {
|
||||
position: relative;
|
||||
flex-basis: 64px;
|
||||
transform: none;
|
||||
transition: flex-basis var(--slow) var(--transition);
|
||||
|
||||
&.is-open {
|
||||
flex-basis: 284px;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +1,4 @@
|
||||
import Vue from 'vue';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
|
||||
import { VTooltip } from 'v-tooltip';
|
||||
@@ -64,7 +65,7 @@ describe('Views / Public', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Uses the project color when the current project key is set, but background image is not', () => {
|
||||
it('Uses the project color when the current project key is set, but background image is not', async () => {
|
||||
store.state.projects = [
|
||||
{
|
||||
...mockProject,
|
||||
@@ -76,6 +77,8 @@ describe('Views / Public', () => {
|
||||
];
|
||||
store.state.currentProjectKey = 'my-project';
|
||||
|
||||
await component.vm.$nextTick();
|
||||
|
||||
expect((component.vm as any).artStyles).toEqual({
|
||||
background: '#4CAF50'
|
||||
});
|
||||
|
||||
12
yarn.lock
12
yarn.lock
@@ -1757,6 +1757,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
|
||||
integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
|
||||
|
||||
"@types/nanoid@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/nanoid/-/nanoid-2.1.0.tgz#41edfda78986e9127d0dc14de982de766f994020"
|
||||
integrity sha512-xdkn/oRTA0GSNPLIKZgHWqDTWZsVrieKomxJBOQUK9YDD+zfSgmwD5t4WJYra5S7XyhTw7tfvwznW+pFexaepQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*":
|
||||
version "13.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.0.tgz#b417deda18cf8400f278733499ad5547ed1abec4"
|
||||
@@ -9780,6 +9787,11 @@ nan@^2.12.1:
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
|
||||
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
|
||||
|
||||
nanoid@^2.1.11:
|
||||
version "2.1.11"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
|
||||
integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==
|
||||
|
||||
nanomatch@^1.2.9:
|
||||
version "1.2.13"
|
||||
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
||||
|
||||
Reference in New Issue
Block a user