feat: Add unread badge to sidebar for inbox view (#10803)
# Pull Request Template ## Description This PR adds a badge to the icon in the sidebar group header and an unread highlight for the inbox view. ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? **Loom video** https://www.loom.com/share/1a880aa16de249a1836b8d014325c067?sid=87cc7b64-667b-4066-8993-d33abccf7b67 ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
This commit is contained in:
@@ -151,6 +151,9 @@ const menuItems = computed(() => {
|
|||||||
icon: 'i-lucide-inbox',
|
icon: 'i-lucide-inbox',
|
||||||
to: accountScopedRoute('inbox_view'),
|
to: accountScopedRoute('inbox_view'),
|
||||||
activeOn: ['inbox_view', 'inbox_view_conversation'],
|
activeOn: ['inbox_view', 'inbox_view_conversation'],
|
||||||
|
getterKeys: {
|
||||||
|
badge: 'notifications/getHasUnreadNotifications',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Conversation',
|
name: 'Conversation',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const props = defineProps({
|
|||||||
to: { type: Object, default: null },
|
to: { type: Object, default: null },
|
||||||
activeOn: { type: Array, default: () => [] },
|
activeOn: { type: Array, default: () => [] },
|
||||||
children: { type: Array, default: undefined },
|
children: { type: Array, default: undefined },
|
||||||
|
getterKeys: { type: Object, default: () => ({}) },
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -143,6 +144,7 @@ onMounted(async () => {
|
|||||||
:name
|
:name
|
||||||
:label
|
:label
|
||||||
:to
|
:to
|
||||||
|
:getter-keys="getterKeys"
|
||||||
:is-active="isActive"
|
:is-active="isActive"
|
||||||
:has-active-child="hasActiveChild"
|
:has-active-child="hasActiveChild"
|
||||||
:expandable="hasChildren"
|
:expandable="hasChildren"
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||||
import Icon from 'next/icon/Icon.vue';
|
import Icon from 'next/icon/Icon.vue';
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
to: { type: [Object, String], default: '' },
|
to: { type: [Object, String], default: '' },
|
||||||
label: { type: String, default: '' },
|
label: { type: String, default: '' },
|
||||||
icon: { type: [String, Object], default: '' },
|
icon: { type: [String, Object], default: '' },
|
||||||
@@ -9,9 +10,12 @@ defineProps({
|
|||||||
isExpanded: { type: Boolean, default: false },
|
isExpanded: { type: Boolean, default: false },
|
||||||
isActive: { type: Boolean, default: false },
|
isActive: { type: Boolean, default: false },
|
||||||
hasActiveChild: { type: Boolean, default: false },
|
hasActiveChild: { type: Boolean, default: false },
|
||||||
|
getterKeys: { type: Object, default: () => ({}) },
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['toggle']);
|
const emit = defineEmits(['toggle']);
|
||||||
|
|
||||||
|
const showBadge = useMapGetter(props.getterKeys.badge);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -28,7 +32,13 @@ const emit = defineEmits(['toggle']);
|
|||||||
}"
|
}"
|
||||||
@click.stop="emit('toggle')"
|
@click.stop="emit('toggle')"
|
||||||
>
|
>
|
||||||
<Icon v-if="icon" :icon="icon" class="size-4" />
|
<div v-if="icon" class="relative flex items-center gap-2">
|
||||||
|
<Icon v-if="icon" :icon="icon" class="size-4" />
|
||||||
|
<span
|
||||||
|
v-if="showBadge"
|
||||||
|
class="size-2 -top-px ltr:-right-px rtl:-left-px bg-n-brand absolute rounded-full border border-n-solid-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<span class="text-sm font-medium leading-5 flex-grow">
|
<span class="text-sm font-medium leading-5 flex-grow">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
useAlert(this.$t('INBOX.ALERTS.MARK_AS_READ'));
|
useAlert(this.$t('INBOX.ALERTS.MARK_AS_READ'));
|
||||||
|
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
markNotificationAsUnRead(notification) {
|
markNotificationAsUnRead(notification) {
|
||||||
@@ -133,6 +134,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
useAlert(this.$t('INBOX.ALERTS.MARK_AS_UNREAD'));
|
useAlert(this.$t('INBOX.ALERTS.MARK_AS_UNREAD'));
|
||||||
|
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteNotification(notification) {
|
deleteNotification(notification) {
|
||||||
@@ -186,12 +188,16 @@ export default {
|
|||||||
notificationType,
|
notificationType,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$store.dispatch('notifications/read', {
|
this.$store
|
||||||
id,
|
.dispatch('notifications/read', {
|
||||||
primaryActorId,
|
id,
|
||||||
primaryActorType,
|
primaryActorId,
|
||||||
unreadCount: this.meta.unreadCount,
|
primaryActorType,
|
||||||
});
|
unreadCount: this.meta.unreadCount,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time
|
||||||
|
});
|
||||||
|
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'inbox_view_conversation',
|
name: 'inbox_view_conversation',
|
||||||
|
|||||||
@@ -35,4 +35,7 @@ export const getters = {
|
|||||||
getNotificationFilters($state) {
|
getNotificationFilters($state) {
|
||||||
return $state.notificationFilters;
|
return $state.notificationFilters;
|
||||||
},
|
},
|
||||||
|
getHasUnreadNotifications: $state => {
|
||||||
|
return $state.meta.unreadCount > 0;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const state = {
|
|||||||
meta: {
|
meta: {
|
||||||
count: 0,
|
count: 0,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
unReadCount: 0,
|
unreadCount: 0,
|
||||||
},
|
},
|
||||||
records: {},
|
records: {},
|
||||||
uiFlags: {
|
uiFlags: {
|
||||||
|
|||||||
@@ -95,4 +95,27 @@ describe('#getters', () => {
|
|||||||
state.notificationFilters
|
state.notificationFilters
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getHasUnreadNotifications', () => {
|
||||||
|
it('should return true when there are unread notifications', () => {
|
||||||
|
const state = {
|
||||||
|
meta: { unreadCount: 5 },
|
||||||
|
};
|
||||||
|
expect(getters.getHasUnreadNotifications(state)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when there are no unread notifications', () => {
|
||||||
|
const state = {
|
||||||
|
meta: { unreadCount: 0 },
|
||||||
|
};
|
||||||
|
expect(getters.getHasUnreadNotifications(state)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when meta is empty', () => {
|
||||||
|
const state = {
|
||||||
|
meta: {},
|
||||||
|
};
|
||||||
|
expect(getters.getHasUnreadNotifications(state)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user