mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge pull request #439 from Human-Connection/347-display_notifications
[WIP] Frontend implementation for notifications
This commit is contained in:
commit
6cd8a4ef21
@ -3,21 +3,26 @@ import uuid from 'uuid/v4'
|
|||||||
export default function (params) {
|
export default function (params) {
|
||||||
const {
|
const {
|
||||||
id = uuid(),
|
id = uuid(),
|
||||||
key,
|
key = '',
|
||||||
type = 'crowdfunding',
|
type = 'crowdfunding',
|
||||||
status = 'permanent',
|
status = 'permanent',
|
||||||
icon
|
icon = '/img/badges/indiegogo_en_panda.svg'
|
||||||
} = params
|
} = params
|
||||||
|
|
||||||
return `
|
return {
|
||||||
mutation {
|
mutation: `
|
||||||
CreateBadge(
|
mutation(
|
||||||
id: "${id}",
|
$id: ID
|
||||||
key: "${key}",
|
$key: String!
|
||||||
type: ${type},
|
$type: BadgeTypeEnum!
|
||||||
status: ${status},
|
$status: BadgeStatusEnum!
|
||||||
icon: "${icon}"
|
$icon: String!
|
||||||
) { id }
|
) {
|
||||||
|
CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { id, key, type, status, icon }
|
||||||
}
|
}
|
||||||
`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,14 +8,15 @@ export default function (params) {
|
|||||||
icon
|
icon
|
||||||
} = params
|
} = params
|
||||||
|
|
||||||
return `
|
return {
|
||||||
mutation {
|
mutation: `
|
||||||
CreateCategory(
|
mutation($id: ID, $name: String!, $slug: String, $icon: String!) {
|
||||||
id: "${id}",
|
CreateCategory(id: $id, name: $name, slug: $slug, icon: $icon) {
|
||||||
name: "${name}",
|
id
|
||||||
slug: "${slug}",
|
name
|
||||||
icon: "${icon}"
|
}
|
||||||
) { id, name }
|
|
||||||
}
|
}
|
||||||
`
|
`,
|
||||||
|
variables: { id, name, slug, icon }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,19 +7,17 @@ export default function (params) {
|
|||||||
content = [
|
content = [
|
||||||
faker.lorem.sentence(),
|
faker.lorem.sentence(),
|
||||||
faker.lorem.sentence()
|
faker.lorem.sentence()
|
||||||
].join('. '),
|
].join('. ')
|
||||||
disabled = false,
|
|
||||||
deleted = false
|
|
||||||
} = params
|
} = params
|
||||||
|
|
||||||
return `
|
return {
|
||||||
mutation {
|
mutation: `
|
||||||
CreateComment(
|
mutation($id: ID!, $content: String!) {
|
||||||
id: "${id}",
|
CreateComment(id: $id, content: $content) {
|
||||||
content: "${content}",
|
id
|
||||||
disabled: ${disabled},
|
}
|
||||||
deleted: ${deleted}
|
}
|
||||||
) { id }
|
`,
|
||||||
}
|
variables: { id, content }
|
||||||
`
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,8 +71,8 @@ export default function Factory (options = {}) {
|
|||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
async create (node, properties) {
|
async create (node, properties) {
|
||||||
const mutation = this.factories[node](properties)
|
const { mutation, variables } = this.factories[node](properties)
|
||||||
this.lastResponse = await this.graphQLClient.request(mutation)
|
this.lastResponse = await this.graphQLClient.request(mutation, variables)
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
async relate (node, relationship, properties) {
|
async relate (node, relationship, properties) {
|
||||||
|
|||||||
@ -6,12 +6,15 @@ export default function (params) {
|
|||||||
read = false
|
read = false
|
||||||
} = params
|
} = params
|
||||||
|
|
||||||
return `
|
return {
|
||||||
mutation {
|
mutation: `
|
||||||
CreateNotification(
|
mutation($id: ID, $read: Boolean) {
|
||||||
id: "${id}",
|
CreateNotification(id: $id, read: $read) {
|
||||||
read: ${read},
|
id
|
||||||
) { id, read }
|
read
|
||||||
}
|
}
|
||||||
`
|
}
|
||||||
|
`,
|
||||||
|
variables: { id, read }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,20 +5,17 @@ export default function create (params) {
|
|||||||
const {
|
const {
|
||||||
id = uuid(),
|
id = uuid(),
|
||||||
name = faker.company.companyName(),
|
name = faker.company.companyName(),
|
||||||
description = faker.company.catchPhrase(),
|
description = faker.company.catchPhrase()
|
||||||
disabled = false,
|
|
||||||
deleted = false
|
|
||||||
} = params
|
} = params
|
||||||
|
|
||||||
return `
|
return {
|
||||||
mutation {
|
mutation: `
|
||||||
CreateOrganization(
|
mutation($id: ID!, $name: String!, $description: String!) {
|
||||||
id: "${id}",
|
CreateOrganization(id: $id, name: $name, description: $description) {
|
||||||
name: "${name}",
|
name
|
||||||
description: "${description}",
|
}
|
||||||
disabled: ${disabled},
|
}
|
||||||
deleted: ${deleted}
|
`,
|
||||||
) { name }
|
variables: { id, name, description }
|
||||||
}
|
}
|
||||||
`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,17 +18,31 @@ export default function (params) {
|
|||||||
deleted = false
|
deleted = false
|
||||||
} = params
|
} = params
|
||||||
|
|
||||||
return `
|
return {
|
||||||
mutation {
|
mutation: `
|
||||||
CreatePost(
|
mutation(
|
||||||
id: "${id}",
|
$id: ID!
|
||||||
slug: "${slug}",
|
$slug: String
|
||||||
title: "${title}",
|
$title: String!
|
||||||
content: "${content}",
|
$content: String!
|
||||||
image: "${image}",
|
$image: String
|
||||||
visibility: ${visibility},
|
$visibility: VisibilityEnum
|
||||||
deleted: ${deleted}
|
$deleted: Boolean
|
||||||
) { title, content }
|
) {
|
||||||
}
|
CreatePost(
|
||||||
`
|
id: $id
|
||||||
|
slug: $slug
|
||||||
|
title: $title
|
||||||
|
content: $content
|
||||||
|
image: $image
|
||||||
|
visibility: $visibility
|
||||||
|
deleted: $deleted
|
||||||
|
) {
|
||||||
|
title
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { id, slug, title, content, image, visibility, deleted }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,15 +6,15 @@ export default function create (params) {
|
|||||||
id
|
id
|
||||||
} = params
|
} = params
|
||||||
|
|
||||||
return `
|
return {
|
||||||
mutation {
|
mutation: `
|
||||||
report(
|
mutation($id: ID!, $description: String!) {
|
||||||
description: "${description}",
|
report(description: $description, id: $id) {
|
||||||
id: "${id}",
|
id
|
||||||
) {
|
createdAt
|
||||||
id,
|
}
|
||||||
createdAt
|
|
||||||
}
|
}
|
||||||
}
|
`,
|
||||||
`
|
variables: { id, description }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,15 +3,17 @@ import uuid from 'uuid/v4'
|
|||||||
export default function (params) {
|
export default function (params) {
|
||||||
const {
|
const {
|
||||||
id = uuid(),
|
id = uuid(),
|
||||||
name
|
name = '#human-connection'
|
||||||
} = params
|
} = params
|
||||||
|
|
||||||
return `
|
return {
|
||||||
mutation {
|
mutation: `
|
||||||
CreateTag(
|
mutation($id: ID!, $name: String!) {
|
||||||
id: "${id}",
|
CreateTag(id: $id, name: $name) {
|
||||||
name: "${name}",
|
name
|
||||||
) { name }
|
}
|
||||||
}
|
}
|
||||||
`
|
`,
|
||||||
|
variables: { id, name }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,34 +10,42 @@ export default function create (params) {
|
|||||||
password = '1234',
|
password = '1234',
|
||||||
role = 'user',
|
role = 'user',
|
||||||
avatar = faker.internet.avatar(),
|
avatar = faker.internet.avatar(),
|
||||||
about = faker.lorem.paragraph(),
|
about = faker.lorem.paragraph()
|
||||||
disabled = false,
|
|
||||||
deleted = false
|
|
||||||
} = params
|
} = params
|
||||||
|
|
||||||
return `
|
return {
|
||||||
mutation {
|
mutation: `
|
||||||
CreateUser(
|
mutation(
|
||||||
id: "${id}",
|
$id: ID!
|
||||||
name: "${name}",
|
$name: String
|
||||||
slug: "${slug}",
|
$slug: String
|
||||||
password: "${password}",
|
$password: String!
|
||||||
email: "${email}",
|
$email: String
|
||||||
avatar: "${avatar}",
|
$avatar: String
|
||||||
about: "${about}",
|
$about: String
|
||||||
role: ${role},
|
$role: UserGroupEnum
|
||||||
disabled: ${disabled},
|
|
||||||
deleted: ${deleted}
|
|
||||||
) {
|
) {
|
||||||
id
|
CreateUser(
|
||||||
name
|
id: $id
|
||||||
slug
|
name: $name
|
||||||
email
|
slug: $slug
|
||||||
avatar
|
password: $password
|
||||||
role
|
email: $email
|
||||||
deleted
|
avatar: $avatar
|
||||||
disabled
|
about: $about
|
||||||
|
role: $role
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
email
|
||||||
|
avatar
|
||||||
|
role
|
||||||
|
deleted
|
||||||
|
disabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
`,
|
||||||
`
|
variables: { id, name, slug, password, email, avatar, about, role }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import faker from 'faker'
|
||||||
import Factory from './factories'
|
import Factory from './factories'
|
||||||
|
|
||||||
/* eslint-disable no-multi-spaces */
|
/* eslint-disable no-multi-spaces */
|
||||||
@ -88,20 +89,23 @@ import Factory from './factories'
|
|||||||
f.create('Tag', { id: 't4', name: 'Freiheit' })
|
f.create('Tag', { id: 't4', name: 'Freiheit' })
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const mention1 = 'Hey <a class="mention" href="/profile/u3">@jenny-rostock</a>, what\'s up?'
|
||||||
|
const mention2 = 'Hey <a class="mention" href="/profile/u3">@jenny-rostock</a>, here is another notification for you!'
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
asAdmin.create('Post', { id: 'p0' }),
|
asAdmin.create('Post', { id: 'p0' }),
|
||||||
asModerator.create('Post', { id: 'p1' }),
|
asModerator.create('Post', { id: 'p1' }),
|
||||||
asUser.create('Post', { id: 'p2', deleted: true }),
|
asUser.create('Post', { id: 'p2' }),
|
||||||
asTick.create('Post', { id: 'p3' }),
|
asTick.create('Post', { id: 'p3' }),
|
||||||
asTrick.create('Post', { id: 'p4' }),
|
asTrick.create('Post', { id: 'p4' }),
|
||||||
asTrack.create('Post', { id: 'p5' }),
|
asTrack.create('Post', { id: 'p5' }),
|
||||||
asAdmin.create('Post', { id: 'p6' }),
|
asAdmin.create('Post', { id: 'p6' }),
|
||||||
asModerator.create('Post', { id: 'p7' }),
|
asModerator.create('Post', { id: 'p7', content: `${mention1} ${faker.lorem.paragraph()}` }),
|
||||||
asUser.create('Post', { id: 'p8' }),
|
asUser.create('Post', { id: 'p8' }),
|
||||||
asTick.create('Post', { id: 'p9' }),
|
asTick.create('Post', { id: 'p9' }),
|
||||||
asTrick.create('Post', { id: 'p10' }),
|
asTrick.create('Post', { id: 'p10' }),
|
||||||
asTrack.create('Post', { id: 'p11' }),
|
asTrack.create('Post', { id: 'p11' }),
|
||||||
asAdmin.create('Post', { id: 'p12' }),
|
asAdmin.create('Post', { id: 'p12', content: `${mention2} ${faker.lorem.paragraph()}` }),
|
||||||
asModerator.create('Post', { id: 'p13' }),
|
asModerator.create('Post', { id: 'p13' }),
|
||||||
asUser.create('Post', { id: 'p14' }),
|
asUser.create('Post', { id: 'p14' }),
|
||||||
asTick.create('Post', { id: 'p15' })
|
asTick.create('Post', { id: 'p15' })
|
||||||
|
|||||||
@ -1,10 +1,23 @@
|
|||||||
# End-to-End Testing
|
# End-to-End Testing
|
||||||
|
|
||||||
|
## Configure cypress
|
||||||
|
|
||||||
|
First, you have to tell cypress how to connect to your local neo4j database
|
||||||
|
among other things. You can copy our template configuration and change the new
|
||||||
|
file according to your needs.
|
||||||
|
|
||||||
|
Make sure you are at the root level of the project. Then:
|
||||||
|
```bash
|
||||||
|
# in the top level folder Human-Connection/
|
||||||
|
$ cp cypress.env.template.json cypress.env.json
|
||||||
|
```
|
||||||
|
|
||||||
## Run Tests
|
## Run Tests
|
||||||
|
|
||||||
To run the tests, make sure you are at the root level of the project, in your console and run the following command:
|
To run the tests, do this:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# in the top level folder Human-Connection/
|
||||||
$ yarn cypress:setup
|
$ yarn cypress:setup
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -83,6 +83,13 @@ The following features will be implemented. This gets done in three steps:
|
|||||||
* Editing Comments
|
* Editing Comments
|
||||||
* Upvote comments of others
|
* Upvote comments of others
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
[Cucumber features](./integration/notifications)
|
||||||
|
|
||||||
|
* User @-mentionings
|
||||||
|
* Notify authors for comments
|
||||||
|
* Administrative notifications to all users
|
||||||
|
|
||||||
### Contribution List
|
### Contribution List
|
||||||
|
|
||||||
* Show Posts by Tiles
|
* Show Posts by Tiles
|
||||||
|
|||||||
@ -293,3 +293,43 @@ Then('I can login successfully with password {string}', password => {
|
|||||||
})
|
})
|
||||||
cy.get('.iziToast-wrapper').should('contain', "You are logged in!")
|
cy.get('.iziToast-wrapper').should('contain', "You are logged in!")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
When('I log in with the following credentials:', table => {
|
||||||
|
const { email, password } = table.hashes()[0]
|
||||||
|
cy.login({ email, password })
|
||||||
|
})
|
||||||
|
|
||||||
|
When('open the notification menu and click on the first item', () => {
|
||||||
|
cy.get('.notifications-menu').click()
|
||||||
|
cy.get('.notification-mention-post').first().click()
|
||||||
|
})
|
||||||
|
|
||||||
|
Then('see {int} unread notifications in the top menu', count => {
|
||||||
|
cy.get('.notifications-menu').should('contain', count)
|
||||||
|
})
|
||||||
|
|
||||||
|
Then('I get to the post page of {string}', path => {
|
||||||
|
path = path.replace('...', '')
|
||||||
|
cy.url().should('contain', '/post/')
|
||||||
|
cy.url().should('contain', path)
|
||||||
|
})
|
||||||
|
|
||||||
|
When('I start to write a new post with the title {string} beginning with:', (title, intro) => {
|
||||||
|
cy.get('.post-add-button').click()
|
||||||
|
cy.get('input[name="title"]').type(title)
|
||||||
|
cy.get('.ProseMirror').type(intro)
|
||||||
|
})
|
||||||
|
|
||||||
|
When('mention {string} in the text', (mention) => {
|
||||||
|
cy.get('.ProseMirror').type(' @')
|
||||||
|
cy.get('.suggestion-list__item').contains(mention).click()
|
||||||
|
cy.debug()
|
||||||
|
})
|
||||||
|
|
||||||
|
Then('the notification gets marked as read', () => {
|
||||||
|
cy.get('.notification').first().should('have.class', 'read')
|
||||||
|
})
|
||||||
|
|
||||||
|
Then('there are no notifications in the top menu', () => {
|
||||||
|
cy.get('.notifications-menu').should('contain', '0')
|
||||||
|
})
|
||||||
|
|||||||
31
cypress/integration/notifications/Mentions.feature
Normal file
31
cypress/integration/notifications/Mentions.feature
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
Feature: Notifications for a mentions
|
||||||
|
As a user
|
||||||
|
I want to be notified if sb. mentions me in a post or comment
|
||||||
|
In order join conversations about or related to me
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given we have the following user accounts:
|
||||||
|
| name | slug | email | password |
|
||||||
|
| Wolle aus Hamburg | wolle-aus-hamburg | wolle@example.org | 1234 |
|
||||||
|
| Matt Rider | matt-rider | matt@example.org | 4321 |
|
||||||
|
|
||||||
|
Scenario: Mention another user, re-login as this user and see notifications
|
||||||
|
Given I log in with the following credentials:
|
||||||
|
| email | password |
|
||||||
|
| wolle@example.org | 1234 |
|
||||||
|
And I start to write a new post with the title "Hey Matt" beginning with:
|
||||||
|
"""
|
||||||
|
Big shout to our fellow contributor
|
||||||
|
"""
|
||||||
|
And mention "@matt-rider" in the text
|
||||||
|
And I click on "Save"
|
||||||
|
When I log out
|
||||||
|
And I log in with the following credentials:
|
||||||
|
| email | password |
|
||||||
|
| matt@example.org | 4321 |
|
||||||
|
And see 1 unread notifications in the top menu
|
||||||
|
And open the notification menu and click on the first item
|
||||||
|
Then I get to the post page of ".../hey-matt"
|
||||||
|
And the notification gets marked as read
|
||||||
|
But when I refresh the page
|
||||||
|
Then there are no notifications in the top menu
|
||||||
@ -46,7 +46,8 @@ Cypress.Commands.add('login', ({ email, password }) => {
|
|||||||
cy.get('button[name=submit]')
|
cy.get('button[name=submit]')
|
||||||
.as('submitButton')
|
.as('submitButton')
|
||||||
.click()
|
.click()
|
||||||
cy.location('pathname').should('eq', '/') // we're in!
|
cy.get('.iziToast-message').should('contain', 'You are logged in!')
|
||||||
|
cy.get('.iziToast-close').click()
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add('logout', (email, password) => {
|
Cypress.Commands.add('logout', (email, password) => {
|
||||||
|
|||||||
@ -4,11 +4,12 @@
|
|||||||
:image="post.image"
|
:image="post.image"
|
||||||
:class="{'post-card': true, 'disabled-content': post.disabled}"
|
:class="{'post-card': true, 'disabled-content': post.disabled}"
|
||||||
>
|
>
|
||||||
<a
|
<nuxt-link
|
||||||
v-router-link
|
|
||||||
class="post-link"
|
class="post-link"
|
||||||
:href="href(post)"
|
:to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }"
|
||||||
>{{ post.title }}</a>
|
>
|
||||||
|
{{ post.title }}
|
||||||
|
</nuxt-link>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<!-- TODO: replace editor content with tiptap render view -->
|
<!-- TODO: replace editor content with tiptap render view -->
|
||||||
<ds-space margin-bottom="large">
|
<ds-space margin-bottom="large">
|
||||||
@ -75,6 +76,7 @@
|
|||||||
import HcUser from '~/components/User'
|
import HcUser from '~/components/User'
|
||||||
import ContentMenu from '~/components/ContentMenu'
|
import ContentMenu from '~/components/ContentMenu'
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HcPostCard',
|
name: 'HcPostCard',
|
||||||
@ -89,26 +91,16 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
user: 'auth/user'
|
||||||
|
}),
|
||||||
excerpt() {
|
excerpt() {
|
||||||
// remove all links from excerpt to prevent issues with the serounding link
|
return this.$filters.removeLinks(this.post.contentExcerpt)
|
||||||
let excerpt = this.post.contentExcerpt.replace(/<a.*>(.+)<\/a>/gim, '$1')
|
|
||||||
// do not display content that is only linebreaks
|
|
||||||
if (excerpt.replace(/<br>/gim, '').trim() === '') {
|
|
||||||
excerpt = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return excerpt
|
|
||||||
},
|
},
|
||||||
isAuthor() {
|
isAuthor() {
|
||||||
return this.$store.getters['auth/user'].id === this.post.author.id
|
const { author } = this.post
|
||||||
}
|
if (!author) return false
|
||||||
},
|
return this.user.id === this.post.author.id
|
||||||
methods: {
|
|
||||||
href(post) {
|
|
||||||
return this.$router.resolve({
|
|
||||||
name: 'post-id-slug',
|
|
||||||
params: { id: post.id, slug: post.slug }
|
|
||||||
}).href
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,6 +122,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.post-link {
|
.post-link {
|
||||||
|
margin: 15px;
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
62
webapp/components/PostCard/spec.js
Normal file
62
webapp/components/PostCard/spec.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { config, mount, createLocalVue, RouterLinkStub } from '@vue/test-utils'
|
||||||
|
import PostCard from '.'
|
||||||
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import Filters from '~/plugins/vue-filters'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
|
localVue.use(Vuex)
|
||||||
|
localVue.use(Styleguide)
|
||||||
|
localVue.use(Filters)
|
||||||
|
|
||||||
|
config.stubs['no-ssr'] = '<span><slot /></span>'
|
||||||
|
config.stubs['v-popover'] = '<span><slot /></span>'
|
||||||
|
|
||||||
|
describe('PostCard', () => {
|
||||||
|
let wrapper
|
||||||
|
let stubs
|
||||||
|
let mocks
|
||||||
|
let propsData
|
||||||
|
let getters
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = {}
|
||||||
|
stubs = {
|
||||||
|
NuxtLink: RouterLinkStub
|
||||||
|
}
|
||||||
|
mocks = {
|
||||||
|
$t: jest.fn()
|
||||||
|
}
|
||||||
|
getters = {
|
||||||
|
'auth/user': () => {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
getters
|
||||||
|
})
|
||||||
|
return mount(PostCard, {
|
||||||
|
stubs,
|
||||||
|
mocks,
|
||||||
|
propsData,
|
||||||
|
store,
|
||||||
|
localVue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('given a post', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData.post = {
|
||||||
|
title: "It's a title"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders title', () => {
|
||||||
|
expect(Wrapper().text()).toContain("It's a title")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
71
webapp/components/notifications/Notification/index.vue
Normal file
71
webapp/components/notifications/Notification/index.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<ds-space
|
||||||
|
:class="{'notification': true, 'read': notification.read}"
|
||||||
|
margin-bottom="x-small"
|
||||||
|
>
|
||||||
|
<no-ssr>
|
||||||
|
<ds-space margin-bottom="x-small">
|
||||||
|
<hc-user
|
||||||
|
:user="post.author"
|
||||||
|
:date-time="post.createdAt"
|
||||||
|
:trunc="35"
|
||||||
|
/>
|
||||||
|
</ds-space>
|
||||||
|
<ds-text color="soft">
|
||||||
|
{{ $t("notifications.menu.mentioned") }}
|
||||||
|
</ds-text>
|
||||||
|
</no-ssr>
|
||||||
|
<ds-space margin-bottom="x-small" />
|
||||||
|
<nuxt-link
|
||||||
|
class="notification-mention-post"
|
||||||
|
:to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }"
|
||||||
|
@click.native="$emit('read')"
|
||||||
|
>
|
||||||
|
<ds-space margin-bottom="x-small">
|
||||||
|
<ds-card
|
||||||
|
:header="post.title"
|
||||||
|
:image="post.image"
|
||||||
|
hover
|
||||||
|
space="x-small"
|
||||||
|
>
|
||||||
|
<ds-space margin-bottom="x-small" />
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
<div v-html="excerpt" />
|
||||||
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
|
</ds-card>
|
||||||
|
</ds-space>
|
||||||
|
</nuxt-link>
|
||||||
|
</ds-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HcUser from '~/components/User'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Notification',
|
||||||
|
components: {
|
||||||
|
HcUser
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
notification: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
excerpt() {
|
||||||
|
return this.$filters.removeLinks(this.post.contentExcerpt)
|
||||||
|
},
|
||||||
|
post() {
|
||||||
|
return this.notification.post || {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.notification.read {
|
||||||
|
opacity: 0.6; /* Real browsers */
|
||||||
|
filter: alpha(opacity = 60); /* MSIE */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
64
webapp/components/notifications/Notification/spec.js
Normal file
64
webapp/components/notifications/Notification/spec.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { config, mount, createLocalVue, RouterLinkStub } from '@vue/test-utils'
|
||||||
|
import Notification from '.'
|
||||||
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
import Filters from '~/plugins/vue-filters'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
|
localVue.use(Styleguide)
|
||||||
|
localVue.use(Filters)
|
||||||
|
|
||||||
|
config.stubs['no-ssr'] = '<span><slot /></span>'
|
||||||
|
|
||||||
|
describe('Notification', () => {
|
||||||
|
let wrapper
|
||||||
|
let stubs
|
||||||
|
let mocks
|
||||||
|
let propsData
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = {}
|
||||||
|
mocks = {
|
||||||
|
$t: jest.fn()
|
||||||
|
}
|
||||||
|
stubs = {
|
||||||
|
NuxtLink: RouterLinkStub
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(Notification, {
|
||||||
|
stubs,
|
||||||
|
mocks,
|
||||||
|
propsData,
|
||||||
|
localVue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('given a notification', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData.notification = {
|
||||||
|
post: {
|
||||||
|
title: "It's a title"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders title', () => {
|
||||||
|
expect(Wrapper().text()).toContain("It's a title")
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has no class "read"', () => {
|
||||||
|
expect(Wrapper().classes()).not.toContain('read')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('that is read', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData.notification.read = true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has class "read"', () => {
|
||||||
|
expect(Wrapper().classes()).toContain('read')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
32
webapp/components/notifications/NotificationList/index.vue
Normal file
32
webapp/components/notifications/NotificationList/index.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<notification
|
||||||
|
v-for="notification in notifications"
|
||||||
|
:key="notification.id"
|
||||||
|
:notification="notification"
|
||||||
|
@read="markAsRead(notification.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Notification from '../Notification'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NotificationList',
|
||||||
|
components: {
|
||||||
|
Notification
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
notifications: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
markAsRead(notificationId) {
|
||||||
|
this.$emit('markAsRead', notificationId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
130
webapp/components/notifications/NotificationList/spec.js
Normal file
130
webapp/components/notifications/NotificationList/spec.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import {
|
||||||
|
config,
|
||||||
|
shallowMount,
|
||||||
|
mount,
|
||||||
|
createLocalVue,
|
||||||
|
RouterLinkStub
|
||||||
|
} from '@vue/test-utils'
|
||||||
|
import NotificationList from '.'
|
||||||
|
import Notification from '../Notification'
|
||||||
|
import Vue from 'vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import Filters from '~/plugins/vue-filters'
|
||||||
|
|
||||||
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
|
localVue.use(Vuex)
|
||||||
|
localVue.use(Styleguide)
|
||||||
|
localVue.use(Filters)
|
||||||
|
localVue.filter('truncate', string => string)
|
||||||
|
|
||||||
|
config.stubs['no-ssr'] = '<span><slot /></span>'
|
||||||
|
config.stubs['v-popover'] = '<span><slot /></span>'
|
||||||
|
|
||||||
|
describe('NotificationList.vue', () => {
|
||||||
|
let wrapper
|
||||||
|
let Wrapper
|
||||||
|
let mocks
|
||||||
|
let stubs
|
||||||
|
let store
|
||||||
|
let propsData
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store = new Vuex.Store({
|
||||||
|
getters: {
|
||||||
|
'auth/user': () => {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
mocks = {
|
||||||
|
$t: jest.fn()
|
||||||
|
}
|
||||||
|
stubs = {
|
||||||
|
NuxtLink: RouterLinkStub
|
||||||
|
}
|
||||||
|
propsData = {
|
||||||
|
notifications: [
|
||||||
|
{
|
||||||
|
id: 'notification-41',
|
||||||
|
read: false,
|
||||||
|
post: {
|
||||||
|
id: 'post-1',
|
||||||
|
title: 'some post title',
|
||||||
|
contentExcerpt: 'this is a post content',
|
||||||
|
author: {
|
||||||
|
id: 'john-1',
|
||||||
|
slug: 'john-doe',
|
||||||
|
name: 'John Doe'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notification-42',
|
||||||
|
read: false,
|
||||||
|
post: {
|
||||||
|
id: 'post-2',
|
||||||
|
title: 'another post title',
|
||||||
|
contentExcerpt: 'this is yet another post content',
|
||||||
|
author: {
|
||||||
|
id: 'john-1',
|
||||||
|
slug: 'john-doe',
|
||||||
|
name: 'John Doe'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('shallowMount', () => {
|
||||||
|
const Wrapper = () => {
|
||||||
|
return shallowMount(NotificationList, {
|
||||||
|
propsData,
|
||||||
|
mocks,
|
||||||
|
store,
|
||||||
|
localVue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders Notification.vue for each notification of the user', () => {
|
||||||
|
expect(wrapper.findAll(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
|
||||||
|
.findAll('.notification-mention-post')
|
||||||
|
.at(1)
|
||||||
|
.trigger('click')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("emits 'markAsRead' with the notificationId", () => {
|
||||||
|
expect(wrapper.emitted('markAsRead')).toBeTruthy()
|
||||||
|
expect(wrapper.emitted('markAsRead')[0]).toEqual(['notification-42'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
112
webapp/components/notifications/NotificationMenu/index.vue
Normal file
112
webapp/components/notifications/NotificationMenu/index.vue
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<ds-button
|
||||||
|
v-if="totalNotifications <= 0"
|
||||||
|
class="notifications-menu"
|
||||||
|
disabled
|
||||||
|
icon="bell"
|
||||||
|
>
|
||||||
|
{{ totalNotifications }}
|
||||||
|
</ds-button>
|
||||||
|
<dropdown
|
||||||
|
v-else
|
||||||
|
class="notifications-menu"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
slot="default"
|
||||||
|
slot-scope="{toggleMenu}"
|
||||||
|
>
|
||||||
|
<ds-button
|
||||||
|
primary
|
||||||
|
icon="bell"
|
||||||
|
@click.prevent="toggleMenu"
|
||||||
|
>
|
||||||
|
{{ totalNotifications }}
|
||||||
|
</ds-button>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
slot="popover"
|
||||||
|
>
|
||||||
|
<div class="notifications-menu-popover">
|
||||||
|
<notification-list
|
||||||
|
:notifications="notifications"
|
||||||
|
@markAsRead="markAsRead"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import NotificationList from '../NotificationList'
|
||||||
|
import Dropdown from '~/components/Dropdown'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
const MARK_AS_READ = gql(`
|
||||||
|
mutation($id: ID!, $read: Boolean!) {
|
||||||
|
UpdateNotification(id: $id, read: $read) {
|
||||||
|
id
|
||||||
|
read
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
const NOTIFICATIONS = gql(`{
|
||||||
|
currentUser {
|
||||||
|
id
|
||||||
|
notifications(read: false, orderBy: createdAt_desc) {
|
||||||
|
id read createdAt
|
||||||
|
post {
|
||||||
|
id createdAt disabled deleted title contentExcerpt slug
|
||||||
|
author { id slug name disabled deleted }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NotificationMenu',
|
||||||
|
components: {
|
||||||
|
NotificationList,
|
||||||
|
Dropdown
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
totalNotifications() {
|
||||||
|
return (this.notifications || []).length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async markAsRead(notificationId) {
|
||||||
|
const variables = { id: notificationId, read: true }
|
||||||
|
try {
|
||||||
|
await this.$apollo.mutate({
|
||||||
|
mutation: MARK_AS_READ,
|
||||||
|
variables
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
notifications: {
|
||||||
|
query: NOTIFICATIONS,
|
||||||
|
update: data => {
|
||||||
|
const {
|
||||||
|
currentUser: { notifications }
|
||||||
|
} = data
|
||||||
|
return notifications
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.notifications-menu {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-menu-popover {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
94
webapp/components/notifications/NotificationMenu/spec.js
Normal file
94
webapp/components/notifications/NotificationMenu/spec.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { config, shallowMount, createLocalVue } from '@vue/test-utils'
|
||||||
|
import NotificationMenu from '.'
|
||||||
|
|
||||||
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
import Filters from '~/plugins/vue-filters'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
|
localVue.use(Styleguide)
|
||||||
|
localVue.use(Filters)
|
||||||
|
localVue.filter('truncate', string => string)
|
||||||
|
|
||||||
|
config.stubs['dropdown'] = '<span class="dropdown"><slot /></span>'
|
||||||
|
|
||||||
|
describe('NotificationMenu.vue', () => {
|
||||||
|
let wrapper
|
||||||
|
let Wrapper
|
||||||
|
let mocks
|
||||||
|
let data
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks = {
|
||||||
|
$t: jest.fn()
|
||||||
|
}
|
||||||
|
data = () => {
|
||||||
|
return {
|
||||||
|
notifications: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('shallowMount', () => {
|
||||||
|
const Wrapper = () => {
|
||||||
|
return shallowMount(NotificationMenu, {
|
||||||
|
data,
|
||||||
|
mocks,
|
||||||
|
localVue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it('counter displays 0', () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
expect(wrapper.find('ds-button-stub').text()).toEqual('0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('no dropdown is rendered', () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
expect(wrapper.contains('.dropdown')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given some notifications', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
data = () => {
|
||||||
|
return {
|
||||||
|
notifications: [
|
||||||
|
{
|
||||||
|
id: 'notification-41',
|
||||||
|
read: false,
|
||||||
|
post: {
|
||||||
|
id: 'post-1',
|
||||||
|
title: 'some post title',
|
||||||
|
contentExcerpt: 'this is a post content',
|
||||||
|
author: {
|
||||||
|
id: 'john-1',
|
||||||
|
slug: 'john-doe',
|
||||||
|
name: 'John Doe'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notification-42',
|
||||||
|
read: false,
|
||||||
|
post: {
|
||||||
|
id: 'post-2',
|
||||||
|
title: 'another post title',
|
||||||
|
contentExcerpt: 'this is yet another post content',
|
||||||
|
author: {
|
||||||
|
id: 'john-1',
|
||||||
|
slug: 'john-doe',
|
||||||
|
name: 'John Doe'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays the total number of notifications', () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
expect(wrapper.find('ds-button-stub').text()).toEqual('2')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -10,7 +10,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import seo from '~/components/mixins/seo'
|
import seo from '~/mixins/seo'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [seo]
|
mixins: [seo]
|
||||||
|
|||||||
@ -31,6 +31,9 @@
|
|||||||
/>
|
/>
|
||||||
</no-ssr>
|
</no-ssr>
|
||||||
<template v-if="isLoggedIn">
|
<template v-if="isLoggedIn">
|
||||||
|
<no-ssr>
|
||||||
|
<notification-menu />
|
||||||
|
</no-ssr>
|
||||||
<no-ssr>
|
<no-ssr>
|
||||||
<dropdown class="avatar-menu">
|
<dropdown class="avatar-menu">
|
||||||
<template
|
<template
|
||||||
@ -113,10 +116,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapActions } from 'vuex'
|
import { mapGetters, mapActions } from 'vuex'
|
||||||
import LocaleSwitch from '~/components/LocaleSwitch'
|
import LocaleSwitch from '~/components/LocaleSwitch'
|
||||||
import Dropdown from '~/components/Dropdown'
|
|
||||||
import SearchInput from '~/components/SearchInput.vue'
|
import SearchInput from '~/components/SearchInput.vue'
|
||||||
import Modal from '~/components/Modal'
|
import Modal from '~/components/Modal'
|
||||||
import seo from '~/components/mixins/seo'
|
import NotificationMenu from '~/components/notifications/NotificationMenu'
|
||||||
|
import Dropdown from '~/components/Dropdown'
|
||||||
|
import seo from '~/mixins/seo'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -124,7 +128,8 @@ export default {
|
|||||||
LocaleSwitch,
|
LocaleSwitch,
|
||||||
SearchInput,
|
SearchInput,
|
||||||
Modal,
|
Modal,
|
||||||
LocaleSwitch
|
LocaleSwitch,
|
||||||
|
NotificationMenu
|
||||||
},
|
},
|
||||||
mixins: [seo],
|
mixins: [seo],
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@ -18,6 +18,11 @@
|
|||||||
"commented": "Kommentiert",
|
"commented": "Kommentiert",
|
||||||
"socialMedia": "Wo sonst finde ich"
|
"socialMedia": "Wo sonst finde ich"
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"menu": {
|
||||||
|
"mentioned": "hat dich in einem Beitrag erwähnt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Suchen",
|
"placeholder": "Suchen",
|
||||||
"hint": "Wonach suchst du?",
|
"hint": "Wonach suchst du?",
|
||||||
|
|||||||
@ -18,6 +18,11 @@
|
|||||||
"commented": "Commented",
|
"commented": "Commented",
|
||||||
"socialMedia": "Where else can I find"
|
"socialMedia": "Where else can I find"
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"menu": {
|
||||||
|
"mentioned": "has mentioned you in a post"
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Search",
|
"placeholder": "Search",
|
||||||
"hint": "What are you searching for?",
|
"hint": "What are you searching for?",
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import uniqBy from 'lodash/uniqBy'
|
import uniqBy from 'lodash/uniqBy'
|
||||||
import HcPostCard from '~/components/PostCard.vue'
|
import HcPostCard from '~/components/PostCard'
|
||||||
import HcLoadMore from '~/components/LoadMore.vue'
|
import HcLoadMore from '~/components/LoadMore.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import HcPostCard from '~/components/PostCard.vue'
|
import HcPostCard from '~/components/PostCard'
|
||||||
import HcEmpty from '~/components/Empty.vue'
|
import HcEmpty from '~/components/Empty.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@ -323,7 +323,7 @@
|
|||||||
import uniqBy from 'lodash/uniqBy'
|
import uniqBy from 'lodash/uniqBy'
|
||||||
|
|
||||||
import User from '~/components/User'
|
import User from '~/components/User'
|
||||||
import HcPostCard from '~/components/PostCard.vue'
|
import HcPostCard from '~/components/PostCard'
|
||||||
import HcFollowButton from '~/components/FollowButton.vue'
|
import HcFollowButton from '~/components/FollowButton.vue'
|
||||||
import HcCountTo from '~/components/CountTo.vue'
|
import HcCountTo from '~/components/CountTo.vue'
|
||||||
import HcBadges from '~/components/Badges.vue'
|
import HcBadges from '~/components/Badges.vue'
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import formatRelative from 'date-fns/formatRelative'
|
|||||||
import addSeconds from 'date-fns/addSeconds'
|
import addSeconds from 'date-fns/addSeconds'
|
||||||
import accounting from 'accounting'
|
import accounting from 'accounting'
|
||||||
|
|
||||||
export default ({ app }) => {
|
export default ({ app = {} }) => {
|
||||||
const locales = {
|
const locales = {
|
||||||
en: enUS,
|
en: enUS,
|
||||||
de: de,
|
de: de,
|
||||||
@ -88,6 +88,17 @@ export default ({ app }) => {
|
|||||||
return index === 0 ? letter.toUpperCase() : letter.toLowerCase()
|
return index === 0 ? letter.toUpperCase() : letter.toLowerCase()
|
||||||
})
|
})
|
||||||
.replace(/\s+/g, '')
|
.replace(/\s+/g, '')
|
||||||
|
},
|
||||||
|
removeLinks: content => {
|
||||||
|
if (!content) return ''
|
||||||
|
// remove all links from excerpt to prevent issues with the surrounding link
|
||||||
|
let excerpt = content.replace(/<a.*>(.+)<\/a>/gim, '$1')
|
||||||
|
// do not display content that is only linebreaks
|
||||||
|
if (excerpt.replace(/<br>/gim, '').trim() === '') {
|
||||||
|
excerpt = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return excerpt
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -74,21 +74,38 @@ export const actions = {
|
|||||||
data: { currentUser }
|
data: { currentUser }
|
||||||
} = await client.query({
|
} = await client.query({
|
||||||
query: gql(`{
|
query: gql(`{
|
||||||
currentUser {
|
currentUser {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
email
|
||||||
|
avatar
|
||||||
|
role
|
||||||
|
about
|
||||||
|
locationName
|
||||||
|
socialMedia {
|
||||||
id
|
id
|
||||||
name
|
url
|
||||||
slug
|
}
|
||||||
email
|
notifications(read: false, orderBy: createdAt_desc) {
|
||||||
avatar
|
id
|
||||||
role
|
read
|
||||||
about
|
createdAt
|
||||||
locationName
|
post {
|
||||||
socialMedia {
|
author {
|
||||||
id
|
id
|
||||||
url
|
slug
|
||||||
|
name
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
}
|
||||||
|
title
|
||||||
|
contentExcerpt
|
||||||
|
slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`)
|
}
|
||||||
|
}`)
|
||||||
})
|
})
|
||||||
if (!currentUser) return dispatch('logout')
|
if (!currentUser) return dispatch('logout')
|
||||||
commit('SET_USER', currentUser)
|
commit('SET_USER', currentUser)
|
||||||
|
|||||||
89
webapp/store/notifications.js
Normal file
89
webapp/store/notifications.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const state = () => {
|
||||||
|
return {
|
||||||
|
notifications: null,
|
||||||
|
pending: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
SET_NOTIFICATIONS(state, notifications) {
|
||||||
|
state.notifications = notifications
|
||||||
|
},
|
||||||
|
SET_PENDING(state, pending) {
|
||||||
|
state.pending = pending
|
||||||
|
},
|
||||||
|
UPDATE_NOTIFICATIONS(state, notification) {
|
||||||
|
const notifications = state.notifications
|
||||||
|
const toBeUpdated = notifications.find(n => {
|
||||||
|
return n.id === notification.id
|
||||||
|
})
|
||||||
|
toBeUpdated = { ...toBeUpdated, ...notification }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const getters = {
|
||||||
|
notifications(state) {
|
||||||
|
return !!state.notifications
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
async init({ getters, commit }) {
|
||||||
|
if (getters.notifications) return
|
||||||
|
commit('SET_PENDING', true)
|
||||||
|
const client = this.app.apolloProvider.defaultClient
|
||||||
|
let notifications
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { currentUser }
|
||||||
|
} = await client.query({
|
||||||
|
query: gql(`{
|
||||||
|
currentUser {
|
||||||
|
id
|
||||||
|
notifications(orderBy: createdAt_desc) {
|
||||||
|
id
|
||||||
|
read
|
||||||
|
createdAt
|
||||||
|
post {
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
}
|
||||||
|
title
|
||||||
|
contentExcerpt
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
})
|
||||||
|
notifications = currentUser.notifications
|
||||||
|
console.log(notifications)
|
||||||
|
commit('SET_NOTIFICATIONS', notifications)
|
||||||
|
} finally {
|
||||||
|
commit('SET_PENDING', false)
|
||||||
|
}
|
||||||
|
return notifications
|
||||||
|
},
|
||||||
|
|
||||||
|
async markAsRead({ commit, rootGetters }, notificationId) {
|
||||||
|
const client = this.app.apolloProvider.defaultClient
|
||||||
|
const mutation = gql(`
|
||||||
|
mutation($id: ID!, $read: Boolean!) {
|
||||||
|
UpdateNotification(id: $id, read: $read) {
|
||||||
|
id
|
||||||
|
read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
const variables = { id: notificationId, read: true }
|
||||||
|
const {
|
||||||
|
data: { UpdateNotification }
|
||||||
|
} = await client.mutate({ mutation, variables })
|
||||||
|
commit('UPDATE_NOTIFICATIONS', UpdateNotification)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user