Skip to content

Commit

Permalink
Merge pull request #4210 from FlowFuse/4184_notifications-inbox-view
Browse files Browse the repository at this point in the history
Add notifications drawer
  • Loading branch information
joepavitt authored Jul 19, 2024
2 parents 168eb19 + a7f8528 commit 940a813
Show file tree
Hide file tree
Showing 18 changed files with 736 additions and 66 deletions.
76 changes: 76 additions & 0 deletions frontend/src/components/NotificationsButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<template>
<div class="notifications-button-wrapper">
<button class="notifications-button" data-el="notifications-button" data-click-exclude="right-drawer" @click="onClick">
<MailIcon />
<ff-notification-pill v-if="hasNotifications" data-el="notification-pill" class="ml-3" :count="notifications.total" />
</button>
</div>
</template>

<script>
import { MailIcon } from '@heroicons/vue/solid'
import { markRaw } from 'vue'
import { mapActions, mapGetters, mapState } from 'vuex'
import NotificationsDrawer from './drawers/notifications/NotificationsDrawer.vue'
export default {
name: 'NotificationsButton',
components: { MailIcon },
computed: {
...mapState('ux', ['rightDrawer']),
...mapGetters('account', ['hasNotifications']),
...mapGetters('account', ['notifications'])
},
methods: {
...mapActions('ux', ['openRightDrawer', 'closeRightDrawer']),
onClick () {
if (this.rightDrawer.state) {
this.closeRightDrawer()
} else this.openRightDrawer({ component: markRaw(NotificationsDrawer) })
}
}
}
</script>

