mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' of github.com:Ocelot-Social-Community/Ocelot-Social into 5627-fix-responsive-view-on-iphone
This commit is contained in:
commit
2d0a60da58
5
.github/workflows/publish-branded.yml
vendored
5
.github/workflows/publish-branded.yml
vendored
@ -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:
|
||||
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@ -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
|
||||
|
||||
65
backend/src/graphql/notifications.js
Normal file
65
backend/src/graphql/notifications.js
Normal 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
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
@ -449,6 +449,7 @@ export default shield(
|
||||
blockUser: isAuthenticated,
|
||||
unblockUser: isAuthenticated,
|
||||
markAsRead: isAuthenticated,
|
||||
markAllAsRead: isAuthenticated,
|
||||
AddEmailAddress: isAuthenticated,
|
||||
VerifyEmailAddress: isAuthenticated,
|
||||
pinPost: isAdmin,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -29,6 +29,7 @@ type Query {
|
||||
|
||||
type Mutation {
|
||||
markAsRead(id: ID!): NOTIFIED
|
||||
markAllAsRead: [NOTIFIED]
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
>
|
||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||
</base-button>
|
||||
<template slot="popover">
|
||||
<template #popover>
|
||||
<filter-menu-component />
|
||||
</template>
|
||||
</dropdown>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 })"
|
||||
/>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
:loading="loading"
|
||||
:disabled="disabled"
|
||||
:filled="shouted"
|
||||
icon="bullhorn"
|
||||
icon="heart-o"
|
||||
circle
|
||||
@click="toggle"
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -412,6 +412,7 @@
|
||||
"read": "Leído",
|
||||
"unread": "No leído"
|
||||
},
|
||||
"markAllAsRead": "Marcar todas como leido",
|
||||
"pageLink": "Todas las notificaciones",
|
||||
"post": "Contribución",
|
||||
"reason": {
|
||||
|
||||
@ -401,6 +401,7 @@
|
||||
"read": "Lire",
|
||||
"unread": "Non lu"
|
||||
},
|
||||
"markAllAsRead": "Tout marquer comme lu",
|
||||
"pageLink": "Toutes les notifications",
|
||||
"post": "Post",
|
||||
"reason": {
|
||||
|
||||
@ -354,6 +354,7 @@
|
||||
"read": null,
|
||||
"unread": null
|
||||
},
|
||||
"markAllAsRead": "Segna tutti come letti",
|
||||
"pageLink": null,
|
||||
"post": null,
|
||||
"reason": {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -199,6 +199,7 @@
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"markAllAsRead": "Oznacz wszystkie jako przeczytane",
|
||||
"menu": {
|
||||
"mentioned": "wspomiał o Tobie we wpisie"
|
||||
}
|
||||
|
||||
@ -390,6 +390,7 @@
|
||||
"read": "Lido",
|
||||
"unread": "Não lido"
|
||||
},
|
||||
"markAllAsRead": "Marcar todas como lidas",
|
||||
"pageLink": "Todas as notificações",
|
||||
"post": "Post",
|
||||
"reason": {
|
||||
|
||||
@ -426,6 +426,7 @@
|
||||
"read": "Прочитанные",
|
||||
"unread": "Непрочитанные"
|
||||
},
|
||||
"markAllAsRead": "Отметить все как прочитанное",
|
||||
"pageLink": "Все уведомления",
|
||||
"post": "Пост",
|
||||
"reason": {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user