User sign out (#234)

* Add fullName getter to user store

* Add user sign out link

* Add tests for module bar avatar
This commit is contained in:
Rijk van Zanten
2020-03-23 14:06:30 -04:00
committed by GitHub
parent 98a9ae0882
commit 7d1df455fd
10 changed files with 399 additions and 12 deletions

View File

@@ -0,0 +1,4 @@
import ModuleBarAvatar from './module-bar-avatar.vue';
export { ModuleBarAvatar };
export default ModuleBarAvatar;

View File

@@ -0,0 +1,73 @@
import markdown from './readme.md';
import withPadding from '../../../../../.storybook/decorators/with-padding';
import { defineComponent } from '@vue/composition-api';
import useProjectsStore from '@/stores/projects';
import useUserStore from '@/stores/user';
import ModuleBarAvatar from './module-bar-avatar.vue';
import { i18n } from '@/lang/';
import VueRouter from 'vue-router';
export default {
title: 'Views / Private / Components / Module Bar Avatar',
decorators: [withPadding],
parameters: {
notes: markdown
}
};
export const basic = () =>
defineComponent({
i18n,
router: new VueRouter(),
components: { ModuleBarAvatar },
setup() {
const req = {};
const projectsStore = useProjectsStore(req);
const userStore = useUserStore(req);
projectsStore.state.currentProjectKey = 'my-project';
userStore.state.currentUser = {
first_name: 'Admin',
last_name: 'User',
avatar: null
} as any;
},
template: `
<div style="width: max-content; padding-top: 128px; background-color: #263238;">
<module-bar-avatar />
</div>
`
});
export const withAvatar = () =>
defineComponent({
i18n,
router: new VueRouter(),
components: { ModuleBarAvatar },
setup() {
const req = {};
const projectsStore = useProjectsStore(req);
const userStore = useUserStore(req);
projectsStore.state.currentProjectKey = 'my-project';
userStore.state.currentUser = {
first_name: 'Admin',
last_name: 'User',
avatar: {
data: {
thumbnails: [
{
key: 'directus-small-crop',
url: 'https://randomuser.me/api/portraits/women/44.jpg'
}
]
}
}
} as any;
},
template: `
<div style="width: max-content; padding-top: 128px; background-color: #263238;">
<module-bar-avatar />
</div>
`
});

View File

@@ -0,0 +1,139 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import useUserStore from '@/stores/user';
import useProjectsStore from '@/stores/projects';
import ModuleBarAvatar from './module-bar-avatar.vue';
import VueCompositionAPI from '@vue/composition-api';
import { i18n } from '@/lang/';
import VueRouter from 'vue-router';
import VIcon from '@/components/v-icon';
import VButton from '@/components/v-button';
import VAvatar from '@/components/v-avatar';
import VDialog from '@/components/v-dialog';
import VOverlay from '@/components/v-dialog';
import VCard, { VCardTitle, VCardActions } from '@/components/v-card';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.use(VueRouter);
localVue.component('v-icon', VIcon);
localVue.component('v-button', VButton);
localVue.component('v-avatar', VAvatar);
localVue.component('v-dialog', VDialog);
localVue.component('v-card', VCard);
localVue.component('v-card-title', VCardTitle);
localVue.component('v-card-actions', VCardActions);
localVue.component('v-overlay', VOverlay);
describe('Views / Private / Module Bar Avatar', () => {
let req: any = {};
beforeEach(() => {
req = {};
});
it('Returns correct avatar url for thumbnail key', () => {
const userStore = useUserStore(req);
useProjectsStore(req);
userStore.state.currentUser = {
id: 1,
avatar: {
data: {
thumbnails: [
{
key: 'test',
url: 'test'
},
{
key: 'directus-small-crop',
url: 'test1'
}
]
}
}
} as any;
const component = shallowMount(ModuleBarAvatar, {
localVue,
i18n,
stubs: {
'v-hover': '<div><slot v-bind="{ hover: false }" /></div>'
}
});
expect((component.vm as any).avatarURL).toBe('test1');
});
it('Returns null if avatar is null', () => {
const userStore = useUserStore(req);
useProjectsStore(req);
userStore.state.currentUser = {
id: 1,
avatar: null
} as any;
const component = shallowMount(ModuleBarAvatar, {
localVue,
i18n,
stubs: {
'v-hover': '<div><slot v-bind="{ hover: false }" /></div>'
}
});
expect((component.vm as any).avatarURL).toBe(null);
});
it('Returns null if thumbnail can not be found', () => {
const userStore = useUserStore(req);
useProjectsStore(req);
userStore.state.currentUser = {
id: 1,
avatar: {
data: {
thumbnails: [
{
key: 'fake',
url: 'non-existent'
}
]
}
}
} as any;
const component = shallowMount(ModuleBarAvatar, {
localVue,
i18n,
stubs: {
'v-hover': '<div><slot v-bind="{ hover: false }" /></div>'
}
});
expect((component.vm as any).avatarURL).toBe(null);
});
it('Calculates correct routes for user profile and sign out', () => {
const userStore = useUserStore(req);
const projectsStore = useProjectsStore(req);
projectsStore.state.currentProjectKey = 'my-project';
userStore.state.currentUser = {
id: 1,
avatar: null
} as any;
const component = shallowMount(ModuleBarAvatar, {
localVue,
i18n,
stubs: {
'v-hover': '<div><slot v-bind="{ hover: false }" /></div>'
}
});
expect((component.vm as any).userProfileLink).toBe('/my-project/users/1');
expect((component.vm as any).signOutLink).toBe('/my-project/logout');
});
});

View File

@@ -0,0 +1,92 @@
<template>
<v-hover class="module-bar-avatar" v-slot="{ hover }">
<v-dialog v-model="signOutActive">
<template #activator="{ on }">
<v-button @click="on" tile icon x-large :class="{ show: hover }" class="sign-out">
<v-icon name="logout" />
</v-button>
</template>
<v-card>
<v-card-title>{{ $t('sign_out_confirm') }}</v-card-title>
<v-card-actions>
<v-button secondary @click="signOutActive = !signOutActive">
{{ $t('cancel') }}
</v-button>
<v-button :to="signOutLink">{{ $t('sign_out') }}</v-button>
</v-card-actions>
</v-card>
</v-dialog>
<router-link :to="userProfileLink">
<v-avatar tile x-large>
<img v-if="avatarURL" :src="avatarURL" :alt="userFullName" />
<v-icon v-else name="account_circle" />
</v-avatar>
</router-link>
</v-hover>
</template>
<script lang="ts">
import { defineComponent, computed, ref } from '@vue/composition-api';
import useUserStore from '@/stores/user/';
import useProjectsStore from '@/stores/projects';
export default defineComponent({
setup() {
const userStore = useUserStore();
const projectsStore = useProjectsStore();
const signOutActive = ref(false);
const avatarURL = computed<string | null>(() => {
if (userStore.state.currentUser === null) return null;
if (userStore.state.currentUser.avatar === null) return null;
const thumbnail = userStore.state.currentUser.avatar.data.thumbnails.find(thumb => {
return thumb.key === 'directus-small-crop';
});
return thumbnail?.url || null;
});
const userProfileLink = computed<string>(() => {
const project = projectsStore.state.currentProjectKey;
// This is rendered in the private view, which is only accessible as a logged in user
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const id = userStore.state.currentUser!.id;
return `/${project}/users/${id}`;
});
const signOutLink = computed<string>(() => {
const project = projectsStore.state.currentProjectKey;
return `/${project}/logout`;
});
const userFullName = userStore.fullName;
return { userFullName, avatarURL, userProfileLink, signOutActive, signOutLink };
}
});
</script>
<style lang="scss" scoped>
.module-bar-avatar {
position: relative;
}
.sign-out {
position: absolute;
top: 0;
left: 0;
&.show {
transform: translateY(-100%);
}
.v-icon {
--v-icon-color: var(--white);
}
}
</style>

View File

@@ -0,0 +1,21 @@
# Module Bar Avatar
Renders the avatar and shows the sign out button on hover.
## Usage
```html
<module-bar-avatar />
```
## Props
n/a
## Events
n/a
## Slots
n/a
## CSS Variables
n/a

View File

@@ -4,6 +4,8 @@ import { defineComponent } from '@vue/composition-api';
import VueRouter from 'vue-router';
import useProjectsStore from '@/stores/projects';
import useRequestsStore from '@/stores/requests';
import useUserStore from '@/stores/user';
import i18n from '@/lang/';
export default {
title: 'Views / Private / Components / Module Bar',
@@ -14,11 +16,16 @@ export default {
export const basic = () =>
defineComponent({
router: new VueRouter(),
i18n,
router: new VueRouter({}),
components: { ModuleBar },
setup() {
useProjectsStore({});
useRequestsStore({});
const req = {};
useRequestsStore(req);
const userStore = useUserStore(req);
userStore.state.currentUser = { id: 1, avatar: null } as any;
const projectsStore = useProjectsStore(req);
projectsStore.state.currentProjectKey = 'my-project';
},
template: `
<module-bar />

View File

@@ -1,21 +1,26 @@
<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 class="modules">
<v-button v-for="module in _modules" :key="module.id" icon x-large :to="module.to">
<v-icon :name="module.icon" />
</v-button>
</div>
<module-bar-avatar />
</div>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import ModuleBarLogo from '../module-bar-logo/';
import { useProjectsStore } from '@/stores/projects';
import { modules } from '@/modules/';
import ModuleBarLogo from '../module-bar-logo/';
import ModuleBarAvatar from '../module-bar-avatar/';
export default defineComponent({
components: {
ModuleBarLogo
ModuleBarLogo,
ModuleBarAvatar
},
setup() {
const projectsStore = useProjectsStore();
@@ -33,10 +38,18 @@ export default defineComponent({
<style lang="scss" scoped>
.module-bar {
display: flex;
flex-direction: column;
width: 64px;
height: 100%;
background-color: #263238;
.modules {
flex-grow: 1;
overflow-x: hidden;
overflow-y: auto;
}
.v-button {
--v-button-color: var(--blue-grey-400);
}

View File

@@ -3,6 +3,10 @@ import PrivateView from './private-view.vue';
import markdown from './readme.md';
import VueRouter from 'vue-router';
import { defineComponent } from '@vue/composition-api';
import useRequestsStore from '@/stores/requests';
import useProjectsStore from '@/stores/projects';
import useUserStore from '@/stores/user';
import { i18n } from '@/lang/';
Vue.component('private-view', PrivateView);
Vue.use(VueRouter);
@@ -16,8 +20,17 @@ export default {
export const basic = () =>
defineComponent({
i18n,
router: new VueRouter(),
setup() {
const req = {};
useRequestsStore(req);
const userStore = useUserStore(req);
userStore.state.currentUser = { id: 1, avatar: null } as any;
const projectsStore = useProjectsStore(req);
projectsStore.state.currentProjectKey = 'my-project';
},
template: `
<private-view />
`
<private-view />
`
});