mirror of
https://github.com/directus/directus.git
synced 2026-04-03 03:00:39 -04:00
Add notifications inline drawer (#292)
* Render missed notifications inline in drawer * Fix tests
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
v-bind="notification"
|
||||
:tail="index === queue.length - 1"
|
||||
dense
|
||||
:show-close="notification.persist === true"
|
||||
/>
|
||||
</transition-group>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import NotificationsPreview from './notifications-preview.vue';
|
||||
|
||||
export { NotificationsPreview };
|
||||
export default NotificationsPreview;
|
||||
@@ -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>
|
||||
`,
|
||||
});
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
# Notifications Preview
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user