mirror of
https://github.com/directus/directus.git
synced 2026-04-03 03:00:39 -04:00
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:
4
src/views/private/components/module-bar-avatar/index.ts
Normal file
4
src/views/private/components/module-bar-avatar/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import ModuleBarAvatar from './module-bar-avatar.vue';
|
||||
|
||||
export { ModuleBarAvatar };
|
||||
export default ModuleBarAvatar;
|
||||
@@ -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>
|
||||
`
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
21
src/views/private/components/module-bar-avatar/readme.md
Normal file
21
src/views/private/components/module-bar-avatar/readme.md
Normal 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
|
||||
@@ -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 />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
`
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user