Skip to content

Commit

Permalink
Merge pull request #4254 from FlowFuse/4245-notifications-rewire
Browse files Browse the repository at this point in the history
Rewire frontend notifications to backend notifications API
  • Loading branch information
knolleary committed Jul 30, 2024
2 parents fbcca84 + 8cd53a3 commit b616d07
Show file tree
Hide file tree
Showing 13 changed files with 188 additions and 145 deletions.
17 changes: 17 additions & 0 deletions frontend/src/api/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@ const deleteUser = async () => {
return res.data
})
}
const getNotifications = async () => {
return client.get('/api/v1/user/notifications').then(res => {
res.data.invitations = res.data.notifications.map(r => {
r.createdSince = daysSince(r.createdAt)
return r
})
return res.data
})
}
const markNotificationRead = async (id) => {
return client.put('/api/v1/user/notifications/' + id, {
read: true
})
}

const getTeamInvitations = async () => {
return client.get('/api/v1/user/invitations').then(res => {
res.data.invitations = res.data.invitations.map(r => {
Expand Down Expand Up @@ -224,6 +239,8 @@ export default {
changePassword,
updateUser,
deleteUser,
getNotifications,
markNotificationRead,
getTeamInvitations,
acceptTeamInvitation,
rejectTeamInvitation,
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/components/NotificationsButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<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" />
<ff-notification-pill v-if="hasNotifications" data-el="notification-pill" class="ml-3" :count="notificationsCount" />
</button>
</div>
</template>
Expand All @@ -20,7 +20,14 @@ export default {
computed: {
...mapState('ux', ['rightDrawer']),
...mapGetters('account', ['hasNotifications']),
...mapGetters('account', ['notifications'])
...mapGetters('account', ['unreadNotificationsCount']),
notificationsCount: function () {
// Return null if count = 0 so we don't show a 0 in the pill
if (!this.unreadNotificationsCount) {
return null
}
return this.unreadNotificationsCount
}
},
methods: {
...mapActions('ux', ['openRightDrawer', 'closeRightDrawer']),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
<!-- </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">
<li v-for="notification in notifications" :key="notification.id" data-el="message">
<component
:is="notificationsComponentMap['team-invitation']"
:is="notificationsComponentMap[notification.type]"
:notification="notification"
:selections="selections"
@selected="onSelected"
Expand All @@ -38,21 +38,21 @@ export default {
return {
notificationsComponentMap: {
// todo replace hardcoded value with actual notification type
'team-invitation': markRaw(TeamInvitationNotification)
'team-invite': markRaw(TeamInvitationNotification)
},
selections: []
}
},
computed: {
...mapGetters('account', ['notificationMessages']),
...mapGetters('account', ['notifications']),
canSelectAll () {
return this.notificationMessages.length !== this.selections.length
return this.notifications.length !== this.selections.length
},
canDeselectAll () {
return this.selections.length > 0
},
hasNotificationMessages () {
return this.notificationMessages.length > 0
return this.notifications.length > 0
}
},
methods: {
Expand All @@ -66,7 +66,7 @@ export default {
}
},
selectAll () {
this.selections = [...this.notificationMessages]
this.selections = [...this.notifications]
},
deselectAll () {
this.selections = []
Expand Down
24 changes: 14 additions & 10 deletions frontend/src/components/notifications/Notification.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="message" @click="go(to)">
<div class="message" :class="{ unread: !notification.read }" @click="go(to)">
<div class="body">
<div class="icon ff-icon ff-icon-lg">
<slot name="icon" />
Expand All @@ -23,6 +23,8 @@
<script>
import { mapActions } from 'vuex'
import userApi from '../../api/user.js'
import NotificationMessageMixin from '../../mixins/NotificationMessage.js'
export default {
Expand All @@ -34,24 +36,26 @@ export default {
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)
this.notification.read = true
userApi.markNotificationRead(this.notification.id)
if (to.url) {
// Handle external links
window.open(to.url, '_blank').focus()
} else {
this.$router.push(to)
}
}
}
}
</script>

<style scoped lang="scss">
.body.unread {
border-left: 3px solid blue;
}
</style>
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<template>
<NotificationMessage data-el="invitation-message" :to="{name: 'User Invitations'}">
<NotificationMessage
:notification="notification"
:selections="selections"
data-el="invitation-message" :to="{name: 'User Invitations'}"
>
<template #icon>
<UserAddIcon />
</template>
Expand Down Expand Up @@ -28,10 +32,10 @@ export default {
mixins: [NotificationMessageMixin],
computed: {
invitorName () {
return this.notification.invitor.name
return this.notification.data.invitor.username
},
teamName () {
return this.notification.team.name
return this.notification.data.team.name
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/account/NoTeamsUser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ export default {
computed: {
...mapState('account', ['settings', 'user']),
...mapGetters('account', {
invitationCount: 'totalNotificationsCount'
invitationCount: 'teamInvitationsCount'
})
},
mounted () {
this.fetchData()
},
methods: {
async fetchData () {
await this.$store.dispatch('account/getNotifications')
await this.$store.dispatch('account/getInvitations')
}
}
}
Expand Down
9 changes: 4 additions & 5 deletions frontend/src/pages/account/Teams/Invitations.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="space-y-6">
<ff-data-table data-el="table" :columns="inviteColumns" :rows="invitations">
<ff-data-table data-el="table" :columns="inviteColumns" :rows="invitations" noDataMessage="No Invitations">
<template #row-actions="{row}">
<ff-button data-action="invite-reject" kind="secondary-danger" @click="rejectInvite(row)">Reject</ff-button>
<ff-button data-action="invite-accept" @click="acceptInvite(row)">Accept</ff-button>
Expand Down Expand Up @@ -43,12 +43,13 @@ export default {
})
},
mounted () {
this.$store.dispatch('account/getNotifications')
this.$store.dispatch('account/getInvitations')
},
methods: {
async acceptInvite (invite) {
await userApi.acceptTeamInvitation(invite.id, invite.team.id)
await this.$store.dispatch('account/getNotifications')
await this.$store.dispatch('account/getInvitations')
await this.$store.dispatch('account/refreshTeams')
Alerts.emit(`Invite to "${invite.team.name}" has been accepted.`, 'confirmation')
// navigate to team dashboad once invite accepted
Expand All @@ -62,10 +63,8 @@ export default {
async rejectInvite (invite) {
await userApi.rejectTeamInvitation(invite.id, invite.team.id)
await this.$store.dispatch('account/getNotifications')
await this.$store.dispatch('account/getInvitations')
Alerts.emit(`Invite to "${invite.team.name}" has been rejected.`, 'confirmation')
},
async fetchData () {
await this.$store.dispatch('account/getNotifications')
}
}
}
Expand Down
14 changes: 7 additions & 7 deletions frontend/src/pages/account/Teams/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ export default {
},
computed: {
...mapState('account', ['user', 'teams']),
...mapGetters('account', ['notifications'])
...mapGetters('account', ['teamInvitationsCount'])
},
watch: {
notifications: {
teamInvitationsCount: {
handler: function () {
this.updateNotifications()
this.updateInvitations()
},
deep: true
}
Expand All @@ -44,12 +44,12 @@ export default {
{ name: 'Teams', path: '/account/teams' }
]
this.sideNavigation.push({ name: 'Invitations', path: '/account/teams/invitations' })
this.updateNotifications()
this.updateInvitations()
},
methods: {
updateNotifications () {
if (this.notifications.invitations > 0) {
this.sideNavigation[1].name = `Invitations (${this.notifications.invitations})`
updateInvitations () {
if (this.teamInvitationsCount > 0) {
this.sideNavigation[1].name = `Invitations (${this.teamInvitationsCount})`
} else {
this.sideNavigation[1].name = 'Invitations'
}
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/pages/account/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ export default {
},
computed: {
...mapState('account', ['user', 'team']),
...mapGetters('account', ['notifications'])
...mapGetters('account', ['teamInvitationsCount'])
},
watch: {
notifications: {
teamInvitationsCount: {
handler: function () {
this.updateNotifications()
},
Expand All @@ -77,7 +77,7 @@ export default {
},
methods: {
updateNotifications () {
this.navigation[1].notifications = this.notifications.invitations
this.navigation[1].notifications = this.teamInvitationsCount
}
}
}
Expand Down
56 changes: 24 additions & 32 deletions frontend/src/store/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const state = () => ({
notifications: {
payload: []
},
invitations: [],
// An error during login
loginError: null,
//
Expand Down Expand Up @@ -60,26 +61,6 @@ const getters = {
teamMembership (state) {
return state.teamMembership
},
notifications (state) {
const n = { ...state.notifications }

let sum = 0
for (const type of Object.keys(n)) {
if (!['total', 'payload'].includes(type)) {
sum += n[type]
}
}

n.total = sum

return n
},
notificationMessages (state, getters) {
const notifications = getters.notifications.payload ?? []

return notifications.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
},
totalNotificationsCount: (state, getters) => getters.notifications.total,
redirectUrlAfterLogin (state) {
return state.redirectUrlAfterLogin
},
Expand All @@ -99,8 +80,14 @@ const getters = {
},
blueprints: state => state.teamBlueprints[state.team?.id] || [],
defaultBlueprint: (state, getters) => getters.blueprints?.find(blueprint => blueprint.default),
hasNotifications: (state, getters) => getters.notifications.total > 0,
teamInvitations: state => state.notifications.payload // filter out team invites by notification.type = invite

notifications: state => state.notifications,
notificationsCount: state => state.notifications?.length || 0,
unreadNotificationsCount: state => state.notifications?.filter(n => !n.read).length || 0,
hasNotifications: (state, getters) => getters.notificationsCount > 0,

teamInvitations: state => state.invitations,
teamInvitationsCount: state => state.invitations?.length || 0
}

const mutations = {
Expand Down Expand Up @@ -140,11 +127,8 @@ const mutations = {
setTeams (state, teams) {
state.teams = teams
},
setNotificationsCount (state, payload) {
state.notifications[payload.type] = payload.count
},
setNotificationsPayload (state, notifications) {
state.notifications.payload = notifications
setNotifications (state, notifications) {
state.notifications = notifications
},
sessionExpired (state) {
state.user = null
Expand All @@ -167,6 +151,9 @@ const mutations = {
},
setTeamBlueprints (state, { teamId, blueprints }) {
state.teamBlueprints[teamId] = blueprints
},
setTeamInvitations (state, invitations) {
state.invitations = invitations
}
}

Expand Down Expand Up @@ -195,6 +182,8 @@ const actions = {

// check notifications count
await dispatch('getNotifications')
// check notifications count
await dispatch('getInvitations')

const teams = await teamApi.getTeams()
commit('setTeams', teams.teams)
Expand Down Expand Up @@ -358,13 +347,16 @@ const actions = {
state.commit('setRedirectUrl', url)
},
async getNotifications (state) {
await userApi.getNotifications()
.then((notifications) => {
state.commit('setNotifications', notifications.notifications)
})
.catch(_ => {})
},
async getInvitations (state) {
await userApi.getTeamInvitations()
.then((invitations) => {
state.commit('setNotificationsCount', {
type: 'invitations',
count: invitations.count
})
state.commit('setNotificationsPayload', invitations.invitations)
state.commit('setTeamInvitations', invitations.invitations)
})
.catch(_ => {})
}
Expand Down
Loading

0 comments on commit b616d07

Please sign in to comment.