diff --git a/.github/workflows/publish-branded.yml b/.github/workflows/publish-branded.yml
index a404453e6..869eb6302 100644
--- a/.github/workflows/publish-branded.yml
+++ b/.github/workflows/publish-branded.yml
@@ -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:
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 01256d719..4452f2286 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -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
diff --git a/backend/src/graphql/notifications.js b/backend/src/graphql/notifications.js
new file mode 100644
index 000000000..233077372
--- /dev/null
+++ b/backend/src/graphql/notifications.js
@@ -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
+ }
+ }
+ `
+}
diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js
index 00a34f9ab..6cd8f39d6 100644
--- a/backend/src/middleware/permissionsMiddleware.js
+++ b/backend/src/middleware/permissionsMiddleware.js
@@ -449,6 +449,7 @@ export default shield(
blockUser: isAuthenticated,
unblockUser: isAuthenticated,
markAsRead: isAuthenticated,
+ markAllAsRead: isAuthenticated,
AddEmailAddress: isAuthenticated,
VerifyEmailAddress: isAuthenticated,
pinPost: isAdmin,
diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js
index 3c01ddb97..a2b850336 100644
--- a/backend/src/schema/resolvers/notifications.js
+++ b/backend/src/schema/resolvers/notifications.js
@@ -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) => {
diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js
index 36bd530eb..47134aea6 100644
--- a/backend/src/schema/resolvers/notifications.spec.js
+++ b/backend/src/schema/resolvers/notifications.spec.js
@@ -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()
+ })
+ })
+ })
+ })
})
diff --git a/backend/src/schema/types/type/NOTIFIED.gql b/backend/src/schema/types/type/NOTIFIED.gql
index 88ecd3882..864cdea4d 100644
--- a/backend/src/schema/types/type/NOTIFIED.gql
+++ b/backend/src/schema/types/type/NOTIFIED.gql
@@ -29,6 +29,7 @@ type Query {
type Mutation {
markAsRead(id: ID!): NOTIFIED
+ markAllAsRead: [NOTIFIED]
}
type Subscription {
diff --git a/neo4j/README.md b/neo4j/README.md
index 885f7f445..df3b5fde6 100644
--- a/neo4j/README.md
+++ b/neo4j/README.md
@@ -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.
diff --git a/webapp/components/FilterMenu/CategoriesMenu.vue b/webapp/components/FilterMenu/CategoriesMenu.vue
index 0b5505503..091aed24f 100644
--- a/webapp/components/FilterMenu/CategoriesMenu.vue
+++ b/webapp/components/FilterMenu/CategoriesMenu.vue
@@ -3,7 +3,7 @@
{{ $t('admin.categories.name') }}
-
+