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:
Sivin Varghese
2025-01-31 15:14:10 +05:30
committed by GitHub
parent b811c27ab5
commit c3aab44b5f
7 changed files with 56 additions and 9 deletions

View File

@@ -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',

View File

@@ -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"

View File

@@ -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>

View File

@@ -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',

View File

@@ -35,4 +35,7 @@ export const getters = {
getNotificationFilters($state) { getNotificationFilters($state) {
return $state.notificationFilters; return $state.notificationFilters;
}, },
getHasUnreadNotifications: $state => {
return $state.meta.unreadCount > 0;
},
}; };

View File

@@ -6,7 +6,7 @@ const state = {
meta: { meta: {
count: 0, count: 0,
currentPage: 1, currentPage: 1,
unReadCount: 0, unreadCount: 0,
}, },
records: {}, records: {},
uiFlags: { uiFlags: {

View File

@@ -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);
});
});
}); });