Merge branch 'master' of https://github.com/Human-Connection/Human-Connection into 1017-send-out-notifications-on-create-omment

This commit is contained in:
Wolfgang Huß 2019-08-14 13:10:19 +02:00
commit 825a5f235a
70 changed files with 1745 additions and 681 deletions

View File

@ -4,7 +4,6 @@ NEO4J_PASSWORD=letmein
GRAPHQL_PORT=4000
GRAPHQL_URI=http://localhost:4000
CLIENT_URI=http://localhost:3000
MOCKS=false
SMTP_HOST=
SMTP_PORT=
SMTP_IGNORE_TLS=true

View File

@ -1,4 +1,4 @@
FROM node:12.7-alpine as base
FROM node:12.8-alpine as base
LABEL Description="Backend of the Social Network Human-Connection.org" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
EXPOSE 4000

View File

@ -44,8 +44,8 @@
"dependencies": {
"@hapi/joi": "^15.1.0",
"activitystrea.ms": "~2.1.3",
"apollo-cache-inmemory": "~1.6.2",
"apollo-client": "~2.6.3",
"apollo-cache-inmemory": "~1.6.3",
"apollo-client": "~2.6.4",
"apollo-link-context": "~1.0.18",
"apollo-link-http": "~1.5.15",
"apollo-server": "~2.8.1",
@ -118,7 +118,7 @@
"eslint-config-prettier": "~6.0.0",
"eslint-config-standard": "~13.0.1",
"eslint-plugin-import": "~2.18.2",
"eslint-plugin-jest": "~22.15.0",
"eslint-plugin-jest": "~22.15.1",
"eslint-plugin-node": "~9.1.0",
"eslint-plugin-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.2.1",

View File

@ -32,7 +32,6 @@ export const serverConfigs = { GRAPHQL_PORT, CLIENT_URI, GRAPHQL_URI }
export const developmentConfigs = {
DEBUG: process.env.NODE_ENV !== 'production' && process.env.DEBUG === 'true',
MOCKS: process.env.MOCKS === 'true',
DISABLED_MIDDLEWARES:
(process.env.NODE_ENV !== 'production' && process.env.DISABLED_MIDDLEWARES) || '',
}

View File

@ -157,6 +157,8 @@ const permissions = shield(
User: or(noEmailFilter, isAdmin),
isLoggedIn: allow,
Badge: allow,
PostsEmotionsCountByEmotion: allow,
PostsEmotionsByCurrentUser: allow,
},
Mutation: {
'*': deny,
@ -178,7 +180,6 @@ const permissions = shield(
// RemoveBadgeRewarded: isAdmin,
reward: isAdmin,
unreward: isAdmin,
// addFruitToBasket: isAuthenticated
follow: isAuthenticated,
unfollow: isAuthenticated,
shout: isAuthenticated,
@ -192,6 +193,8 @@ const permissions = shield(
DeleteUser: isDeletingOwnAccount,
requestPasswordReset: allow,
resetPassword: allow,
AddPostEmotions: isAuthenticated,
RemovePostEmotions: isAuthenticated,
},
User: {
email: isMyOwn,

View File

@ -1,14 +0,0 @@
import faker from 'faker'
export default {
User: () => ({
name: () => `${faker.name.firstName()} ${faker.name.lastName()}`,
email: () => `${faker.internet.email()}`,
}),
Post: () => ({
title: () => faker.lorem.lines(1),
slug: () => faker.lorem.slug(3),
content: () => faker.lorem.paragraphs(5),
contentExcerpt: () => faker.lorem.paragraphs(1),
}),
}

View File

@ -0,0 +1,34 @@
import uuid from 'uuid/v4'
module.exports = {
id: { type: 'string', primary: true, default: uuid },
activityId: { type: 'string', allow: [null] },
objectId: { type: 'string', allow: [null] },
author: {
type: 'relationship',
relationship: 'WROTE',
target: 'User',
direction: 'in',
},
title: { type: 'string', disallow: [null], min: 3 },
slug: { type: 'string', allow: [null] },
content: { type: 'string', disallow: [null], min: 3 },
contentExcerpt: { type: 'string', allow: [null] },
image: { type: 'string', allow: [null] },
deleted: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false },
disabledBy: {
type: 'relationship',
relationship: 'DISABLED',
target: 'User',
direction: 'in',
},
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
updatedAt: {
type: 'string',
isoDate: true,
required: true,
default: () => new Date().toISOString(),
},
language: { type: 'string', allow: [null] },
}

View File

@ -56,4 +56,19 @@ module.exports = {
required: true,
default: () => new Date().toISOString(),
},
emoted: {
type: 'relationships',
relationship: 'EMOTED',
target: 'Post',
direction: 'out',
properties: {
emotion: {
type: 'string',
valid: ['happy', 'cry', 'surprised', 'angry', 'funny'],
invalid: [null],
},
},
eager: true,
cascade: true,
},
}

View File

@ -6,4 +6,5 @@ export default {
InvitationCode: require('./InvitationCode.js'),
EmailAddress: require('./EmailAddress.js'),
SocialMedia: require('./SocialMedia.js'),
Post: require('./Post.js'),
}

View File

@ -44,6 +44,7 @@ export default {
delete params.categoryIds
params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
params.id = params.id || uuid()
let createPostCypher = `CREATE (post:Post {params})
WITH post
MATCH (author:User {id: $userId})
@ -70,5 +71,80 @@ export default {
return post.properties
},
AddPostEmotions: async (object, params, context, resolveInfo) => {
const session = context.driver.session()
const { to, data } = params
const { user } = context
const transactionRes = await session.run(
`MATCH (userFrom:User {id: $user.id}), (postTo:Post {id: $to.id})
MERGE (userFrom)-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo)
RETURN userFrom, postTo, emotedRelation`,
{ user, to, data },
)
session.close()
const [emoted] = transactionRes.records.map(record => {
return {
from: { ...record.get('userFrom').properties },
to: { ...record.get('postTo').properties },
...record.get('emotedRelation').properties,
}
})
return emoted
},
RemovePostEmotions: async (object, params, context, resolveInfo) => {
const session = context.driver.session()
const { to, data } = params
const { id: from } = context.user
const transactionRes = await session.run(
`MATCH (userFrom:User {id: $from})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id})
DELETE emotedRelation
RETURN userFrom, postTo`,
{ from, to, data },
)
session.close()
const [emoted] = transactionRes.records.map(record => {
return {
from: { ...record.get('userFrom').properties },
to: { ...record.get('postTo').properties },
emotion: data.emotion,
}
})
return emoted
},
},
Query: {
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {
const session = context.driver.session()
const { postId, data } = params
const transactionRes = await session.run(
`MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
RETURN COUNT(DISTINCT emoted) as emotionsCount
`,
{ postId, data },
)
session.close()
const [emotionsCount] = transactionRes.records.map(record => {
return record.get('emotionsCount').low
})
return emotionsCount
},
PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => {
const session = context.driver.session()
const { postId } = params
const transactionRes = await session.run(
`MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
RETURN collect(emoted.emotion) as emotion`,
{ userId: context.user.id, postId },
)
session.close()
const [emotions] = transactionRes.records.map(record => {
return record.get('emotion')
})
return emotions
},
},
}

View File

@ -1,8 +1,14 @@
import { GraphQLClient } from 'graphql-request'
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
import { host, login, gql } from '../../jest/helpers'
import { neode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server'
const driver = getDriver()
const factory = Factory()
const instance = neode()
let client
let userParams
let authorParams
@ -14,7 +20,7 @@ const oldContent = 'Old content'
const newTitle = 'New title'
const newContent = 'New content'
const createPostVariables = { title: postTitle, content: postContent }
const createPostWithCategoriesMutation = `
const createPostWithCategoriesMutation = gql`
mutation($title: String!, $content: String!, $categoryIds: [ID]) {
CreatePost(title: $title, content: $content, categoryIds: $categoryIds) {
id
@ -27,7 +33,7 @@ const createPostWithCategoriesVariables = {
content: postContent,
categoryIds: ['cat9', 'cat4', 'cat15'],
}
const postQueryWithCategories = `
const postQueryWithCategories = gql`
query($id: ID) {
Post(id: $id) {
categories {
@ -41,9 +47,9 @@ const createPostWithoutCategoriesVariables = {
content: 'I should be able to filter it out',
categoryIds: null,
}
const postQueryFilteredByCategory = `
query Post($filter: _PostFilter) {
Post(filter: $filter) {
const postQueryFilteredByCategory = gql`
query Post($filter: _PostFilter) {
Post(filter: $filter) {
title
id
categories {
@ -56,13 +62,28 @@ const postCategoriesFilterParam = { categories_some: { id_in: ['cat4'] } }
const postQueryFilteredByCategoryVariables = {
filter: postCategoriesFilterParam,
}
const createPostMutation = gql`
mutation($title: String!, $content: String!) {
CreatePost(title: $title, content: $content) {
id
title
content
slug
disabled
deleted
}
}
`
beforeEach(async () => {
userParams = {
id: 'u198',
name: 'TestUser',
email: 'test@example.org',
password: '1234',
}
authorParams = {
id: 'u25',
email: 'author@example.org',
password: '1234',
}
@ -74,22 +95,12 @@ afterEach(async () => {
})
describe('CreatePost', () => {
const mutation = `
mutation($title: String!, $content: String!) {
CreatePost(title: $title, content: $content) {
title
content
slug
disabled
deleted
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(mutation, createPostVariables)).rejects.toThrow('Not Authorised')
await expect(client.request(createPostMutation, createPostVariables)).rejects.toThrow(
'Not Authorised',
)
})
})
@ -107,19 +118,23 @@ describe('CreatePost', () => {
content: postContent,
},
}
await expect(client.request(mutation, createPostVariables)).resolves.toMatchObject(expected)
await expect(client.request(createPostMutation, createPostVariables)).resolves.toMatchObject(
expected,
)
})
it('assigns the authenticated user as author', async () => {
await client.request(mutation, createPostVariables)
await client.request(createPostMutation, createPostVariables)
const { User } = await client.request(
`{
User(name: "TestUser") {
contributions {
title
gql`
{
User(name: "TestUser") {
contributions {
title
}
}
}
}`,
`,
{ headers },
)
expect(User).toEqual([{ contributions: [{ title: postTitle }] }])
@ -128,13 +143,15 @@ describe('CreatePost', () => {
describe('disabled and deleted', () => {
it('initially false', async () => {
const expected = { CreatePost: { disabled: false, deleted: false } }
await expect(client.request(mutation, createPostVariables)).resolves.toMatchObject(expected)
await expect(
client.request(createPostMutation, createPostVariables),
).resolves.toMatchObject(expected)
})
})
describe('language', () => {
it('allows a user to set the language of the post', async () => {
const createPostWithLanguageMutation = `
const createPostWithLanguageMutation = gql`
mutation($title: String!, $content: String!, $language: String) {
CreatePost(title: $title, content: $content, language: $language) {
language
@ -222,7 +239,7 @@ describe('UpdatePost', () => {
title: oldTitle,
content: oldContent,
})
updatePostMutation = `
updatePostMutation = gql`
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
id
@ -328,7 +345,7 @@ describe('UpdatePost', () => {
})
describe('DeletePost', () => {
const mutation = `
const mutation = gql`
mutation($id: ID!) {
DeletePost(id: $id) {
id
@ -383,3 +400,315 @@ describe('DeletePost', () => {
})
})
})
describe('emotions', () => {
let addPostEmotionsVariables,
someUser,
ownerNode,
owner,
postMutationAction,
user,
postQueryAction,
postToEmote,
postToEmoteNode
const PostsEmotionsCountQuery = `
query($id: ID!) {
Post(id: $id) {
emotionsCount
}
}
`
const PostsEmotionsQuery = gql`
query($id: ID!) {
Post(id: $id) {
emotions {
emotion
User {
id
}
}
}
}
`
const addPostEmotionsMutation = gql`
mutation($to: _PostInput!, $data: _EMOTEDInput!) {
AddPostEmotions(to: $to, data: $data) {
from {
id
}
to {
id
}
emotion
}
}
`
beforeEach(async () => {
userParams.id = 'u1987'
authorParams.id = 'u257'
createPostVariables.id = 'p1376'
const someUserNode = await instance.create('User', userParams)
someUser = await someUserNode.toJson()
ownerNode = await instance.create('User', authorParams)
owner = await ownerNode.toJson()
postToEmoteNode = await instance.create('Post', createPostVariables)
postToEmote = await postToEmoteNode.toJson()
await postToEmoteNode.relateTo(ownerNode, 'author')
postMutationAction = async (user, mutation, variables) => {
const { server } = createServer({
context: () => {
return {
user,
driver,
}
},
})
const { mutate } = createTestClient(server)
return mutate({
mutation,
variables,
})
}
postQueryAction = async (postQuery, variables) => {
const { server } = createServer({
context: () => {
return {
user,
driver,
}
},
})
const { query } = createTestClient(server)
return query({ query: postQuery, variables })
}
addPostEmotionsVariables = {
to: { id: postToEmote.id },
data: { emotion: 'happy' },
}
})
describe('AddPostEmotions', () => {
let postsEmotionsQueryVariables
beforeEach(async () => {
postsEmotionsQueryVariables = { id: postToEmote.id }
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
user = null
const addPostEmotions = await postMutationAction(
user,
addPostEmotionsMutation,
addPostEmotionsVariables,
)
expect(addPostEmotions.errors[0]).toHaveProperty('message', 'Not Authorised!')
})
})
describe('authenticated and not the author', () => {
beforeEach(() => {
user = someUser
})
it('adds an emotion to the post', async () => {
const expected = {
data: {
AddPostEmotions: {
from: { id: user.id },
to: addPostEmotionsVariables.to,
emotion: 'happy',
},
},
}
await expect(
postMutationAction(user, addPostEmotionsMutation, addPostEmotionsVariables),
).resolves.toEqual(expect.objectContaining(expected))
})
it('limits the addition of the same emotion to 1', async () => {
const expected = {
data: {
Post: [
{
emotionsCount: 1,
},
],
},
}
await postMutationAction(user, addPostEmotionsMutation, addPostEmotionsVariables)
await postMutationAction(user, addPostEmotionsMutation, addPostEmotionsVariables)
await expect(
postQueryAction(PostsEmotionsCountQuery, postsEmotionsQueryVariables),
).resolves.toEqual(expect.objectContaining(expected))
})
it('allows a user to add more than one emotion', async () => {
const expectedEmotions = [
{ emotion: 'happy', User: { id: user.id } },
{ emotion: 'surprised', User: { id: user.id } },
]
const expectedResponse = {
data: { Post: [{ emotions: expect.arrayContaining(expectedEmotions) }] },
}
await postMutationAction(user, addPostEmotionsMutation, addPostEmotionsVariables)
addPostEmotionsVariables.data.emotion = 'surprised'
await postMutationAction(user, addPostEmotionsMutation, addPostEmotionsVariables)
await expect(
postQueryAction(PostsEmotionsQuery, postsEmotionsQueryVariables),
).resolves.toEqual(expect.objectContaining(expectedResponse))
})
})
describe('authenticated as author', () => {
beforeEach(() => {
user = owner
})
it('adds an emotion to the post', async () => {
const expected = {
data: {
AddPostEmotions: {
from: { id: owner.id },
to: addPostEmotionsVariables.to,
emotion: 'happy',
},
},
}
await expect(
postMutationAction(user, addPostEmotionsMutation, addPostEmotionsVariables),
).resolves.toEqual(expect.objectContaining(expected))
})
})
})
describe('RemovePostEmotions', () => {
let removePostEmotionsVariables, postsEmotionsQueryVariables
const removePostEmotionsMutation = gql`
mutation($to: _PostInput!, $data: _EMOTEDInput!) {
RemovePostEmotions(to: $to, data: $data) {
from {
id
}
to {
id
}
emotion
}
}
`
beforeEach(async () => {
await ownerNode.relateTo(postToEmoteNode, 'emoted', { emotion: 'cry' })
await postMutationAction(user, addPostEmotionsMutation, addPostEmotionsVariables)
postsEmotionsQueryVariables = { id: postToEmote.id }
removePostEmotionsVariables = {
to: { id: postToEmote.id },
data: { emotion: 'cry' },
}
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
user = null
const removePostEmotions = await postMutationAction(
user,
removePostEmotionsMutation,
removePostEmotionsVariables,
)
expect(removePostEmotions.errors[0]).toHaveProperty('message', 'Not Authorised!')
})
})
describe('authenticated', () => {
describe('but not the emoter', () => {
it('returns null if the emotion could not be found', async () => {
user = someUser
const removePostEmotions = await postMutationAction(
user,
removePostEmotionsMutation,
removePostEmotionsVariables,
)
expect(removePostEmotions).toEqual(
expect.objectContaining({ data: { RemovePostEmotions: null } }),
)
})
})
describe('as the emoter', () => {
it('removes an emotion from a post', async () => {
user = owner
const expected = {
data: {
RemovePostEmotions: {
to: { id: postToEmote.id },
from: { id: user.id },
emotion: 'cry',
},
},
}
await expect(
postMutationAction(user, removePostEmotionsMutation, removePostEmotionsVariables),
).resolves.toEqual(expect.objectContaining(expected))
})
it('removes only the requested emotion, not all emotions', async () => {
const expectedEmotions = [{ emotion: 'happy', User: { id: authorParams.id } }]
const expectedResponse = {
data: { Post: [{ emotions: expect.arrayContaining(expectedEmotions) }] },
}
await postMutationAction(user, removePostEmotionsMutation, removePostEmotionsVariables)
await expect(
postQueryAction(PostsEmotionsQuery, postsEmotionsQueryVariables),
).resolves.toEqual(expect.objectContaining(expectedResponse))
})
})
})
})
describe('posts emotions count', () => {
let PostsEmotionsCountByEmotionVariables
let PostsEmotionsByCurrentUserVariables
const PostsEmotionsCountByEmotionQuery = gql`
query($postId: ID!, $data: _EMOTEDInput!) {
PostsEmotionsCountByEmotion(postId: $postId, data: $data)
}
`
const PostsEmotionsByCurrentUserQuery = gql`
query($postId: ID!) {
PostsEmotionsByCurrentUser(postId: $postId)
}
`
beforeEach(async () => {
await ownerNode.relateTo(postToEmoteNode, 'emoted', { emotion: 'cry' })
PostsEmotionsCountByEmotionVariables = {
postId: postToEmote.id,
data: { emotion: 'cry' },
}
PostsEmotionsByCurrentUserVariables = { postId: postToEmote.id }
})
describe('PostsEmotionsCountByEmotion', () => {
it("returns a post's emotions count", async () => {
const expectedResponse = { data: { PostsEmotionsCountByEmotion: 1 } }
await expect(
postQueryAction(PostsEmotionsCountByEmotionQuery, PostsEmotionsCountByEmotionVariables),
).resolves.toEqual(expect.objectContaining(expectedResponse))
})
})
describe('PostsEmotionsCountByEmotion', () => {
it("returns a currentUser's emotions on a post", async () => {
const expectedResponse = { data: { PostsEmotionsByCurrentUser: ['cry'] } }
await expect(
postQueryAction(PostsEmotionsByCurrentUserQuery, PostsEmotionsByCurrentUserVariables),
).resolves.toEqual(expect.objectContaining(expectedResponse))
})
})
})
})

View File

@ -7,4 +7,4 @@ type EMOTED @relation(name: "EMOTED") {
#updatedAt: DateTime
createdAt: String
updatedAt: String
}
}

View File

@ -50,6 +50,8 @@ type Post {
)
emotions: [EMOTED]
emotionsCount: Int!
@cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)")
}
type Mutation {
@ -89,4 +91,11 @@ type Mutation {
language: String
categoryIds: [ID]
): Post
AddPostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED
}
type Query {
PostsEmotionsCountByEmotion(postId: ID!, data: _EMOTEDInput!): Int!
PostsEmotionsByCurrentUser(postId: ID!): [String]
}

View File

@ -128,6 +128,22 @@ export default function Factory(options = {}) {
this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver })
return this
},
async emote({ to, data }) {
const mutation = `
mutation {
AddPostEmotions(
to: { id: "${to}" },
data: { emotion: ${data} }
) {
from { id }
to { id }
emotion
}
}
`
this.lastResponse = await this.graphQLClient.request(mutation)
return this
},
}
result.authenticateAs.bind(result)
result.create.bind(result)

