Merge branch 'master' of github.com:Ocelot-Social-Community/Ocelot-Social into 5627-fix-responsive-view-on-iphone

This commit is contained in:
Wolfgang Huß 2023-03-14 07:39:31 +01:00
commit 2d0a60da58
30 changed files with 433 additions and 134 deletions

View File

@ -46,6 +46,11 @@ jobs:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: ${{ github.event.client_payload.ref }}
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v2
with:

View File

@ -293,7 +293,7 @@ jobs:
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v1
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ github.token }}
event-type: trigger-build-success

View File

@ -0,0 +1,65 @@
import gql from 'graphql-tag'
// ------ mutations
export const markAsReadMutation = () => {
return gql`
mutation ($id: ID!) {
markAsRead(id: $id) {
from {
__typename
... on Post {
content
}
... on Comment {
content
}
}
read
createdAt
}
}
`
}
export const markAllAsReadMutation = () => {
return gql`
mutation {
markAllAsRead {
from {
__typename
... on Post {
content
}
... on Comment {
content
}
}
read
createdAt
}
}
`
}
// ------ queries
export const notificationQuery = () => {
return gql`
query ($read: Boolean, $orderBy: NotificationOrdering) {
notifications(read: $read, orderBy: $orderBy) {
from {
__typename
... on Post {
content
}
... on Comment {
content
}
}
read
createdAt
}
}
`
}

View File

