fix(webapp): notifications - UI Improvements (#8559)

* Notifications view:
- restructured broken layout
- joined several columns for mobile view
- moved button from footer to header
- set alternating colors for the table rows

UserTeaser
- added injectedText
- added injectedDate
- fixed padding

* fixed race-condition with default behavior of browser

* - fixed: jumping menu / menu should get closed by click on notification
- fixed: NotificationList replaced by NotificationTable

* - fixed: menu gets closed when cursor leaves content area, but it is still within popup

* - fixed: menu top buttons should be next to each other

* - fixed: popup background overlay remains after NotificationMenu disappeared after viewport change to mobile

* - fixed lint errors

* - fixed tests + snapshots

* - fixed e2e test

* fix lint error

Co-authored-by: Sebastian Stein <sebastian@codepassion.de>

* Fix locale identifier to have single quotes 'notifications.reason.on_date'

---------

Co-authored-by: Sebastian Stein <sebastian@codepassion.de>
Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
Co-authored-by: Ulf Gebhardt <ulf.gebhardt@webcraft-media.de>
This commit is contained in:
sebastian2357 2025-05-25 18:44:33 +02:00 committed by GitHub
parent 18ae2a04ab
commit a69006873d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 436 additions and 574 deletions

View File

@ -4,7 +4,7 @@ defineStep('open the notification menu and click on the first item', () => {
cy.get('.notifications-menu')
.invoke('show')
.click() // 'invoke('show')' because of the delay for show the menu
cy.get('.notification .link')
cy.get('.notification-content a')
.first()
.click({force: true})
})

View File

@ -1,200 +0,0 @@
import { mount, RouterLinkStub } from '@vue/test-utils'
import Notification from './Notification.vue'
import Vuex from 'vuex'
const localVue = global.localVue
describe('Notification', () => {
let stubs
let getters
let mocks
let propsData
let wrapper
beforeEach(() => {
propsData = {}
mocks = {
$t: (key) => key,
}
stubs = {
NuxtLink: RouterLinkStub,
'client-only': true,
}
getters = {
'auth/user': () => {
return {}
},
'auth/isModerator': () => false,
}
})
const Wrapper = () => {
const store = new Vuex.Store({
getters,
})
return mount(Notification, {
stubs,
store,
mocks,
propsData,
localVue,
})
}
describe('given a notification about a comment on a post', () => {
beforeEach(() => {
propsData.notification = {
reason: 'commented_on_post',
from: {
__typename: 'Comment',
id: 'comment-1',
contentExcerpt:
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',
post: {
title: "It's a post title",
id: 'post-1',
slug: 'its-a-title',
contentExcerpt: 'Post content.',
},
},
}
})
it('renders reason', () => {
wrapper = Wrapper()
expect(wrapper.find('.notification > .description').text()).toEqual(
'notifications.reason.commented_on_post',
)
})
it('renders title', () => {
wrapper = Wrapper()
expect(wrapper.text()).toContain("It's a post title")
})
it('renders the identifier "notifications.comment"', () => {
wrapper = Wrapper()
expect(wrapper.text()).toContain('notifications.comment')
})
it('renders the contentExcerpt', () => {
wrapper = Wrapper()
expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.')
})
it('has no class "--read"', () => {
wrapper = Wrapper()
expect(wrapper.classes()).not.toContain('--read')
})
describe('that is read', () => {
beforeEach(() => {
propsData.notification.read = true
wrapper = Wrapper()
})
it('has class "--read"', () => {
expect(wrapper.classes()).toContain('--read')
})
})
})
describe('given a notification about a mention in a post', () => {
beforeEach(() => {
propsData.notification = {
reason: 'mentioned_in_post',
from: {
__typename: 'Post',
title: "It's a post title",
id: 'post-1',
slug: 'its-a-title',
contentExcerpt:
'<a href="/profile/u3" target="_blank">@jenny-rostock</a> is the best on this post.',
},
}
})
it('renders reason', () => {
wrapper = Wrapper()
expect(wrapper.find('.notification > .description').text()).toEqual(
'notifications.reason.mentioned_in_post',
)
})
it('renders title', () => {
wrapper = Wrapper()
expect(wrapper.text()).toContain("It's a post title")
})
it('renders the contentExcerpt', () => {
wrapper = Wrapper()
expect(wrapper.text()).toContain('@jenny-rostock is the best on this post.')
})
it('has no class "--read"', () => {
wrapper = Wrapper()
expect(wrapper.classes()).not.toContain('--read')
})
describe('that is read', () => {
beforeEach(() => {
propsData.notification.read = true
wrapper = Wrapper()
})
it('has class "--read"', () => {
expect(wrapper.classes()).toContain('--read')
})
})
})
describe('given a notification about a mention in a comment', () => {
beforeEach(() => {
propsData.notification = {
reason: 'mentioned_in_comment',
from: {
__typename: 'Comment',
id: 'comment-1',
contentExcerpt:
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',
post: {
title: "It's a post title",
id: 'post-1',
slug: 'its-a-title',
contentExcerpt: 'Post content.',
},
},
}
})
it('renders reason', () => {
wrapper = Wrapper()
expect(wrapper.find('.notification > .description').text()).toEqual(
'notifications.reason.mentioned_in_comment',
)
})
it('renders title', () => {
wrapper = Wrapper()
expect(wrapper.text()).toContain("It's a post title")
})
it('renders the identifier "notifications.comment"', () => {
wrapper = Wrapper()
expect(wrapper.text()).toContain('notifications.comment')
})
it('renders the contentExcerpt', () => {
wrapper = Wrapper()
expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.')
})
it('has no class "--read"', () => {
wrapper = Wrapper()
expect(wrapper.classes()).not.toContain('--read')
})
describe('that is read', () => {
beforeEach(() => {
propsData.notification.read = true
wrapper = Wrapper()
})
it('has class "--read"', () => {
expect(wrapper.classes()).toContain('--read')
})
})
})
})

View File

@ -1,100 +0,0 @@
<template>
<article :class="{ '--read': notification.read, notification: true }">
<client-only>
<user-teaser
:user="isGroup ? notification.relatedUser : from.author"
:date-time="from.createdAt"
:show-popover="false"
/>
</client-only>
<p class="description">{{ $t(`notifications.reason.${notification.reason}`) }}</p>
<nuxt-link
class="link"
:to="{ name: isGroup ? 'groups-id-slug' : 'post-id-slug', params, ...hashParam }"
@click.native="$emit('read')"
>
<base-card wideContent>
<h2 class="title">{{ from.title || from.groupName || from.post.title }}</h2>
<p>
<strong v-if="isComment" class="comment">{{ $t(`notifications.comment`) }}:</strong>
{{ from.contentExcerpt | removeHtml }}
<strong v-if="isGroup" class="comment">{{ $t(`notifications.group`) }}:</strong>
{{ from.descriptionExcerpt | removeHtml }}
</p>
</base-card>
</nuxt-link>
</article>
</template>
<script>
import UserTeaser from '~/components/UserTeaser/UserTeaser'
export default {
name: 'Notification',
components: {
UserTeaser,
},
props: {
notification: {
type: Object,
required: true,
},
},
computed: {
from() {
return this.notification.from
},
isComment() {
return this.from.__typename === 'Comment'
},
isGroup() {
return this.from.__typename === 'Group'
},
params() {
const target = this.isComment ? this.from.post : this.from
return {
id: target.id,
slug: target.slug,
}
},
hashParam() {
return this.isComment ? { hash: `#commentId-${this.from.id}` } : {}
},
},
}
</script>
<style lang="scss">
.notification {
margin-bottom: $space-base;
&:first-of-type {
margin-top: $space-x-small;
}
&.--read {
opacity: $opacity-disabled;
}
> .description {
margin-bottom: $space-x-small;
}
> .link {
display: block;
color: $text-color-base;
&:hover {
color: $color-primary;
}
}
.user-teaser {
margin-bottom: $space-x-small;
}
.comment {
font-weight: $font-weight-bold;
}
}
</style>

View File

@ -1,103 +0,0 @@
import { shallowMount, mount, RouterLinkStub } from '@vue/test-utils'
import NotificationList from './NotificationList'
import Notification from '../Notification/Notification'
import Vuex from 'vuex'
import { notifications } from '~/components/utils/Notifications'
const localVue = global.localVue
localVue.filter('truncate', (string) => string)
describe('NotificationList.vue', () => {
let wrapper
let mocks
let stubs
let store
let propsData
beforeEach(() => {
store = new Vuex.Store({
getters: {
'auth/isModerator': () => false,
'auth/user': () => {
return {}
},
},
})
mocks = {
$t: jest.fn(),
}
stubs = {
NuxtLink: RouterLinkStub,
'client-only': true,
'v-popover': true,
}
propsData = { notifications }
})
describe('shallowMount', () => {
const Wrapper = () => {
return shallowMount(NotificationList, {
propsData,
mocks,
store,
localVue,
stubs,
})
}
beforeEach(() => {
wrapper = Wrapper()
})
it('renders Notification.vue for each notification of the user', () => {
expect(wrapper.findAllComponents(Notification)).toHaveLength(2)
})
})
describe('mount', () => {
const Wrapper = () => {
return mount(NotificationList, {
propsData,
mocks,
stubs,
store,
localVue,
})
}
beforeEach(() => {
wrapper = Wrapper()
})
describe('click on a notification', () => {
beforeEach(() => {
wrapper.find('.notification > .link').trigger('click')
})
it("emits 'markAsRead' with the id of the notification source", () => {
expect(wrapper.emitted('markAsRead')[0]).toEqual(['post-1'])
})
})
})
describe('shallowMount with no notifications', () => {
const Wrapper = () => {
return shallowMount(NotificationList, {
propsData: {},
mocks,
store,
localVue,
})
}
beforeEach(() => {
wrapper = Wrapper()
})
it('renders Notification.vue zero times', () => {
expect(wrapper.findAllComponents(Notification)).toHaveLength(0)
})
})
})

View File

@ -1,32 +0,0 @@
<template>
<div>
<notification
v-for="notification in notifications"
:key="notification.id"
:notification="notification"
@read="markAsRead(notification.from.id)"
/>
</div>
</template>
<script>
import Notification from '../Notification/Notification'
export default {
name: 'NotificationList',
components: {
Notification,
},
props: {
notifications: {
type: Array,
default: () => [],
},
},
methods: {
markAsRead(notificationSourceId) {
this.$emit('markAsRead', notificationSourceId)
},
},
}
</script>

View File

@ -26,7 +26,14 @@
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
</base-button>
</nuxt-link>
<dropdown v-else class="notifications-menu" offset="8" :placement="placement">
<dropdown
v-else
class="notifications-menu"
offset="8"
:placement="placement"
noMouseLeaveClosing
ref="dropdown"
>
<template #default="{ toggleMenu }">
<base-button
ghost
@ -42,14 +49,12 @@
</template>
<template #popover="{ closeMenu }">
<ds-flex class="notifications-link-container">
<ds-flex-item class="notifications-link-container-item" :width="{ base: '100%' }" centered>
<ds-flex-item>
<nuxt-link :to="{ name: 'notifications' }">
<base-button ghost primary>
{{ $t('notifications.pageLink') }}
</base-button>
</nuxt-link>
</ds-flex-item>
<ds-flex-item class="notifications-link-container-item" :width="{ base: '100%' }" centered>
<base-button
ghost
primary
@ -61,7 +66,11 @@
</ds-flex-item>
</ds-flex>
<div class="notifications-menu-popover">
<notification-list :notifications="notifications" @markAsRead="markAsRead" />
<notifications-table
@markNotificationAsRead="markAsReadAndCloseMenu($event, closeMenu)"
:notifications="notifications"
:show-popover="false"
/>
</div>
</template>
</dropdown>
@ -78,14 +87,14 @@ import {
} from '~/graphql/User'
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
import Dropdown from '~/components/Dropdown'
import NotificationList from '../NotificationList/NotificationList'
import NotificationsTable from '../NotificationsTable/NotificationsTable.vue'
export default {
name: 'NotificationMenu',
components: {
NotificationsTable,
CounterIcon,
Dropdown,
NotificationList,
},
data() {
return {
@ -96,14 +105,25 @@ export default {
placement: { type: String },
noMenu: { type: Boolean, default: false },
},
mounted() {
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
},
methods: {
async markAsRead(notificationSourceId) {
const variables = { id: notificationSourceId }
handleResize() {
// When the viewport get resized close menu
this.$refs?.dropdown?.closeMenu?.()
},
async markAsReadAndCloseMenu(notificationSourceId, closeMenu) {
try {
await this.$apollo.mutate({
mutation: markAsReadMutation(this.$i18n),
variables,
variables: { id: notificationSourceId },
})
closeMenu?.()
} catch (error) {
this.$toast.error(error.message)
}
@ -113,7 +133,7 @@ export default {
return
}
closeMenu()
closeMenu?.()
try {
await this.$apollo.mutate({
mutation: markAllAsReadMutation(this.$i18n),
@ -178,12 +198,8 @@ export default {
}
.notifications-link-container {
background-color: $background-color-softer-active;
justify-content: center;
padding: $space-x-small;
text-align: right;
padding: $space-x-small 0;
flex-direction: row;
}
.notifications-link-container-item {
justify-content: center;
display: flex;
}
</style>

View File

@ -63,11 +63,7 @@ describe('NotificationsTable.vue', () => {
expect(wrapper.find('.notification-grid').exists()).toBe(true)
})
describe('renders 4 columns', () => {
it('for icon', () => {
expect(wrapper.vm.fields.icon).toBeTruthy()
})
describe('renders 2 columns', () => {
it('for user', () => {
expect(wrapper.vm.fields.user).toBeTruthy()
})
@ -75,10 +71,6 @@ describe('NotificationsTable.vue', () => {
it('for post', () => {
expect(wrapper.vm.fields.post).toBeTruthy()
})
it('for content', () => {
expect(wrapper.vm.fields.content).toBeTruthy()
})
})
describe('Post', () => {
@ -93,9 +85,9 @@ describe('NotificationsTable.vue', () => {
})
it('renders the reason for the notification', () => {
const dsTexts = firstRowNotification.findAll('.ds-text')
const reason = dsTexts.filter(
(element) => element.text() === 'notifications.reason.mentioned_in_post',
const dsTexts = firstRowNotification.findAll('.info span')
const reason = dsTexts.filter((element) =>
element.text().startsWith('notifications.reason.mentioned_in_post'),
)
expect(reason.exists()).toBe(true)
})
@ -106,7 +98,7 @@ describe('NotificationsTable.vue', () => {
})
it("renders the Post's content", () => {
const boldTags = firstRowNotification.findAll('b')
const boldTags = firstRowNotification.findAll('p')
const content = boldTags.filter(
(element) => element.text() === postNotification.from.contentExcerpt,
)
@ -126,9 +118,9 @@ describe('NotificationsTable.vue', () => {
})
it('renders the reason for the notification', () => {
const dsTexts = secondRowNotification.findAll('.ds-text')
const reason = dsTexts.filter(
(element) => element.text() === 'notifications.reason.mentioned_in_comment',
const dsTexts = secondRowNotification.findAll('.info span')
const reason = dsTexts.filter((element) =>
element.text().startsWith('notifications.reason.mentioned_in_comment'),
)
expect(reason.exists()).toBe(true)
})
@ -139,7 +131,7 @@ describe('NotificationsTable.vue', () => {
})
it("renders the Post's content", () => {
const boldTags = secondRowNotification.findAll('b')
const boldTags = secondRowNotification.findAll('p')
const content = boldTags.filter(
(element) => element.text() === commentNotification.from.contentExcerpt,
)

View File

@ -17,50 +17,45 @@
<ds-grid>
<ds-grid-item>
<ds-flex class="user-section">
<ds-flex-item :width="{ base: '20%' }">
<div>
<base-card :wide-content="true">
<base-icon
v-if="notification.from.post"
name="comment"
v-tooltip="{ content: $t('notifications.comment'), placement: 'right' }"
/>
<base-icon
v-else
name="bookmark"
v-tooltip="{ content: $t('notifications.post'), placement: 'right' }"
/>
</base-card>
</div>
</ds-flex-item>
<ds-flex-item>
<div>
<base-card :wide-content="true">
<ds-space margin-bottom="base">
<client-only>
<user-teaser
:user="
isGroup(notification.from)
? notification.relatedUser
: notification.from.author
"
:date-time="notification.from.createdAt"
:class="{ 'notification-status': notification.read }"
/>
</client-only>
</ds-space>
<ds-text :class="{ 'notification-status': notification.read, reason: true }">
{{ $t(`notifications.reason.${notification.reason}`) }}
</ds-text>
</base-card>
</div>
<base-card :wide-content="true">
<client-only>
<user-teaser
:user="
isGroup(notification.from)
? notification.relatedUser
: notification.from.author
"
:class="{ 'notification-status': notification.read }"
:date-time="notification.from.createdAt"
:injected-text="$t(`notifications.reason.${notification.reason}`)"
:injected-date="true"
:show-popover="showPopover"
/>
</client-only>
</base-card>
</ds-flex-item>
</ds-flex>
</ds-grid-item>
<ds-grid-item>
<ds-flex class="content-section" :direction="{ base: 'column', xs: 'row' }">
<ds-flex-item>
<base-card :wide-content="true">
<base-card :wide-content="true">
<div class="notification-container">
<!-- Icon with responsive sizing -->
<div class="notification-icon">
<base-icon
v-if="notification.from.post"
name="comment"
v-tooltip="{ content: $t('notifications.comment'), placement: 'right' }"
/>
<base-icon
v-else
name="bookmark"
v-tooltip="{ content: $t('notifications.post'), placement: 'right' }"
/>
</div>
<!-- Content section with title and description -->
<div class="notification-content">
<nuxt-link
class="notification-mention-post"
:class="{ 'notification-status': notification.read }"
@ -69,7 +64,7 @@
params: params(notification.from),
hash: hashParam(notification.from),
}"
@click.native="markNotificationAsRead(notification.from.id)"
@click.native.prevent="handleNotificationClick(notification)"
>
<b>
{{
@ -79,19 +74,18 @@
}}
</b>
</nuxt-link>
</base-card>
</ds-flex-item>
<ds-flex-item>
<base-card :wide-content="true">
<b :class="{ 'notification-status': notification.read }">
<p
class="notification-description"
:class="{ 'notification-status': notification.read }"
>
{{
notification.from.contentExcerpt ||
notification.from.descriptionExcerpt | removeHtml
}}
</b>
</base-card>
</ds-flex-item>
</ds-flex>
</p>
</div>
</div>
</base-card>
</ds-grid-item>
</ds-grid>
</ds-grid-item>
@ -116,25 +110,18 @@ export default {
mixins: [mobile(maxMobileWidth)],
props: {
notifications: { type: Array, default: () => [] },
showPopover: { type: Boolean, default: true },
},
computed: {
fields() {
return {
icon: {
label: ' ',
width: '5',
},
user: {
label: this.$t('notifications.user'),
width: '45%',
width: '50%',
},
post: {
label: this.$t('notifications.post'),
width: '25%',
},
content: {
label: this.$t('notifications.content'),
width: '25%',
width: '50%',
},
}
},
@ -159,7 +146,23 @@ export default {
return this.isComment(notificationSource) ? `#commentId-${notificationSource.id}` : ''
},
markNotificationAsRead(notificationSourceId) {
this.$emit('markNotificationAsRead', notificationSourceId)
return new Promise((resolve) => {
this.$emit('markNotificationAsRead', notificationSourceId)
resolve()
})
},
async handleNotificationClick(notification) {
const route = {
name: this.isGroup(notification.from) ? 'groups-id-slug' : 'post-id-slug',
params: this.params(notification.from),
hash: this.hashParam(notification.from),
}
await this.markNotificationAsRead(notification.from.id)
setTimeout(() => {
this.$router.push(route)
}, 10)
},
},
}
@ -172,12 +175,6 @@ export default {
.notification-grid .content-section {
flex-wrap: nowrap;
}
.notification-grid .ds-grid.header-grid {
grid-template-columns: 1fr 4fr 3fr 3fr !important;
}
.notification-grid-row {
border-top: 1px dotted #e5e3e8;
}
.notification-grid .base-card {
border-radius: 0;
box-shadow: none;
@ -190,6 +187,19 @@ export default {
grid-template-rows: 1fr;
gap: 0px !important;
}
.notification-grid-row {
padding: 10px;
border-bottom: 1px dotted #e5e3e8;
background-color: white;
&:nth-child(odd) {
background-color: $color-neutral-90;
}
.base-card {
padding: 8px 0;
background-color: unset !important;
}
}
@media screen and (max-width: 768px) {
.notification-grid .ds-grid {
grid-template-columns: 1fr !important;
@ -197,10 +207,47 @@ export default {
.notification-grid .content-section {
border-top: 1px dotted #e5e3e8;
}
.notification-grid-row {
box-shadow: 0px 12px 26px -4px rgb(0 0 0 / 10%);
margin-top: 5px;
border-top: none;
}
.notification-description {
margin-top: 4px;
}
.notification-container {
display: flex;
align-items: flex-start;
gap: 10px;
.notification-icon {
flex-shrink: 0;
}
}
/* Desktop icon size */
@media (min-width: 768px) {
.notification-icon {
width: 18px;
}
.notification-icon :deep(svg) {
width: 25px;
height: 25px;
}
}
/* Mobile icon size */
@media (max-width: 767px) {
.notification-icon {
width: 34px;
text-align: center;
}
.notification-icon :deep(svg) {
width: 50px;
height: 50px;
}
}
.notification-content {
flex: 1;
}
</style>

View File

@ -15,6 +15,8 @@
:show-avatar="showAvatar"
:date-time="dateTime"
:show-popover="showPopover"
:injectedText="injectedText"
:injectedDate="injectedDate"
@close="closeMenu"
/>
</client-only>
@ -41,6 +43,8 @@ export default {
showAvatar: { type: Boolean, default: true },
dateTime: { type: [Date, String], default: null },
showPopover: { type: Boolean, default: true },
injectedText: { type: String, default: null },
injectedDate: { type: Boolean, default: false },
},
computed: {
...mapGetters({
@ -76,10 +80,10 @@ export default {
}
.info {
padding-left: $space-xx-small;
padding-left: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
white-space: break-spaces;
&.anonymous {
font-size: $font-size-base;

View File

@ -36,8 +36,10 @@
</span>
</nuxt-link>
</span>
<!-- eslint-disable-next-line prettier/prettier -->
<span>{{ injectedText }}<span v-if="injectedText && injectedDate && !userOnly && dateTime"> {{$t('notifications.reason.on_date')}} <date-time :date-time="dateTime" /></span></span>
</div>
<span v-if="!userOnly && dateTime" class="text">
<span v-if="!userOnly && !injectedDate && dateTime" class="text">
<base-icon name="clock" />
<date-time :date-time="dateTime" />
<slot name="dateTime"></slot>
@ -81,6 +83,8 @@ export default {
showAvatar: { type: Boolean, default: true },
dateTime: { type: [Date, String], default: null },
showPopover: { type: Boolean, default: true },
injectedText: { type: String, default: null },
injectedDate: { type: Boolean, default: false },
},
computed: {
...mapGetters({

View File

@ -51,6 +51,11 @@ exports[`UserTeaser given an user avatar is disabled does not render the avatar
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -199,6 +204,11 @@ exports[`UserTeaser given an user user is disabled current user is a moderator r
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -315,6 +325,11 @@ exports[`UserTeaser given an user with linkToProfile, on desktop renders 1`] = `
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -400,6 +415,11 @@ exports[`UserTeaser given an user with linkToProfile, on desktop when hovering t
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -489,6 +509,11 @@ exports[`UserTeaser given an user with linkToProfile, on touch screen renders 1`
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -574,6 +599,11 @@ exports[`UserTeaser given an user with linkToProfile, on touch screen when click
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -663,6 +693,11 @@ exports[`UserTeaser given an user without linkToProfile, on desktop renders 1`]
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -748,6 +783,11 @@ exports[`UserTeaser given an user without linkToProfile, on desktop when hoverin
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -837,6 +877,11 @@ exports[`UserTeaser given an user without linkToProfile, on desktop when hoverin
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -925,6 +970,11 @@ exports[`UserTeaser given an user without linkToProfile, on touch screen renders
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -1010,6 +1060,11 @@ exports[`UserTeaser given an user without linkToProfile, on touch screen when cl
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -1099,6 +1154,11 @@ exports[`UserTeaser given an user without linkToProfile, on touch screen when cl
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->

View File

@ -804,15 +804,16 @@
"pageLink": "Alle Benachrichtigungen",
"post": "Beitrag oder Gruppe",
"reason": {
"changed_group_member_role": "Hat deine Rolle in der Gruppe geändert …",
"commented_on_post": "Hat einen Beitrag den du beobachtest kommentiert …",
"followed_user_posted": "Hat einen neuen Betrag geschrieben …",
"mentioned_in_comment": "Hat dich in einem Kommentar erwähnt …",
"mentioned_in_post": "Hat dich in einem Beitrag erwähnt …",
"post_in_group": "Hat einen Beitrag in der Gruppe geschrieben …",
"removed_user_from_group": "Hat dich aus der Gruppe entfernt …",
"user_joined_group": "Ist deiner Gruppe beigetreten …",
"user_left_group": "Hat deine Gruppe verlassen …"
"changed_group_member_role": "Hat deine Rolle in der Gruppe geändert",
"commented_on_post": "Hat einen Beitrag den du beobachtest kommentiert",
"followed_user_posted": "Hat einen neuen Betrag geschrieben",
"mentioned_in_comment": "Hat dich in einem Kommentar erwähnt",
"mentioned_in_post": "Hat dich in einem Beitrag erwähnt",
"on_date": "am",
"post_in_group": "Hat einen Beitrag in der Gruppe geschrieben",
"removed_user_from_group": "Hat dich aus der Gruppe entfernt",
"user_joined_group": "Ist deiner Gruppe beigetreten",
"user_left_group": "Hat deine Gruppe verlassen"
},
"title": "Benachrichtigungen",
"user": "Nutzer"

View File

@ -804,15 +804,16 @@
"pageLink": "All notifications",
"post": "Post or Group",
"reason": {
"changed_group_member_role": "Changed your role in group …",
"commented_on_post": "Commented on a post you observe …",
"followed_user_posted": "Wrote a new post …",
"mentioned_in_comment": "Mentioned you in a comment …",
"mentioned_in_post": "Mentioned you in a post …",
"post_in_group": "Posted in a group …",
"removed_user_from_group": "Removed you from group …",
"user_joined_group": "Joined your group …",
"user_left_group": "Left your group …"
"changed_group_member_role": "Changed your role in group",
"commented_on_post": "Commented on a post you observe",
"followed_user_posted": "Wrote a new post",
"mentioned_in_comment": "Mentioned you in a comment",
"mentioned_in_post": "Mentioned you in a post",
"on_date": "on",
"post_in_group": "Posted in a group",
"removed_user_from_group": "Removed you from group",
"user_joined_group": "Joined your group",
"user_left_group": "Left your group"
},
"title": "Notifications",
"user": "User"

View File

@ -805,10 +805,11 @@
"post": "Contribución",
"reason": {
"changed_group_member_role": null,
"commented_on_post": "Comentó su contribución ...",
"commented_on_post": "Comentó su contribución",
"followed_user_posted": null,
"mentioned_in_comment": "Le mencionó en un comentario …",
"mentioned_in_post": "Le mencionó en una contribución …",
"mentioned_in_comment": "Le mencionó en un comentario",
"mentioned_in_post": "Le mencionó en una contribución",
"on_date": "de",
"post_in_group": null,
"removed_user_from_group": null,
"user_joined_group": null,

View File

@ -809,6 +809,7 @@
"followed_user_posted": null,
"mentioned_in_comment": "Vous a mentionné dans un commentaire…",
"mentioned_in_post": "Vous a mentionné dans un post…",
"on_date": null,
"post_in_group": null,
"removed_user_from_group": null,
"user_joined_group": null,

View File

@ -809,6 +809,7 @@
"followed_user_posted": null,
"mentioned_in_comment": null,
"mentioned_in_post": null,
"on_date": null,
"post_in_group": null,
"removed_user_from_group": null,
"user_joined_group": null,

View File

@ -809,6 +809,7 @@
"followed_user_posted": null,
"mentioned_in_comment": null,
"mentioned_in_post": null,
"on_date": null,
"post_in_group": null,
"removed_user_from_group": null,
"user_joined_group": null,

View File

@ -809,6 +809,7 @@
"followed_user_posted": null,
"mentioned_in_comment": "Mentionou você em um comentário …",
"mentioned_in_post": "Mencinou você em um post …",
"on_date": null,
"post_in_group": null,
"removed_user_from_group": null,
"user_joined_group": null,

View File

@ -809,6 +809,7 @@
"followed_user_posted": null,
"mentioned_in_comment": "Упоминание в комментарии....",
"mentioned_in_post": "Упоминание в посте....",
"on_date": null,
"post_in_group": null,
"removed_user_from_group": null,
"user_joined_group": null,

View File

@ -579,6 +579,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -660,6 +665,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -741,6 +751,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -822,6 +837,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -2455,6 +2475,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -2536,6 +2561,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -2617,6 +2647,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -2698,6 +2733,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -3434,6 +3474,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -3515,6 +3560,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -3596,6 +3646,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -3677,6 +3732,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -4257,6 +4317,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -4338,6 +4403,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -4419,6 +4489,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -4500,6 +4575,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -5078,6 +5158,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -5159,6 +5244,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -5240,6 +5330,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -5321,6 +5416,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -5969,6 +6069,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -6050,6 +6155,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -6131,6 +6241,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -6212,6 +6327,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -6995,6 +7115,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -7076,6 +7201,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -7157,6 +7287,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -7238,6 +7373,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -7974,6 +8114,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -8055,6 +8200,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -8136,6 +8286,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->
@ -8217,6 +8372,11 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<!---->
<!---->
<span>
<!---->
</span>
</div>
<!---->

View File

@ -11,6 +11,17 @@
</ds-flex-item>
</ds-flex>
<ds-space />
<ds-flex-item class="notifications-header-button" :width="{ base: 'auto' }" centered>
<base-button
primary
:disabled="unreadNotificationsCount === 0"
@click="markAllAsRead"
data-test="markAllAsRead-button"
>
{{ $t('notifications.markAllAsRead') }}
</base-button>
</ds-flex-item>
<ds-space />
<notifications-table
@markNotificationAsRead="markNotificationAsRead"
:notifications="notifications"
@ -25,16 +36,6 @@
@next="next"
/>
</ds-flex-item>
<ds-flex-item class="notifications-footer-button" :width="{ base: 'auto' }" centered>
<base-button
primary
:disabled="unreadNotificationsCount === 0"
@click="markAllAsRead"
data-test="markAllAsRead-button"
>
{{ $t('notifications.markAllAsRead') }}
</base-button>
</ds-flex-item>
</ds-flex>
</base-card>
</template>
@ -153,10 +154,15 @@ export default {
</script>
<style lang="scss">
.notifications-page-flex {
padding: 8px;
justify-content: space-between;
}
.notifications-header-button {
text-align: right;
}
.notifications-footer {
margin-top: 1.5rem;
justify-content: space-evenly;
}
</style>