View File

@ -503,6 +503,116 @@ import Factory from './factories'
from: 'p15',
to: 'Demokratie',
}),
f.emote({
from: 'u1',
to: 'p15',
data: 'surprised',
}),
f.emote({
from: 'u2',
to: 'p15',
data: 'surprised',
}),
f.emote({
from: 'u3',
to: 'p15',
data: 'surprised',
}),
f.emote({
from: 'u4',
to: 'p15',
data: 'surprised',
}),
f.emote({
from: 'u5',
to: 'p15',
data: 'surprised',
}),
f.emote({
from: 'u6',
to: 'p15',
data: 'surprised',
}),
f.emote({
from: 'u7',
to: 'p15',
data: 'surprised',
}),
f.emote({
from: 'u2',
to: 'p14',
data: 'cry',
}),
f.emote({
from: 'u3',
to: 'p13',
data: 'angry',
}),
f.emote({
from: 'u4',
to: 'p12',
data: 'funny',
}),
f.emote({
from: 'u5',
to: 'p11',
data: 'surprised',
}),
f.emote({
from: 'u6',
to: 'p10',
data: 'cry',
}),
f.emote({
from: 'u5',
to: 'p9',
data: 'happy',
}),
f.emote({
from: 'u4',
to: 'p8',
data: 'angry',
}),
f.emote({
from: 'u3',
to: 'p7',
data: 'funny',
}),
f.emote({
from: 'u2',
to: 'p6',
data: 'surprised',
}),
f.emote({
from: 'u1',
to: 'p5',
data: 'cry',
}),
f.emote({
from: 'u2',
to: 'p4',
data: 'happy',
}),
f.emote({
from: 'u3',
to: 'p3',
data: 'angry',
}),
f.emote({
from: 'u4',
to: 'p2',
data: 'funny',
}),
f.emote({
from: 'u5',
to: 'p1',
data: 'surprised',
}),
f.emote({
from: 'u6',
to: 'p0',
data: 'cry',
}),
])
await Promise.all([

View File

@ -2,7 +2,6 @@ import express from 'express'
import helmet from 'helmet'
import { ApolloServer } from 'apollo-server-express'
import CONFIG, { requiredConfigs } from './config'
import mocks from './mocks'
import middleware from './middleware'
import { getDriver } from './bootstrap/neo4j'
import decode from './jwt/decode'
@ -34,7 +33,6 @@ const createServer = options => {
schema: middleware(schema),
debug: CONFIG.DEBUG,
tracing: CONFIG.DEBUG,
mocks: CONFIG.MOCKS ? mocks : false,
}
const server = new ApolloServer(Object.assign({}, defaults, options))

View File

@ -1493,14 +1493,14 @@ apollo-cache-control@0.8.1:
apollo-server-env "2.4.1"
graphql-extensions "0.8.1"
apollo-cache-inmemory@~1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.2.tgz#bbf2e4e1eacdf82b2d526f5c2f3b37e5acee3c5e"
integrity sha512-AyCl3PGFv5Qv1w4N9vlg63GBPHXgMCekZy5mhlS042ji0GW84uTySX+r3F61ZX3+KM1vA4m9hQyctrEGiv5XjQ==
apollo-cache-inmemory@~1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.3.tgz#826861d20baca4abc45f7ca7a874105905b8525d"
integrity sha512-S4B/zQNSuYc0M/1Wq8dJDTIO9yRgU0ZwDGnmlqxGGmFombOZb9mLjylewSfQKmjNpciZ7iUIBbJ0mHlPJTzdXg==
dependencies:
apollo-cache "^1.3.2"
apollo-utilities "^1.3.2"
optimism "^0.9.0"
optimism "^0.10.0"
ts-invariant "^0.4.0"
tslib "^1.9.3"
@ -1512,10 +1512,10 @@ apollo-cache@1.3.2, apollo-cache@^1.3.2:
apollo-utilities "^1.3.2"
tslib "^1.9.3"
apollo-client@~2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.3.tgz#9bb2d42fb59f1572e51417f341c5f743798d22db"
integrity sha512-DS8pmF5CGiiJ658dG+mDn8pmCMMQIljKJSTeMNHnFuDLV0uAPZoeaAwVFiAmB408Ujqt92oIZ/8yJJAwSIhd4A==
apollo-client@~2.6.4:
version "2.6.4"
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.4.tgz#872c32927263a0d34655c5ef8a8949fbb20b6140"
integrity sha512-oWOwEOxQ9neHHVZrQhHDbI6bIibp9SHgxaLRVPoGvOFy7OH5XUykZE7hBQAVxq99tQjBzgytaZffQkeWo1B4VQ==
dependencies:
"@types/zen-observable" "^0.8.0"
apollo-cache "1.3.2"
@ -3292,10 +3292,10 @@ eslint-plugin-import@~2.18.2:
read-pkg-up "^2.0.0"
resolve "^1.11.0"
eslint-plugin-jest@~22.15.0:
version "22.15.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.15.0.tgz#fe70bfff7eeb47ca0ab229588a867f82bb8592c5"
integrity sha512-hgnPbSqAIcLLS9ePb12hNHTRkXnkVaCfOwCt2pzQ8KpOKPWGA4HhLMaFN38NBa/0uvLfrZpcIRjT+6tMAfr58Q==
eslint-plugin-jest@~22.15.1:
version "22.15.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.15.1.tgz#54c4a752a44c4bc5a564ecc22b32e1cd16a2961a"
integrity sha512-CWq/RR/3tLaKFB+FZcCJwU9hH5q/bKeO3rFP8G07+q7hcDCFNqpvdphVbEbGE6o6qo1UbciEev4ejUWv7brUhw==
dependencies:
"@typescript-eslint/experimental-utils" "^1.13.0"
@ -6478,10 +6478,10 @@ onetime@^2.0.0:
dependencies:
mimic-fn "^1.0.0"
optimism@^0.9.0:
version "0.9.5"
resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.9.5.tgz#b8b5dc9150e97b79ddbf2d2c6c0e44de4d255527"
integrity sha512-lNvmuBgONAGrUbj/xpH69FjMOz1d0jvMNoOCKyVynUPzq2jgVlGL4jFYJqrUHzUfBv+jAFSCP61x5UkfbduYJA==
optimism@^0.10.0:
version "0.10.2"
resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.10.2.tgz#626b6fd28b0923de98ecb36a3fd2d3d4e5632dd9"
integrity sha512-zPfBIxFFWMmQboM9+Z4MSJqc1PXp82v1PFq/GfQaufI69mHKlup7ykGNnfuGIGssXJQkmhSodQ/k9EWwjd8O8A==
dependencies:
"@wry/context" "^0.4.0"

View File

@ -29,9 +29,9 @@ Feature: Tags and Categories
Scenario: See an overview of tags
When I navigate to the administration dashboard
And I click on the menu item "Tags"
And I click on the menu item "Hashtags"
Then I can see the following table:
| | Name | Users | Posts |
| 1 | Democracy | 3 | 4 |
| 2 | Nature | 2 | 3 |
| 3 | Ecology | 1 | 1 |
| No. | Hashtags | Users | Posts |
| 1 | #Democracy | 3 | 4 |
| 2 | #Nature | 2 | 3 |
| 3 | #Ecology | 1 | 1 |

View File

@ -6,7 +6,6 @@
SMTP_PORT: "25"
GRAPHQL_PORT: "4000"
GRAPHQL_URI: "http://nitro-backend.human-connection:4000"
MOCKS: "false"
NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687"
NEO4J_AUTH: "none"
CLIENT_URI: "https://nitro-staging.human-connection.org"

View File

@ -19,7 +19,6 @@ services:
- GRAPHQL_URI=http://localhost:4000
- CLIENT_URI=http://localhost:3000
- JWT_SECRET=b/&&7b78BF&fv/Vd
- MOCKS=false
- MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ
- PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78
- NEO4J_apoc_import_file_enabled=true

View File

@ -33,7 +33,6 @@ services:
- GRAPHQL_URI=http://localhost:4000
- CLIENT_URI=http://localhost:3000
- JWT_SECRET=b/&&7b78BF&fv/Vd
- MOCKS=false
- MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ
- PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78
neo4j:

View File

@ -23,7 +23,7 @@
"codecov": "^3.5.0",
"cross-env": "^5.2.0",
"cypress": "^3.4.1",
"cypress-cucumber-preprocessor": "^1.13.0",
"cypress-cucumber-preprocessor": "^1.13.1",
"cypress-file-upload": "^3.3.3",
"cypress-plugin-retries": "^1.2.2",
"dotenv": "^8.0.0",
@ -34,4 +34,4 @@
"npm-run-all": "^4.1.5",
"slug": "^1.1.0"
}
}
}

View File

@ -1,4 +1,4 @@
FROM node:12.7-alpine as base
FROM node:12.8-alpine as base
LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
EXPOSE 3000
@ -18,7 +18,7 @@ COPY . .
FROM base as build-and-test
RUN cp .env.template .env
RUN yarn install --ignore-engines --production=false --frozen-lockfile --non-interactive
RUN yarn install --production=false --frozen-lockfile --non-interactive
RUN yarn run build
FROM base as production

View File