@ -449,6 +449,7 @@ export default shield(
blockUser: isAuthenticated,
unblockUser: isAuthenticated,
markAsRead: isAuthenticated,
markAllAsRead: isAuthenticated,
AddEmailAddress: isAuthenticated,
VerifyEmailAddress: isAuthenticated,
pinPost: isAdmin,

View File

@ -99,6 +99,35 @@ export default {
session.close()
}
},
markAllAsRead: async (parent, args, context, resolveInfo) => {
const { user: currentUser } = context
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const markAllNotificationAsReadTransactionResponse = await transaction.run(
`
MATCH (resource)-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
SET notification.read = TRUE
WITH user, notification, resource,
[(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors,
[(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts
WITH resource, user, notification, authors, posts,
resource {.*, __typename: labels(resource)[0], author: authors[0], post: posts[0]} AS finalResource
RETURN notification {.*, from: finalResource, to: properties(user)}
`,
{ id: currentUser.id },
)
log(markAllNotificationAsReadTransactionResponse)
return markAllNotificationAsReadTransactionResponse.records.map((record) =>
record.get('notification'),
)
})
try {
const notifications = await writeTxResultPromise
return notifications
} finally {
session.close()
}
},
},
NOTIFIED: {
id: async (parent) => {

View File

@ -3,6 +3,11 @@ import gql from 'graphql-tag'
import { getDriver } from '../../db/neo4j'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server'
import {
markAsReadMutation,
markAllAsReadMutation,
notificationQuery,
} from '../../graphql/notifications'
const driver = getDriver()
let authenticatedUser
@ -146,26 +151,9 @@ describe('given some notifications', () => {
})
describe('notifications', () => {
const notificationQuery = gql`
query ($read: Boolean, $orderBy: NotificationOrdering) {
notifications(read: $read, orderBy: $orderBy) {
from {
__typename
... on Post {
content
}
... on Comment {
content
}
}
read
createdAt
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const { errors } = await query({ query: notificationQuery })
const { errors } = await query({ query: notificationQuery() })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -212,7 +200,7 @@ describe('given some notifications', () => {
},
]
await expect(query({ query: notificationQuery, variables })).resolves.toMatchObject({
await expect(query({ query: notificationQuery(), variables })).resolves.toMatchObject({
data: {
notifications: expect.arrayContaining(expected),
},
@ -246,7 +234,7 @@ describe('given some notifications', () => {
},
})
const response = await query({
query: notificationQuery,
query: notificationQuery(),
variables: { ...variables, read: false },
})
await expect(response).toMatchObject(expected)
@ -275,14 +263,14 @@ describe('given some notifications', () => {
it('reduces notifications list', async () => {
await expect(
query({ query: notificationQuery, variables: { ...variables, read: false } }),
query({ query: notificationQuery(), variables: { ...variables, read: false } }),
).resolves.toMatchObject({
data: { notifications: [expect.any(Object), expect.any(Object)] },
errors: undefined,
})
await deletePostAction()
await expect(
query({ query: notificationQuery, variables: { ...variables, read: false } }),
query({ query: notificationQuery(), variables: { ...variables, read: false } }),
).resolves.toMatchObject({ data: { notifications: [] }, errors: undefined })
})
})
@ -291,27 +279,10 @@ describe('given some notifications', () => {
})
describe('markAsRead', () => {
const markAsReadMutation = gql`
mutation ($id: ID!) {
markAsRead(id: $id) {
from {
__typename
... on Post {
content
}
... on Comment {
content
}
}
read
createdAt
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const result = await mutate({
mutation: markAsReadMutation,
mutation: markAsReadMutation(),
variables: { ...variables, id: 'p1' },
})
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
@ -332,7 +303,7 @@ describe('given some notifications', () => {
})
it('returns null', async () => {
const response = await mutate({ mutation: markAsReadMutation, variables })
const response = await mutate({ mutation: markAsReadMutation(), variables })
expect(response.data.markAsRead).toEqual(null)
expect(response.errors).toBeUndefined()
})
@ -348,7 +319,7 @@ describe('given some notifications', () => {
})
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
const { data } = await mutate({ mutation: markAsReadMutation, variables })
const { data } = await mutate({ mutation: markAsReadMutation(), variables })
expect(data).toEqual({
markAsRead: {
from: {
@ -369,7 +340,7 @@ describe('given some notifications', () => {
}
})
it('returns null', async () => {
const response = await mutate({ mutation: markAsReadMutation, variables })
const response = await mutate({ mutation: markAsReadMutation(), variables })
expect(response.data.markAsRead).toEqual(null)
expect(response.errors).toBeUndefined()
})
@ -385,7 +356,7 @@ describe('given some notifications', () => {
})
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
const { data } = await mutate({ mutation: markAsReadMutation, variables })
const { data } = await mutate({ mutation: markAsReadMutation(), variables })
expect(data).toEqual({
markAsRead: {
from: {
@ -401,4 +372,46 @@ describe('given some notifications', () => {
})
})
})
describe('markAllAsRead', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const result = await mutate({
mutation: markAllAsReadMutation(),
})
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
})
})
describe('authenticated', () => {
beforeEach(async () => {
authenticatedUser = await user.toJson()
})
describe('not being notified at all', () => {
beforeEach(async () => {
variables = {
...variables,
}
})
it('returns all as read', async () => {
const response = await mutate({ mutation: markAllAsReadMutation(), variables })
expect(response.data.markAllAsRead).toEqual([
{
createdAt: '2019-08-30T19:33:48.651Z',
from: { __typename: 'Comment', content: 'You have been mentioned in a comment' },
read: true,
},
{
createdAt: '2019-08-31T17:33:48.651Z',
from: { __typename: 'Post', content: 'You have been mentioned in a post' },
read: true,
},
])
expect(response.errors).toBeUndefined()
})
})
})
})
})

View File

@ -29,6 +29,7 @@ type Query {
type Mutation {
markAsRead(id: ID!): NOTIFIED
markAllAsRead: [NOTIFIED]
}
type Subscription {

View File

@ -55,7 +55,7 @@ Start Neo4J and confirm the database is running at [http://localhost:7474](http:
Here we describe some rarely used Cypher commands for Neo4j that are needed from time to time:
### Index And Contraint Commands
### Index And Constraint Commands
If indexes or constraints are missing or not set correctly, the browser search will not work or the database seed for development will not work.

View File

@ -3,7 +3,7 @@
<a href="#" slot="default" slot-scope="{ toggleMenu }" @click.prevent="toggleMenu()">
<ds-text bold size="large">{{ $t('admin.categories.name') }}</ds-text>
</a>
<template slot="popover">
<template #popover>
<div class="category-menu-options">
<h2 class="title">{{ $t('filter-menu.filter-by') }}</h2>
<categories-filter v-if="categoriesActive" />

View File

@ -10,7 +10,7 @@
>
<base-icon class="dropdown-arrow" name="angle-down" />
</base-button>
<template slot="popover">
<template #popover>
<filter-menu-component />
</template>
</dropdown>

View File

@ -1,7 +1,16 @@
<template>
<dropdown class="invite-button" offset="8" :placement="placement">
<template #default="{ toggleMenu }">
<base-button icon="user-plus" circle ghost @click.prevent="toggleMenu" />
<base-button
icon="user-plus"
circle
ghost
v-tooltip="{
content: $t('invite-codes.button.tooltip'),
placement: 'bottom-start',
}"
@click.prevent="toggleMenu"
/>
</template>
<template #popover>
<div class="invite-button-menu-popover">
@ -15,10 +24,7 @@
ghost
@click="copyInviteLink"
>
<ds-text bold>
{{ $t('invite-codes.copy-code') }}
{{ inviteCode.code }}
</ds-text>
<ds-text bold>{{ $t('invite-codes.copy-code') }}</ds-text>
</base-button>
</base-card>
</div>
@ -108,6 +114,6 @@ export default {
}
.invite-code {
left: 50%;
margin-left: 25%;
}
</style>

View File

@ -30,7 +30,7 @@ export default {
/* dirty fix to override broken styleguide inline-styles */
.ds-grid {
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)) !important;
gap: 16px !important;
gap: 32px 16px !important;
grid-auto-rows: 20px;
}

View File

@ -12,15 +12,24 @@
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
</base-button>
</template>
<template slot="popover">
<template #popover>
<div class="notifications-menu-popover">
<notification-list :notifications="notifications" @markAsRead="markAsRead" />
</div>
<div class="notifications-link-container">
<nuxt-link :to="{ name: 'notifications' }">
{{ $t('notifications.pageLink') }}
</nuxt-link>
</div>
<ds-flex class="notifications-link-container">
<ds-flex-item :width="{ base: 'auto' }" centered>
<nuxt-link :to="{ name: 'notifications' }">
<ds-button ghost primary>
{{ $t('notifications.pageLink') }}
</ds-button>
</nuxt-link>
</ds-flex-item>
<ds-flex-item :width="{ base: 'auto' }" centered>
<ds-button ghost primary @click="markAllAsRead" data-test="markAllAsRead-button">
{{ $t('notifications.markAllAsRead') }}
</ds-button>
</ds-flex-item>
</ds-flex>
</template>
</dropdown>
</template>
@ -28,7 +37,12 @@
<script>
import { mapGetters } from 'vuex'
import unionBy from 'lodash/unionBy'
import { notificationQuery, markAsReadMutation, notificationAdded } from '~/graphql/User'
import {
notificationQuery,
markAsReadMutation,
notificationAdded,
markAllAsReadMutation,
} from '~/graphql/User'
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
import Dropdown from '~/components/Dropdown'
import NotificationList from '../NotificationList/NotificationList'
@ -56,8 +70,21 @@ export default {
mutation: markAsReadMutation(this.$i18n),
variables,
})
} catch (err) {
this.$toast.error(err.message)
} catch (error) {
this.$toast.error(error.message)
}
},
async markAllAsRead() {
if (!this.hasNotifications) {
return
}
try {
await this.$apollo.mutate({
mutation: markAllAsReadMutation(this.$i18n),
})
} catch (error) {
this.$toast.error(error.message)
}
},
},
@ -71,6 +98,9 @@ export default {
}, 0)
return result
},
hasNotifications() {
return this.notifications.length
},
},
apollo: {
notifications: {
@ -118,7 +148,7 @@ export default {
}
.notifications-link-container {
background-color: $background-color-softer-active;
text-align: center;
justify-content: center;
position: fixed;
bottom: 0;
left: 0;

View File

@ -42,7 +42,7 @@
</div>
<div v-else class="categories-placeholder"></div>
<counter-icon
icon="bullhorn"
icon="heart-o"
:count="post.shoutedCount"
:title="$t('contribution.amount-shouts', { amount: post.shoutedCount })"
/>

View File

@ -4,7 +4,7 @@
:loading="loading"
:disabled="disabled"
:filled="shouted"
icon="bullhorn"
icon="heart-o"
circle
@click="toggle"
/>

View File

@ -21,7 +21,7 @@ storiesOf('Generic/BaseButton', module)
template: `
<div>
<base-button icon="edit">With Text</base-button>
<base-button icon="bullhorn" />
<base-button icon="heart-o" />
<base-button icon="trash" disabled />
<base-button icon="trash" loading />
</div>

View File

@ -4,7 +4,7 @@
<div class="metadata">
<span class="counts">
<counter-icon icon="comments" :count="option.commentsCount" soft />
<counter-icon icon="bullhorn" :count="option.shoutedCount" soft />
<counter-icon icon="heart-o" :count="option.shoutedCount" soft />
<counter-icon icon="hand-pointer" :count="option.clickedCount" soft />
<counter-icon icon="eye" :count="option.viewedTeaserCount" soft />
</span>

View File

@ -108,7 +108,7 @@ export const mapUserQuery = (i18n) => {
`
}
export const notificationQuery = (i18n) => {
export const notificationQuery = (_i18n) => {
return gql`
${userFragment}
${commentFragment}
@ -147,7 +147,7 @@ export const notificationQuery = (i18n) => {
`
}
export const markAsReadMutation = (i18n) => {
export const markAsReadMutation = (_i18n) => {
return gql`
${userFragment}
${commentFragment}
@ -183,6 +183,42 @@ export const markAsReadMutation = (i18n) => {
`
}
export const markAllAsReadMutation = (_i18n) => {
return gql`
${userFragment}
${commentFragment}
${postFragment}
mutation {
markAllAsRead {
id
read
reason
createdAt
updatedAt
from {
__typename
... on Post {
...post
author {
...user
}
}
... on Comment {
...comment
post {
...post
author {
...user
}
}
}
}
}
}
`
}
export const notificationAdded = () => {
return gql`
${userFragment}

View File

@ -270,7 +270,7 @@
"filterFollow": "Beiträge von Nutzern filtern, denen ich folge",
"filterMasonryGrid": {
"myFriends": "Nutzer denen ich folge",
"myGroups": "Meine Gruppen",
"myGroups": "Aus meinen Gruppen",
"myTopics": "Meine Themen",
"noFilter": "Beiträge filtern"
},
@ -515,10 +515,13 @@
"no-results": "Keine Beiträge gefunden."
},
"invite-codes": {
"copy-code": "Code:",
"button": {
"tooltip": "Lade deine Freunde ein"
},
"copy-code": "Einladungslink kopieren",
"copy-success": "Einladungscode erfolgreich in die Zwischenablage kopiert",
"not-available": "Du hast keinen Einladungscode zur Verfügung!",
"your-code": "Kopiere deinen Einladungscode in die Ablage:"
"your-code": "Sende diesen Link per E-Mail oder in sozialen Medien, um deine Freunde einzuladen:"
},
"login": {
"email": "Deine E-Mail",
@ -640,6 +643,7 @@
"read": "Gelesen",
"unread": "Ungelesen"
},
"markAllAsRead": "Markiere alle als gelesen",
"pageLink": "Alle Benachrichtigungen",
"post": "Beitrag",
"reason": {

View File

@ -270,7 +270,7 @@
"filterFollow": "Filter contributions from users I follow",
"filterMasonryGrid": {
"myFriends": "Users I follow",
"myGroups": "My groups",
"myGroups": "By my groups",
"myTopics": "My topics",
"noFilter": "Filter posts"
},
@ -515,10 +515,13 @@
"no-results": "No contributions found."
},
"invite-codes": {
"copy-code": "Code:",
"button": {
"tooltip": "Invite your friends"
},
"copy-code": "Copy Invite Link",
"copy-success": "Invite code copied to clipboard",
"not-available": "You have no valid invite code available!",
"your-code": "Copy your invite code to the clipboard:"
"your-code": "Send this link per e-mail or in social media to invite your friends:"
},
"login": {
"email": "Your E-mail",
@ -640,6 +643,7 @@
"read": "Read",
"unread": "Unread"
},
"markAllAsRead": "Mark all as read",
"pageLink": "All notifications",
"post": "Post",
"reason": {

View File

@ -412,6 +412,7 @@
"read": "Leído",
"unread": "No leído"
},
"markAllAsRead": "Marcar todas como leido",
"pageLink": "Todas las notificaciones",
"post": "Contribución",
"reason": {

View File

@ -401,6 +401,7 @@
"read": "Lire",
"unread": "Non lu"
},
"markAllAsRead": "Tout marquer comme lu",
"pageLink": "Toutes les notifications",
"post": "Post",
"reason": {

View File

@ -354,6 +354,7 @@
"read": null,
"unread": null
},
"markAllAsRead": "Segna tutti come letti",
"pageLink": null,
"post": null,
"reason": {

View File

@ -100,6 +100,26 @@
"moreInfo": "Wat is {APPLICATION_NAME}?",
"password": "Uw Wachtwoord"
},
"notifications": {
"comment": null,
"content": null,
"empty": null,
"filterLabel": {
"all": null,
"read": null,
"unread": null
},
"markAllAsRead": "Markeer alles als gelezen",
"pageLink": null,
"post": null,
"reason": {
"commented_on_post": null,
"mentioned_in_comment": null,
"mentioned_in_post": null
},
"title": null,
"user": null
},
"post": {
"moreInfo": {
"name": "Meer info"

View File

@ -199,6 +199,7 @@
}
},
"notifications": {
"markAllAsRead": "Oznacz wszystkie jako przeczytane",
"menu": {
"mentioned": "wspomiał o Tobie we wpisie"
}

View File

@ -390,6 +390,7 @@
"read": "Lido",
"unread": "Não lido"
},
"markAllAsRead": "Marcar todas como lidas",
"pageLink": "Todas as notificações",
"post": "Post",
"reason": {

View File

@ -426,6 +426,7 @@
"read": "Прочитанные",
"unread": "Непрочитанные"
},
"markAllAsRead": "Отметить все как прочитанное",
"pageLink": "Все уведомления",
"post": "Пост",
"reason": {

View File

@ -92,16 +92,16 @@
</div>
</div>
</ds-grid-item>
<ds-space :margin-bottom="{ base: 'small', md: 'base', lg: 'large' }" />
<!-- Placeholder/Space Row -->
<ds-grid-item :row-span="1" column-span="fullWidth" />
<!-- hashtag filter -->
<ds-grid-item v-if="hashtag" :row-span="2" column-span="fullWidth">
<hashtags-filter :hashtag="hashtag" @clearSearch="clearSearch" />
</ds-grid-item>
<ds-space :margin-bottom="{ base: 'small', md: 'base', lg: 'large' }" />
<!-- donation info -->
<ds-grid-item v-if="showDonations" class="top-info-bar" :row-span="1" column-span="fullWidth">
<donation-info :goal="goal" :progress="progress" />
</ds-grid-item>
<ds-space :margin-bottom="{ base: 'small', md: 'base', lg: 'large' }" />
<!-- news feed -->
<template v-if="hasResults">
<masonry-grid-item
@ -418,5 +418,8 @@ export default {
font-size: 23px;
z-index: 10;
}
.ds-grid {
padding-top: 1em;
}
}
</style>

View File

@ -1,21 +1,22 @@
import { shallowMount, mount } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import NotificationsPage from './index.vue'
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
import NotificationsTable from '~/components/NotificationsTable/NotificationsTable'
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
import { markAsReadMutation, markAllAsReadMutation } from '~/graphql/User'
const localVue = global.localVue
const stubs = {
'client-only': true,
'notifications-table': true,
}
describe('PostIndex', () => {
let wrapper, Wrapper, mocks, propsData
let wrapper, Wrapper, mocks
beforeEach(() => {
propsData = {}
mocks = {
$t: (string) => string,
$toast: {
@ -37,86 +38,94 @@ describe('PostIndex', () => {
}
})
describe('shallowMount', () => {
beforeEach(() => {
Wrapper = () => {
return shallowMount(NotificationsPage, {
mocks,
localVue,
propsData,
stubs,
})
}
wrapper = Wrapper()
})
it('renders a Notications header', () => {
expect(wrapper.find('ds-heading-stub').exists()).toBe(true)
})
it('renders a `dropdown-filter` component', () => {
expect(wrapper.find('dropdown-filter-stub').exists()).toBe(true)
})
it('renders a `notifications-table` component', () => {
expect(wrapper.find('notifications-table-stub').exists()).toBe(true)
})
})
describe('mount', () => {
jest.clearAllMocks()
beforeEach(() => {
Wrapper = () => {
return mount(NotificationsPage, {
mocks,
localVue,
propsData,
stubs,
})
}
wrapper = Wrapper()
wrapper.setData({
notifications: [
{
id: 'mentioned_in_comment/c4-1/u1',
read: false,
reason: 'mentioned_in_comment',
createdAt: '2023-03-06T14:32:47.924Z',
updatedAt: '2023-03-06T14:32:47.924Z',
},
{
id: 'mentioned_in_post/p8/u1',
read: false,
reason: 'mentioned_in_post',
createdAt: '2023-03-06T14:32:47.667Z',
updatedAt: '2023-03-06T14:32:47.667Z',
},
],
})
})
it('renders a Notications header', () => {
expect(wrapper.find('.ds-heading').exists()).toBe(true)
})
it('renders a `dropdown-filter` component', () => {
expect(wrapper.find('.dropdown-filter').exists()).toBe(true)
})
it('renders a `notifications-table` component', () => {
expect(wrapper.findComponent(NotificationsTable).exists()).toBe(true)
})
it('renders a `mark-all-as-read` button', () => {
expect(wrapper.find('[data-test="markAllAsRead-button"]').exists()).toBe(true)
})
describe('filter', () => {
beforeEach(() => {
propsData.filterOptions = [
{ label: 'All', value: null },
{ label: 'Read', value: true },
{ label: 'Unread', value: false },
]
wrapper = Wrapper()
wrapper.findComponent(DropdownFilter).vm.$emit('filter', propsData.filterOptions[1])
it('has "All" as default', () => {
expect(wrapper.find('a.dropdown-filter').text()).toBe('notifications.filterLabel.all')
})
it('sets `notificationRead` to value of received option', () => {
expect(wrapper.vm.notificationRead).toEqual(propsData.filterOptions[1].value)
})
describe('select Read', () => {
beforeEach(() => {
wrapper.findComponent(DropdownFilter).vm.$emit('filter', wrapper.vm.filterOptions[1])
})
it('set label to the label of the received option', () => {
expect(wrapper.vm.selected).toEqual(propsData.filterOptions[1].label)
})
it('sets `notificationRead` to value of received option', () => {
expect(wrapper.vm.notificationRead).toEqual(wrapper.vm.filterOptions[1].value)
})
it('refreshes the notifications', () => {
expect(mocks.$apollo.queries.notifications.refresh).toHaveBeenCalledTimes(1)
it('sets label to the label of the received option', () => {
expect(wrapper.vm.selected).toEqual(wrapper.vm.filterOptions[1].label)
})
it('refreshes the notifications', () => {
expect(mocks.$apollo.queries.notifications.refresh).toHaveBeenCalledTimes(1)
})
})
})
describe('markNotificationAsRead', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper
.findComponent(NotificationsTable)
.vm.$emit('markNotificationAsRead', 'notificationSourceId')
})
it('calls markNotificationAsRead mutation', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({ variables: { id: 'notificationSourceId' } }),
)
it('calls markAllAsRead mutation', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledWith({
mutation: markAsReadMutation(),
variables: { id: 'notificationSourceId' },
})
})
describe('error handling', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest.fn().mockRejectedValueOnce({ message: 'Some error message' })
wrapper = Wrapper()
wrapper
.findComponent(NotificationsTable)
.vm.$emit('markNotificationAsRead', 'notificationSourceId')
@ -128,6 +137,26 @@ describe('PostIndex', () => {
})
})
describe('markAllNotificationAsRead', () => {
it('calls markAllNotificationAsRead mutation and refreshes notification', async () => {
wrapper.find('button[data-test="markAllAsRead-button"]').trigger('click')
await expect(mocks.$apollo.mutate).toHaveBeenCalledWith({
mutation: markAllAsReadMutation(),
})
expect(mocks.$apollo.queries.notifications.refresh).toHaveBeenCalledTimes(1)
})
describe('error handling', () => {
it('shows an error message if there is an error', async () => {
mocks.$apollo.mutate = jest
.fn()
.mockRejectedValueOnce({ message: 'Another error message' })
await wrapper.find('button[data-test="markAllAsRead-button"]').trigger('click')
expect(mocks.$toast.error).toHaveBeenCalledWith('Another error message')
})
})
})
describe('PaginationButtons', () => {
beforeEach(() => {
wrapper = Wrapper()

View File

@ -15,7 +15,27 @@
@markNotificationAsRead="markNotificationAsRead"
:notifications="notifications"
/>
<pagination-buttons :hasNext="hasNext" :hasPrevious="hasPrevious" @back="back" @next="next" />
<ds-flex class="notifications-footer">
<ds-flex-item :width="{ base: 'auto' }" centered>
<pagination-buttons
:hasNext="hasNext"
:hasPrevious="hasPrevious"
@back="back"
@next="next"
/>
</ds-flex-item>
<ds-flex-item class="notifications-footer-button" :width="{ base: 'auto' }" centered>
<ds-button
primary
:disabled="unreadNotificationsCount === 0"
@click="markAllAsRead"
data-test="markAllAsRead-button"
>
{{ $t('notifications.markAllAsRead') }}
</ds-button>
</ds-flex-item>
</ds-flex>
</base-card>
</template>
@ -23,7 +43,7 @@
import NotificationsTable from '~/components/NotificationsTable/NotificationsTable'
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
import { notificationQuery, markAsReadMutation } from '~/graphql/User'
import { notificationQuery, markAsReadMutation, markAllAsReadMutation } from '~/graphql/User'
export default {
components: {
@ -54,6 +74,15 @@ export default {
{ label: this.$t('notifications.filterLabel.unread'), value: false },
]
},
hasNotifications() {
return this.notifications.length
},
unreadNotificationsCount() {
const result = this.notifications.reduce((count, notification) => {
return notification.read ? count : count + 1
}, 0)
return result
},
},
methods: {
filter(option) {
@ -77,6 +106,20 @@ export default {
next() {
this.offset += this.pageSize
},
async markAllAsRead() {
if (!this.hasNotifications) {
return
}
try {
await this.$apollo.mutate({
mutation: markAllAsReadMutation(this.$i18n),
})
this.$apollo.queries.notifications.refresh()
} catch (error) {
this.$toast.error(error.message)
}
},
},
apollo: {
notifications: {
@ -112,4 +155,8 @@ export default {
.notifications-page-flex {
justify-content: space-between;
}
.notifications-footer {
justify-content: space-evenly;
}
</style>