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') }} -