@ -0,0 +1,127 @@
import { mount, createLocalVue } from '@vue/test-utils'
import Emotions from './Emotions.vue'
import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex'
import PostMutations from '~/graphql/PostMutations.js'
const localVue = createLocalVue()
localVue.use(Styleguide)
localVue.use(Vuex)
describe('Emotions.vue', () => {
let wrapper
let mocks
let propsData
let getters
let funnyButton
let funnyImage
const funnyImageSrc = '/img/svg/emoji/funny_color.svg'
beforeEach(() => {
mocks = {
$apollo: {
mutate: jest
.fn()
.mockResolvedValueOnce({
data: {
AddPostEmotions: {
to: { id: 'p143' },
data: { emotion: 'happy' },
},
},
})
.mockResolvedValueOnce({
data: {
RemovePostEmotions: {
from: { id: 'u176' },
to: { id: 'p143' },
data: { emotion: 'happy' },
},
},
}),
query: jest.fn().mockResolvedValue({
data: {
PostsEmotionsCountByEmotion: 1,
},
}),
},
$t: jest.fn(),
}
propsData = {
post: { id: 'p143' },
}
getters = {
'auth/user': () => {
return { id: 'u176' }
},
}
})
describe('mount', () => {
const Wrapper = () => {
const store = new Vuex.Store({
getters,
})
return mount(Emotions, { mocks, propsData, store, localVue })
}
beforeEach(() => {
wrapper = Wrapper()
})
it("queries the post's emotions count for each of the 5 emotions", () => {
expect(mocks.$apollo.query).toHaveBeenCalledTimes(5)
})
describe('adding emotions', () => {
let expectedParams
beforeEach(() => {
wrapper.vm.PostsEmotionsCountByEmotion.funny = 0
funnyButton = wrapper.findAll('button').at(0)
funnyButton.trigger('click')
})
it('shows the colored image when the button is active', () => {
funnyImage = wrapper.findAll('img').at(0)
expect(funnyImage.attributes().src).toEqual(funnyImageSrc)
})
it('sends the AddPostEmotionsMutation for an emotion when clicked', () => {
expectedParams = {
mutation: PostMutations().AddPostEmotionsMutation,
variables: { to: { id: 'p143' }, data: { emotion: 'funny' } },
}
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
it('increases the PostsEmotionsCountByEmotion for the emotion clicked', () => {
expect(wrapper.vm.PostsEmotionsCountByEmotion.funny).toEqual(1)
})
it('adds an emotion to selectedEmotions to show the colored image when the button is active', () => {
expect(wrapper.vm.selectedEmotions).toEqual(['funny'])
})
describe('removing emotions', () => {
beforeEach(() => {
funnyButton.trigger('click')
})
it('sends the RemovePostEmotionsMutation when a user clicks on an active emotion', () => {
expectedParams = {
mutation: PostMutations().RemovePostEmotionsMutation,
variables: { to: { id: 'p143' }, data: { emotion: 'funny' } },
}
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
it('decreases the PostsEmotionsCountByEmotion for the emotion clicked', async () => {
expect(wrapper.vm.PostsEmotionsCountByEmotion.funny).toEqual(0)
})
it('removes an emotion from selectedEmotions to show the default image', async () => {
expect(wrapper.vm.selectedEmotions).toEqual([])
})
})
})
})
})

View File

@ -0,0 +1,115 @@
<template>
<ds-flex :gutter="{ lg: 'large' }" class="emotions-flex">
<div v-for="emotion in Object.keys(PostsEmotionsCountByEmotion)" :key="emotion">
<ds-flex-item :width="{ lg: '100%' }">
<hc-emotions-button
@toggleEmotion="toggleEmotion"
:PostsEmotionsCountByEmotion="PostsEmotionsCountByEmotion"
:iconPath="iconPath(emotion)"
:emotion="emotion"
/>
</ds-flex-item>
</div>
</ds-flex>
</template>
<script>
import gql from 'graphql-tag'
import { mapGetters } from 'vuex'
import HcEmotionsButton from '~/components/EmotionsButton/EmotionsButton'
import { PostsEmotionsByCurrentUser } from '~/graphql/PostQuery.js'
import PostMutations from '~/graphql/PostMutations.js'
export default {
components: {
HcEmotionsButton,
},
props: {
post: { type: Object, default: () => {} },
},
data() {
return {
selectedEmotions: [],
PostsEmotionsCountByEmotion: { funny: 0, happy: 0, surprised: 0, cry: 0, angry: 0 },
}
},
computed: {
...mapGetters({
currentUser: 'auth/user',
}),
},
created() {
Object.keys(this.PostsEmotionsCountByEmotion).map(emotion => {
this.emotionsCount(emotion)
})
},
methods: {
iconPath(emotion) {
if (this.isActive(emotion)) {
return `/img/svg/emoji/${emotion}_color.svg`
}
return `/img/svg/emoji/${emotion}.svg`
},
toggleEmotion(emotion) {
this.$apollo
.mutate({
mutation: this.isActive(emotion)
? PostMutations().RemovePostEmotionsMutation
: PostMutations().AddPostEmotionsMutation,
variables: {
to: { id: this.post.id },
data: { emotion },
},
})
.then(() => {
this.isActive(emotion)
? this.PostsEmotionsCountByEmotion[emotion]--
: this.PostsEmotionsCountByEmotion[emotion]++
const index = this.selectedEmotions.indexOf(emotion)
if (index > -1) {
this.selectedEmotions.splice(index, 1)
} else {
this.selectedEmotions.push(emotion)
}
})
},
isActive(emotion) {
const index = this.selectedEmotions.indexOf(emotion)
if (index > -1) {
return true
}
return false
},
emotionsCount(emotion) {
this.$apollo
.query({
query: gql`
query($postId: ID!, $data: _EMOTEDInput!) {
PostsEmotionsCountByEmotion(postId: $postId, data: $data)
}
`,
variables: { postId: this.post.id, data: { emotion } },
fetchPolicy: 'no-cache',
})
.then(({ data: { PostsEmotionsCountByEmotion } }) => {
this.PostsEmotionsCountByEmotion[emotion] = PostsEmotionsCountByEmotion
})
},
},
apollo: {
PostsEmotionsByCurrentUser: {
query() {
return PostsEmotionsByCurrentUser()
},
variables() {
return {
postId: this.post.id,
}
},
result({ data: { PostsEmotionsByCurrentUser } }) {
this.selectedEmotions = PostsEmotionsByCurrentUser
},
},
},
}
</script>

View File

@ -0,0 +1,50 @@
<template>
<div>
<ds-button size="large" ghost @click="toggleEmotion(emotion)" class="emotions-buttons">
<img :src="iconPath" width="40" />
</ds-button>
<ds-space margin-bottom="xx-small" />
<div class="emotions-mobile-space">
<p class="emotions-label">{{ $t(`contribution.emotions-label.${emotion}`) }}</p>
<p style="display: inline" :key="PostsEmotionsCountByEmotion[emotion]">
{{ PostsEmotionsCountByEmotion[emotion] }}x
</p>
{{ $t('contribution.emotions-label.emoted') }}
</div>
</div>
</template>
<script>
export default {
props: {
iconPath: { type: String, default: null },
PostsEmotionsCountByEmotion: { type: Object, default: () => {} },
emotion: { type: String, default: null },
},
methods: {
toggleEmotion(emotion) {
this.$emit('toggleEmotion', emotion)
},
},
}
</script>
<style lang="scss">
.emotions-flex {
justify-content: space-evenly;
text-align: center;
}
.emotions-label {
font-size: $font-size-small;
}
.emotions-buttons {
&:hover {
background-color: $background-color-base;
}
}
@media only screen and (max-width: 960px) {
.emotions-mobile-space {
margin-bottom: 32px;
}
}
</style>

View File

@ -13,57 +13,39 @@ describe('FilterMenu.vue', () => {
let mocks
let propsData
const createWrapper = mountMethod => {
return mountMethod(FilterMenu, {
propsData,
mocks,
localVue,
})
}
beforeEach(() => {
mocks = { $t: () => {} }
propsData = {}
})
describe('given a user', () => {
beforeEach(() => {
propsData = {
user: {
id: '4711',
},
hashtag: null,
}
})
describe('mount', () => {
const Wrapper = () => {
return mount(FilterMenu, { mocks, localVue, propsData })
}
beforeEach(() => {
wrapper = createWrapper(mount)
wrapper = Wrapper()
})
it('renders a card', () => {
it('does not render a card if there are no hashtags', () => {
expect(wrapper.is('.ds-card')).toBe(true)
})
describe('click "filter-by-followed-authors-only" button', () => {
it('emits filterBubble object', () => {
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
expect(wrapper.emitted('changeFilterBubble')).toBeTruthy()
})
it('renders a card if there are hashtags', () => {
propsData.hashtag = 'Frieden'
wrapper = Wrapper()
expect(wrapper.is('.ds-card')).toBe(true)
})
it('toggles filterBubble.author property', () => {
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
expect(wrapper.emitted('changeFilterBubble')[0]).toEqual([
{ author: { followedBy_some: { id: '4711' } } },
])
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
expect(wrapper.emitted('changeFilterBubble')[1]).toEqual([{}])
})
it('makes button primary', () => {
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
expect(
wrapper.find({ name: 'filter-by-followed-authors-only' }).classes('ds-button-primary'),
).toBe(true)
describe('click "clear-search-button" button', () => {
it('emits clearSearch', () => {
wrapper.find({ name: 'clear-search-button' }).trigger('click')
expect(wrapper.emitted().clearSearch).toHaveLength(1)
})
})
})

View File

@ -1,26 +1,6 @@
<template>
<ds-card class="filter-menu-card">
<ds-flex>
<ds-flex-item class="filter-menu-title">
<ds-heading size="h3">{{ $t('filter-menu.title') }}</ds-heading>
</ds-flex-item>
<ds-flex-item>
<div class="filter-menu-buttons">
<ds-button
v-tooltip="{
content: this.$t('contribution.filterFollow'),
placement: 'left',
delay: { show: 500 },
}"
name="filter-by-followed-authors-only"
icon="user-plus"
:primary="!!filterAuthorIsFollowedById"
@click="toggleOnlyFollowed"
/>
</div>
</ds-flex-item>
</ds-flex>
<div v-if="hashtag">
<ds-card v-show="hashtag" class="filter-menu-card">
<div>
<ds-space margin-bottom="x-small" />
<ds-flex>
<ds-flex-item>
@ -34,7 +14,7 @@
placement: 'left',
delay: { show: 500 },
}"
name="filter-by-followed-authors-only"
name="clear-search-button"
icon="close"
@click="clearSearch"
/>
@ -48,31 +28,9 @@
<script>
export default {
props: {
user: { type: Object, required: true },
hashtag: { type: Object, default: null },
},
data() {
return {
filter: {},
}
},
computed: {
filterAuthorIsFollowedById() {
const { author = {} } = this.filter
/* eslint-disable camelcase */
const { followedBy_some = {} } = author
const { id } = followedBy_some
/* eslint-enable */
return id
},
hashtag: { type: String, default: null },
},
methods: {
toggleOnlyFollowed() {
this.filter = this.filterAuthorIsFollowedById
? {}
: { author: { followedBy_some: { id: this.user.id } } }
this.$emit('changeFilterBubble', this.filter)
},
clearSearch() {
this.$emit('clearSearch')
},

View File

@ -3,23 +3,21 @@ import VTooltip from 'v-tooltip'
import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex'
import FilterPosts from './FilterPosts.vue'
import FilterPostsMenuItem from './FilterPostsMenuItems.vue'
import { mutations } from '~/store/posts'
const localVue = createLocalVue()
localVue.use(Styleguide)
localVue.use(VTooltip)
localVue.use(Vuex)
let mutations
let getters
describe('FilterPosts.vue', () => {
let wrapper
let mocks
let propsData
let menuToggle
let allCategoriesButton
let environmentAndNatureButton
let consumptionAndSustainabiltyButton
let democracyAndPoliticsButton
beforeEach(() => {
@ -50,22 +48,28 @@ describe('FilterPosts.vue', () => {
})
describe('mount', () => {
const store = new Vuex.Store({
mutations: {
'posts/SET_POSTS': mutations.SET_POSTS,
},
})
const Wrapper = () => {
return mount(FilterPosts, { mocks, localVue, propsData, store })
mutations = {
'postsFilter/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(),
'postsFilter/RESET_CATEGORIES': jest.fn(),
'postsFilter/TOGGLE_CATEGORY': jest.fn(),
}
beforeEach(() => {
wrapper = Wrapper()
getters = {
'auth/user': () => {
return { id: 'u34' }
},
'postsFilter/filteredCategoryIds': jest.fn(() => []),
'postsFilter/filteredByUsersFollowed': jest.fn(),
}
const openFilterPosts = () => {
const store = new Vuex.Store({ mutations, getters })
const wrapper = mount(FilterPosts, { mocks, localVue, propsData, store })
menuToggle = wrapper.findAll('a').at(0)
menuToggle.trigger('click')
})
return wrapper
}
it('groups the categories by pair', () => {
const wrapper = openFilterPosts()
expect(wrapper.vm.chunk).toEqual([
[
{ id: 'cat4', name: 'Environment & Nature', icon: 'tree' },
@ -76,59 +80,43 @@ describe('FilterPosts.vue', () => {
})
it('starts with all categories button active', () => {
const wrapper = openFilterPosts()
allCategoriesButton = wrapper.findAll('button').at(0)
expect(allCategoriesButton.attributes().class).toContain('ds-button-primary')
})
it('adds a categories id to selectedCategoryIds when clicked', () => {
it('calls TOGGLE_CATEGORY when clicked', () => {
const wrapper = openFilterPosts()
environmentAndNatureButton = wrapper.findAll('button').at(1)
environmentAndNatureButton.trigger('click')
const filterPostsMenuItem = wrapper.find(FilterPostsMenuItem)
expect(filterPostsMenuItem.vm.selectedCategoryIds).toEqual(['cat4'])
expect(mutations['postsFilter/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, 'cat4')
})
it('sets primary to true when the button is clicked', () => {
it('sets category button attribute `primary` when corresponding category is filtered', () => {
getters['postsFilter/filteredCategoryIds'] = jest.fn(() => ['cat9'])
const wrapper = openFilterPosts()
democracyAndPoliticsButton = wrapper.findAll('button').at(3)
democracyAndPoliticsButton.trigger('click')
expect(democracyAndPoliticsButton.attributes().class).toContain('ds-button-primary')
})
it('queries a post by its categories', () => {
consumptionAndSustainabiltyButton = wrapper.findAll('button').at(2)
consumptionAndSustainabiltyButton.trigger('click')
expect(mocks.$apollo.query).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
filter: { categories_some: { id_in: ['cat15'] } },
first: expect.any(Number),
offset: expect.any(Number),
},
}),
)
it('sets "filter-by-followed-authors-only" button attribute `primary`', () => {
getters['postsFilter/filteredByUsersFollowed'] = jest.fn(() => true)
const wrapper = openFilterPosts()
expect(
wrapper.find({ name: 'filter-by-followed-authors-only' }).classes('ds-button-primary'),
).toBe(true)
})
it('supports a query of multiple categories', () => {
environmentAndNatureButton = wrapper.findAll('button').at(1)
environmentAndNatureButton.trigger('click')
consumptionAndSustainabiltyButton = wrapper.findAll('button').at(2)
consumptionAndSustainabiltyButton.trigger('click')
expect(mocks.$apollo.query).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
filter: { categories_some: { id_in: ['cat4', 'cat15'] } },
first: expect.any(Number),
offset: expect.any(Number),
},
}),
)
})
describe('click "filter-by-followed-authors-only" button', () => {
let wrapper
beforeEach(() => {
wrapper = openFilterPosts()
wrapper.find({ name: 'filter-by-followed-authors-only' }).trigger('click')
})
it('toggles the categoryIds when clicked more than once', () => {
environmentAndNatureButton = wrapper.findAll('button').at(1)
environmentAndNatureButton.trigger('click')
environmentAndNatureButton.trigger('click')
const filterPostsMenuItem = wrapper.find(FilterPostsMenuItem)
expect(filterPostsMenuItem.vm.selectedCategoryIds).toEqual([])
it('calls TOGGLE_FILTER_BY_FOLLOWED', () => {
expect(mutations['postsFilter/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34')
})
})
})
})

View File

@ -5,15 +5,14 @@
<ds-icon style="margin: 7px 0px 0px 2px" size="xx-small" name="angle-down" />
</a>
<template slot="popover">
<filter-posts-menu-items :chunk="chunk" @filterPosts="filterPosts" />
<filter-posts-menu-items :chunk="chunk" :user="currentUser" />
</template>
</dropdown>
</template>
<script>
import _ from 'lodash'
import Dropdown from '~/components/Dropdown'
import { filterPosts } from '~/graphql/PostQuery.js'
import { mapMutations } from 'vuex'
import { mapGetters } from 'vuex'
import FilterPostsMenuItems from '~/components/FilterPosts/FilterPostsMenuItems'
export default {
@ -26,36 +25,13 @@ export default {
offset: { type: [String, Number] },
categories: { type: Array, default: () => [] },
},
data() {
return {
pageSize: 12,
}
},
computed: {
...mapGetters({
currentUser: 'auth/user',
}),
chunk() {
return _.chunk(this.categories, 2)
},
},
methods: {
...mapMutations({
setPosts: 'posts/SET_POSTS',
}),
filterPosts(categoryIds) {
const filter = categoryIds.length ? { categories_some: { id_in: categoryIds } } : {}
this.$apollo
.query({
query: filterPosts(this.$i18n),
variables: {
filter: filter,
first: this.pageSize,
offset: 0,
},
})
.then(({ data: { Post } }) => {
this.setPosts(Post)
})
.catch(error => this.$toast.error(error.message))
},
},
}
</script>

View File

@ -2,7 +2,7 @@
<ds-container>
<ds-space />
<ds-flex id="filter-posts-header">
<ds-heading tag="h4">{{ $t('filter-posts.header') }}</ds-heading>
<ds-heading tag="h4">{{ $t('filter-posts.categories.header') }}</ds-heading>
<ds-space margin-bottom="large" />
</ds-flex>
<ds-flex>
@ -15,11 +15,11 @@
<ds-flex-item width="100%">
<ds-button
icon="check"
@click.stop.prevent="toggleCategory()"
:primary="allCategories"
@click.stop.prevent="resetCategories"
:primary="!filteredCategoryIds.length"
/>
<ds-flex-item>
<label class="category-labels">{{ $t('filter-posts.all') }}</label>
<label class="category-labels">{{ $t('filter-posts.categories.all') }}</label>
</ds-flex-item>
<ds-space />
</ds-flex-item>
@ -40,7 +40,7 @@
<ds-flex-item width="100%" class="categories-menu-item">
<ds-button
:icon="category.icon"
:primary="isActive(category.id)"
:primary="filteredCategoryIds.includes(category.id)"
@click.stop.prevent="toggleCategory(category.id)"
/>
<ds-space margin-bottom="small" />
@ -55,42 +55,68 @@
</ds-flex>
</ds-flex-item>
</ds-flex>
<ds-space />
<ds-flex id="filter-posts-by-followers-header">
<ds-heading tag="h4">{{ $t('filter-posts.general.header') }}</ds-heading>
<ds-space margin-bottom="large" />
</ds-flex>
<ds-flex>
<ds-flex-item
:width="{ base: '100%', sm: '100%', md: '100%', lg: '10%' }"
class="categories-menu-item"
>
<ds-flex>
<ds-flex-item width="10%" />
<ds-flex-item width="100%">
<div class="follow-button">
<ds-button
v-tooltip="{
content: this.$t('contribution.filterFollow'),
placement: 'left',
delay: { show: 500 },
}"
name="filter-by-followed-authors-only"
icon="user-plus"
:primary="filteredByUsersFollowed"
@click="toggleFilteredByFollowed(user.id)"
/>
<ds-flex-item>
<label class="follow-label">{{ $t('filter-posts.followers.label') }}</label>
</ds-flex-item>
<ds-space />
</div>
</ds-flex-item>
</ds-flex>
</ds-flex-item>
<ds-space margin-bottom="large" />
</ds-flex>
</ds-container>
</template>
<script>
import { mapGetters, mapMutations } from 'vuex'
export default {
props: {
user: { type: Object, required: true },
chunk: { type: Array, default: () => [] },
},
data() {
return {
selectedCategoryIds: [],
allCategories: true,
filter: {},
}
},
computed: {
...mapGetters({
filteredCategoryIds: 'postsFilter/filteredCategoryIds',
filteredByUsersFollowed: 'postsFilter/filteredByUsersFollowed',
}),
},
methods: {
isActive(id) {
const index = this.selectedCategoryIds.indexOf(id)
if (index > -1) {
return true
}
return false
},
toggleCategory(id) {
if (!id) {
this.selectedCategoryIds = []
this.allCategories = true
} else {
const index = this.selectedCategoryIds.indexOf(id)
if (index > -1) {
this.selectedCategoryIds.splice(index, 1)
} else {
this.selectedCategoryIds.push(id)
}
this.allCategories = false
}
this.$emit('filterPosts', this.selectedCategoryIds)
},
...mapMutations({
toggleFilteredByFollowed: 'postsFilter/TOGGLE_FILTER_BY_FOLLOWED',
resetCategories: 'postsFilter/RESET_CATEGORIES',
toggleCategory: 'postsFilter/TOGGLE_CATEGORY',
}),
},
}
</script>
@ -99,6 +125,10 @@ export default {
display: block;
}
#filter-posts-by-followers-header {
display: block;
}
.categories-menu-item {
text-align: center;
}
@ -107,7 +137,8 @@ export default {
justify-content: center;
}
.category-labels {
.category-labels,
.follow-label {
font-size: $font-size-small;
}
@ -122,5 +153,8 @@ export default {
#filter-posts-header {
text-align: center;
}
.follow-button {
float: left;
}
}
</style>

View File

@ -28,8 +28,13 @@
</template>
<script>
import { SweetalertIcon } from 'vue-sweetalert-icons'
export default {
name: 'ConfirmModal',
components: {
SweetalertIcon,
},
props: {
name: { type: String, default: '' },
type: { type: String, required: true },

View File

@ -27,9 +27,13 @@
<script>
import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export default {
name: 'ReportModal',
components: {
SweetalertIcon,
},
props: {
name: { type: String, default: '' },
type: { type: String, required: true },

View File

@ -54,10 +54,12 @@
<script>
import PasswordStrength from '../Password/Strength'
import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import PasswordForm from '~/components/utils/PasswordFormHelper'
export default {
components: {
SweetalertIcon,
PasswordStrength,
},
props: {

View File

@ -48,8 +48,12 @@
<script>
import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export default {
components: {
SweetalertIcon,
},
data() {
return {
formData: {

View File

@ -71,6 +71,7 @@
<script>
import gql from 'graphql-tag'
import PasswordStrength from '../Password/Strength'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import PasswordForm from '~/components/utils/PasswordFormHelper'
export const SignupVerificationMutation = gql`
@ -85,6 +86,7 @@ export const SignupVerificationMutation = gql`
export default {
components: {
PasswordStrength,
SweetalertIcon,
},
data() {
const passwordForm = PasswordForm({ translate: this.$t })

View File

@ -56,6 +56,7 @@
<script>
import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export const SignupMutation = gql`
mutation($email: String!) {
@ -72,6 +73,9 @@ export const SignupByInvitationMutation = gql`
}
`
export default {
components: {
SweetalertIcon,
},
props: {
token: { type: String, default: null },
},

View File

@ -1,5 +1,5 @@
<template>
<ds-space margin="large" style="text-align: center">
<ds-space margin="xx-small" class="text-align-center">
<ds-button
:loading="loading"
:disabled="disabled"
@ -88,4 +88,7 @@ export default {
.shout-button-text {
user-select: none;
}
.text-align-center {
text-align: center;
}
</style>

View File

@ -60,5 +60,31 @@ export default () => {
}
}
`,
AddPostEmotionsMutation: gql`
mutation($to: _PostInput!, $data: _EMOTEDInput!) {
AddPostEmotions(to: $to, data: $data) {
emotion
from {
id
}
to {
id
}
}
}
`,
RemovePostEmotionsMutation: gql`
mutation($to: _PostInput!, $data: _EMOTEDInput!) {
RemovePostEmotions(to: $to, data: $data) {
emotion
from {
id
}
to {
id
}
}
}
`,
}
}

View File

@ -71,6 +71,7 @@ export default i18n => {
}
shoutedCount
shoutedByCurrentUser
emotionsCount
}
}
`
@ -120,3 +121,11 @@ export const filterPosts = i18n => {
}
`
}
export const PostsEmotionsByCurrentUser = () => {
return gql`
query PostsEmotionsByCurrentUser($postId: ID!) {
PostsEmotionsByCurrentUser(postId: $postId)
}
`
}

View File

@ -38,7 +38,12 @@
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
>
<no-ssr>
<filter-posts placement="top-start" offset="8" :categories="categories" />
<filter-posts
v-show="showFilterPostsDropdown"
placement="top-start"
offset="8"
:categories="categories"
/>
</no-ssr>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '10%', lg: '2%' }" />
@ -142,7 +147,7 @@
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import { mapGetters, mapActions, mapMutations } from 'vuex'
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import SearchInput from '~/components/SearchInput.vue'
import Modal from '~/components/Modal'
@ -178,6 +183,8 @@ export default {
isAdmin: 'auth/isAdmin',
quickSearchResults: 'search/quickResults',
quickSearchPending: 'search/quickPending',
usersFollowedFilter: 'posts/usersFollowedFilter',
categoriesFilter: 'posts/categoriesFilter',
}),
userName() {
const { name } = this.user || {}
@ -215,6 +222,10 @@ export default {
}
return routes
},
showFilterPostsDropdown() {
const [firstRoute] = this.$route.matched
return firstRoute.name === 'index'
},
},
watch: {
Category(category) {
@ -227,6 +238,9 @@ export default {
quickSearch: 'search/quickSearch',
fetchPosts: 'posts/fetchPosts',
}),
...mapMutations({
setFilteredByFollowers: 'posts/SET_FILTERED_BY_FOLLOWERS',
}),
goToPost(item) {
this.$nextTick(() => {
this.$router.push({
@ -247,7 +261,14 @@ export default {
},
redirectToRoot() {
this.$router.replace('/')
this.fetchPosts({ i18n: this.$i18n, filter: {} })
this.fetchPosts({
i18n: this.$i18n,
filter: {
...this.usersFollowedFilter,
...this.categoriesFilter,
...this.filter,
},
})
},
},
apollo: {

View File

@ -5,8 +5,16 @@
"clearSearch": "Suche löschen"
},
"filter-posts": {
"header": "Themenkategorien",
"all": "Alle"
"categories": {
"header": "Themenkategorien",
"all": "Alle"
},
"general": {
"header": "Filtern nach..."
},
"followers": {
"label": "Benutzern, denen ich folge"
}
},
"site": {
"made": "Mit &#10084; gemacht",
@ -165,7 +173,7 @@
"commentsCount": "Meine {count} Kommentare löschen",
"accountDescription": "Sei dir bewusst, dass deine Beiträge und Kommentare für unsere Community wichtig sind. Wenn du sie trotzdem löschen möchtest, musst du sie unten markieren.",
"accountWarning": "Dein Konto, deine Beiträge oder Kommentare kannst du nach dem Löschen <b>WEDER VERWALTEN NOCH WIEDERHERSTELLEN!</b>",
"success": "Konto erfolgreich gelöscht",
"success": "Konto erfolgreich gelöscht!",
"pleaseConfirm": "<b class='is-danger'>Zerstörerische Aktion!</b> Gib <b>{confirm}</b> ein, um zu bestätigen."
},
"organizations": {
@ -207,6 +215,7 @@
},
"table": {
"columns": {
"number": "Nr.",
"name": "Name",
"slug": "Slug",
"role": "Rolle",
@ -227,7 +236,9 @@
"postCount": "Beiträge"
},
"tags": {
"name": "Schlagworte",
"name": "Hashtags",
"number": "Nr.",
"nameOfHashtag": "Name",
"tagCountUnique": "Benutzer",
"tagCount": "Beiträge"
},
@ -243,7 +254,12 @@
"post": {
"name": "Beitrag",
"moreInfo": {
"name": "Mehr Info"
"name": "Mehr Info",
"title": "Mehr Informationen",
"description": "Hier findest du weitere Infos zum Thema.",
"titleOfCategoriesSection": "Kategorien",
"titleOfHashtagsSection": "Hashtags",
"titleOfRelatedContributionsSection": "Verwandte Beiträge"
},
"takeAction": {
"name": "Aktiv werden"
@ -321,7 +337,7 @@
"disable": {
"submit": "Deaktivieren",
"cancel": "Abbrechen",
"success": "Erfolgreich deaktiviert",
"success": "Erfolgreich deaktiviert!",
"user": {
"title": "Nutzer sperren",
"type": "Nutzer",
@ -420,6 +436,14 @@
"languageSelectLabel": "Sprache",
"categories": {
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
},
"emotions-label": {
"funny": "Lustig",
"happy": "Glücklich",
"surprised": "Erstaunt",
"cry": "Zum Weinen",
"angry": "Verärgert",
"emoted": "angegeben"
}
},
"changelog": {

View File

@ -5,8 +5,16 @@
"clearSearch": "Clear search"
},
"filter-posts": {
"header": "Categories of Content",
"all": "All"
"categories": {
"header": "Categories of Content",
"all": "All"
},
"general": {
"header": "Filter by..."
},
"followers": {
"label": "Users I follow"
}
},
"site": {
"made": "Made with &#10084;",
@ -28,7 +36,7 @@
"newest": "Newest",
"oldest": "Oldest",
"popular": "Popular",
"commented": "most Commented"
"commented": "Most commented"
},
"login": {
"copy": "If you already have a human-connection account, login here.",
@ -99,7 +107,7 @@
"follow": "Follow",
"followers": "Followers",
"following": "Following",
"shouted": "Recommended",
"shouted": "Shouted",
"commented": "Commented",
"userAnonym": "Anonymous",
"socialMedia": "Where else can I find",
@ -160,12 +168,12 @@
"name": "Download Data"
},
"deleteUserAccount": {
"name": "Delete Data",
"name": "Delete data",
"contributionsCount": "Delete my {count} posts",
"commentsCount": "Delete my {count} comments",
"accountDescription": "Be aware that your Post and Comments are important to our community. If you still choose to delete them, you have to mark them below.",
"accountWarning": "You <b>CAN'T MANAGE</b> and <b>CAN'T RECOVER</b> your Account, Posts, or Comments after deleting your account!",
"success": "Account successfully deleted",
"success": "Account successfully deleted!",
"pleaseConfirm": "<b class='is-danger'>Destructive action!</b> Type <b>{confirm}</b> to confirm"
},
"organizations": {
@ -195,7 +203,7 @@
"projects": "Projects",
"invites": "Invites",
"follows": "Follows",
"shouts": "Recommended"
"shouts": "Shouts"
},
"organizations": {
"name": "Organizations"
@ -207,6 +215,7 @@
},
"table": {
"columns": {
"number": "No.",
"name": "Name",
"slug": "Slug",
"role": "Role",
@ -227,7 +236,9 @@
"postCount": "Posts"
},
"tags": {
"name": "Tags",
"name": "Hashtags",
"number": "No.",
"nameOfHashtag": "Name",
"tagCountUnique": "Users",
"tagCount": "Posts"
},
@ -243,7 +254,12 @@
"post": {
"name": "Post",
"moreInfo": {
"name": "More info"
"name": "More info",
"title": "More information",
"description": "Here you can find more information about this topic.",
"titleOfCategoriesSection": "Categories",
"titleOfHashtagsSection": "Hashtags",
"titleOfRelatedContributionsSection": "Related posts"
},
"takeAction": {
"name": "Take action"
@ -321,7 +337,7 @@
"disable": {
"submit": "Disable",
"cancel": "Cancel",
"success": "Disabled successfully",
"success": "Disabled successfully!",
"user": {
"title": "Disable User",
"type": "User",
@ -409,7 +425,7 @@
},
"user": {
"avatar": {
"submitted": "Upload successful"
"submitted": "Upload successful!"
}
},
"contribution": {
@ -420,6 +436,14 @@
"languageSelectLabel": "Language",
"categories": {
"infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected"
},
"emotions-label": {
"funny": "Funny",
"happy": "Happy",
"surprised": "Surprised",
"cry": "Cry",
"angry": "Angry",
"emoted": "emoted"
}
},
"changelog": {

View File

@ -115,7 +115,6 @@ module.exports = {
{ src: '~/plugins/v-tooltip.js', ssr: false },
{ src: '~/plugins/izi-toast.js', ssr: false },
{ src: '~/plugins/vue-filters.js' },
{ src: '~/plugins/vue-sweetalert-icons.js' },
],
router: {

View File

@ -56,8 +56,8 @@
"@nuxtjs/dotenv": "~1.4.0",
"@nuxtjs/style-resources": "~1.0.0",
"accounting": "~0.4.1",
"apollo-cache-inmemory": "~1.6.2",
"apollo-client": "~2.6.3",
"apollo-cache-inmemory": "~1.6.3",
"apollo-client": "~2.6.4",
"cookie-universal-nuxt": "~2.0.17",
"cross-env": "~5.2.0",
"date-fns": "2.0.0-beta.4",
@ -76,18 +76,18 @@
"tiptap-extensions": "~1.26.1",
"v-tooltip": "~2.0.2",
"vue-count-to": "~1.0.13",
"vue-izitoast": "1.1.2",
"vue-izitoast": "roschaefer/vue-izitoast#patch-1",
"vuex-i18n": "~1.13.1",
"vue-sweetalert-icons": "~4.0.0",
"vue-sweetalert-icons": "~4.2.0",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/core": "~7.5.5",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "~7.5.5",
"@storybook/addon-a11y": "^5.1.9",
"@storybook/addon-actions": "^5.1.9",
"@storybook/vue": "~5.1.9",
"@storybook/addon-a11y": "^5.1.11",
"@storybook/addon-actions": "^5.1.11",
"@storybook/vue": "~5.1.11",
"@vue/cli-shared-utils": "~3.10.0",
"@vue/eslint-config-prettier": "~5.0.0",
"@vue/server-test-utils": "~1.0.0-beta.29",
@ -97,14 +97,14 @@
"babel-jest": "~24.8.0",
"babel-loader": "~8.0.6",
"babel-preset-vue": "~2.0.2",
"css-loader": "~2.1.1",
"core-js": "~2.6.9",
"css-loader": "~2.1.1",
"eslint": "~5.16.0",
"eslint-config-prettier": "~6.0.0",
"eslint-config-standard": "~12.0.0",
"eslint-loader": "~2.2.1",
"eslint-plugin-import": "~2.18.2",
"eslint-plugin-jest": "~22.14.1",
"eslint-plugin-jest": "~22.15.1",
"eslint-plugin-node": "~9.1.0",
"eslint-plugin-prettier": "~3.1.0",
"eslint-plugin-promise": "~4.2.1",
@ -117,7 +117,7 @@
"node-sass": "~4.12.0",
"nodemon": "~1.19.1",
"prettier": "~1.18.2",
"sass-loader": "~7.1.0",
"sass-loader": "~7.2.0",
"style-loader": "~0.23.1",
"style-resources-loader": "~1.2.1",
"tippy.js": "^4.3.5",

View File

@ -31,7 +31,7 @@ export default {
},
apollo: {
Category: {
query: gql(`
query: gql`
query {
Category(orderBy: postCount_desc) {
id
@ -41,7 +41,7 @@ export default {
postCount
}
}
`),
`,
},
},
}

View File

@ -133,7 +133,7 @@ export default {
},
apollo: {
statistics: {
query: gql(`
query: gql`
query {
statistics {
countUsers
@ -147,7 +147,7 @@ export default {
countShouts
}
}
`),
`,
},
},
}

View File

@ -2,7 +2,12 @@
<ds-card :header="$t('admin.tags.name')">
<ds-table :data="Tag" :fields="fields" condensed>
<template slot="id" slot-scope="scope">
{{ scope.index + 1 }}
{{ scope.index + 1 }}.
</template>
<template slot="name" slot-scope="scope">
<nuxt-link :to="{ path: '/', query: { hashtag: scope.row.id } }">
<b>#{{ scope.row.name | truncate(20) }}</b>
</nuxt-link>
</template>
</ds-table>
</ds-card>
@ -20,8 +25,8 @@ export default {
computed: {
fields() {
return {
id: '#',
name: 'Name',
id: this.$t('admin.tags.number'),
name: this.$t('admin.tags.name'),
taggedCountUnique: {
label: this.$t('admin.tags.tagCountUnique'),
align: 'right',
@ -35,7 +40,7 @@ export default {
},
apollo: {
Tag: {
query: gql(`
query: gql`
query {
Tag(first: 20, orderBy: taggedCountUnique_desc) {
id
@ -44,7 +49,7 @@ export default {
taggedCountUnique
}
}
`),
`,
},
},
}

View File

@ -21,7 +21,7 @@
<ds-card v-if="User && User.length">
<ds-table :data="User" :fields="fields" condensed>
<template slot="index" slot-scope="scope">
{{ scope.row.index }}.
{{ scope.row.index + 1 }}.
</template>
<template slot="name" slot-scope="scope">
<nuxt-link
@ -57,9 +57,7 @@
</ds-flex>
</ds-card>
<ds-card v-else>
<ds-placeholder>
{{ $t('admin.users.empty') }}
</ds-placeholder>
<ds-placeholder>{{ $t('admin.users.empty') }}</ds-placeholder>
</ds-card>
</div>
</template>
@ -92,7 +90,7 @@ export default {
},
fields() {
return {
index: '#',
index: this.$t('admin.users.table.columns.number'),
name: this.$t('admin.users.table.columns.name'),
slug: this.$t('admin.users.table.columns.slug'),
createdAt: this.$t('admin.users.table.columns.createdAt'),
@ -118,20 +116,26 @@ export default {
apollo: {
User: {
query() {
return gql(`
query($filter: _UserFilter, $first: Int, $offset: Int, $email: String) {
User(email: $email, filter: $filter, first: $first, offset: $offset, orderBy: createdAt_desc) {
id
name
slug
role
createdAt
contributionsCount
commentedCount
shoutedCount
return gql`
query($filter: _UserFilter, $first: Int, $offset: Int, $email: String) {
User(
email: $email
filter: $filter
first: $first
offset: $offset
orderBy: createdAt_desc
) {
id
name
slug
role
createdAt
contributionsCount
commentedCount
shoutedCount
}
}
}
`)
`
},
variables() {
const { offset, first, email, filter } = this

View File

@ -26,17 +26,7 @@ describe('PostIndex', () => {
beforeEach(() => {
store = new Vuex.Store({
getters: {
'posts/posts': () => {
return [
{
id: 'p23',
name: 'It is a post',
author: {
id: 'u1',
},
},
]
},
'postsFilter/postsFilter': () => ({}),
'auth/user': () => {
return { id: 'u23' }
},
@ -95,22 +85,11 @@ describe('PostIndex', () => {
wrapper = Wrapper()
})
it('refetches Posts when changeFilterBubble is emitted', () => {
wrapper.find(FilterMenu).vm.$emit('changeFilterBubble')
expect(mocks.$apollo.queries.Post.refetch).toHaveBeenCalledTimes(1)
})
it('clears the search when the filter menu emits clearSearch', () => {
wrapper.find(FilterMenu).vm.$emit('clearSearch')
expect(wrapper.vm.hashtag).toBeNull()
})
it('calls the changeFilterBubble if there are hasthags in the route query', () => {
mocks.$route.query.hashtag = { id: 'hashtag' }
wrapper = Wrapper()
expect(mocks.$apollo.queries.Post.refetch).toHaveBeenCalledTimes(1)
})
describe('mount', () => {
beforeEach(() => {
wrapper = mount(PostIndex, {
@ -128,12 +107,9 @@ describe('PostIndex', () => {
expect(wrapper.vm.sorting).toEqual('createdAt_desc')
})
it('loads more posts when a user clicks on the load more button', () => {
wrapper
.findAll('button')
.at(2)
.trigger('click')
expect(mocks.$apollo.queries.Post.fetchMore).toHaveBeenCalledTimes(1)
it('updates offset when a user clicks on the load more button', () => {
wrapper.find('.load-more button').trigger('click')
expect(wrapper.vm.offset).toEqual(12)
})
})
})

View File

@ -2,12 +2,7 @@
<div>
<ds-flex :width="{ base: '100%' }" gutter="base">
<ds-flex-item>
<filter-menu
:user="currentUser"
@changeFilterBubble="changeFilterBubble"
:hashtag="hashtag"
@clearSearch="clearSearch"
/>
<filter-menu :hashtag="hashtag" @clearSearch="clearSearch" />
</ds-flex-item>
<ds-flex-item>
<div class="sorting-dropdown">
@ -21,7 +16,7 @@
</div>
</ds-flex-item>
<hc-post-card
v-for="(post, index) in posts"
v-for="post in posts"
:key="post.id"
:post="post"
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
@ -38,7 +33,7 @@
primary
/>
</no-ssr>
<hc-load-more v-if="true" :loading="$apollo.loading" @click="showMoreContributions" />
<hc-load-more v-if="hasMore" :loading="$apollo.loading" @click="showMoreContributions" />
</div>
</template>
@ -47,7 +42,7 @@ import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
import uniqBy from 'lodash/uniqBy'
import HcPostCard from '~/components/PostCard'
import HcLoadMore from '~/components/LoadMore.vue'
import { mapGetters, mapMutations } from 'vuex'
import { mapGetters } from 'vuex'
import { filterPosts } from '~/graphql/PostQuery.js'
export default {
@ -59,10 +54,11 @@ export default {
data() {
const { hashtag = null } = this.$route.query
return {
posts: [],
hasMore: true,
// Initialize your apollo data
page: 1,
offset: 0,
pageSize: 12,
filter: {},
hashtag,
placeholder: this.$t('sorting.newest'),
selected: this.$t('sorting.newest'),
@ -96,55 +92,37 @@ export default {
],
}
},
mounted() {
if (this.hashtag) {
this.changeFilterBubble({ tags_some: { name: this.hashtag } })
}
},
watch: {
Post(post) {
this.setPosts(this.Post)
},
},
computed: {
...mapGetters({
currentUser: 'auth/user',
posts: 'posts/posts',
postsFilter: 'postsFilter/postsFilter',
}),
tags() {
return this.posts ? this.posts.tags.map(tag => tag.name) : '-'
},
offset() {
return (this.page - 1) * this.pageSize
},
},
methods: {
...mapMutations({
setPosts: 'posts/SET_POSTS',
}),
changeFilterBubble(filter) {
finalFilters() {
let filter = this.postsFilter
if (this.hashtag) {
filter = {
...filter,
tags_some: { name: this.hashtag },
}
}
this.filter = filter
this.$apollo.queries.Post.refetch()
return filter
},
},
watch: {
postsFilter() {
this.offset = 0
this.posts = []
},
},
methods: {
toggleOnlySorting(x) {
this.offset = 0
this.posts = []
this.sortingIcon = x.icons
this.sorting = x.order
this.$apollo.queries.Post.refetch()
},
clearSearch() {
this.$router.push({ path: '/' })
this.hashtag = null
delete this.filter.tags_some
this.changeFilterBubble(this.filter)
},
uniq(items, field = 'id') {
return uniqBy(items, field)
},
href(post) {
return this.$router.resolve({
@ -153,31 +131,12 @@ export default {
}).href
},
showMoreContributions() {
// this.page++
// Fetch more data and transform the original result
this.page++
this.$apollo.queries.Post.fetchMore({
variables: {
filter: this.filter,
first: this.pageSize,
offset: this.offset,
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
let output = { Post: this.Post }
output.Post = [...previousResult.Post, ...fetchMoreResult.Post]
return output
},
fetchPolicy: 'cache-and-network',
})
this.offset += this.pageSize
},
deletePost(_index, postId) {
this.Post = this.Post.filter(post => {
this.posts = this.posts.filter(post => {
return post.id !== postId
})
// Why "uniq(Post)" is used in the array for list creation?
// Ideal solution here:
// this.Post.splice(index, 1)
},
},
apollo: {
@ -186,12 +145,21 @@ export default {
return filterPosts(this.$i18n)
},
variables() {
return {
filter: this.filter,
const result = {
filter: this.finalFilters,
first: this.pageSize,
offset: 0,
offset: this.offset,
orderBy: this.sorting,
}
return result
},
update({ Post }) {
// TODO: find out why `update` gets called twice initially.
// We have to filter for uniq posts only because we get the same
// result set twice.
this.hasMore = Post.length >= this.pageSize
const posts = uniqBy([...this.posts, ...Post], 'id')
this.posts = posts
},
fetchPolicy: 'cache-and-network',
},

View File

@ -31,7 +31,7 @@ describe('PostSlug', () => {
$filters: {
truncate: a => a,
},
// If you mocking router, than don't use VueRouter with lacalVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
// If you are mocking the router, then don't use VueRouter with localVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
$router: {
history: {
push: jest.fn(),

View File

@ -40,14 +40,30 @@
<ds-space margin="xx-small" />
<hc-tag v-for="tag in post.tags" :key="tag.id" :name="tag.name" />
</div>
<!-- Shout Button -->
<hc-shout-button
v-if="post.author"
:disabled="isAuthor(post.author.id)"
:count="post.shoutedCount"
:is-shouted="post.shoutedByCurrentUser"
:post-id="post.id"
/>
<ds-space margin-top="x-large">
<ds-flex :gutter="{ lg: 'small' }">
<ds-flex-item
:width="{ lg: '75%', md: '75%', sm: '75%' }"
class="emotions-buttons-mobile"
>
<hc-emotions :post="post" />
</ds-flex-item>
<ds-flex-item :width="{ lg: '10%', md: '3%', sm: '3%' }" />
<!-- Shout Button -->
<ds-flex-item
:width="{ lg: '15%', md: '22%', sm: '22%', base: '100%' }"
class="shout-button"
>
<hc-shout-button
v-if="post.author"
:disabled="isAuthor(post.author.id)"
:count="post.shoutedCount"
:is-shouted="post.shoutedByCurrentUser"
:post-id="post.id"
/>
</ds-flex-item>
</ds-flex>
</ds-space>
<!-- Comments -->
<ds-section slot="footer">
<hc-comment-list :post="post" />
@ -69,6 +85,7 @@ import HcCommentForm from '~/components/comments/CommentForm/CommentForm'
import HcCommentList from '~/components/comments/CommentList'
import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers'
import PostQuery from '~/graphql/PostQuery.js'
import HcEmotions from '~/components/Emotions/Emotions'
export default {
name: 'PostSlug',
@ -84,6 +101,7 @@ export default {
ContentMenu,
HcCommentForm,
HcCommentList,
HcEmotions,
ContentViewer,
},
head() {
@ -200,4 +218,9 @@ export default {
}
}
}
@media only screen and (max-width: 960px) {
.shout-button {
float: left;
}
}
</style>

View File

@ -1,11 +1,11 @@
<template>
<ds-card>
<h2 style="margin-bottom: .2em;">Mehr Informationen</h2>
<p>Hier findest du weitere infos zum Thema.</p>
<h2 style="margin-bottom: .2em;">{{ $t('post.moreInfo.title') }}</h2>
<p>{{ $t('post.moreInfo.description') }}</p>
<ds-space />
<h3>
<ds-icon name="compass" />
Themenkategorien
<!-- <ds-icon name="compass" /> -->
{{ $t('post.moreInfo.titleOfCategoriesSection') }}
</h3>
<div class="tags">
<ds-icon
@ -22,8 +22,8 @@
</div>
<template v-if="post.tags && post.tags.length">
<h3>
<ds-icon name="tags" />
Schlagwörter
<!-- <ds-icon name="tags" /> -->
{{ $t('post.moreInfo.titleOfHashtagsSection') }}
</h3>
<div class="tags">
<ds-tag v-for="tag in post.tags" :key="tag.id">
@ -32,7 +32,7 @@
</ds-tag>
</div>
</template>
<h3>Verwandte Beiträge</h3>
<h3>{{ $t('post.moreInfo.titleOfRelatedContributionsSection') }}</h3>
<ds-section style="margin: 0 -1.5rem; padding: 1.5rem;">
<ds-flex v-if="post.relatedContributions && post.relatedContributions.length" gutter="small">
<hc-post-card
@ -71,7 +71,7 @@ export default {
apollo: {
Post: {
query() {
return gql(`
return gql`
query Post($slug: String!) {
Post(slug: $slug) {
id
@ -118,7 +118,7 @@ export default {
shoutedCount
}
}
`)
`
},
variables() {
return {

View File

@ -1,4 +0,0 @@
import Vue from 'vue'
import VueSweetalertIcons from 'vue-sweetalert-icons'
Vue.use(VueSweetalertIcons)

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><circle data-name="&lt;Pfad&gt;" cx="40" cy="40.1" r="40" fill="#cac9c9"/><ellipse data-name="&lt;Pfad&gt;" cx="38.4" cy="38.8" rx="37.4" ry="37.6" fill="#d7d8d8"/><path d="M39.8 55.6c8.8 0 13.4 7.7 13.4 11.6a.8.8 0 0 1-1 1s-5-6-12.4-6-12.4 6-12.4 6a.8.8 0 0 1-1-1c0-4 4.6-11.6 13.4-11.6z" fill="#303030"/><ellipse cx="26.4" cy="40.3" rx="4" ry="7.7" fill="#303030"/><ellipse cx="50.4" cy="39.9" rx="4" ry="7.7" fill="#303030"/><path d="M14.5 27.2s14.7-2 18.6 10c.2.3 0 .5-.4 0a22.8 22.8 0 0 0-18-6.8v-3.2zm47.5 0s-14.5-2-18.4 10c0 .3 0 .5.4 0a22.8 22.8 0 0 1 18.2-6.8v-3.2z" fill="#303030"/></svg>

After

Width:  |  Height:  |  Size: 658 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><defs><radialGradient id="a" cx="37.4" cy="38.6" r="37.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ed6c70" stop-opacity=".7"/><stop offset=".3" stop-color="#ed6c70" stop-opacity=".5"/><stop offset=".8" stop-color="#ed6c70" stop-opacity=".1"/><stop offset="1" stop-color="#ed6c70" stop-opacity="0"/></radialGradient><radialGradient id="b" cx="37.4" cy="38.6" r="37.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fcea1c" stop-opacity=".2"/><stop offset=".8" stop-color="#fcea1c" stop-opacity=".1"/><stop offset="1" stop-color="#fcea1c" stop-opacity="0"/></radialGradient></defs><circle data-name="&lt;Pfad&gt;" cx="40" cy="40" r="40" fill="#dedc03"/><ellipse data-name="&lt;Pfad&gt;" cx="38.4" cy="38.7" rx="37.4" ry="37.6" fill="#fcea1c"/><ellipse data-name="&lt;Pfad&gt;" cx="37.4" cy="38.6" rx="37.4" ry="37.6" fill="url(#a)"/><ellipse data-name="&lt;Pfad&gt;" cx="37.4" cy="38.6" rx="37.4" ry="37.6" fill="url(#b)"/><ellipse cx="26.4" cy="40.2" rx="4" ry="7.7" fill="#303030"/><ellipse cx="50.4" cy="39.8" rx="4" ry="7.7" fill="#303030"/><path d="M14.5 27S29.2 25.3 33 37c.2.4 0 .6-.4 0a22.8 22.8 0 0 0-18-6.7V27zM62 27s-14.5-1.8-18.4 10c0 .4 0 .6.4 0a22.8 22.8 0 0 1 18.2-6.7V27zM39.8 55.5c8.8 0 13.4 7.7 13.4 11.6a.8.8 0 0 1-1 1s-5-6-12.4-6-12.4 6-12.4 6a.8.8 0 0 1-1-1c0-3.8 4.6-11.5 13.4-11.5z" fill="#303030"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><circle data-name="&lt;Pfad&gt;" cx="40" cy="40" r="40" fill="#cbc9c9"/><ellipse data-name="&lt;Pfad&gt;" cx="38.5" cy="38.1" rx="37.4" ry="37.6" fill="#d7d7d7"/><path d="M15.8 24.5v3.2s10 1.3 12-11c0 0-6 8.8-12 7.8zm48.4 0v3.2s-10 1-12-11c0 0 6 9 12 7.8zm-23.8 27c13 0 18.4 7.7 18.4 11.6a2 2 0 0 1-2 2c-1 0-9-6-16.4-6S25 65 24 65a2 2 0 0 1-2-2c0-3.8 5.4-11.5 18.4-11.5z" fill="#303030"/><path d="M40.5 55a7.5 7.5 0 0 1 7.6 5.7 21 21 0 0 0-7.6-1.7 21.5 21.5 0 0 0-7.5 1.7 7.4 7.4 0 0 1 7.5-5.8z" fill="#ed6b70"/><path d="M35.6 42.2s-4.2-3-11.4-3-11.4 3-11.4 3S14.6 35 24.2 35s11.4 7 11.4 7zm32.4 0s-4-3-11.3-3-11.4 3-11.4 3S47 35 56.7 35 68 42 68 42z" fill="#303030"/><path d="M13.8 42.5v35c0 3.4 6 3.4 6 0v-35c0-3.7-6-3.7-6-.4v.5zm47.2 0v35c0 3.4 6 3.4 6 0v-35c0-3.7-6-3.7-6-.4v.5z" fill="#71caeb"/></svg>

After

Width:  |  Height:  |  Size: 866 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><circle data-name="&lt;Pfad&gt;" cx="40" cy="40" r="40" fill="#dedc03"/><ellipse data-name="&lt;Pfad&gt;" cx="38.5" cy="38.2" rx="37.4" ry="37.6" fill="#fcea1c"/><path d="M15.8 24.5v3.3s10 1.2 12-11c0 0-6 8.8-12 7.7zm48.4 0v3.3s-10 1-12-11c0 0 6 8.8 12 7.7zm-23.8 27c13 0 18.4 7.7 18.4 11.6a2 2 0 0 1-2 2c-1 0-9-6-16.4-6S25 65 24 65a2 2 0 0 1-2-2c0-3.8 5.4-11.5 18.4-11.5z" fill="#303030"/><path d="M40.5 55a7.5 7.5 0 0 1 7.6 5.8 21 21 0 0 0-7.6-1.7A21.5 21.5 0 0 0 33 61a7.4 7.4 0 0 1 7.5-5.8z" fill="#ed6c70"/><path d="M35.6 42.2s-4.2-3-11.4-3-11.4 3-11.4 3S14.6 35 24.2 35s11.4 7.2 11.4 7.2zm32.4 0s-4-3-11.3-3-11.4 3-11.4 3S47 35 56.7 35 68 42.2 68 42.2z" fill="#303030"/><path d="M13.8 42.5v35c0 3.4 6 3.4 6 0v-35c0-3.6-6-3.6-6-.3v.3zm47.2 0v35c0 3.4 6 3.4 6 0v-35c0-3.6-6-3.6-6-.3v.3z" fill="#71caeb"/></svg>

After

Width:  |  Height:  |  Size: 874 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><circle data-name="&lt;Pfad&gt;" cx="40.1" cy="40" r="40" fill="#cbc9c9"/><ellipse data-name="&lt;Pfad&gt;" cx="38.4" cy="38.6" rx="37.4" ry="37.6" fill="#d7d7d7"/><path d="M17.6 50h45.6s-3.7 19.6-22.8 19.6S17.6 50 17.6 50z" fill="#303030"/><path d="M40.4 59.8c8 0 9.8 7.8 9.8 7.8a22.6 22.6 0 0 1-9.8 2 22.7 22.7 0 0 1-9.7-2s1.5-7.8 9.7-7.8z" fill="#ed6b70"/><path d="M14.2 33.3s15-2.6 21.5 7.4A83.5 83.5 0 0 0 14.2 42c-10.3 2 4.6-4 14-3 0 0-.8-4-14-5.7zm51.6 0s-15-2.6-21.5 7.4A83.5 83.5 0 0 1 65.8 42c10.3 2-4.6-4-14-3 0 0 1-4 14-5.7z" fill="#303030"/></svg>

After

Width:  |  Height:  |  Size: 620 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><circle data-name="&lt;Pfad&gt;" cx="40.1" cy="40" r="40" fill="#dedc03"/><ellipse data-name="&lt;Pfad&gt;" cx="38.4" cy="38.7" rx="37.4" ry="37.6" fill="#fcea1c"/><path d="M17.6 50h45.6s-3.7 19.7-22.8 19.7S17.6 50 17.6 50z" fill="#303030"/><path d="M40.4 60c8 0 9.8 7.6 9.8 7.6a22.6 22.6 0 0 1-9.8 2 22.7 22.7 0 0 1-9.7-2s1.5-7.7 9.7-7.7z" fill="#ed6b70"/><path d="M14.2 33.4s15-2.6 21.5 7.3A83.5 83.5 0 0 0 14.2 42c-10.3 2 4.6-4 14-3 0 0-.8-4-14-5.6zm51.6 0s-15-2.6-21.5 7.3A83.5 83.5 0 0 1 65.8 42c10.3 2-4.6-4-14-3 0 0 1-4 14-5.6z" fill="#303030"/></svg>

After

Width:  |  Height:  |  Size: 618 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><circle data-name="&lt;Pfad&gt;" cx="40" cy="40" r="40" fill="#cbc9c9"/><ellipse data-name="&lt;Pfad&gt;" cx="38.4" cy="38.6" rx="37.4" ry="37.6" fill="#d7d7d7"/><ellipse cx="55.8" cy="39" rx="4" ry="7.7" fill="#303030"/><ellipse cx="24.1" cy="39" rx="4" ry="7.7" fill="#303030"/><ellipse cx="24.1" cy="45.9" rx="5.7" ry=".8" fill="#303030"/><ellipse cx="55.8" cy="45.9" rx="5.7" ry=".8" fill="#303030"/><path d="M17.5 55s8.3 4 22.8 4S63 55 63 55s-3.6 14.6-22.7 14.6S17.5 55 17.5 55z" fill="#303030"/><path d="M40.3 62.8c8 0 9.4 5.4 9.2 5.5a28.8 28.8 0 0 1-9.2 1.3 28.8 28.8 0 0 1-9.2-1.3s1-5.5 9.3-5.5z" fill="#ed6b70"/></svg>

After

Width:  |  Height:  |  Size: 687 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><circle data-name="&lt;Pfad&gt;" cx="40" cy="40" r="40" fill="#dedc03"/><ellipse data-name="&lt;Pfad&gt;" cx="38.4" cy="38.7" rx="37.4" ry="37.6" fill="#fcea1c"/><ellipse cx="55.8" cy="39" rx="4" ry="7.7" fill="#303030"/><ellipse cx="24.1" cy="39" rx="4" ry="7.7" fill="#303030"/><ellipse cx="24.1" cy="45.9" rx="5.7" ry=".8" fill="#303030"/><ellipse cx="55.8" cy="45.9" rx="5.7" ry=".8" fill="#303030"/><path d="M17.5 55s8.3 4 22.8 4S63 55 63 55s-3.6 14.7-22.7 14.7S17.5 55 17.5 55z" fill="#303030"/><path d="M40.3 63c8 0 9.4 5.3 9.2 5.3a28.8 28.8 0 0 1-9.2 1.4 28.8 28.8 0 0 1-9.2-1.4s1-5.4 9.3-5.4z" fill="#ed6b70"/></svg>

After

Width:  |  Height:  |  Size: 685 B

View File

@ -0,0 +1 @@
<svg id="new" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><style></style><circle id="XMLID_40_" cx="40" cy="40" r="40" fill="#cac9c9"/><ellipse id="XMLID_39_" cx="38.4" cy="38.7" rx="37.4" ry="37.6" fill="#d7d8d8"/><ellipse id="Oval" fill="#303030" cx="55.8" cy="39" rx="4" ry="7.7"/><ellipse id="Oval" fill="#303030" cx="24.1" cy="39" rx="4" ry="7.7"/><path d="M45 61.4c0-2.3-1.2-4.4-6-4.4-5 0-6 2.2-6 4.4 0 2.3 1 6.6 6 6.6 4.8 0 6-4.4 6-6.6zM51.1 19L50 16s8.9-4.6 15 6c0 0-8.6-6.1-13.9-3zM26.4 18.4l.6-3.1S17 12.6 13 24c0 0 7.5-7.6 13.4-5.6z" id="Path" fill="#303030"/></svg>

After

Width:  |  Height:  |  Size: 588 B

View File

@ -0,0 +1 @@
<svg id="new" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><style></style><circle id="XMLID_40_" cx="40" cy="40" r="40" fill="#dedc03"/><ellipse id="XMLID_39_" cx="38.4" cy="38.7" rx="37.4" ry="37.6" fill="#fcea1c"/><ellipse id="Oval" fill="#303030" cx="55.8" cy="39" rx="4" ry="7.7"/><ellipse id="Oval" fill="#303030" cx="24.1" cy="39" rx="4" ry="7.7"/><path d="M45 61.4c0-2.3-1.2-4.4-6-4.4-5 0-6 2.2-6 4.4 0 2.3 1 6.6 6 6.6 4.8 0 6-4.4 6-6.6zM51.1 19L50 16s8.9-4.6 15 6c0 0-8.6-6.1-13.9-3zM26.4 18.4l.6-3.1S17 12.6 13 24c0 0 7.5-7.6 13.4-5.6z" id="Path" fill="#303030"/></svg>

After

Width:  |  Height:  |  Size: 588 B

View File

@ -3,6 +3,11 @@ import gql from 'graphql-tag'
export const state = () => {
return {
posts: [],
filteredByUsersFollowed: false,
filteredByCategories: false,
usersFollowedFilter: {},
categoriesFilter: {},
selectedCategoryIds: [],
}
}
@ -10,12 +15,51 @@ export const mutations = {
SET_POSTS(state, posts) {
state.posts = posts || null
},
SET_FILTERED_BY_FOLLOWERS(state, boolean) {
state.filteredByUsersFollowed = boolean || null
},
SET_FILTERED_BY_CATEGORIES(state, boolean) {
state.filteredByCategories = boolean || null
},
SET_USERS_FOLLOWED_FILTER(state, filter) {
state.usersFollowedFilter = filter || null
},
SET_CATEGORIES_FILTER(state, filter) {
state.categoriesFilter = filter || null
},
SET_SELECTED_CATEGORY_IDS(state, categoryId) {
if (!categoryId) {
state.selectedCategoryIds = []
} else {
const index = state.selectedCategoryIds.indexOf(categoryId)
if (index > -1) {
state.selectedCategoryIds.splice(index, 1)
} else {
state.selectedCategoryIds.push(categoryId)
}
}
},
}
export const getters = {
posts(state) {
return state.posts || []
},
filteredByUsersFollowed(state) {
return state.filteredByUsersFollowed || false
},
filteredByCategories(state) {
return state.filteredByCategories || false
},
usersFollowedFilter(state) {
return state.usersFollowedFilter || {}
},
categoriesFilter(state) {
return state.categoriesFilter || {}
},
selectedCategoryIds(state) {
return state.selectedCategoryIds || []
},
}
export const actions = {
@ -24,7 +68,7 @@ export const actions = {
const {
data: { Post },
} = await client.query({
query: gql(`
query: gql`
query Post($filter: _PostFilter, $first: Int, $offset: Int) {
Post(filter: $filter, first: $first, offset: $offset) {
id
@ -63,7 +107,7 @@ export const actions = {
}
shoutedCount
}
}`),
}`,
variables: {
filter,
first: 12,

View File

@ -0,0 +1,50 @@
import get from 'lodash/get'
import update from 'lodash/update'
import xor from 'lodash/xor'
import isEmpty from 'lodash/isEmpty'
import clone from 'lodash/clone'
export const state = () => {
return {
filter: {},
}
}
export const mutations = {
TOGGLE_FILTER_BY_FOLLOWED(state, currentUserId) {
const filter = clone(state.filter)
const id = get(filter, 'author.followedBy_some.id')
if (id) {
delete filter.author
state.filter = filter
} else {
state.filter = {
...filter,
author: { followedBy_some: { id: currentUserId } },
}
}
},
RESET_CATEGORIES(state) {
const filter = clone(state.filter)
delete filter.categories_some
state.filter = filter
},
TOGGLE_CATEGORY(state, categoryId) {
const filter = clone(state.filter)
update(filter, 'categories_some.id_in', categoryIds => xor(categoryIds, [categoryId]))
if (isEmpty(get(filter, 'categories_some.id_in'))) delete filter.categories_some
state.filter = filter
},
}
export const getters = {
postsFilter(state) {
return state.filter
},
filteredCategoryIds(state) {
return get(state.filter, 'categories_some.id_in') || []
},
filteredByUsersFollowed(state) {
return !!get(state.filter, 'author.followedBy_some.id')
},
}

View File

@ -0,0 +1,126 @@
import { getters, mutations } from './postsFilter.js'
let state
let testAction
describe('getters', () => {
describe('filteredCategoryIds', () => {
it('returns category ids if filter is set', () => {
state = { filter: { categories_some: { id_in: [24] } } }
expect(getters.filteredCategoryIds(state)).toEqual([24])
})
it('returns empty array if filter is not set', () => {
state = { filter: { author: { followedBy_some: { id: 7 } } } }
expect(getters.filteredCategoryIds(state)).toEqual([])
})
})
describe('postsFilter', () => {
it('returns filter', () => {
state = { filter: { author: { followedBy_some: { id: 7 } } } }
expect(getters.postsFilter(state)).toEqual({ author: { followedBy_some: { id: 7 } } })
})
})
describe('filteredByUsersFollowed', () => {
it('returns true if filter is set', () => {
state = { filter: { author: { followedBy_some: { id: 7 } } } }
expect(getters.filteredByUsersFollowed(state)).toBe(true)
})
it('returns false if filter is not set', () => {
state = { filter: { categories_some: { id_in: [23] } } }
expect(getters.filteredByUsersFollowed(state)).toBe(false)
})
})
})
describe('mutations', () => {
describe('RESET_CATEGORIES', () => {
beforeEach(() => {
testAction = categoryId => {
mutations.RESET_CATEGORIES(state, categoryId)
return getters.postsFilter(state)
}
})
it('resets the categories filter', () => {
state = {
filter: {
author: { followedBy_some: { id: 7 } },
categories_some: { id_in: [23] },
},
}
expect(testAction(23)).toEqual({ author: { followedBy_some: { id: 7 } } })
})
})
describe('TOGGLE_CATEGORY', () => {
beforeEach(() => {
testAction = categoryId => {
mutations.TOGGLE_CATEGORY(state, categoryId)
return getters.postsFilter(state)
}
})
it('creates category filter if empty', () => {
state = { filter: {} }
expect(testAction(23)).toEqual({ categories_some: { id_in: [23] } })
})
it('adds category id not present', () => {
state = { filter: { categories_some: { id_in: [24] } } }
expect(testAction(23)).toEqual({ categories_some: { id_in: [24, 23] } })
})
it('removes category id if present', () => {
state = { filter: { categories_some: { id_in: [23, 24] } } }
const result = testAction(23)
expect(result).toEqual({ categories_some: { id_in: [24] } })
})
it('removes category filter if empty', () => {
state = { filter: { categories_some: { id_in: [23] } } }
expect(testAction(23)).toEqual({})
})
it('does not get in the way of other filters', () => {
state = {
filter: {
author: { followedBy_some: { id: 7 } },
categories_some: { id_in: [23] },
},
}
expect(testAction(23)).toEqual({ author: { followedBy_some: { id: 7 } } })
})
})
describe('TOGGLE_FILTER_BY_FOLLOWED', () => {
beforeEach(() => {
testAction = userId => {
mutations.TOGGLE_FILTER_BY_FOLLOWED(state, userId)
return getters.postsFilter(state)
}
})
describe('given empty filter', () => {
beforeEach(() => {
state = { filter: {} }
})
it('attaches the id of the current user to the filter object', () => {
expect(testAction(4711)).toEqual({ author: { followedBy_some: { id: 4711 } } })
})
})
describe('already filtered', () => {
beforeEach(() => {
state = { filter: { author: { followedBy_some: { id: 4711 } } } }
})
it('remove the id of the current user from the filter object', () => {
expect(testAction(4711)).toEqual({})
})
})
})
})

View File

@ -1645,17 +1645,17 @@
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
"@storybook/addon-a11y@^5.1.9":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/addon-a11y/-/addon-a11y-5.1.10.tgz#0bf37a8e2827cdaa199b293b14e71e8434246591"
integrity sha512-YiRj/8IQ5zq/I+x+aRyfS5PP9nTfuTU7O90+WtNomqCJPMBOrR3BYsEcl510jOy2iwhQwh76MFT5s1tKpMclAA==
"@storybook/addon-a11y@^5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/addon-a11y/-/addon-a11y-5.1.11.tgz#170e0f406b6c0a07fd1e28e6323a694df3f087fc"
integrity sha512-kMBDPl0DslamNCtOGGqGlTjRDTxmEcu8JMTaZSa4GnTwzfN+ugb+aUEkbKl3VjMW7GsdpgizMTWBtgf6SwNj8w==
dependencies:
"@storybook/addons" "5.1.10"
"@storybook/api" "5.1.10"
"@storybook/client-logger" "5.1.10"
"@storybook/components" "5.1.10"
"@storybook/core-events" "5.1.10"
"@storybook/theming" "5.1.10"
"@storybook/addons" "5.1.11"
"@storybook/api" "5.1.11"
"@storybook/client-logger" "5.1.11"
"@storybook/components" "5.1.11"
"@storybook/core-events" "5.1.11"
"@storybook/theming" "5.1.11"
axe-core "^3.2.2"
common-tags "^1.8.0"
core-js "^3.0.1"
@ -1668,16 +1668,16 @@
redux "^4.0.1"
util-deprecate "^1.0.2"
"@storybook/addon-actions@^5.1.9":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-5.1.10.tgz#8ed4272a6afc68f4a30372da2eeff414f0fe6ecd"
integrity sha512-njl2AHBGi27NvisOB8LFnWH/3RcyJT/CW7tl1cvV2j5FH2oBjq5MsjxKyJIcKwC677k1Wr8G8fw/zSEHrPpmgA==
"@storybook/addon-actions@^5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-5.1.11.tgz#ebc299b9dfe476b5c65eb5d148c4b064f682ca08"
integrity sha512-Fp4b8cBYrl9zudvamVYTxE1XK2tzg91hgBDoVxIbDvSMZ2aQXSq8B5OFS4eSdvg+ldEOBbvIgUNS1NIw+FGntQ==
dependencies:
"@storybook/addons" "5.1.10"
"@storybook/api" "5.1.10"
"@storybook/components" "5.1.10"
"@storybook/core-events" "5.1.10"
"@storybook/theming" "5.1.10"
"@storybook/addons" "5.1.11"
"@storybook/api" "5.1.11"
"@storybook/components" "5.1.11"
"@storybook/core-events" "5.1.11"
"@storybook/theming" "5.1.11"
core-js "^3.0.1"
fast-deep-equal "^2.0.1"
global "^4.3.2"
@ -1688,28 +1688,28 @@
react-inspector "^3.0.2"
uuid "^3.3.2"
"@storybook/addons@5.1.10":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-5.1.10.tgz#2d8d8ca20b6d9b4652744f5fc00ead483f705435"
integrity sha512-M9b2PCp9RZxDC6wL7vVt2SCKCGXrrEAOsdpMvU569yB1zoUPEiiqElVDwb91O2eAGPnmd2yjImp90kOpKUW0EA==
"@storybook/addons@5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-5.1.11.tgz#27f9cfed8d7f7c8a3fc341cdba3b0bdf608f02aa"
integrity sha512-714Xg6pX4rjDY1urL94w4oOxIiK6jCFSp4oKvqLj7dli5CG7d34Yt9joyTgOb2pkbrgmbMWAZJq0L0iOjHzpzw==
dependencies:
"@storybook/api" "5.1.10"
"@storybook/channels" "5.1.10"
"@storybook/client-logger" "5.1.10"
"@storybook/api" "5.1.11"
"@storybook/channels" "5.1.11"
"@storybook/client-logger" "5.1.11"
core-js "^3.0.1"
global "^4.3.2"
util-deprecate "^1.0.2"
"@storybook/api@5.1.10":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/api/-/api-5.1.10.tgz#5eeb5d9a7c268e5c89bd40c9a80293a7c72343b8"
integrity sha512-YeZe/71zLMmgT95IMAEZOc9AwL6Y23mWvkZMwFbkokxS9+bU/qmVlQ0B9c3JBzO3OSs7sXaRqyP1o3QkQgVsiw==
"@storybook/api@5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/api/-/api-5.1.11.tgz#71ef00285cd8602aad24cdb26c60c5d3c76631e5"
integrity sha512-zzPZM6W67D4YKCbUN4RhC/w+/CtnH/hFbSh/QUBdwXFB1aLh2qA1UTyB8i6m6OA6JgVHBqEkl10KhmeILLv/eA==
dependencies:
"@storybook/channels" "5.1.10"
"@storybook/client-logger" "5.1.10"
"@storybook/core-events" "5.1.10"
"@storybook/router" "5.1.10"
"@storybook/theming" "5.1.10"
"@storybook/channels" "5.1.11"
"@storybook/client-logger" "5.1.11"
"@storybook/core-events" "5.1.11"
"@storybook/router" "5.1.11"
"@storybook/theming" "5.1.11"
core-js "^3.0.1"
fast-deep-equal "^2.0.1"
global "^4.3.2"
@ -1723,33 +1723,33 @@
telejson "^2.2.1"
util-deprecate "^1.0.2"
"@storybook/channel-postmessage@5.1.10":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-5.1.10.tgz#e0a58461d56ef20a87d8bc4df1067e7afc76950e"
integrity sha512-kQZIwltN2cWDXluhCfdModFDK1LHV9ZhNQ1b/uD9vn1c65rQ9u7r4lRajCfS0X1dmAWqz48cBcEurAubNgmswg==
"@storybook/channel-postmessage@5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-5.1.11.tgz#e75ab7d59ba19476eb631cdb69ee713c3b956c2b"
integrity sha512-S7Uq7+c9kOJ9BB4H9Uro2+dVhqoMchYCipQzAkD4jIIwK99RNzGdAaRipDC1k0k/C+v2SOa+D5xBbb3XVYPSrg==
dependencies:
"@storybook/channels" "5.1.10"
"@storybook/client-logger" "5.1.10"
"@storybook/channels" "5.1.11"
"@storybook/client-logger" "5.1.11"
core-js "^3.0.1"
global "^4.3.2"
telejson "^2.2.1"
"@storybook/channels@5.1.10":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-5.1.10.tgz#04fd35c05032c675f7816ea1ca873c1a0415c6d9"
integrity sha512-w7n/bV1BLu51KI1eLc75lN9H1ssBc3PZMXk88GkMiKyBVRzPlJA5ixnzH86qwYGReE0dhRpsgHXZ5XmoKaVmPA==
"@storybook/channels@5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-5.1.11.tgz#77ddf9d777891f975ac10095772c840fed4c4620"
integrity sha512-MlrjVGNvYOnDvv2JDRhr4wikbnZ8HCFCpVsFqKPFxj7I3OYBR417RvFkydX3Rtx4kwB9rmZEgLhfAfsSytkALg==
dependencies:
core-js "^3.0.1"
"@storybook/client-api@5.1.10":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-5.1.10.tgz#a10f028f2d33d044e5c3b3daea5d8375323e6a66"
integrity sha512-v2PqiNUhwDlVDLYL94f6LFjdYMToTpuwWh9aeqzt/4PAJUnIcA+2P8+qXiYdJTqQy/u7P72HFMlc9Ru4tl3QFg==
"@storybook/client-api@5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-5.1.11.tgz#30d82c09c6c40aa70d932e77b1d1e65526bddc0c"
integrity sha512-znzSxZ1ZCqtEKrFoW7xT8iBbdiAXaQ8RNxQFKHuYPqWX+RLol6S3duEOxu491X2SzUg0StUmrX5qL9Rnth8dRQ==
dependencies:
"@storybook/addons" "5.1.10"
"@storybook/client-logger" "5.1.10"
"@storybook/core-events" "5.1.10"
"@storybook/router" "5.1.10"
"@storybook/addons" "5.1.11"
"@storybook/client-logger" "5.1.11"
"@storybook/core-events" "5.1.11"
"@storybook/router" "5.1.11"
common-tags "^1.8.0"
core-js "^3.0.1"
eventemitter3 "^3.1.0"
@ -1759,20 +1759,20 @@
memoizerific "^1.11.3"
qs "^6.6.0"
"@storybook/client-logger@5.1.10":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-5.1.10.tgz#f83a8717924dd222e0a6df82ae74701f27e0bb35"
integrity sha512-vB1NoFWRTgcERwodhbgoDwI00eqU8++nXI7GhMS1CY8haZaSp3gyKfHRWyfH+M+YjQuGBRUcvIk4gK6OtSrDOw==
"@storybook/client-logger@5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-5.1.11.tgz#9509af3021b7a9977f9dba1f2ff038fd3c994437"
integrity sha512-je4To+9zD3SEJsKe9R4u15N4bdXFBR7pdBToaRIur+XSvvShLFehZGseQi+4uPAj8vyG34quGTCeUC/BKY0LwQ==
dependencies:
core-js "^3.0.1"
"@storybook/components@5.1.10":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/components/-/components-5.1.10.tgz#4b6436f0b5bb2483fb231bee263d173a9ed7d241"
integrity sha512-QUQeeQp1xNWiL4VlxFAea0kqn2zvBfmfPlUddOFO9lBhT6pVy0xYPjXjbTVWjVcYzZpyUNWw5GplqrR5jhlaCA==
"@storybook/components@5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/components/-/components-5.1.11.tgz#da253af0a8cb1b063c5c2e8016c4540c983f717d"
integrity sha512-EQgD7HL2CWnnY968KrwUSU2dtKFGTGRJVc4vwphYEeZwAI0lX6qbTMuwEP22hDZ2OSRBxcvcXT8cvduDlZlFng==
dependencies:
"@storybook/client-logger" "5.1.10"
"@storybook/theming" "5.1.10"
"@storybook/client-logger" "5.1.11"
"@storybook/theming" "5.1.11"
core-js "^3.0.1"
global "^4.3.2"
markdown-to-jsx "^6.9.1"
@ -1790,32 +1790,32 @@
recompose "^0.30.0"
simplebar-react "^1.0.0-alpha.6"
"@storybook/core-events@5.1.10":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-5.1.10.tgz#5aed88c572036b6bd6dfff28976ee96e6e175d7a"
integrity sha512-Lvu/rNcgS+XCkQKSGdNpUSWjpFF9AOSHPXsvkwHbRwJYdMDn3FznlXfDUiubOWtsziXHB6vl3wkKDlH+ckb32Q==
"@storybook/core-events@5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-5.1.11.tgz#9d00503a936d30398f7a64336eb956303d053765"
integrity sha512-m+yIFRdB47+IPBFBGS2OUXrSLkoz5iAXvb3c0lGAePf5wSR+o/Ni/9VD5l6xBf+InxHLSc9gcDEJehrT0fJAaQ==
dependencies:
core-js "^3.0.1"
"@storybook/core@5.1.10":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/core/-/core-5.1.10.tgz#53d23d07716aa2721e1572d44a7f05967d7da39e"
integrity sha512-zkNjufOFrLpFpmr73F/gaJh0W0vWqXIo5zrKvQt1LqmMeCU/v8MstHi4XidlK43UpeogfaXl5tjNCQDO/bd0Dw==
"@storybook/core@5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/core/-/core-5.1.11.tgz#d7c4b14b02f74c183ab5baffe9b3e5ec8289b320"
integrity sha512-LkSoAJlLEtrzFcoINX3dz4oT6xUPEHEp2/WAXLqUFeCnzJHAxIsRvbVxB49Kh/2TrgDFZpL9Or8XXMzZtE6KYw==
dependencies:
"@babel/plugin-proposal-class-properties" "^7.3.3"
"@babel/plugin-proposal-object-rest-spread" "^7.3.2"
"@babel/plugin-syntax-dynamic-import" "^7.2.0"
"@babel/plugin-transform-react-constant-elements" "^7.2.0"
"@babel/preset-env" "^7.4.5"
"@storybook/addons" "5.1.10"
"@storybook/channel-postmessage" "5.1.10"
"@storybook/client-api" "5.1.10"
"@storybook/client-logger" "5.1.10"
"@storybook/core-events" "5.1.10"
"@storybook/node-logger" "5.1.10"
"@storybook/router" "5.1.10"
"@storybook/theming" "5.1.10"
"@storybook/ui" "5.1.10"
"@storybook/addons" "5.1.11"
"@storybook/channel-postmessage" "5.1.11"
"@storybook/client-api" "5.1.11"
"@storybook/client-logger" "5.1.11"
"@storybook/core-events" "5.1.11"
"@storybook/node-logger" "5.1.11"
"@storybook/router" "5.1.11"
"@storybook/theming" "5.1.11"
"@storybook/ui" "5.1.11"
airbnb-js-shims "^1 || ^2"
autoprefixer "^9.4.9"
babel-plugin-add-react-displayname "^0.0.5"
@ -1863,16 +1863,17 @@
shelljs "^0.8.3"
style-loader "^0.23.1"
terser-webpack-plugin "^1.2.4"
unfetch "^4.1.0"
url-loader "^1.1.2"
util-deprecate "^1.0.2"
webpack "^4.33.0"
webpack-dev-middleware "^3.7.0"
webpack-hot-middleware "^2.25.0"
"@storybook/node-logger@5.1.10":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-5.1.10.tgz#92c80b46177687cd8fda1f93a055c22711984154"
integrity sha512-Z4UKh7QBOboQhUF5S/dKOx3OWWCNZGwYu8HZa/O+P68+XnQDhuZCYwqWG49xFhZd0Jb0W9gdUL2mWJw5POG9PA==
"@storybook/node-logger@5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-5.1.11.tgz#bbf5ad0d148e6c9a9b7cf6f62ad4df4e9fa19e5d"
integrity sha512-LG0KM4lzb9LEffcO3Ps9FcHHsVgQUc/oG+kz3p0u9fljFoL3cJHF1Mb4o+HrSydtdWZs/spwZ/BLEo5n/AByDw==
dependencies:
chalk "^2.4.2"
core-js "^3.0.1"
@ -1880,10 +1881,10 @@
pretty-hrtime "^1.0.3"
regenerator-runtime "^0.12.1"
"@storybook/router@5.1.10":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/router/-/router-5.1.10.tgz#d3cffd3f1105eb665882f389746ccabbb98c3c16"
integrity sha512-BdG6/essPZFHCP2ewCG0gYFQfmuuTSHXAB5fd/rwxLSYj1IzNznC5OxkvnSaTr4rgoxxaW/z1hbN1NuA0ivlFA==
"@storybook/router@5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/router/-/router-5.1.11.tgz#75089e9e623482e52ed894c3f0cb0fc6a5372da9"
integrity sha512-Xt7R1IOWLlIxis6VKV9G8F+e/G4G8ng1zXCqoDq+/RlWzlQJ5ccO4bUm2/XGS1rEgY4agMzmzjum18HoATpLGA==
dependencies:
"@reach/router" "^1.2.1"
core-js "^3.0.1"
@ -1891,14 +1892,14 @@
memoizerific "^1.11.3"
qs "^6.6.0"
"@storybook/theming@5.1.10":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-5.1.10.tgz#f9bd519cdf9cccf730656e3f5fd56a339dd07c9f"
integrity sha512-5cN1lmdVUwAR8U3T49Lfb8JW5RBvxBSPGZpUmbLGz1zi0tWBJgYXoGtw4RbTBjV9kCQOXkHGH12AsdDxHh931w==
"@storybook/theming@5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-5.1.11.tgz#0d1af46535f2e601293c999a314905069a93ec3b"
integrity sha512-PtRPfiAWx5pQbTm45yyPB+CuW/vyDmcmNOt+xnDzK52omeWaSD7XK2RfadN3u4QXCgha7zs35Ppx1htJio2NRA==
dependencies:
"@emotion/core" "^10.0.9"
"@emotion/styled" "^10.0.7"
"@storybook/client-logger" "5.1.10"
"@storybook/client-logger" "5.1.11"
common-tags "^1.8.0"
core-js "^3.0.1"
deep-object-diff "^1.1.0"
@ -1909,19 +1910,19 @@
prop-types "^15.7.2"
resolve-from "^5.0.0"
"@storybook/ui@5.1.10":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-5.1.10.tgz#4262b1b09efa43d125d694452ae879b89071edd1"
integrity sha512-ezkoVtzoKh93z2wzkqVIqyrIzTkj8tizgAkoPa7mUAbLCxu6LErHITODQoyEiJWI4Epy3yU9GYXFWwT71hdwsA==
"@storybook/ui@5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-5.1.11.tgz#02246f7656f644a36908430de12abbdf4e2a8a72"
integrity sha512-mopuFSwtodvH4HRdaSBlgYxzYca1qyvzZ0BxOPocXhiFfFR+V9NyNJqKKRA3vinWuuZWpYcnPTu3h8skmjMirg==
dependencies:
"@storybook/addons" "5.1.10"
"@storybook/api" "5.1.10"
"@storybook/channels" "5.1.10"
"@storybook/client-logger" "5.1.10"
"@storybook/components" "5.1.10"
"@storybook/core-events" "5.1.10"
"@storybook/router" "5.1.10"
"@storybook/theming" "5.1.10"
"@storybook/addons" "5.1.11"
"@storybook/api" "5.1.11"
"@storybook/channels" "5.1.11"
"@storybook/client-logger" "5.1.11"
"@storybook/components" "5.1.11"
"@storybook/core-events" "5.1.11"
"@storybook/router" "5.1.11"
"@storybook/theming" "5.1.11"
copy-to-clipboard "^3.0.8"
core-js "^3.0.1"
core-js-pure "^3.0.1"
@ -1949,12 +1950,12 @@
telejson "^2.2.1"
util-deprecate "^1.0.2"
"@storybook/vue@~5.1.9":
version "5.1.10"
resolved "https://registry.yarnpkg.com/@storybook/vue/-/vue-5.1.10.tgz#37916c93faf2eca21497b359748109727ccf3216"
integrity sha512-UeRbQ5bOWUTx5oBMfPf+ZtP5E5X74nFFhrkg0yNakohW6pLuTVoci/G8hDJ4wsjT7PgNjoE1/dggf4JKCU9tjA==
"@storybook/vue@~5.1.11":
version "5.1.11"
resolved "https://registry.yarnpkg.com/@storybook/vue/-/vue-5.1.11.tgz#440c260afa46247e80431470ee50e2725e06b579"
integrity sha512-hhCBfYyoBHehZf2P4BO9C1CuvY9m9GfiaWwqKl8WTGSdy8H6no5ZCRkG2SskS/h+mzJ1+WIGqnHkm0iIDz6KSg==
dependencies:
"@storybook/core" "5.1.10"
"@storybook/core" "5.1.11"
common-tags "^1.8.0"
core-js "^3.0.1"
global "^4.3.2"
@ -2777,14 +2778,14 @@ apollo-cache-control@0.8.1:
apollo-server-env "2.4.1"
graphql-extensions "0.8.1"
apollo-cache-inmemory@^1.6.2, apollo-cache-inmemory@~1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.2.tgz#bbf2e4e1eacdf82b2d526f5c2f3b37e5acee3c5e"
integrity sha512-AyCl3PGFv5Qv1w4N9vlg63GBPHXgMCekZy5mhlS042ji0GW84uTySX+r3F61ZX3+KM1vA4m9hQyctrEGiv5XjQ==
apollo-cache-inmemory@^1.6.2, apollo-cache-inmemory@~1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.3.tgz#826861d20baca4abc45f7ca7a874105905b8525d"
integrity sha512-S4B/zQNSuYc0M/1Wq8dJDTIO9yRgU0ZwDGnmlqxGGmFombOZb9mLjylewSfQKmjNpciZ7iUIBbJ0mHlPJTzdXg==
dependencies:
apollo-cache "^1.3.2"
apollo-utilities "^1.3.2"
optimism "^0.9.0"
optimism "^0.10.0"
ts-invariant "^0.4.0"
tslib "^1.9.3"
@ -2796,10 +2797,10 @@ apollo-cache@1.3.2, apollo-cache@^1.3.2:
apollo-utilities "^1.3.2"
tslib "^1.9.3"
apollo-client@^2.6.3, apollo-client@~2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.3.tgz#9bb2d42fb59f1572e51417f341c5f743798d22db"
integrity sha512-DS8pmF5CGiiJ658dG+mDn8pmCMMQIljKJSTeMNHnFuDLV0uAPZoeaAwVFiAmB408Ujqt92oIZ/8yJJAwSIhd4A==
apollo-client@^2.6.3, apollo-client@~2.6.4:
version "2.6.4"
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.4.tgz#872c32927263a0d34655c5ef8a8949fbb20b6140"
integrity sha512-oWOwEOxQ9neHHVZrQhHDbI6bIibp9SHgxaLRVPoGvOFy7OH5XUykZE7hBQAVxq99tQjBzgytaZffQkeWo1B4VQ==
dependencies:
"@types/zen-observable" "^0.8.0"
apollo-cache "1.3.2"
@ -3014,17 +3015,7 @@ apollo-link-ws@^1.0.18:
apollo-link "^1.2.12"
tslib "^1.9.3"
apollo-link@^1.0.0, apollo-link@^1.2.1, apollo-link@^1.2.11, apollo-link@^1.2.3:
version "1.2.11"
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.11.tgz#493293b747ad3237114ccd22e9f559e5e24a194d"
integrity sha512-PQvRCg13VduLy3X/0L79M6uOpTh5iHdxnxYuo8yL7sJlWybKRJwsv4IcRBJpMFbChOOaHY7Og9wgPo6DLKDKDA==
dependencies:
apollo-utilities "^1.2.1"
ts-invariant "^0.3.2"
tslib "^1.9.3"
zen-observable-ts "^0.8.18"
apollo-link@^1.2.12:
apollo-link@^1.0.0, apollo-link@^1.2.1, apollo-link@^1.2.11, apollo-link@^1.2.12, apollo-link@^1.2.3:
version "1.2.12"
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.12.tgz#014b514fba95f1945c38ad4c216f31bcfee68429"
integrity sha512-fsgIAXPKThyMVEMWQsUN22AoQI+J/pVXcjRGAShtk97h7D8O+SPskFinCGEkxPeQpE83uKaqafB2IyWdjN+J3Q==
@ -4764,15 +4755,14 @@ cliui@^4.0.0:
strip-ansi "^4.0.0"
wrap-ansi "^2.0.0"
clone-deep@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713"
integrity sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==
clone-deep@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
dependencies:
for-own "^1.0.0"
is-plain-object "^2.0.4"
kind-of "^6.0.0"
shallow-clone "^1.0.0"
kind-of "^6.0.2"
shallow-clone "^3.0.0"
clone-response@1.0.2:
version "1.0.2"
@ -5113,12 +5103,7 @@ core-js@^1.0.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
core-js@^2.4.0, core-js@^2.6.5:
version "2.6.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895"
integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==
core-js@^2.5.0, core-js@~2.6.9:
core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.5, core-js@~2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==
@ -6219,10 +6204,10 @@ eslint-plugin-import@~2.18.2:
read-pkg-up "^2.0.0"
resolve "^1.11.0"
eslint-plugin-jest@~22.14.1:
version "22.14.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.14.1.tgz#32287dade9bc0a1920c61e25a71cf11363d78015"
integrity sha512-mpLjhADl+HjagrlaGNx95HIji089S18DhnU/Ee8P8VP+dhEnuEzb43BXEaRmDgQ7BiSUPcSCvt1ydtgPkjOF/Q==
eslint-plugin-jest@~22.15.1:
version "22.15.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.15.1.tgz#54c4a752a44c4bc5a564ecc22b32e1cd16a2961a"
integrity sha512-CWq/RR/3tLaKFB+FZcCJwU9hH5q/bKeO3rFP8G07+q7hcDCFNqpvdphVbEbGE6o6qo1UbciEev4ejUWv7brUhw==
dependencies:
"@typescript-eslint/experimental-utils" "^1.13.0"
@ -6918,23 +6903,11 @@ follow-redirects@^1.0.0:
dependencies:
debug "^3.2.6"
for-in@^0.1.3:
version "0.1.8"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=
for-in@^1.0.1, for-in@^1.0.2:
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
for-own@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b"
integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=
dependencies:
for-in "^1.0.1"
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
@ -9574,11 +9547,6 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
lodash.tail@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=
lodash.template@^4.2.4, lodash.template@^4.4.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
@ -9614,12 +9582,7 @@ lodash.uniqueid@^4.0.1:
resolved "https://registry.yarnpkg.com/lodash.uniqueid/-/lodash.uniqueid-4.0.1.tgz#3268f26a7c88e4f4b1758d679271814e31fa5b26"
integrity sha1-MmjyanyI5PSxdY1nknGBTjH6WyY=
lodash@4.x, lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.10:
version "4.17.14"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
lodash@^4.17.12:
lodash@4.x, lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.10:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@ -10046,14 +10009,6 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
mixin-object@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e"
integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=
dependencies:
for-in "^0.1.3"
is-extendable "^0.1.1"
mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
@ -10149,12 +10104,7 @@ negotiator@0.6.2:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
neo-async@^2.5.0, neo-async@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.0.tgz#b9d15e4d71c6762908654b5183ed38b753340835"
integrity sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==
neo-async@^2.6.1:
neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
@ -10704,10 +10654,10 @@ opn@^3.0.3:
dependencies:
object-assign "^4.0.1"
optimism@^0.9.0:
version "0.9.6"
resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.9.6.tgz#5621195486b294c3bfc518d17ac47767234b029f"
integrity sha512-bWr/ZP32UgFCQAoSkz33XctHwpq2via2sBvGvO5JIlrU8gaiM0LvoKj3QMle9LWdSKlzKik8XGSerzsdfYLNxA==
optimism@^0.10.0:
version "0.10.2"
resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.10.2.tgz#626b6fd28b0923de98ecb36a3fd2d3d4e5632dd9"
integrity sha512-zPfBIxFFWMmQboM9+Z4MSJqc1PXp82v1PFq/GfQaufI69mHKlup7ykGNnfuGIGssXJQkmhSodQ/k9EWwjd8O8A==
dependencies:
"@wry/context" "^0.4.0"
@ -13121,16 +13071,15 @@ sass-graph@^2.2.4:
scss-tokenizer "^0.2.3"
yargs "^7.0.0"
sass-loader@^7.1.0, sass-loader@~7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.1.0.tgz#16fd5138cb8b424bf8a759528a1972d72aad069d"
integrity sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==
sass-loader@^7.1.0, sass-loader@~7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.2.0.tgz#e34115239309d15b2527cb62b5dfefb62a96ff7f"
integrity sha512-h8yUWaWtsbuIiOCgR9fd9c2lRXZ2uG+h8Dzg/AGNj+Hg/3TO8+BBAW9mEP+mh8ei+qBKqSJ0F1FLlYjNBc61OA==
dependencies:
clone-deep "^2.0.1"
clone-deep "^4.0.1"
loader-utils "^1.0.1"
lodash.tail "^4.1.1"
neo-async "^2.5.0"
pify "^3.0.0"
pify "^4.0.1"
semver "^5.5.0"
sass-resources-loader@^2.0.0:
@ -13313,14 +13262,12 @@ sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8:
inherits "^2.0.1"
safe-buffer "^5.0.1"
shallow-clone@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571"
integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==
shallow-clone@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
dependencies:
is-extendable "^0.1.1"
kind-of "^5.0.0"
mixin-object "^2.0.1"
kind-of "^6.0.2"
shallow-equal@^1.1.0:
version "1.2.0"
@ -14459,16 +14406,11 @@ tsconfig@^7.0.0:
strip-bom "^3.0.0"
strip-json-comments "^2.0.0"
tslib@^1:
tslib@^1, tslib@^1.9.0, tslib@^1.9.3:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
tslib@^1.9.0, tslib@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@ -14945,10 +14887,9 @@ vue-hot-reload-api@^2.3.0:
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz#2756f46cb3258054c5f4723de8ae7e87302a1ccf"
integrity sha512-KmvZVtmM26BQOMK1rwUZsrqxEGeKiYSZGA7SNWE6uExx8UX/cj9hq2MRV/wWC3Cq6AoeDGk57rL9YMFRel/q+g==
vue-izitoast@1.1.2:
vue-izitoast@roschaefer/vue-izitoast#patch-1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vue-izitoast/-/vue-izitoast-1.1.2.tgz#0cf8290f045f8a389ccce4c238836c75a130eb03"
integrity sha512-/sNVrYhFg7Moyny5tFNt2e7TTmgPB1xyy04BChKQJkN5r9/D/6vYI7KQWEtG+v9VofnIVg5Em7HXtOL8IOeT7w==
resolved "https://codeload.github.com/roschaefer/vue-izitoast/tar.gz/c246fd78b1964c71b1889683379902d8d6284280"
dependencies:
izitoast "^1.3.0"
@ -15045,10 +14986,10 @@ vue-svg-loader@~0.12.0:
loader-utils "^1.2.3"
svg-to-vue "^0.4.0"
vue-sweetalert-icons@~4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/vue-sweetalert-icons/-/vue-sweetalert-icons-4.0.0.tgz#49dfda05b7f8539288734d7a110d9a6ab53fd324"
integrity sha512-C1VJpLpUSBn387VNcaBAPfsqnHdRSvJmCascLFWHrs0AXtOKEbG+XiRIHnR/K7IR3SinASPM/uBmSHFsES/PEw==
vue-sweetalert-icons@~4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/vue-sweetalert-icons/-/vue-sweetalert-icons-4.2.0.tgz#218d2b151ef1d364f5d147f87f03aacd92c1730f"
integrity sha512-RNnWgdzui9mQ8bwRlJ7HkOEfAEZhTXdpIdXT8pcesFWg1y13UnqjUVvgdg8K6kqPHuVUfipMLjbewrHHjewTmg==
dependencies:
node-sass "^4.12.0"
sass-loader "^7.1.0"
@ -15607,14 +15548,6 @@ yn@^3.0.0:
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.0.tgz#fcbe2db63610361afcc5eb9e0ac91e976d046114"
integrity sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg==
zen-observable-ts@^0.8.18:
version "0.8.18"
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.18.tgz#ade44b1060cc4a800627856ec10b9c67f5f639c8"
integrity sha512-q7d05s75Rn1j39U5Oapg3HI2wzriVwERVo4N7uFGpIYuHB9ff02P/E92P9B8T7QVC93jCMHpbXH7X0eVR5LA7A==
dependencies:
tslib "^1.9.3"
zen-observable "^0.8.0"
zen-observable-ts@^0.8.19:
version "0.8.19"
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.19.tgz#c094cd20e83ddb02a11144a6e2a89706946b5694"

View File

@ -1847,10 +1847,10 @@ cucumber@^4.2.1:
util-arity "^1.0.2"
verror "^1.9.0"
cypress-cucumber-preprocessor@^1.13.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-1.13.0.tgz#efacd70ce21c7d0adc60e25af166f5fb2e990fb8"
integrity sha512-Y3B4El3oYqKUvEhfn7k7NrX/hMJvOCJIO+sgMbvvPXsUngzLWUdiS2LOAaSxpV4t2BCyFuvfzGH0j+C3tu4UvA==
cypress-cucumber-preprocessor@^1.13.1:
version "1.13.1"
resolved "https://registry.yarnpkg.com/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-1.13.1.tgz#d33350343a617c7579e1fed16e169d0a23b18d7a"
integrity sha512-gNmSVTmSVbUftvdTk0MnGGERwKTxtEQ1CwUOK4ujv5kANX29eV3XH9MYMe6gZQlVbLZN9kxz1EhopRF2bqmcwg==
dependencies:
"@cypress/browserify-preprocessor" "^1.1.2"
chai "^4.1.2"