Add notifications inline drawer (#292)

* Render missed notifications inline in drawer

* Fix tests
This commit is contained in:
Rijk van Zanten
2020-04-02 15:35:46 -04:00
committed by GitHub
parent 170487e3b2
commit 9f79f744f3
14 changed files with 273 additions and 70 deletions

View File

@@ -2,6 +2,7 @@
<component
class="drawer-button"
:is="to ? 'router-link' : 'button'"
:class="{ active }"
@click="$emit('click', $event)"
>
<div class="icon">
@@ -26,6 +27,10 @@ export default defineComponent({
type: String,
default: 'box',
},
active: {
type: Boolean,
default: false,
},
},
setup() {
const drawerOpen = inject('drawer-open', ref(false));
@@ -64,12 +69,8 @@ export default defineComponent({
transform: translateY(-50%);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity var(--medium) var(--transition);
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
&.active {
background-color: var(--background-normal-alt);
}
}
</style>

View File

@@ -9,39 +9,6 @@ localVue.use(VueCompositionAPI);
localVue.component('v-icon', VIcon);
describe('Views / Private / Components / Notification Item', () => {
it('Calls remove with the notification ID automatically', () => {
jest.useFakeTimers();
const notificationsStore = useNotificationsStore();
jest.spyOn(notificationsStore as any, 'remove');
mount(NotificationItem, {
localVue,
propsData: {
id: '123',
title: 'Test',
},
});
jest.runAllTimers();
expect(notificationsStore.remove).toHaveBeenCalledWith('123');
});
it('Does not call remove with id after 1 second when persist is enabled', () => {
jest.useFakeTimers();
const notificationsStore = useNotificationsStore();
jest.spyOn(notificationsStore as any, 'remove');
mount(NotificationItem, {
localVue,
propsData: {
id: '123',
title: 'Test',
persist: true,
},
});
jest.runAllTimers();
expect(notificationsStore.remove).not.toHaveBeenCalledWith('123');
});
it('Calls remove with id on close click if persist is enabled', () => {
const notificationsStore = useNotificationsStore();
jest.spyOn(notificationsStore as any, 'remove');
@@ -51,6 +18,7 @@ describe('Views / Private / Components / Notification Item', () => {
id: '123',
title: 'Test',
persist: true,
showClose: true,
},
});

View File

@@ -9,7 +9,7 @@
<p v-if="text" class="text selectable">{{ text }}</p>
</div>
<v-icon v-if="persist" name="close" @click="close" class="close" />
<v-icon v-if="showClose" name="close" @click="close" class="close" />
</div>
</template>
@@ -40,10 +40,6 @@ export default defineComponent({
default: 'info',
validator: (val: string) => ['info', 'success', 'warning', 'error'].includes(val),
},
persist: {
type: Boolean,
default: false,
},
tail: {
type: Boolean,
default: false,
@@ -52,20 +48,20 @@ export default defineComponent({
type: Boolean,
default: false,
},
showClose: {
type: Boolean,
default: false,
},
},
setup(props) {
const notificationsStore = useNotificationsStore();
if (props.persist !== true) {
setTimeout(() => {
close();
}, 3000);
}
return { close };
function close() {
notificationsStore.remove(props.id);
if (props.showClose === true) {
notificationsStore.remove(props.id);
}
}
},
});

View File

@@ -6,6 +6,7 @@
v-bind="notification"
:tail="index === queue.length - 1"
dense
:show-close="notification.persist === true"
/>
</transition-group>
</template>

View File

@@ -0,0 +1,4 @@
import NotificationsPreview from './notifications-preview.vue';
export { NotificationsPreview };
export default NotificationsPreview;

View File

@@ -0,0 +1,71 @@
import readme from './readme.md';
import { defineComponent, ref, provide } from '@vue/composition-api';
import NotificationsPreview from './notifications-preview.vue';
import NotificationItem from '../notification-item/';
import DrawerButton from '../drawer-button/';
import useNotificationsStore from '@/stores/notifications';
import { NotificationRaw } from '@/stores/notifications/types';
import { i18n } from '@/lang';
import withPadding from '../../../../../.storybook/decorators/with-padding';
import VueRouter from 'vue-router';
export default {
title: 'Views / Private / Components / Notifications Preview',
parameters: {
notes: readme,
},
decorators: [withPadding],
};
const demoNotifications: NotificationRaw[] = [
{
title: 'Saved successfully',
icon: 'check',
type: 'success',
},
{
title: 'Whoops...',
text: 'Something went wrong. Please try again later.',
persist: true,
icon: 'error',
type: 'error',
},
{
title: 'Multiple users are editing this item',
text: 'Ben Haynes is currently editing this item.',
type: 'warning',
icon: 'warning',
},
{
title: 'Update available',
text: 'Directus v9 has been released.',
type: 'info',
},
];
export const basic = () =>
defineComponent({
i18n,
router: new VueRouter(),
components: { NotificationsPreview, NotificationItem, DrawerButton },
setup() {
const notificationsStore = useNotificationsStore({});
const active = ref(false);
provide('drawer-open', ref(true));
return { add, active };
function add() {
const randomIndex = Math.floor(Math.random() * demoNotifications.length);
notificationsStore.add(demoNotifications[randomIndex]);
}
},
template: `
<div style="width: 284px; background-color: var(--background-normal); height: 100%; display: flex; flex-direction: column;">
<v-button style="margin: 8px" @click="add">Add notification</v-button>
<div style="flex-grow: 1" />
<notifications-preview v-model="active" />
</div>
`,
});

View File

@@ -0,0 +1,111 @@
<template>
<div class="notifications-preview">
<transition-expand tag="div">
<div v-if="active" class="inline">
<div class="padding-box">
<router-link class="link" :to="activityLink">
{{ $t('show_all_activity') }}
</router-link>
<transition-group tag="div" name="notification" class="transition">
<notification-item
v-for="notification in lastFour"
:key="notification.id"
v-bind="notification"
/>
</transition-group>
</div>
</div>
</transition-expand>
<drawer-button
:active="active"
@click="$emit('toggle', !active)"
class="toggle"
icon="notifications"
>
{{ $t('notifications') }}
</drawer-button>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import DrawerButton from '../drawer-button';
import NotificationItem from '../notification-item';
import useNotificationsStore from '@/stores/notifications';
import useProjectsStore from '@/stores/projects';
export default defineComponent({
components: { DrawerButton, NotificationItem },
model: {
prop: 'active',
event: 'toggle',
},
props: {
active: {
type: Boolean,
default: false,
},
},
setup() {
const notificationsStore = useNotificationsStore();
const projectsStore = useProjectsStore();
const activityLink = computed(() => `/${projectsStore.state.currentProjectKey}/activity`);
return { lastFour: notificationsStore.lastFour, activityLink };
},
});
</script>
<style lang="scss" scoped>
.notifications-preview {
position: relative;
}
.link {
display: block;
color: var(--foreground-subdued);
text-align: center;
text-decoration: none;
&:hover {
color: var(--foreground-normal);
}
}
.transition {
position: relative;
width: 100%;
}
.inline {
position: absolute;
right: 0;
bottom: 100%;
width: 100%;
.padding-box {
position: relative;
width: 100%;
padding: 12px;
}
}
.notification-enter-active,
.notification-leave-active {
transition: all var(--slow) var(--transition);
}
.notification-leave-active {
position: absolute;
}
.notification-move {
transition: all 500ms var(--transition);
}
.notification-enter,
.notification-leave-to {
opacity: 0;
}
</style>

View File

@@ -0,0 +1 @@
# Notifications Preview

View File

@@ -4,10 +4,13 @@ import PrivateView from './private-view.vue';
import VOverlay from '@/components/v-overlay';
import VProgressCircular from '@/components/v-progress/circular';
import PortalVue from 'portal-vue';
import VueI18n from 'vue-i18n';
import { i18n } from '@/lang';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.use(PortalVue);
localVue.use(VueI18n);
localVue.component('v-overlay', VOverlay);
localVue.component('v-progress-circular', VProgressCircular);
@@ -15,6 +18,7 @@ describe('Views / Private', () => {
it('Adds the is-open class to the nav', async () => {
const component = shallowMount(PrivateView, {
localVue,
i18n,
propsData: {
title: 'Title',
},
@@ -32,6 +36,7 @@ describe('Views / Private', () => {
it('Adds the is-open class to the drawer', async () => {
const component = shallowMount(PrivateView, {
localVue,
i18n,
propsData: {
title: 'Title',
},

View File

@@ -40,30 +40,39 @@
:class="{ 'is-open': drawerOpen }"
@click="drawerOpen = true"
>
<drawer-button class="drawer-toggle" @click.stop="drawerOpen = !drawerOpen" icon="info">
Close Drawer
<drawer-button
class="drawer-toggle"
@click.stop="drawerOpen = !drawerOpen"
:icon="drawerOpen ? 'chevron_right' : 'chevron_left'"
>
{{ $t('collapse_sidebar') }}
</drawer-button>
<drawer-detail-group :drawer-open="drawerOpen">
<slot name="drawer" />
</drawer-detail-group>
<div class="spacer" />
<notifications-preview v-model="navigationsInline" />
</aside>
<v-overlay class="nav-overlay" :active="navOpen" @click="navOpen = false" />
<v-overlay class="drawer-overlay" :active="drawerOpen" @click="drawerOpen = false" />
<notifications-group />
<notifications-group v-if="navigationsInline === false" />
</div>
</template>
<script lang="ts">
import { defineComponent, ref, provide } from '@vue/composition-api';
import { defineComponent, ref, provide, watch } from '@vue/composition-api';
import ModuleBar from './components/module-bar/';
import DrawerDetailGroup from './components/drawer-detail-group/';
import HeaderBar from './components/header-bar';
import ProjectChooser from './components/project-chooser';
import DrawerButton from './components/drawer-button/';
import NotificationsGroup from './components/notifications-group/';
import NotificationsPreview from './components/notifications-preview/';
export default defineComponent({
components: {
@@ -73,6 +82,7 @@ export default defineComponent({
ProjectChooser,
DrawerButton,
NotificationsGroup,
NotificationsPreview,
},
props: {
title: {
@@ -84,6 +94,13 @@ export default defineComponent({
const navOpen = ref(false);
const drawerOpen = ref(false);
const contentEl = ref<Element>();
const navigationsInline = ref(false);
watch(drawerOpen, (open: boolean) => {
if (open === false) {
navigationsInline.value = false;
}
});
provide('drawer-open', drawerOpen);
provide('main-element', contentEl);
@@ -92,6 +109,7 @@ export default defineComponent({
navOpen,
drawerOpen,
contentEl,
navigationsInline,
};
},
});
@@ -182,12 +200,18 @@ export default defineComponent({
top: 0;
right: 0;
z-index: 30;
display: flex;
flex-direction: column;
width: 284px;
height: 100%;
background-color: var(--background-normal);
transform: translateX(100%);
transition: transform var(--slow) var(--transition);
.spacer {
flex-grow: 1;
}
&.is-open {
transform: translateX(0);
}