<style scoped lang="scss">
.notifications-button-wrapper {
.notifications-button {
border-left: 1px solid $ff-grey-500;
color: white;
display: flex;
align-items: center;
flex: 1;
justify-content: center;
width: 100%;
height: 100%;
padding: 18px;
position: relative;
> * {
pointer-events: none;
}
&:hover {
background-color: $ff-grey-700;
}
svg {
flex: 1;
width: 24px;
height: 24px;
}
.ff-notification-pill {
bottom: 10px;
right: 5px;
position: absolute;
font-size: 0.65rem;
padding: 0 7px;
background-color: $ff-red-500;
}
}
}
</style>
18 changes: 6 additions & 12 deletions frontend/src/components/PageHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,11 @@
</ff-button>
</div>
<!-- Desktop: User Options -->
<NotificationsButton />
<ff-dropdown v-if="user" class="ff-navigation ff-user-options" options-align="right" data-action="user-options" data-cy="user-options">
<template #placeholder>
<div class="ff-user">
<img :src="user.avatar" class="ff-avatar">
<ff-notification-pill v-if="notifications.total > 0" data-el="notification-pill" class="ml-3" :count="notifications.total" />
<!-- <label>{{ user.name }}</label> -->
</div>
</template>
<template #default>
Expand All @@ -62,13 +61,15 @@
</div>
</template>
<script>
import { AdjustmentsIcon, CogIcon, LogoutIcon, MenuIcon, PlusIcon, QuestionMarkCircleIcon, UserAddIcon, UserGroupIcon } from '@heroicons/vue/solid'
import { AdjustmentsIcon, CogIcon, LogoutIcon, MenuIcon, PlusIcon, QuestionMarkCircleIcon, UserAddIcon } from '@heroicons/vue/solid'
import { ref } from 'vue'
import { mapGetters, mapState } from 'vuex'
import navigationMixin from '../mixins/Navigation.js'
import NavItem from './NavItem.vue'
import NotificationsButton from './NotificationsButton.vue'
import TeamSelection from './TeamSelection.vue'
export default {
Expand All @@ -92,14 +93,6 @@ export default {
onclick: this.$router.push,
onclickparams: { name: 'User Settings' }
},
{
label: 'Team Invitations',
icon: UserGroupIcon,
tag: 'team-invitations',
onclick: this.$router.push,
onclickparams: { name: 'User Invitations' },
notifications: this.notifications.invitations
},
this.user.admin
? {
label: 'Admin Settings',
Expand Down Expand Up @@ -139,7 +132,8 @@ export default {
NavItem,
'ff-team-selection': TeamSelection,
MenuIcon,
UserAddIcon
UserAddIcon,
NotificationsButton
},
data () {
return {
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/components/drawers/RightDrawer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<template>
<div v-click-outside="{handler: closeRightDrawer, exclude: ['right-drawer']}" class="right-drawer" :class="{open: rightDrawer.state}" data-el="right-drawer">
<component :is="rightDrawer.component" v-if="rightDrawer.component" />
</div>
</template>

<script>
import { mapActions, mapState } from 'vuex'
export default {
name: 'RightDrawer',
computed: {
...mapState('ux', ['rightDrawer'])
},
methods: {
...mapActions('ux', ['closeRightDrawer'])
}
}
</script>

<style scoped lang="scss">
.right-drawer {
position: absolute;
border-left: 1px solid $ff-grey-300;
background: white;
height: 100%;
right: -1000px;
z-index: 500;
width: 100%;
max-width: 0;
min-width: 0;
transition: ease-in-out .3s;
box-shadow: -5px 0px 8px rgba(0, 0, 0, 0.1);
&.open {
right: 0;
width: 100%;
max-width: 30vw;
min-width: 400px;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<template>
<div class="ff-notifications-drawer" data-el="notifications-drawer">
<div class="header">
<h2 class="title">Notifications</h2>
<!-- <div class="actions">-->
<!-- <span class="forge-badge" :class="{disabled: !canSelectAll}" @click="selectAll">select all</span>-->
<!-- <span class="forge-badge" :class="{disabled: !canDeselectAll}" @click="deselectAll">deselect all</span>-->
<!-- <span class="forge-badge disabled">mark as read</span>-->
<!-- <span class="forge-badge disabled">mark as unread</span>-->
<!-- </div>-->
</div>
<ul v-if="hasNotificationMessages" class="messages-wrapper" data-el="messages-wrapper">
<li v-for="notification in notificationMessages" :key="notification.id" data-el="message">
<component
:is="notificationsComponentMap['team-invitation']"
:notification="notification"
:selections="selections"
@selected="onSelected"
@deselected="onDeselected"
/>
</li>
</ul>
<div v-else class="empty">
<p>Nothing so far...</p>
</div>
</div>
</template>

<script>
import { markRaw } from 'vue'
import { mapGetters } from 'vuex'
import TeamInvitationNotification from '../../notifications/TeamInvitationNotification.vue'
export default {
name: 'NotificationsDrawer',
data () {
return {
notificationsComponentMap: {
// todo replace hardcoded value with actual notification type
'team-invitation': markRaw(TeamInvitationNotification)
},
selections: []
}
},
computed: {
...mapGetters('account', ['notificationMessages']),
canSelectAll () {
return this.notificationMessages.length !== this.selections.length
},
canDeselectAll () {
return this.selections.length > 0
},
hasNotificationMessages () {
return this.notificationMessages.length > 0
}
},
methods: {
onSelected (notification) {
this.selections.push(notification)
},
onDeselected (notification) {
const index = this.selections.findIndex(n => n.id === notification.id)
if (index > -1) {
this.selections.splice(index, 1)
}
},
selectAll () {
this.selections = [...this.notificationMessages]
},
deselectAll () {
this.selections = []
}
}
}
</script>

<style scoped lang="scss">
</style>
57 changes: 57 additions & 0 deletions frontend/src/components/notifications/Notification.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<template>
<div class="message" @click="go(to)">
<div class="body">
<div class="icon ff-icon ff-icon-lg">
<slot name="icon" />
</div>
<div class="text">
<div class="header">
<h4 class="title"><slot name="title" /></h4>
<!-- <ff-checkbox :model-value="isSelected" label="" @click.prevent.stop="toggleSelection" />-->
</div>
<div class="content">
<slot name="message" />
</div>
</div>
</div>
<div class="footer">
<slot name="timestamp" />
</div>
</div>
</template>

<script>
import { mapActions } from 'vuex'
import NotificationMessageMixin from '../../mixins/NotificationMessage.js'
export default {
name: 'TeamInvitationNotification',
mixins: [NotificationMessageMixin],
props: {
to: {
type: Object,
required: true
}
},
computed: {
invitorName () {
return this.notification.invitor.name
},
teamName () {
return this.notification.team.name
}
},
methods: {
...mapActions('ux', ['closeRightDrawer']),
go (to) {
this.closeRightDrawer()
this.$router.push(to)
}
}
}
</script>

<style scoped lang="scss">
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<template>
<NotificationMessage data-el="invitation-message" :to="{name: 'User Invitations'}">
<template #icon>
<UserAddIcon />
</template>
<template #title>
Team Invitation
</template>
<template #message>
You have been invited by <i>"{{ invitorName }}"</i> to join <i>"{{ teamName }}"</i>.
</template>
<template #timestamp>
{{ notification.createdSince }}
</template>
</NotificationMessage>
</template>

<script>
import { UserAddIcon } from '@heroicons/vue/solid'
import NotificationMessageMixin from '../../mixins/NotificationMessage.js'
import NotificationMessage from './Notification.vue'
export default {
name: 'TeamInvitationNotification',
components: { NotificationMessage, UserAddIcon },
mixins: [NotificationMessageMixin],
computed: {
invitorName () {
return this.notification.invitor.name
},
teamName () {
return this.notification.team.name
}
}
}
</script>

<style scoped lang="scss">
</style>
3 changes: 3 additions & 0 deletions frontend/src/layouts/Platform.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</div>
<div class="ff-view">
<div id="platform-banner" />
<RightDrawer />
<slot />
</div>
<TransitionGroup class="ff-notifications" name="notifications-list" tag="div">
Expand Down Expand Up @@ -36,12 +37,14 @@ import { mapState } from 'vuex'
import InterviewPopup from '../components/InterviewPopup.vue'
import PageHeader from '../components/PageHeader.vue'
import RightDrawer from '../components/drawers/RightDrawer.vue'
import AlertsMixin from '../mixins/Alerts.js'
import DialogMixin from '../mixins/Dialog.js'
export default {
name: 'ff-layout-platform',
components: {
RightDrawer,
PageHeader,
InterviewPopup
},
Expand Down
Loading

0 comments on commit 940a813

Please sign in to comment.