Merge branch 'master' into 2019/kw22/refactor_backend_structure

# Conflicts:
#	backend/src/middleware/index.js
#	backend/src/schema/resolvers/comments.spec.js
#	backend/src/schema/resolvers/socialMedia.spec.js
This commit is contained in:
Ulf Gebhardt 2019-06-03 19:48:47 +02:00
commit 6948666cfe
No known key found for this signature in database
GPG Key ID: 44C888923CC8E7F3
86 changed files with 1366 additions and 1389 deletions

View File

@ -47,7 +47,7 @@
"apollo-client": "~2.5.1",
"apollo-link-context": "~1.0.14",
"apollo-link-http": "~1.5.14",
"apollo-server": "~2.5.1",
"apollo-server": "~2.6.1",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
@ -88,7 +88,7 @@
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.4.5",
"@babel/register": "~7.4.4",
"apollo-server-testing": "~2.5.1",
"apollo-server-testing": "~2.6.1",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.1",
"babel-jest": "~24.8.0",

View File

@ -1,42 +1,63 @@
import activityPubMiddleware from './activityPubMiddleware'
import passwordMiddleware from './passwordMiddleware'
import softDeleteMiddleware from './softDeleteMiddleware'
import sluggifyMiddleware from './sluggifyMiddleware'
import fixImageUrlsMiddleware from './fixImageUrlsMiddleware'
import excerptMiddleware from './excerptMiddleware'
import dateTimeMiddleware from './dateTimeMiddleware'
import xssMiddleware from './xssMiddleware'
import permissionsMiddleware from './permissionsMiddleware'
import userMiddleware from './userMiddleware'
import includedFieldsMiddleware from './includedFieldsMiddleware'
import orderByMiddleware from './orderByMiddleware'
import validationMiddleware from './validation'
import notificationsMiddleware from './notifications'
import CONFIG from './../config'
import activityPub from './activityPubMiddleware'
import password from './passwordMiddleware'
import softDelete from './softDeleteMiddleware'
import sluggify from './sluggifyMiddleware'
import fixImageUrls from './fixImageUrlsMiddleware'
import excerpt from './excerptMiddleware'
import dateTime from './dateTimeMiddleware'
import xss from './xssMiddleware'
import permissions from './permissionsMiddleware'
import user from './userMiddleware'
import includedFields from './includedFieldsMiddleware'
import orderBy from './orderByMiddleware'
import validation from './validation'
import notifications from './notifications'
export default schema => {
let middleware = [
passwordMiddleware,
dateTimeMiddleware,
validationMiddleware,
sluggifyMiddleware,
excerptMiddleware,
notificationsMiddleware,
xssMiddleware,
fixImageUrlsMiddleware,
softDeleteMiddleware,
userMiddleware,
includedFieldsMiddleware,
orderByMiddleware,
const middlewares = {
permissions: permissions,
activityPub: activityPub,
password: password,
dateTime: dateTime,
validation: validation,
sluggify: sluggify,
excerpt: excerpt,
notifications: notifications,
xss: xss,
fixImageUrls: fixImageUrls,
softDelete: softDelete,
user: user,
includedFields: includedFields,
orderBy: orderBy,
}
let order = [
'permissions',
'activityPub',
'password',
'dateTime',
'validation',
'sluggify',
'excerpt',
'notifications',
'xss',
'fixImageUrls',
'softDelete',
'user',
'includedFields',
'orderBy',
]
// add permisions middleware at the first position (unless we're seeding)
// NOTE: DO NOT SET THE PERMISSION FLAT YOUR SELF
if (CONFIG.DEBUG) {
const disabled = CONFIG.DISABLED_MIDDLEWARES.split(',')
if (!disabled.includes('activityPub')) middleware.unshift(activityPubMiddleware)
if (!disabled.includes('permissions'))
middleware.unshift(permissionsMiddleware.generate(schema))
const disabledMiddlewares = CONFIG.DISABLED_MIDDLEWARES.split(',')
order = order.filter(key => {
return !disabledMiddlewares.includes(key)
})
/* eslint-disable-next-line no-console */
console.log(`Warning: "${disabledMiddlewares}" middlewares have been disabled.`)
}
return middleware
return order.map(key => middlewares[key])
}

View File

@ -16,11 +16,15 @@ const isAdmin = rule()(async (parent, args, { user }, info) => {
return user && user.role === 'admin'
})
const isMyOwn = rule({ cache: 'no_cache' })(async (parent, args, context, info) => {
const isMyOwn = rule({
cache: 'no_cache',
})(async (parent, args, context, info) => {
return context.user.id === parent.id
})
const belongsToMe = rule({ cache: 'no_cache' })(async (_, args, context) => {
const belongsToMe = rule({
cache: 'no_cache',
})(async (_, args, context) => {
const {
driver,
user: { id: userId },
@ -32,7 +36,10 @@ const belongsToMe = rule({ cache: 'no_cache' })(async (_, args, context) => {
MATCH (u:User {id: $userId})<-[:NOTIFIED]-(n:Notification {id: $notificationId})
RETURN n
`,
{ userId, notificationId },
{
userId,
notificationId,
},
)
const [notification] = result.records.map(record => {
return record.get('n')
@ -41,12 +48,16 @@ const belongsToMe = rule({ cache: 'no_cache' })(async (_, args, context) => {
return Boolean(notification)
})
const onlyEnabledContent = rule({ cache: 'strict' })(async (parent, args, ctx, info) => {
const onlyEnabledContent = rule({
cache: 'strict',
})(async (parent, args, ctx, info) => {
const { disabled, deleted } = args
return !(disabled || deleted)
})
const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver }) => {
const isAuthor = rule({
cache: 'no_cache',
})(async (parent, args, { user, driver }) => {
if (!user) return false
const session = driver.session()
const { id: postId } = args
@ -55,7 +66,9 @@ const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver
MATCH (post:Post {id: $postId})<-[:WROTE]-(author)
RETURN author
`,
{ postId },
{
postId,
},
)
const [author] = result.records.map(record => {
return record.get('author')
@ -100,6 +113,7 @@ const permissions = shield({
enable: isModerator,
disable: isModerator,
CreateComment: isAuthenticated,
DeleteComment: isAuthenticated,
// CreateUser: allow,
},
User: {

View File

@ -53,6 +53,11 @@ export default {
)
session.close()
return comment
},
DeleteComment: async (object, params, context, resolveInfo) => {
const comment = await neo4jgraphql(object, params, context, resolveInfo, false)
return comment
},
},

View File

@ -1,3 +1,4 @@
import gql from 'graphql-tag'
import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
@ -5,6 +6,7 @@ import { host, login } from '../../jest/helpers'
const factory = Factory()
let client
let createCommentVariables
let deleteCommentVariables
let createPostVariables
let createCommentVariablesSansPostId
let createCommentVariablesWithNonExistentPost
@ -21,22 +23,22 @@ afterEach(async () => {
})
describe('CreateComment', () => {
const createCommentMutation = `
mutation($postId: ID, $content: String!) {
CreateComment(postId: $postId, content: $content) {
id
content
const createCommentMutation = gql`
mutation($postId: ID, $content: String!) {
CreateComment(postId: $postId, content: $content) {
id
content
}
}
}
`
const createPostMutation = `
mutation($id: ID!, $title: String!, $content: String!) {
CreatePost(id: $id, title: $title, content: $content) {
id
const createPostMutation = gql`
mutation($id: ID!, $title: String!, $content: String!) {
CreatePost(id: $id, title: $title, content: $content) {
id
}
}
}
`
const commentQueryForPostId = `
const commentQueryForPostId = gql`
query($content: String) {
Comment(content: $content) {
postId
@ -59,8 +61,13 @@ describe('CreateComment', () => {
describe('authenticated', () => {
let headers
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
headers = await login({
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
createCommentVariables = {
postId: 'p1',
content: "I'm authorised to comment",
@ -88,15 +95,25 @@ describe('CreateComment', () => {
it('assigns the authenticated user as author', async () => {
await client.request(createCommentMutation, createCommentVariables)
const { User } = await client.request(`{
const { User } = await client.request(gql`
{
User(email: "test@example.org") {
comments {
content
}
}
}`)
}
`)
expect(User).toEqual([{ comments: [{ content: "I'm authorised to comment" }] }])
expect(User).toEqual([
{
comments: [
{
content: "I'm authorised to comment",
},
],
},
])
})
it('throw an error if an empty string is sent from the editor as content', async () => {
@ -186,7 +203,92 @@ describe('CreateComment', () => {
commentQueryForPostId,
commentQueryVariablesByContent,
)
expect(Comment).toEqual([{ postId: null }])
expect(Comment).toEqual([
{
postId: null,
},
])
})
})
})
describe('DeleteComment', () => {
const createCommentMutation = gql`
mutation($postId: ID, $content: String!) {
CreateComment(postId: $postId, content: $content) {
id
content
}
}
`
const deleteCommentMutation = gql`
mutation($id: ID!) {
DeleteComment(id: $id) {
id
}
}
`
const createPostMutation = gql`
mutation($id: ID!, $title: String!, $content: String!) {
CreatePost(id: $id, title: $title, content: $content) {
id
}
}
`
describe('unauthenticated', () => {
it('throws authorization error', async () => {
deleteCommentVariables = {
id: 'c1',
}
client = new GraphQLClient(host)
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow(
'Not Authorised',
)
})
})
describe('authenticated', () => {
let headers
beforeEach(async () => {
headers = await login({
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
createCommentVariables = {
id: 'c1',
postId: 'p1',
content: "I'm authorised to comment",
}
deleteCommentVariables = {
id: 'c1',
}
createPostVariables = {
id: 'p1',
title: 'post to comment on',
content: 'please comment on me',
}
await client.request(createPostMutation, createPostVariables)
})
it('deletes the authors comment', async () => {
const { CreateComment } = await client.request(createCommentMutation, createCommentVariables)
deleteCommentVariables = {
id: CreateComment.id,
}
const expected = {
DeleteComment: {
id: CreateComment.id,
},
}
await expect(
client.request(deleteCommentMutation, deleteCommentVariables),
).resolves.toMatchObject(expected)
})
it.todo('throws an error if it tries to delete a comment not from this author')
})
})

View File

@ -1,13 +1,15 @@
import gql from 'graphql-tag'
import { GraphQLClient } from 'graphql-request'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory()
describe('CreateSocialMedia', () => {
describe('SocialMedia', () => {
let client
let headers
const mutationC = `
const mutationC = gql`
mutation($url: String!) {
CreateSocialMedia(url: $url) {
id
@ -15,7 +17,7 @@ describe('CreateSocialMedia', () => {
}
}
`
const mutationD = `
const mutationD = gql`
mutation($id: ID!) {
DeleteSocialMedia(id: $id) {
id
@ -42,19 +44,28 @@ describe('CreateSocialMedia', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
const variables = { url: 'http://nsosp.org' }
const variables = {
url: 'http://nsosp.org',
}
await expect(client.request(mutationC, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated', () => {
beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers })
headers = await login({
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
})
it('creates social media with correct URL', async () => {
const variables = { url: 'http://nsosp.org' }
const variables = {
url: 'http://nsosp.org',
}
await expect(client.request(mutationC, variables)).resolves.toEqual(
expect.objectContaining({
CreateSocialMedia: {
@ -66,11 +77,15 @@ describe('CreateSocialMedia', () => {
})
it('deletes social media', async () => {
const creationVariables = { url: 'http://nsosp.org' }
const creationVariables = {
url: 'http://nsosp.org',
}
const { CreateSocialMedia } = await client.request(mutationC, creationVariables)
const { id } = CreateSocialMedia
const deletionVariables = { id }
const deletionVariables = {
id,
}
const expected = {
DeleteSocialMedia: {
id: id,
@ -81,12 +96,16 @@ describe('CreateSocialMedia', () => {
})
it('rejects empty string', async () => {
const variables = { url: '' }
const variables = {
url: '',
}
await expect(client.request(mutationC, variables)).rejects.toThrow('Input is not a URL')
})
it('validates URLs', async () => {
const variables = { url: 'not-a-url' }
const variables = {
url: 'not-a-url',
}
await expect(client.request(mutationC, variables)).rejects.toThrow('Input is not a URL')
})
})

View File

@ -9,13 +9,6 @@
dependencies:
apollo-env "0.5.1"
"@apollographql/apollo-tools@^0.3.6-alpha.1":
version "0.3.6-alpha.1"
resolved "https://registry.yarnpkg.com/@apollographql/apollo-tools/-/apollo-tools-0.3.6-alpha.1.tgz#5199b36c65c2fddc4f8bc8bb97642f74e9fb00c5"
integrity sha512-fq74In3Vw9OmtKHze0L5/Ns/pdTZOqUeFVC6Um9NRgziVehXz/qswsh2r3+dsn82uqoa/AlvckHtd6aPPPYj9g==
dependencies:
apollo-env "0.4.1-alpha.1"
"@apollographql/graphql-playground-html@1.6.20":
version "1.6.20"
resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.20.tgz#bf9f2acdf319c0959fad8ec1239741dd2ead4e8d"
@ -1288,13 +1281,13 @@ anymatch@^2.0.0:
micromatch "^3.1.4"
normalize-path "^2.1.1"
apollo-cache-control@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.6.1.tgz#c73ff521fe606faf18edcbd3463c421a966f3e5d"
integrity sha512-M3cDeQDXtRxYPQ/sL4pu3IVE5Q/9jpBlENB2IjwxTDir+WFZbJV1CAnvVwyJdL1DvS6ESR35DFOurJH4Ws/OPA==
apollo-cache-control@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.7.1.tgz#3d4fba232f561f096f61051e103bf58ee4bf8b54"
integrity sha512-3h1TEoMnzex6IIiFb5Ja3owTyLwT5YzK69cRgrSpSscdpYc3ID4KVs0Ht9cbOUmb/L/UKtYVkRC8KeVAYmHEjQ==
dependencies:
apollo-server-env "2.3.0"
graphql-extensions "0.6.1"
apollo-server-env "2.4.0"
graphql-extensions "0.7.1"
apollo-cache-control@^0.1.0:
version "0.1.1"
@ -1345,13 +1338,13 @@ apollo-client@~2.5.1:
tslib "^1.9.3"
zen-observable "^0.8.0"
apollo-datasource@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.4.0.tgz#f042641fd2593fa5f4f002fc30d1fb1a20284df8"
integrity sha512-6QkgnLYwQrW0qv+yXIf617DojJbGmza2XJXUlgnzrGGhxzfAynzEjaLyYkc8rYS1m82vjrl9EOmLHTcnVkvZAQ==
apollo-datasource@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.5.0.tgz#7a8c97e23da7b9c15cb65103d63178ab19eca5e9"
integrity sha512-SVXxJyKlWguuDjxkY/WGlC/ykdsTmPxSF0z8FenagcQ91aPURXzXP1ZDz5PbamY+0iiCRubazkxtTQw4GWTFPg==
dependencies:
apollo-server-caching "0.4.0"
apollo-server-env "2.3.0"
apollo-server-env "2.4.0"
apollo-engine-reporting-protobuf@0.3.0:
version "0.3.0"
@ -1360,26 +1353,17 @@ apollo-engine-reporting-protobuf@0.3.0:
dependencies:
protobufjs "^6.8.6"
apollo-engine-reporting@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.1.1.tgz#f5a3240bc5c5afb210ff8c45d72995de7b0d2a13"
integrity sha512-K7BDsj99jr8ftd9NIuHL4oF/S7CBFcgMGjL0ChhfxpkgUv80FPxJ+9Fs+9ZkKIVylV3PCi2WnihpDeEO10eZAw==
apollo-engine-reporting@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.2.1.tgz#0b77fad2e9221d62f4a29b8b4fab8f7f47dcc1d6"
integrity sha512-DVXZhz/nSZR4lphakjb1guAD0qJ7Wm1PVtZEBjN097cnOor4XSOzQlPfTaYtVuhlxUKUuCx1syiBbOuV8sKqXg==
dependencies:
apollo-engine-reporting-protobuf "0.3.0"
apollo-graphql "^0.2.1-alpha.1"
apollo-server-core "2.5.1"
apollo-server-env "2.3.0"
apollo-graphql "^0.3.0"
apollo-server-core "2.6.1"
apollo-server-env "2.4.0"
async-retry "^1.2.1"
graphql-extensions "0.6.1"
apollo-env@0.4.1-alpha.1:
version "0.4.1-alpha.1"
resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.4.1-alpha.1.tgz#10d3ea508b8f3ba03939ef4e6ec4b2b5db77e8f1"
integrity sha512-4qWiaUKWh92jvKxxRsiZSjmW9YH9GWSG1W6X+S1BcC1uqtPiHsem7ExG9MMTt+UrzHsbpQLctj12xk8lI4lgCg==
dependencies:
core-js "3.0.0-beta.13"
node-fetch "^2.2.0"
sha.js "^2.4.11"
graphql-extensions "0.7.1"
apollo-env@0.5.1:
version "0.5.1"
@ -1398,12 +1382,12 @@ apollo-errors@^1.9.0:
assert "^1.4.1"
extendable-error "^0.1.5"
apollo-graphql@^0.2.1-alpha.1:
version "0.2.1-alpha.1"
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.2.1-alpha.1.tgz#a0cc0bd65e03c7e887c96c9f53421f3c6dd7b599"
integrity sha512-kObCSpYRHEf4IozJV+TZAXEL2Yni2DpzQckohJNYXg5/KRAF20jJ7lHxuJz+kMQrc7QO4wYGSa29HuFZH2AtQA==
apollo-graphql@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.1.tgz#d13b80cc0cae3fe7066b81b80914c6f983fac8d7"
integrity sha512-tbhtzNAAhNI34v4XY9OlZGnH7U0sX4BP1cJrUfSiNzQnZRg1UbQYZ06riHSOHpi5RSndFcA9LDM5C1ZKKOUeBg==
dependencies:
apollo-env "0.4.1-alpha.1"
apollo-env "0.5.1"
lodash.sortby "^4.7.0"
apollo-link-context@~1.0.14:
@ -1456,24 +1440,24 @@ apollo-server-caching@0.4.0:
dependencies:
lru-cache "^5.0.0"
apollo-server-core@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.5.1.tgz#0fdb6cfca56a0f5b5b3aecffb48db17b3c8e1d71"
integrity sha512-4QNrW1AUM3M/p0+hbBX/MsjSjZTy+2rt7JpiKKkG9RmeEIzd/VG7hwwwloAZSLjYx3twz0+BnASJ9y+rGEPC8A==
apollo-server-core@2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.1.tgz#d0d878b0a4959b6c661fc43300ce45b29996176a"
integrity sha512-jO2BtcP7ozSSK5qtw1gGDwO66WSNtzhvpDJD7erkA9byv8Z0jB2QIPNWN6iaj311LaPahM05k+8hMIhFy9oHWg==
dependencies:
"@apollographql/apollo-tools" "^0.3.6"
"@apollographql/graphql-playground-html" "1.6.20"
"@types/ws" "^6.0.0"
apollo-cache-control "0.6.1"
apollo-datasource "0.4.0"
apollo-engine-reporting "1.1.1"
apollo-cache-control "0.7.1"
apollo-datasource "0.5.0"
apollo-engine-reporting "1.2.1"
apollo-server-caching "0.4.0"
apollo-server-env "2.3.0"
apollo-server-env "2.4.0"
apollo-server-errors "2.3.0"
apollo-server-plugin-base "0.4.1"
apollo-tracing "0.6.1"
apollo-server-plugin-base "0.5.1"
apollo-tracing "0.7.1"
fast-json-stable-stringify "^2.0.0"
graphql-extensions "0.6.1"
graphql-extensions "0.7.1"
graphql-subscriptions "^1.0.0"
graphql-tag "^2.9.2"
graphql-tools "^4.0.0"
@ -1491,10 +1475,10 @@ apollo-server-core@^1.3.6, apollo-server-core@^1.4.0:
apollo-tracing "^0.1.0"
graphql-extensions "^0.0.x"
apollo-server-env@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.3.0.tgz#f0bf4484a6cc331a8c13763ded56e91beb16ba17"
integrity sha512-WIwlkCM/gir0CkoYWPMTCH8uGCCKB/aM074U1bKayvkFOBVO2VgG5x2kgsfkyF05IMQq2/GOTsKhNY7RnUEhTA==
apollo-server-env@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.0.tgz#6611556c6b627a1636eed31317d4f7ea30705872"
integrity sha512-7ispR68lv92viFeu5zsRUVGP+oxsVI3WeeBNniM22Cx619maBUwcYTIC3+Y3LpXILhLZCzA1FASZwusgSlyN9w==
dependencies:
node-fetch "^2.1.2"
util.promisify "^1.0.0"
@ -1504,10 +1488,10 @@ apollo-server-errors@2.3.0:
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.0.tgz#700622b66a16dffcad3b017e4796749814edc061"
integrity sha512-rUvzwMo2ZQgzzPh2kcJyfbRSfVKRMhfIlhY7BzUfM4x6ZT0aijlgsf714Ll3Mbf5Fxii32kD0A/DmKsTecpccw==
apollo-server-express@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.5.1.tgz#b112d9795f2fb39076d9cbc109f5eeb7835bed6b"
integrity sha512-528wDQnOMIenDaICkYPFWQykdXQZwpygxd+Ar0PmZiaST042NSVExV4iRWI09p1THqfsuyHygqpkK+K94bUtBA==
apollo-server-express@2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.1.tgz#1e2649d3fd38c0c0a2c830090fd41e086b259c9f"
integrity sha512-TVu68LVp+COMGOXuxc0OFeCUQiPApxy7Isv2Vk85nikZV4t4FXlODB6PrRKf5rfvP31dvGsfE6GHPJTLLbKfyg==
dependencies:
"@apollographql/graphql-playground-html" "1.6.20"
"@types/accepts" "^1.3.5"
@ -1515,7 +1499,7 @@ apollo-server-express@2.5.1:
"@types/cors" "^2.8.4"
"@types/express" "4.16.1"
accepts "^1.3.5"
apollo-server-core "2.5.1"
apollo-server-core "2.6.1"
body-parser "^1.18.3"
cors "^2.8.4"
graphql-subscriptions "^1.0.0"
@ -1543,36 +1527,36 @@ apollo-server-module-graphiql@^1.3.4, apollo-server-module-graphiql@^1.4.0:
resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz#c559efa285578820709f1769bb85d3b3eed3d8ec"
integrity sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA==
apollo-server-plugin-base@0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.4.1.tgz#be380b28d71ad3b6b146d0d6a8f7ebf5675b07ff"
integrity sha512-D2G6Ca/KBdQgEbmSfYqZqYbdVJnk/rrSv7Vj2NntwjfL7WJf0TjufxYJlrTH5jF6xCbsszDNGqfmt2Nm8x/o4g==
apollo-server-plugin-base@0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.1.tgz#b81056666763879bdc98d8d58f3c4c43cbb30da6"
integrity sha512-UejnBk6XDqYQ+Ydkbm+gvlOzP+doQA8glVUULs8rCi0/MshsFSsBVl6rtzruELDBVgZhJgGsd4pUexcvNc3aZA==
apollo-server-testing@~2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.5.1.tgz#9f210caa738a275a30269b6d8a3b29bce01b69a1"
integrity sha512-npaj92Z33sNt4beBuoLJqlicqtawqI/41CA9/IrcswI9WwlWIxNKhfpcMYo9MpQWRT8aCIGOOAkBdG2jZlhH4Q==
apollo-server-testing@~2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.6.1.tgz#447f34980819fa52b120f26c632fab4efc55435b"
integrity sha512-Qq0u79uKw3g14bq0nGxtUUiueFOv2ETkAax2mum+3f9Lm85VXELkY6c9bCWDVGjkUtt9Aog5qwSzWELb1KiUug==
dependencies:
apollo-server-core "2.5.1"
apollo-server-core "2.6.1"
apollo-server@~2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.5.1.tgz#bfcfbebc123f692c0e6d85b0c56739646bd1bb7e"
integrity sha512-eH3ubq300xhpFAxek28kb+5WZINXpWcwzyNqBQDbuasTlW8qSsqY7xrV6IIz6WUYKdX+ET0mx+Ta1DdaYQPrqw==
apollo-server@~2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.1.tgz#1b1fc6020b75c0913550da5fa0f2005c62f1bc53"
integrity sha512-Ed0zZjluRYPMC3Yr6oXQjcR11izu86nkjiS2MhjJA1mF8IXJfxbPp2hnX4Jf4vXPSkOP2e5ZHw0cdaIcu9GnRw==
dependencies:
apollo-server-core "2.5.1"
apollo-server-express "2.5.1"
apollo-server-core "2.6.1"
apollo-server-express "2.6.1"
express "^4.0.0"
graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0"
apollo-tracing@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.6.1.tgz#48a6d6040f9b2f2b4365a890c2e97cb763eb2392"
integrity sha512-rrDBgTHa9GDA3wY8O7rDsFwC6ePIVzRGxpUsThgmLvIVkkCr0KS4wJJ4C01c+v4xsOXNuQwx0IyYhxZt4twwcA==
apollo-tracing@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.7.1.tgz#6a7356b619f3aa0ca22c623b5d8bb1af5ca1c74c"
integrity sha512-1BYQua+jCWFkZZJP0/rSpzY4XbLLbCrRHCYu8sJn0RCH/hs34BMdFXscS9uSglgIpXwUAIafgsU0hAVCrJjbTw==
dependencies:
apollo-server-env "2.3.0"
graphql-extensions "0.6.1"
apollo-server-env "2.4.0"
graphql-extensions "0.7.1"
apollo-tracing@^0.1.0:
version "0.1.4"
@ -2441,11 +2425,6 @@ core-js-pure@3.1.2:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.1.2.tgz#62fc435f35b7374b9b782013cdcb2f97e9f6dffa"
integrity sha512-5ckIdBF26B3ldK9PM177y2ZcATP2oweam9RskHSoqfZCrJ2As6wVg8zJ1zTriFsZf6clj/N1ThDFRGaomMsh9w==
core-js@3.0.0-beta.13:
version "3.0.0-beta.13"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.0-beta.13.tgz#7732c69be5e4758887917235fe7c0352c4cb42a1"
integrity sha512-16Q43c/3LT9NyePUJKL8nRIQgYWjcBhjJSMWg96PVSxoS0PeE0NHitPI3opBrs9MGGHjte1KoEVr9W63YKlTXQ==
core-js@^2.4.0, core-js@^2.5.3, core-js@^2.5.7:
version "2.6.2"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.2.tgz#267988d7268323b349e20b4588211655f0e83944"
@ -3749,12 +3728,12 @@ graphql-deduplicator@^2.0.1:
resolved "https://registry.yarnpkg.com/graphql-deduplicator/-/graphql-deduplicator-2.0.2.tgz#d8608161cf6be97725e178df0c41f6a1f9f778f3"
integrity sha512-0CGmTmQh4UvJfsaTPppJAcHwHln8Ayat7yXXxdnuWT+Mb1dBzkbErabCWzjXyKh/RefqlGTTA7EQOZHofMaKJA==
graphql-extensions@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.6.1.tgz#e61c4cb901e336dc5993a61093a8678a021dda59"
integrity sha512-vB2WNQJn99pncHfvxgcdyVoazmG3cD8XzkgcaDrHTvV+xJGJEBP6056EWi0mNt1d6ukYyRS2zexdekmMCjcq0w==
graphql-extensions@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.1.tgz#f55b01ac8ddf09a215e21f34caeee3ae66a88f21"
integrity sha512-4NkAz/f0j5a1DSPl3V77OcesBmwhHz56Soj0yTImlcDdRv9knyO2e+ehi1TIeKBOyIKS7d3A7zqOW/4ieGxlVA==
dependencies:
"@apollographql/apollo-tools" "^0.3.6-alpha.1"
"@apollographql/apollo-tools" "^0.3.6"
graphql-extensions@^0.0.x, graphql-extensions@~0.0.9:
version "0.0.10"

View File

@ -12,5 +12,6 @@
# On Windows this resolves to C:\Users\dornhoeschen\AppData\Local\Temp\mongo-export (MinGW)
EXPORT_PATH='/tmp/mongo-export/'
EXPORT_MONGOEXPORT_BIN='mongoexport'
MONGO_EXPORT_SPLIT_SIZE=100
# On Windows use something like this
# EXPORT_MONGOEXPORT_BIN='C:\Program Files\MongoDB\Server\3.6\bin\mongoexport.exe'
# EXPORT_MONGOEXPORT_BIN='C:\Program Files\MongoDB\Server\3.6\bin\mongoexport.exe'

View File

@ -10,7 +10,7 @@ set +o allexport
function export_collection () {
"${EXPORT_MONGOEXPORT_BIN}" --db ${MONGODB_DATABASE} --host localhost -d ${MONGODB_DATABASE} --port 27018 --username ${MONGODB_USERNAME} --password ${MONGODB_PASSWORD} --authenticationDatabase ${MONGODB_AUTH_DB} --collection $1 --collection $1 --out "${EXPORT_PATH}$1.json"
mkdir -p ${EXPORT_PATH}splits/$1/
split -l 1000 -a 3 ${EXPORT_PATH}$1.json ${EXPORT_PATH}splits/$1/
split -l ${MONGO_EXPORT_SPLIT_SIZE} -a 3 ${EXPORT_PATH}$1.json ${EXPORT_PATH}splits/$1/
}
# Delete old export & ensure directory

View File

@ -83,17 +83,17 @@ import_collection "contributions"
import_collection "shouts"
import_collection "comments"
import_collection "emotions"
import_collection "invites"
import_collection "notifications"
import_collection "organizations"
import_collection "pages"
import_collection "projects"
import_collection "settings"
import_collection "status"
import_collection "systemnotifications"
import_collection "userscandos"
import_collection "usersettings"
# import_collection "emotions"
# import_collection "invites"
# import_collection "notifications"
# import_collection "organizations"
# import_collection "pages"
# import_collection "projects"
# import_collection "settings"
# import_collection "status"
# import_collection "systemnotifications"
# import_collection "userscandos"
# import_collection "usersettings"
echo "DONE"

View File

@ -30,6 +30,7 @@ services:
- "MONGODB_AUTH_DB=${MONGODB_AUTH_DB}"
- "MONGODB_DATABASE=${MONGODB_DATABASE}"
- "UPLOADS_DIRECTORY=${UPLOADS_DIRECTORY}"
- "MONGO_EXPORT_SPLIT_SIZE=${MONGO_EXPORT_SPLIT_SIZE}"
ports:
- 7687:7687
- 7474:7474

View File

@ -2,5 +2,7 @@
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-backend:latest $TRAVIS_BUILD_DIR/backend
docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-web:latest $TRAVIS_BUILD_DIR/webapp
docker build -t humanconnection/nitro-maintenance-worker:latest $TRAVIS_BUILD_DIR/deployment/legacy-migration/maintenance-worker
docker push humanconnection/nitro-backend:latest
docker push humanconnection/nitro-web:latest
docker push humanconnection/nitro-maintenance-worker:latest

View File

@ -10,7 +10,7 @@ module.exports = {
},
extends: [
'standard',
'plugin:vue/recommended',
'plugin:vue/essential',
'plugin:prettier/recommended'
],
// required to lint *.vue files
@ -25,7 +25,9 @@ module.exports = {
'no-console': ['error'],
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'vue/component-name-in-template-casing': ['error', 'kebab-case'],
'prettier/prettier': ['error'],
'prettier/prettier': ['error', {
htmlWhitespaceSensitivity: 'ignore'
}],
// 'newline-per-chained-call': [2]
}
}

View File

@ -1,4 +1,4 @@
FROM node:10-alpine as base
FROM node:12.3.1-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 --production=false --frozen-lockfile --non-interactive
RUN yarn install --ignore-engines --production=false --frozen-lockfile --non-interactive
RUN yarn run build
FROM base as production

View File

@ -1,10 +1,5 @@
<template>
<ds-avatar
:image="avatarUrl"
:name="userName"
class="avatar"
:size="size"
/>
<ds-avatar :image="avatarUrl" :name="userName" class="avatar" :size="size" />
</template>
<script>

View File

@ -1,20 +1,7 @@
<template>
<div
:class="[
(badges.length === 2) && 'hc-badges-dual'
]"
class="hc-badges"
>
<div
v-for="badge in badges"
:key="badge.key"
class="hc-badge-container"
>
<hc-image
:title="badge.key"
:image-props="{ src: badge.icon}"
class="hc-badge"
/>
<div :class="[badges.length === 2 && 'hc-badges-dual']" class="hc-badges">
<div v-for="badge in badges" :key="badge.key" class="hc-badge-container">
<hc-image :title="badge.key" :image-props="{ src: badge.icon }" class="hc-badge" />
</div>
</div>
</template>

View File

@ -1,9 +1,6 @@
<template>
<ds-tag>
<ds-icon
size="large"
:name="icon"
/>
<ds-icon size="large" :name="icon" />
{{ name }}
</ds-tag>
</template>

View File

@ -14,11 +14,20 @@ describe('Comment.vue', () => {
let propsData
let mocks
let getters
let wrapper
let Wrapper
beforeEach(() => {
propsData = {}
mocks = {
$t: jest.fn(),
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$apollo: {
mutate: jest.fn().mockResolvedValue(),
},
}
getters = {
'auth/user': () => {
@ -29,11 +38,16 @@ describe('Comment.vue', () => {
})
describe('shallowMount', () => {
const Wrapper = () => {
Wrapper = () => {
const store = new Vuex.Store({
getters,
})
return shallowMount(Comment, { store, propsData, mocks, localVue })
return shallowMount(Comment, {
store,
propsData,
mocks,
localVue,
})
}
describe('given a comment', () => {
@ -45,7 +59,7 @@ describe('Comment.vue', () => {
})
it('renders content', () => {
const wrapper = Wrapper()
wrapper = Wrapper()
expect(wrapper.text()).toMatch('Hello I am a comment content')
})
@ -55,17 +69,17 @@ describe('Comment.vue', () => {
})
it('renders no comment data', () => {
const wrapper = Wrapper()
wrapper = Wrapper()
expect(wrapper.text()).not.toMatch('comment content')
})
it('has no "disabled-content" css class', () => {
const wrapper = Wrapper()
wrapper = Wrapper()
expect(wrapper.classes()).not.toContain('disabled-content')
})
it('translates a placeholder', () => {
/* const wrapper = */ Wrapper()
wrapper = Wrapper()
const calls = mocks.$t.mock.calls
const expected = [['comment.content.unavailable-placeholder']]
expect(calls).toEqual(expect.arrayContaining(expected))
@ -77,16 +91,46 @@ describe('Comment.vue', () => {
})
it('renders comment data', () => {
const wrapper = Wrapper()
wrapper = Wrapper()
expect(wrapper.text()).toMatch('comment content')
})
it('has a "disabled-content" css class', () => {
const wrapper = Wrapper()
wrapper = Wrapper()
expect(wrapper.classes()).toContain('disabled-content')
})
})
})
beforeEach(jest.useFakeTimers)
describe('test callbacks', () => {
beforeEach(() => {
wrapper = Wrapper()
})
describe('deletion of Comment from List by invoking "deleteCommentCallback()"', () => {
beforeEach(() => {
wrapper.vm.deleteCommentCallback()
})
describe('after timeout', () => {
beforeEach(jest.runAllTimers)
it('emits "deleteComment"', () => {
expect(wrapper.emitted().deleteComment.length).toBe(1)
})
it('does call mutation', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
it('mutation is successful', () => {
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
})
})
})
})
})
})

View File

@ -1,17 +1,11 @@
<template>
<div v-if="(comment.deleted || comment.disabled) && !isModerator">
<ds-text
style="padding-left: 40px; font-weight: bold;"
color="soft"
>
<ds-text style="padding-left: 40px; font-weight: bold;" color="soft">
<ds-icon name="ban" />
{{ this.$t('comment.content.unavailable-placeholder') }}
</ds-text>
</div>
<div
v-else
:class="{'comment': true, 'disabled-content': (comment.deleted || comment.disabled)}"
>
<div v-else :class="{ comment: true, 'disabled-content': comment.deleted || comment.disabled }">
<ds-space margin-bottom="x-small">
<hc-user :user="author" />
</ds-space>
@ -20,6 +14,7 @@
placement="bottom-end"
resource-type="comment"
:resource="comment"
:callbacks="{ confirm: deleteCommentCallback, cancel: null }"
style="float-right"
:is-owner="isAuthor(author.id)"
/>
@ -27,15 +22,13 @@
<!-- eslint-disable vue/no-v-html -->
<!-- TODO: replace editor content with tiptap render view -->
<ds-space margin-bottom="small" />
<div
style="padding-left: 40px;"
v-html="comment.contentExcerpt"
/>
<div style="padding-left: 40px;" v-html="comment.contentExcerpt" />
<!-- eslint-enable vue/no-v-html -->
</div>
</template>
<script>
import gql from 'graphql-tag'
import { mapGetters } from 'vuex'
import HcUser from '~/components/User'
import ContentMenu from '~/components/ContentMenu'
@ -70,6 +63,25 @@ export default {
isAuthor(id) {
return this.user.id === id
},
async deleteCommentCallback() {
try {
var gqlMutation = gql`
mutation($id: ID!) {
DeleteComment(id: $id) {
id
}
}
`
await this.$apollo.mutate({
mutation: gqlMutation,
variables: { id: this.comment.id },
})
this.$toast.success(this.$t(`delete.comment.success`))
this.$emit('deleteComment')
} catch (err) {
this.$toast.error(err.message)
}
},
},
}
</script>

View File

@ -1,32 +1,13 @@
<template>
<dropdown
class="content-menu"
:placement="placement"
offset="5"
>
<template
slot="default"
slot-scope="{toggleMenu}"
>
<slot
name="button"
:toggleMenu="toggleMenu"
>
<ds-button
class="content-menu-trigger"
size="small"
ghost
@click.prevent="toggleMenu"
>
<dropdown class="content-menu" :placement="placement" offset="5">
<template slot="default" slot-scope="{ toggleMenu }">
<slot name="button" :toggleMenu="toggleMenu">
<ds-button class="content-menu-trigger" size="small" ghost @click.prevent="toggleMenu">
<ds-icon name="ellipsis-v" />
</ds-button>
</slot>
</template>
<div
slot="popover"
slot-scope="{toggleMenu}"
class="content-menu-popover"
>
<div slot="popover" slot-scope="{ toggleMenu }" class="content-menu-popover">
<ds-menu :routes="routes">
<ds-menu-item
slot="menuitem"
@ -47,6 +28,7 @@
import Dropdown from '~/components/Dropdown'
export default {
name: 'ContentMenu',
components: {
Dropdown,
},
@ -61,6 +43,7 @@ export default {
return value.match(/(contribution|comment|organization|user)/)
},
},
callbacks: { type: Object, required: true },
},
computed: {
routes() {
@ -68,7 +51,7 @@ export default {
if (this.isOwner && this.resourceType === 'contribution') {
routes.push({
name: this.$t(`contribution.edit`),
name: this.$t(`post.menu.edit`),
path: this.$router.resolve({
name: 'post-edit-id',
params: {
@ -78,21 +61,29 @@ export default {
icon: 'edit',
})
routes.push({
name: this.$t(`post.delete.title`),
name: this.$t(`post.menu.delete`),
callback: () => {
this.openModal('delete')
},
icon: 'trash',
})
}
if (this.isOwner && this.resourceType === 'comment') {
// routes.push({
// name: this.$t(`comment.menu.edit`),
// callback: () => {
// /* eslint-disable-next-line no-console */
// console.log('EDIT COMMENT')
// },
// icon: 'edit'
// })
routes.push({
name: this.$t(`comment.edit`),
name: this.$t(`comment.menu.delete`),
callback: () => {
/* eslint-disable-next-line no-console */
console.log('EDIT COMMENT')
this.openModal('delete')
},
icon: 'edit',
icon: 'trash',
})
}
@ -144,6 +135,7 @@ export default {
data: {
type: this.resourceType,
resource: this.resource,
callbacks: this.callbacks,
},
})
},

View File

@ -1,35 +1,13 @@
<template>
<ds-form
ref="contributionForm"
v-model="form"
:schema="formSchema"
@submit="submit"
>
<ds-form ref="contributionForm" v-model="form" :schema="formSchema" @submit="submit">
<template slot-scope="{ errors }">
<ds-card>
<ds-input
model="title"
class="post-title"
placeholder="Title"
name="title"
autofocus
/>
<ds-input model="title" class="post-title" placeholder="Title" name="title" autofocus />
<no-ssr>
<hc-editor
:users="users"
:value="form.content"
@input="updateEditorContent"
/>
<hc-editor :users="users" :value="form.content" @input="updateEditorContent" />
</no-ssr>
<div
slot="footer"
style="text-align: right"
>
<ds-button
:disabled="loading || disabled"
ghost
@click.prevent="$router.back()"
>
<div slot="footer" style="text-align: right">
<ds-button :disabled="loading || disabled" ghost @click.prevent="$router.back()">
{{ $t('actions.cancel') }}
</ds-button>
<ds-button

View File

@ -1,9 +1,6 @@
<template>
<span>
<no-ssr
placeholder="0"
tag="span"
>
<no-ssr placeholder="0" tag="span">
<count-to
:start-val="lastEndVal || startVal"
:end-val="endVal"

View File

@ -7,17 +7,8 @@
trigger="manual"
:offset="offset"
>
<slot
:toggleMenu="toggleMenu"
:openMenu="openMenu"
:closeMenu="closeMenu"
:isOpen="isOpen"
/>
<div
slot="popover"
@mouseover="popoverMouseEnter"
@mouseleave="popoveMouseLeave"
>
<slot :toggleMenu="toggleMenu" :openMenu="openMenu" :closeMenu="closeMenu" :isOpen="isOpen" />
<div slot="popover" @mouseover="popoverMouseEnter" @mouseleave="popoveMouseLeave">
<slot
name="popover"
:toggleMenu="toggleMenu"

View File

@ -1,10 +1,6 @@
<template>
<div class="editor">
<div
v-show="showSuggestions"
ref="suggestions"
class="suggestion-list"
>
<div v-show="showSuggestions" ref="suggestions" class="suggestion-list">
<template v-if="hasResults">
<div
v-for="(user, index) in filteredUsers"
@ -16,10 +12,7 @@
@{{ user.slug }}
</div>
</template>
<div
v-else
class="suggestion-list__item is-empty"
>
<div v-else class="suggestion-list__item is-empty">
No users found
</div>
</div>
@ -154,10 +147,7 @@
</ds-button>
</div>
</editor-floating-menu>
<editor-content
ref="editor"
:editor="editor"
/>
<editor-content ref="editor" :editor="editor" />
</div>
</template>

View File

@ -1,9 +1,5 @@
<template>
<ds-space
class="hc-empty"
centered
:margin="margin"
>
<ds-space class="hc-empty" centered :margin="margin">
<ds-text>
<img
:src="iconPath"
@ -11,12 +7,9 @@
class="hc-empty-icon"
style="margin-bottom: 5px"
alt="Empty"
><br>
<ds-text
v-show="message"
class="hc-empty-message"
color="softer"
>
/>
<br />
<ds-text v-show="message" class="hc-empty-message" color="softer">
{{ message }}
</ds-text>
</ds-text>

View File

@ -1,8 +1,5 @@
<template>
<img
v-bind="imageProps"
:src="imageSrc"
>
<img v-bind="imageProps" :src="imageSrc" />
</template>
<script>

View File

@ -1,14 +1,6 @@
<template>
<ds-space
margin-top="large"
style="text-align: center"
>
<ds-button
:loading="loading"
icon="arrow-down"
ghost
@click="$emit('click')"
>
<ds-space margin-top="large" style="text-align: center">
<ds-button :loading="loading" icon="arrow-down" ghost @click="$emit('click')">
{{ $t('actions.loadMore') }}
</ds-button>
</ds-space>

View File

@ -1,30 +1,19 @@
<template>
<dropdown
ref="menu"
:placement="placement"
:offset="offset"
>
<dropdown ref="menu" :placement="placement" :offset="offset">
<a
slot="default"
slot-scope="{toggleMenu}"
slot-scope="{ toggleMenu }"
class="locale-menu"
href="#"
@click.prevent="toggleMenu()"
>
<ds-icon
style="margin-right: 2px;"
name="globe"
/>
<ds-icon style="margin-right: 2px;" name="globe" />
{{ current.code.toUpperCase() }}
<ds-icon
style="margin-left: 2px"
size="xx-small"
name="angle-down"
/>
<ds-icon style="margin-left: 2px" size="xx-small" name="angle-down" />
</a>
<ds-menu
slot="popover"
slot-scope="{toggleMenu}"
slot-scope="{ toggleMenu }"
class="locale-menu-popover"
:matcher="matcher"
:routes="routes"

View File

@ -1,5 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Modal from './Modal.vue'
import DeleteModal from './Modal/DeleteModal.vue'
import DisableModal from './Modal/DisableModal.vue'
import ReportModal from './Modal/ReportModal.vue'
import Vuex from 'vuex'
@ -29,7 +30,11 @@ describe('Modal.vue', () => {
'modal/SET_OPEN': mutations.SET_OPEN,
},
})
return mountMethod(Modal, { store, mocks, localVue })
return mountMethod(Modal, {
store,
mocks,
localVue,
})
}
}
@ -55,6 +60,7 @@ describe('Modal.vue', () => {
it('initially empty', () => {
wrapper = Wrapper()
expect(wrapper.contains(DeleteModal)).toBe(false)
expect(wrapper.contains(DisableModal)).toBe(false)
expect(wrapper.contains(ReportModal)).toBe(false)
})
@ -69,6 +75,10 @@ describe('Modal.vue', () => {
id: 'c456',
title: 'some title',
},
callbacks: {
confirm: null,
cancel: null,
},
},
}
wrapper = Wrapper()
@ -83,6 +93,10 @@ describe('Modal.vue', () => {
type: 'contribution',
name: 'some title',
id: 'c456',
callbacks: {
confirm: null,
cancel: null,
},
})
})
@ -97,23 +111,49 @@ describe('Modal.vue', () => {
it('passes author name to disable modal', () => {
state.data = {
type: 'comment',
resource: { id: 'c456', author: { name: 'Author name' } },
resource: {
id: 'c456',
author: {
name: 'Author name',
},
},
callbacks: {
confirm: null,
cancel: null,
},
}
wrapper = Wrapper()
expect(wrapper.find(DisableModal).props()).toEqual({
type: 'comment',
name: 'Author name',
id: 'c456',
callbacks: {
confirm: null,
cancel: null,
},
})
})
it('does not crash if author is undefined', () => {
state.data = { type: 'comment', resource: { id: 'c456' } }
state.data = {
type: 'comment',
resource: {
id: 'c456',
},
callbacks: {
confirm: null,
cancel: null,
},
}
wrapper = Wrapper()
expect(wrapper.find(DisableModal).props()).toEqual({
type: 'comment',
name: '',
id: 'c456',
callbacks: {
confirm: null,
cancel: null,
},
})
})
})

View File

@ -1,10 +1,12 @@
<template>
<div class="modal-wrapper">
<!-- Todo: Put all modals with 2 buttons and equal properties in one customiced 'danger-action-modal' -->
<disable-modal
v-if="open === 'disable'"
:id="data.resource.id"
:type="data.type"
:name="name"
:callbacks="data.callbacks"
@close="close"
/>
<report-modal
@ -12,6 +14,7 @@
:id="data.resource.id"
:type="data.type"
:name="name"
:callbacks="data.callbacks"
@close="close"
/>
<delete-modal
@ -19,15 +22,16 @@
:id="data.resource.id"
:type="data.type"
:name="name"
:callbacks="data.callbacks"
@close="close"
/>
</div>
</template>
<script>
import DeleteModal from '~/components/Modal/DeleteModal'
import DisableModal from '~/components/Modal/DisableModal'
import ReportModal from '~/components/Modal/ReportModal'
import DeleteModal from '~/components/Modal/DeleteModal'
import { mapGetters } from 'vuex'
export default {

View File

@ -2,17 +2,14 @@ import { shallowMount, mount, createLocalVue } from '@vue/test-utils'
import DeleteModal from './DeleteModal.vue'
import Vuex from 'vuex'
import Styleguide from '@human-connection/styleguide'
import VueRouter from 'vue-router'
const routes = [{ path: '/' }]
const router = new VueRouter({ routes })
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
localVue.use(VueRouter)
describe('DeleteModal.vue', () => {
let Wrapper
let wrapper
let propsData
let mocks
@ -20,79 +17,113 @@ describe('DeleteModal.vue', () => {
beforeEach(() => {
propsData = {
type: 'contribution',
id: 'c300',
id: 'p23',
name: 'It is a post',
callbacks: {
confirm: jest.fn(),
cancel: jest.fn(),
},
}
mocks = {
$t: jest.fn(),
$filters: {
truncate: a => a,
},
$toast: {
success: () => {},
error: () => {},
},
$apollo: {
mutate: jest.fn().mockResolvedValue(),
},
}
})
describe('shallowMount', () => {
const Wrapper = () => {
return shallowMount(DeleteModal, { propsData, mocks, localVue, router })
Wrapper = () => {
return shallowMount(DeleteModal, {
propsData,
mocks,
localVue,
})
}
describe('defaults', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('success false', () => {
expect(Wrapper().vm.success).toBe(false)
expect(wrapper.vm.success).toBe(false)
})
it('loading false', () => {
expect(Wrapper().vm.loading).toBe(false)
expect(wrapper.vm.loading).toBe(false)
})
})
describe('given a post', () => {
beforeEach(() => {
propsData = {
...propsData,
type: 'contribution',
id: 'p23',
type: 'post',
name: 'It is a post',
}
wrapper = Wrapper()
})
it('mentions post title', () => {
Wrapper()
const calls = mocks.$t.mock.calls
const expected = [['post.delete.message', { name: 'It is a post' }]]
const expected = [
[
'delete.contribution.message',
{
name: 'It is a post',
},
],
]
expect(calls).toEqual(expect.arrayContaining(expected))
})
})
describe('given a comment', () => {
beforeEach(() => {
propsData = {
...propsData,
type: 'comment',
id: 'c4',
name: 'It is the user of the comment',
}
wrapper = Wrapper()
})
it('mentions comments user name', () => {
const calls = mocks.$t.mock.calls
const expected = [
[
'delete.comment.message',
{
name: 'It is the user of the comment',
},
],
]
expect(calls).toEqual(expect.arrayContaining(expected))
})
})
})
describe('mount', () => {
const Wrapper = () => {
return mount(DeleteModal, { propsData, mocks, localVue, router })
Wrapper = () => {
return mount(DeleteModal, {
propsData,
mocks,
localVue,
})
}
beforeEach(jest.useFakeTimers)
it('renders', () => {
expect(Wrapper().is('div')).toBe(true)
})
describe('given id', () => {
describe('given post id', () => {
beforeEach(() => {
propsData = {
type: 'user',
id: 'u3',
}
wrapper = Wrapper()
})
describe('click cancel button', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper.find('button.cancel').trigger('click')
})
@ -103,12 +134,12 @@ describe('DeleteModal.vue', () => {
expect(wrapper.vm.isOpen).toBe(false)
})
it('emits "close"', () => {
expect(wrapper.emitted().close).toBeTruthy()
it('does call the cancel callback', () => {
expect(propsData.callbacks.cancel).toHaveBeenCalledTimes(1)
})
it('does not call mutation', () => {
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
it('emits "close"', () => {
expect(wrapper.emitted().close).toBeTruthy()
})
})
})
@ -118,20 +149,10 @@ describe('DeleteModal.vue', () => {
wrapper.find('button.confirm').trigger('click')
})
it('calls delete mutation', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalled()
})
it('sets success', () => {
expect(wrapper.vm.success).toBe(true)
})
it('displays a success message', () => {
const calls = mocks.$t.mock.calls
const expected = [['post.delete.success']]
expect(calls).toEqual(expect.arrayContaining(expected))
})
describe('after timeout', () => {
beforeEach(jest.runAllTimers)
@ -139,6 +160,9 @@ describe('DeleteModal.vue', () => {
expect(wrapper.vm.isOpen).toBe(false)
})
it('does call the confirm callback', () => {
expect(propsData.callbacks.confirm).toHaveBeenCalledTimes(1)
})
it('emits close', () => {
expect(wrapper.emitted().close).toBeTruthy()
})

View File

@ -1,15 +1,7 @@
<template>
<ds-modal
:title="title"
:is-open="isOpen"
@cancel="cancel"
>
<ds-modal :title="title" :is-open="isOpen" @cancel="cancel">
<transition name="ds-transition-fade">
<ds-flex
v-if="success"
class="hc-modal-success"
centered
>
<ds-flex v-if="success" class="hc-modal-success" centered>
<sweetalert-icon icon="success" />
</ds-flex>
</transition>
@ -17,32 +9,17 @@
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="message" />
<template
slot="footer"
>
<ds-button
class="cancel"
icon="close"
@click="cancel"
>
{{ $t('post.delete.cancel') }}
</ds-button>
<template slot="footer">
<ds-button class="cancel" icon="close" @click="cancel">{{ $t('delete.cancel') }}</ds-button>
<ds-button
danger
class="confirm"
icon="trash"
:loading="loading"
@click="confirm"
>
{{ $t('post.delete.submit') }}
<ds-button danger class="confirm" icon="trash" :loading="loading" @click="confirm">
{{ $t('delete.submit') }}
</ds-button>
</template>
</ds-modal>
</template>
<script>
import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export default {
@ -53,6 +30,7 @@ export default {
props: {
name: { type: String, default: '' },
type: { type: String, required: true },
callbacks: { type: Object, required: true },
id: { type: String, required: true },
},
data() {
@ -64,15 +42,18 @@ export default {
},
computed: {
title() {
return this.$t(`post.delete.title`)
return this.$t(`delete.${this.type}.title`)
},
message() {
const name = this.$filters.truncate(this.name, 30)
return this.$t(`post.delete.message`, { name })
return this.$t(`delete.${this.type}.message`, { name })
},
},
methods: {
async cancel() {
if (this.callbacks.cancel) {
await this.callbacks.cancel()
}
this.isOpen = false
setTimeout(() => {
this.$emit('close')
@ -81,35 +62,19 @@ export default {
async confirm() {
this.loading = true
try {
await this.$apollo.mutate({
mutation: gql`
mutation($id: ID!) {
DeletePost(id: $id) {
id
}
}
`,
variables: { id: this.id },
})
if (this.callbacks.confirm) {
await this.callbacks.confirm()
}
this.success = true
this.$toast.success(this.$t('post.delete.success'))
setTimeout(() => {
this.isOpen = false
setTimeout(() => {
this.success = false
this.$emit('close')
if (this.$router.history.current.name === 'post-id-slug') {
// redirect to index
this.$router.history.push('/')
} else {
// reload the page (when deleting from profile or index)
window.location.assign(window.location.href)
}
}, 500)
}, 1500)
} catch (err) {
this.success = false
this.$toast.error(err.message)
} finally {
this.loading = false
}

View File

@ -14,8 +14,12 @@ describe('DisableModal.vue', () => {
beforeEach(() => {
propsData = {
type: 'contribution',
name: 'blah',
id: 'c42',
name: 'blah',
callbacks: {
confirm: jest.fn(),
cancel: jest.fn(),
},
}
mocks = {
$filters: {
@ -34,22 +38,34 @@ describe('DisableModal.vue', () => {
describe('shallowMount', () => {
const Wrapper = () => {
return shallowMount(DisableModal, { propsData, mocks, localVue })
return shallowMount(DisableModal, {
propsData,
mocks,
localVue,
})
}
describe('given a user', () => {
beforeEach(() => {
propsData = {
...propsData,
type: 'user',
id: 'u2',
name: 'Bob Ross',
id: 'u2',
}
})
it('mentions user name', () => {
Wrapper()
const calls = mocks.$t.mock.calls
const expected = [['disable.user.message', { name: 'Bob Ross' }]]
const expected = [
[
'disable.user.message',
{
name: 'Bob Ross',
},
],
]
expect(calls).toEqual(expect.arrayContaining(expected))
})
})
@ -57,16 +73,24 @@ describe('DisableModal.vue', () => {
describe('given a contribution', () => {
beforeEach(() => {
propsData = {
...propsData,
type: 'contribution',
id: 'c3',
name: 'This is some post title.',
id: 'c3',
}
})
it('mentions contribution title', () => {
Wrapper()
const calls = mocks.$t.mock.calls
const expected = [['disable.contribution.message', { name: 'This is some post title.' }]]
const expected = [
[
'disable.contribution.message',
{
name: 'This is some post title.',
},
],
]
expect(calls).toEqual(expect.arrayContaining(expected))
})
})
@ -74,13 +98,18 @@ describe('DisableModal.vue', () => {
describe('mount', () => {
const Wrapper = () => {
return mount(DisableModal, { propsData, mocks, localVue })
return mount(DisableModal, {
propsData,
mocks,
localVue,
})
}
beforeEach(jest.useFakeTimers)
describe('given id', () => {
beforeEach(() => {
propsData = {
...propsData,
type: 'user',
id: 'u4711',
}
@ -126,7 +155,9 @@ describe('DisableModal.vue', () => {
it('passes id to mutation', () => {
const calls = mocks.$apollo.mutate.mock.calls
const [[{ variables }]] = calls
expect(variables).toEqual({ id: 'u4711' })
expect(variables).toEqual({
id: 'u4711',
})
})
it('fades away', () => {

View File

@ -1,26 +1,14 @@
<template>
<ds-modal
:title="title"
:is-open="isOpen"
@cancel="cancel"
>
<ds-modal :title="title" :is-open="isOpen" @cancel="cancel">
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="message" />
<template slot="footer">
<ds-button
class="cancel"
@click="cancel"
>
<ds-button class="cancel" @click="cancel">
{{ $t('disable.cancel') }}
</ds-button>
<ds-button
danger
class="confirm"
icon="exclamation-circle"
@click="confirm"
>
<ds-button danger class="confirm" icon="exclamation-circle" @click="confirm">
{{ $t('disable.submit') }}
</ds-button>
</template>
@ -31,9 +19,11 @@
import gql from 'graphql-tag'
export default {
name: 'DisableModal',
props: {
name: { type: String, default: '' },
type: { type: String, required: true },
callbacks: { type: Object, required: true },
id: { type: String, required: true },
},
data() {
@ -53,7 +43,10 @@ export default {
},
},
methods: {
cancel() {
async cancel() {
if (this.callbacks.cancel) {
await this.callbacks.cancel()
}
this.isOpen = false
setTimeout(() => {
this.$emit('close')
@ -61,6 +54,9 @@ export default {
},
async confirm() {
try {
if (this.callbacks.confirm) {
await this.callbacks.confirm()
}
await this.$apollo.mutate({
mutation: gql`
mutation($id: ID!) {

View File

@ -17,6 +17,10 @@ describe('ReportModal.vue', () => {
propsData = {
type: 'contribution',
id: 'c43',
callbacks: {
confirm: jest.fn(),
cancel: jest.fn(),
},
}
mocks = {
$t: jest.fn(),
@ -35,7 +39,11 @@ describe('ReportModal.vue', () => {
describe('shallowMount', () => {
const Wrapper = () => {
return shallowMount(ReportModal, { propsData, mocks, localVue })
return shallowMount(ReportModal, {
propsData,
mocks,
localVue,
})
}
describe('defaults', () => {
@ -51,6 +59,7 @@ describe('ReportModal.vue', () => {
describe('given a user', () => {
beforeEach(() => {
propsData = {
...propsData,
type: 'user',
id: 'u4',
name: 'Bob Ross',
@ -60,7 +69,14 @@ describe('ReportModal.vue', () => {
it('mentions user name', () => {
Wrapper()
const calls = mocks.$t.mock.calls
const expected = [['report.user.message', { name: 'Bob Ross' }]]
const expected = [
[
'report.user.message',
{
name: 'Bob Ross',
},
],
]
expect(calls).toEqual(expect.arrayContaining(expected))
})
})
@ -68,8 +84,9 @@ describe('ReportModal.vue', () => {
describe('given a post', () => {
beforeEach(() => {
propsData = {
id: 'p23',
...propsData,
type: 'post',
id: 'p23',
name: 'It is a post',
}
})
@ -77,7 +94,14 @@ describe('ReportModal.vue', () => {
it('mentions post title', () => {
Wrapper()
const calls = mocks.$t.mock.calls
const expected = [['report.post.message', { name: 'It is a post' }]]
const expected = [
[
'report.post.message',
{
name: 'It is a post',
},
],
]
expect(calls).toEqual(expect.arrayContaining(expected))
})
})
@ -85,7 +109,11 @@ describe('ReportModal.vue', () => {
describe('mount', () => {
const Wrapper = () => {
return mount(ReportModal, { propsData, mocks, localVue })
return mount(ReportModal, {
propsData,
mocks,
localVue,
})
}
beforeEach(jest.useFakeTimers)
@ -97,6 +125,7 @@ describe('ReportModal.vue', () => {
describe('given id', () => {
beforeEach(() => {
propsData = {
...propsData,
type: 'user',
id: 'u4711',
}

View File

@ -1,15 +1,7 @@
<template>
<ds-modal
:title="title"
:is-open="isOpen"
@cancel="cancel"
>
<ds-modal :title="title" :is-open="isOpen" @cancel="cancel">
<transition name="ds-transition-fade">
<ds-flex
v-if="success"
class="hc-modal-success"
centered
>
<ds-flex v-if="success" class="hc-modal-success" centered>
<sweetalert-icon icon="success" />
</ds-flex>
</transition>
@ -17,14 +9,8 @@
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="message" />
<template
slot="footer"
>
<ds-button
class="cancel"
icon="close"
@click="cancel"
>
<template slot="footer">
<ds-button class="cancel" icon="close" @click="cancel">
{{ $t('report.cancel') }}
</ds-button>
@ -53,6 +39,7 @@ export default {
props: {
name: { type: String, default: '' },
type: { type: String, required: true },
callbacks: { type: Object, required: true },
id: { type: String, required: true },
},
data() {
@ -73,6 +60,9 @@ export default {
},
methods: {
async cancel() {
if (this.callbacks.cancel) {
await this.callbacks.cancel()
}
this.isOpen = false
setTimeout(() => {
this.$emit('close')
@ -81,6 +71,9 @@ export default {
async confirm() {
this.loading = true
try {
if (this.callbacks.confirm) {
await this.callbacks.confirm()
}
await this.$apollo.mutate({
mutation: gql`
mutation($id: ID!) {

View File

@ -27,11 +27,7 @@
/>
<password-strength :password="formData.newPassword" />
<ds-space margin-top="base">
<ds-button
:loading="loading"
:disabled="disabled"
primary
>
<ds-button :loading="loading" :disabled="disabled" primary>
{{ $t('settings.security.change-password.button') }}
</ds-button>
</ds-space>

View File

@ -1,16 +1,10 @@
<template>
<div class="field">
<div class="password-strength-meter">
<div
class="password-strength-meter-inner"
:class="'strength-' + strength"
/>
<div class="password-strength-meter-inner" :class="'strength-' + strength" />
</div>
<p class="help">
<span
v-if="pass"
:class="{ insecure: !isSecure }"
>
<span v-if="pass" :class="{ insecure: !isSecure }">
{{ $t('settings.security.change-password.passwordSecurity') }}:
<strong>{{ $t(`settings.security.change-password.passwordStrength${strength}`) }}</strong>
</span>

View File

@ -1,78 +1,68 @@
<template>
<ds-card
:image="post.image"
:class="{'post-card': true, 'disabled-content': post.disabled}"
>
<!-- Post Link Target -->
<nuxt-link
class="post-link"
:to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }"
>
{{ post.title }}
</nuxt-link>
<ds-space margin-bottom="small" />
<!-- Username, Image & Date of Post -->
<div>
<no-ssr>
<hc-user
:user="post.author"
:trunc="35"
:date-time="post.createdAt"
/>
</no-ssr>
<hc-ribbon :text="$t('post.name')" />
</div>
<ds-space margin-bottom="small" />
<!-- Post Title -->
<ds-heading
tag="h3"
no-margin
>
{{ post.title }}
</ds-heading>
<ds-space margin-bottom="small" />
<!-- Post Content Excerpt -->
<!-- eslint-disable vue/no-v-html -->
<!-- TODO: replace editor content with tiptap render view -->
<div
class="hc-editor-content"
v-html="excerpt"
/>
<!-- eslint-enable vue/no-v-html -->
<!-- Footer o the Post -->
<template slot="footer">
<div style="display: inline-block; opacity: .5;">
<!-- Categories -->
<hc-category
v-for="category in post.categories"
:key="category.id"
v-tooltip="{content: category.name, placement: 'bottom-start', delay: { show: 500 }}"
:icon="category.icon"
/>
<ds-flex-item :width="width">
<ds-card :image="post.image" :class="{ 'post-card': true, 'disabled-content': post.disabled }">
<!-- Post Link Target -->
<nuxt-link
class="post-link"
:to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }"
>
{{ post.title }}
</nuxt-link>
<ds-space margin-bottom="small" />
<!-- Username, Image & Date of Post -->
<div>
<no-ssr>
<hc-user :user="post.author" :trunc="35" :date-time="post.createdAt" />
</no-ssr>
<hc-ribbon :text="$t('post.name')" />
</div>
<no-ssr>
<div style="display: inline-block; float: right">
<!-- Shouts Count -->
<span :style="{ opacity: post.shoutedCount ? 1 : .5 }">
<ds-icon name="bullhorn" />
<small>{{ post.shoutedCount }}</small>
</span>
&nbsp;
<!-- Comments Count -->
<span :style="{ opacity: post.commentsCount ? 1 : .5 }">
<ds-icon name="comments" />
<small>{{ post.commentsCount }}</small>
</span>
<!-- Menu -->
<content-menu
resource-type="contribution"
:resource="post"
:is-owner="isAuthor"
<ds-space margin-bottom="small" />
<!-- Post Title -->
<ds-heading tag="h3" no-margin>
{{ post.title }}
</ds-heading>
<ds-space margin-bottom="small" />
<!-- Post Content Excerpt -->
<!-- eslint-disable vue/no-v-html -->
<!-- TODO: replace editor content with tiptap render view -->
<div class="hc-editor-content" v-html="excerpt" />
<!-- eslint-enable vue/no-v-html -->
<!-- Footer o the Post -->
<template slot="footer">
<div style="display: inline-block; opacity: .5;">
<!-- Categories -->
<hc-category
v-for="category in post.categories"
:key="category.id"
v-tooltip="{ content: category.name, placement: 'bottom-start', delay: { show: 500 } }"
:icon="category.icon"
/>
</div>
</no-ssr>
</template>
</ds-card>
<no-ssr>
<div style="display: inline-block; float: right">
<!-- Shouts Count -->
<span :style="{ opacity: post.shoutedCount ? 1 : 0.5 }">
<ds-icon name="bullhorn" />
<small>{{ post.shoutedCount }}</small>
</span>
&nbsp;
<!-- Comments Count -->
<span :style="{ opacity: post.commentsCount ? 1 : 0.5 }">
<ds-icon name="comments" />
<small>{{ post.commentsCount }}</small>
</span>
<!-- Menu -->
<content-menu
resource-type="contribution"
:resource="post"
:callbacks="{ confirm: deletePostCallback, cancel: null }"
:is-owner="isAuthor"
/>
</div>
</no-ssr>
</template>
</ds-card>
</ds-flex-item>
</template>
<script>
@ -82,6 +72,7 @@ import HcCategory from '~/components/Category'
import HcRibbon from '~/components/Ribbon'
// import { randomBytes } from 'crypto'
import { mapGetters } from 'vuex'
import PostMutationHelpers from '~/mixins/PostMutationHelpers'
export default {
name: 'HcPostCard',
@ -91,11 +82,16 @@ export default {
HcRibbon,
ContentMenu,
},
mixins: [PostMutationHelpers],
props: {
post: {
type: Object,
required: true,
},
width: {
type: Object,
default: () => {},
},
},
computed: {
...mapGetters({

View File

@ -5,16 +5,12 @@
role="search"
:class="{
'is-active': isActive,
'is-open': isOpen
'is-open': isOpen,
}"
>
<div class="field">
<div class="control">
<a
v-if="isActive"
class="search-clear-btn"
@click="clear"
>
<a v-if="isActive" class="search-clear-btn" @click="clear">
&nbsp;
</a>
<ds-select
@ -42,42 +38,31 @@
@input.native="handleInput"
@click.capture.native="isOpen = true"
>
<template
slot="option"
slot-scope="{option}"
>
<template slot="option" slot-scope="{ option }">
<ds-flex>
<ds-flex-item class="search-option-label">
<ds-text>
{{ option.label | truncate(70) }}
</ds-text>
</ds-flex-item>
<ds-flex-item
class="search-option-meta"
width="280px"
>
<ds-flex-item class="search-option-meta" width="280px">
<ds-flex>
<ds-flex-item>
<ds-text
size="small"
color="softer"
class="search-meta"
>
<ds-text size="small" color="softer" class="search-meta">
<span style="text-align: right;">
<b>{{ option.commentsCount }}</b> <ds-icon name="comments" />
<b>{{ option.commentsCount }}</b>
<ds-icon name="comments" />
</span>
<span style="width: 36px; display: inline-block; text-align: right;">
<b>{{ option.shoutedCount }}</b> <ds-icon name="bullhorn" />
<b>{{ option.shoutedCount }}</b>
<ds-icon name="bullhorn" />
</span>
</ds-text>
</ds-flex-item>
<ds-flex-item>
<ds-text
size="small"
color="softer"
align="right"
>
{{ option.author.name | truncate(32) }} - {{ option.createdAt | dateTime('dd.MM.yyyy') }}
<ds-text size="small" color="softer" align="right">
{{ option.author.name | truncate(32) }} -
{{ option.createdAt | dateTime('dd.MM.yyyy') }}
</ds-text>
</ds-flex-item>
</ds-flex>

View File

@ -1,8 +1,5 @@
<template>
<ds-space
margin="large"
style="text-align: center"
>
<ds-space margin="large" style="text-align: center">
<ds-button
:loading="loading"
:disabled="disabled"
@ -13,16 +10,9 @@
@click="toggle"
/>
<ds-space margin-bottom="xx-small" />
<ds-text
color="soft"
class="shout-button-text"
>
<ds-heading
style="display: inline"
tag="h3"
>
{{ shoutedCount }}x
</ds-heading> {{ $t('shoutButton.shouted') }}
<ds-text color="soft" class="shout-button-text">
<ds-heading style="display: inline" tag="h3">{{ shoutedCount }}x</ds-heading>
{{ $t('shoutButton.shouted') }}
</ds-text>
</ds-space>
</template>

View File

@ -8,7 +8,7 @@
:include-styling="false"
:style="backgroundImage"
@vdropzone-thumbnail="thumbnail"
@vdropzone-drop="vddrop"
@vdropzone-error="verror"
/>
</div>
</template>
@ -27,10 +27,11 @@ export default {
return {
dropzoneOptions: {
url: this.vddrop,
maxFilesize: 0.5,
maxFilesize: 5.0,
previewTemplate: this.template(),
dictDefaultMessage: '',
},
error: false,
}
},
computed: {
@ -44,6 +45,14 @@ export default {
}
},
},
watch: {
error() {
let that = this
setTimeout(function() {
that.error = false
}, 2000)
},
},
methods: {
template() {
return `<div class="dz-preview dz-file-preview">
@ -89,6 +98,12 @@ export default {
})
.catch(error => this.$toast.error(error.message))
},
verror(file, message) {
if (file.status === 'error') {
this.error = true
this.$toast.error(file.status, message)
}
},
},
}
</script>

View File

@ -26,6 +26,7 @@ describe('Upload', () => {
success: jest.fn(),
error: jest.fn(),
},
$t: jest.fn(),
}
const propsData = {
@ -34,7 +35,7 @@ describe('Upload', () => {
},
}
const file = {
const fileSuccess = {
filename: 'avatar.jpg',
previewElement: {
classList: {
@ -59,13 +60,38 @@ describe('Upload', () => {
wrapper = shallowMount(Upload, { localVue, propsData, mocks })
})
afterEach(() => {
jest.clearAllMocks()
})
it('sends a the UpdateUser mutation when vddrop is called', () => {
wrapper.vm.vddrop([{ filename: 'avatar.jpg' }])
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
it('thumbnail', () => {
wrapper.vm.thumbnail(file, dataUrl)
expect(file.previewElement.classList.add).toHaveBeenCalledTimes(1)
wrapper.vm.thumbnail(fileSuccess, dataUrl)
expect(fileSuccess.previewElement.classList.add).toHaveBeenCalledTimes(1)
})
describe('error handling', () => {
const message = 'File upload failed'
const fileError = { status: 'error' }
it('defaults to error false', () => {
expect(wrapper.vm.error).toEqual(false)
})
it('shows an error toaster when verror is called', () => {
wrapper.vm.verror(fileError, message)
expect(mocks.$toast.error).toHaveBeenCalledWith(fileError.status, message)
})
it('changes error status from false to true to false', () => {
wrapper.vm.verror(fileError, message)
expect(wrapper.vm.error).toEqual(true)
jest.runAllTimers()
expect(wrapper.vm.error).toEqual(false)
})
})
})

View File

@ -6,53 +6,24 @@
<hc-avatar />
</div>
<div style="display: inline-block; height: 100%; vertical-align: middle;">
<b
class="username"
style="vertical-align: middle;"
>{{ $t('profile.userAnonym') }}</b>
<b class="username" style="vertical-align: middle;">{{ $t('profile.userAnonym') }}</b>
</div>
</div>
<dropdown
v-else
:class="{'disabled-content': user.disabled}"
placement="top-start"
offset="0"
>
<template
slot="default"
slot-scope="{openMenu, closeMenu, isOpen}"
>
<nuxt-link
:to="userLink"
:class="['user', isOpen && 'active']"
>
<div
@mouseover="openMenu(true)"
@mouseleave="closeMenu(true)"
>
<dropdown v-else :class="{ 'disabled-content': user.disabled }" placement="top-start" offset="0">
<template slot="default" slot-scope="{ openMenu, closeMenu, isOpen }">
<nuxt-link :to="userLink" :class="['user', isOpen && 'active']">
<div @mouseover="openMenu(true)" @mouseleave="closeMenu(true)">
<div
style="display: inline-block; float: left; margin-right: 4px; height: 100%; vertical-align: middle;"
>
<hc-avatar
:user="user"
/>
<hc-avatar :user="user" />
</div>
<div style="display: inline-block; height: 100%; vertical-align: middle;">
<b
class="username"
style="vertical-align: middle;"
>{{ userName | truncate(18) }}</b>
<b class="username" style="vertical-align: middle;">{{ userName | truncate(18) }}</b>
</div>
<!-- Time -->
<div
v-if="dateTime"
style="display: inline;"
>
<ds-text
align="left"
size="small"
color="soft"
>
<div v-if="dateTime" style="display: inline;">
<ds-text align="left" size="small" color="soft">
<ds-icon name="clock" />
<no-ssr>
<hc-relative-date-time :date-time="dateTime" />
@ -64,10 +35,7 @@
</template>
<template slot="popover">
<div style="min-width: 250px">
<hc-badges
v-if="user.badges && user.badges.length"
:badges="user.badges"
/>
<hc-badges v-if="user.badges && user.badges.length" :badges="user.badges" />
<ds-text
v-if="user.location"
align="center"
@ -82,11 +50,7 @@
<ds-flex style="margin-top: -10px">
<ds-flex-item class="ds-tab-nav-item">
<ds-space margin="small">
<ds-number
:count="fanCount"
:label="$t('profile.followers')"
size="x-large"
/>
<ds-number :count="fanCount" :label="$t('profile.followers')" size="x-large" />
</ds-space>
</ds-flex-item>
<ds-flex-item class="ds-tab-nav-item ds-tab-nav-item-active">
@ -106,20 +70,16 @@
</ds-space>
</ds-flex-item>
</ds-flex>
<ds-flex
v-if="!itsMe"
gutter="x-small"
style="margin-bottom: 0;"
>
<ds-flex-item :width="{base: 3}">
<ds-flex v-if="!itsMe" gutter="x-small" style="margin-bottom: 0;">
<ds-flex-item :width="{ base: 3 }">
<hc-follow-button
:follow-id="user.id"
:is-followed="user.followedByCurrentUser"
@optimistic="follow => user.followedByCurrentUser = follow"
@update="follow => user.followedByCurrentUser = follow"
@optimistic="follow => (user.followedByCurrentUser = follow)"
@update="follow => (user.followedByCurrentUser = follow)"
/>
</ds-flex-item>
<ds-flex-item :width="{base: 1}">
<ds-flex-item :width="{ base: 1 }">
<ds-button fullwidth>
<ds-icon name="user-times" />
</ds-button>

View File

@ -1,36 +1,18 @@
<template>
<ds-form
v-model="form"
@submit="handleSubmit"
>
<ds-form v-model="form" @submit="handleSubmit">
<template slot-scope="{ errors }">
<ds-card>
<hc-editor
ref="editor"
:users="users"
:value="form.content"
@input="updateEditorContent"
/>
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
<ds-space />
<ds-flex :gutter="{ base: 'small', md: 'small', sm: 'x-large', xs: 'x-large' }">
<ds-flex-item :width="{ base: '0%', md: '50%', sm: '0%', xs: '0%' }" />
<ds-flex-item :width="{ base: '40%', md: '20%', sm: '30%', xs: '30%' }">
<ds-button
:disabled="disabled"
ghost
class="cancelBtn"
@click.prevent="clear"
>
<ds-button :disabled="disabled" ghost class="cancelBtn" @click.prevent="clear">
{{ $t('actions.cancel') }}
</ds-button>
</ds-flex-item>
<ds-flex-item :width="{ base: '40%', md: '20%', sm: '40%', xs: '40%' }">
<ds-button
type="submit"
:loading="loading"
:disabled="disabled || errors"
primary
>
<ds-button type="submit" :loading="loading" :disabled="disabled || errors" primary>
{{ $t('post.comment.submit') }}
</ds-button>
</ds-flex-item>

View File

@ -9,26 +9,22 @@
color="primary"
size="small"
round
>{{ comments.length }}</ds-tag>&nbsp; Comments
>
{{ comments.length }}
</ds-tag>
&nbsp; Comments
</span>
</h3>
<ds-space margin-bottom="large" />
<div
v-if="comments && comments.length"
id="comments"
class="comments"
>
<div v-if="comments && comments.length" id="comments" class="comments">
<comment
v-for="comment in comments"
v-for="(comment, index) in comments"
:key="comment.id"
:comment="comment"
@deleteComment="comments.splice(index, 1)"
/>
</div>
<hc-empty
v-else
name="empty"
icon="messages"
/>
<hc-empty v-else name="empty" icon="messages" />
</div>
</template>
<script>

View File

@ -1,18 +1,11 @@
<template>
<ds-space
:class="{'notification': true, 'read': notification.read}"
margin-bottom="x-small"
>
<ds-space :class="{ notification: true, read: notification.read }" margin-bottom="x-small">
<no-ssr>
<ds-space margin-bottom="x-small">
<hc-user
:user="post.author"
:date-time="post.createdAt"
:trunc="35"
/>
<hc-user :user="post.author" :date-time="post.createdAt" :trunc="35" />
</ds-space>
<ds-text color="soft">
{{ $t("notifications.menu.mentioned") }}
{{ $t('notifications.menu.mentioned') }}
</ds-text>
</no-ssr>
<ds-space margin-bottom="x-small" />
@ -22,16 +15,11 @@
@click.native="$emit('read')"
>
<ds-space margin-bottom="x-small">
<ds-card
:header="post.title"
:image="post.image"
hover
space="x-small"
>
<ds-card :header="post.title" :image="post.image" hover space="x-small">
<ds-space margin-bottom="x-small" />
<!-- eslint-disable vue/no-v-html -->
<div v-html="excerpt" />
<!-- eslint-enable vue/no-v-html -->
<!-- eslint-enable vue/no-v-html -->
</ds-card>
</ds-space>
</nuxt-link>

View File

@ -1,36 +1,16 @@
<template>
<ds-button
v-if="totalNotifications <= 0"
class="notifications-menu"
disabled
icon="bell"
>
<ds-button v-if="totalNotifications <= 0" class="notifications-menu" disabled icon="bell">
{{ totalNotifications }}
</ds-button>
<dropdown
v-else
class="notifications-menu"
>
<template
slot="default"
slot-scope="{toggleMenu}"
>
<ds-button
primary
icon="bell"
@click.prevent="toggleMenu"
>
<dropdown v-else class="notifications-menu">
<template slot="default" slot-scope="{ toggleMenu }">
<ds-button primary icon="bell" @click.prevent="toggleMenu">
{{ totalNotifications }}
</ds-button>
</template>
<template
slot="popover"
>
<template slot="popover">
<div class="notifications-menu-popover">
<notification-list
:notifications="notifications"
@markAsRead="markAsRead"
/>
<notification-list :notifications="notifications" @markAsRead="markAsRead" />
</div>
</template>
</dropdown>

View File

@ -3,11 +3,7 @@
<div class="main-navigation">
<ds-container class="main-navigation-container">
<div class="main-navigation-left">
<a
v-router-link
style="display: inline-flex"
href="/"
>
<a v-router-link style="display: inline-flex" href="/">
<ds-logo />
</a>
</div>
@ -24,11 +20,7 @@
</div>
<div class="main-navigation-right">
<no-ssr>
<locale-switch
class="topbar-locale-switch"
placement="bottom"
offset="23"
/>
<locale-switch class="topbar-locale-switch" placement="bottom" offset="23" />
</no-ssr>
<template v-if="isLoggedIn">
<no-ssr>
@ -36,45 +28,32 @@
</no-ssr>
<no-ssr>
<dropdown class="avatar-menu">
<template
slot="default"
slot-scope="{toggleMenu}"
>
<template slot="default" slot-scope="{ toggleMenu }">
<a
class="avatar-menu-trigger"
:href="$router.resolve({name: 'profile-id-slug', params: {id: user.id, slug: user.slug}}).href"
:href="
$router.resolve({
name: 'profile-id-slug',
params: { id: user.id, slug: user.slug },
}).href
"
@click.prevent="toggleMenu"
>
<hc-avatar
:user="user"
/>
<ds-icon
size="xx-small"
name="angle-down"
/>
<hc-avatar :user="user" />
<ds-icon size="xx-small" name="angle-down" />
</a>
</template>
<template
slot="popover"
slot-scope="{closeMenu}"
>
<template slot="popover" slot-scope="{ closeMenu }">
<div class="avatar-menu-popover">
{{ $t('login.hello') }}
<b>{{ userName }}</b>
<template v-if="user.role !== 'user'">
<ds-text
color="softer"
size="small"
style="margin-bottom: 0"
>
<ds-text color="softer" size="small" style="margin-bottom: 0">
{{ user.role | camelCase }}
</ds-text>
</template>
<hr>
<ds-menu
:routes="routes"
:matcher="matcher"
>
<hr />
<ds-menu :routes="routes" :matcher="matcher">
<ds-menu-item
slot="menuitem"
slot-scope="item"
@ -86,11 +65,8 @@
{{ item.route.name }}
</ds-menu-item>
</ds-menu>
<hr>
<nuxt-link
class="logout-link"
:to="{ name: 'logout'}"
>
<hr />
<nuxt-link class="logout-link" :to="{ name: 'logout' }">
<ds-icon name="sign-out" />
{{ $t('login.logout') }}
</nuxt-link>

View File

@ -135,19 +135,24 @@
"takeAction": {
"name": "Aktiv werden"
},
"delete": {
"submit": "Löschen",
"cancel": "Abbrechen",
"success": "Beitrag erfolgreich gelöscht",
"title": "Beitrag löschen",
"type": "Beitrag",
"message": "Möchtest Du wirklich den Beitrag \"<b>{name}</b>\" löschen?"
"menu": {
"edit": "Beitrag bearbeiten",
"delete": "Beitrag löschen"
},
"comment": {
"submit": "Kommentiere",
"submitted": "Kommentar Gesendet"
}
},
"comment": {
"content": {
"unavailable-placeholder": "...dieser Kommentar ist nicht mehr verfügbar"
},
"menu": {
"edit": "Kommentar bearbeiten",
"delete": "Kommentar löschen"
}
},
"quotes": {
"african": {
"quote": "Viele kleine Leute an vielen kleinen Orten, die viele kleine Dinge tun, werden das Antlitz dieser Welt verändern.",
@ -210,6 +215,22 @@
"message": "Bist du sicher, dass du den Kommentar \"<b>{name}</b>\" deaktivieren möchtest?"
}
},
"delete": {
"submit": "Löschen",
"cancel": "Abbrechen",
"contribution": {
"title": "Lösche Beitrag",
"type": "Contribution",
"message": "Bist du sicher, dass du den Beitrag \"<b>{name}</b>\" löschen möchtest?",
"success": "Beitrag erfolgreich gelöscht!"
},
"comment": {
"title": "Lösche Kommentar",
"type": "Comment",
"message": "Bist du sicher, dass du den Kommentar von \"<b>{name}</b>\" löschen möchtest?",
"success": "Kommentar erfolgreich gelöscht!"
}
},
"report": {
"submit": "Melden",
"cancel": "Abbrechen",
@ -230,17 +251,6 @@
"message": "Bist du sicher, dass du den Kommentar von \"<b>{name}</b>\" melden möchtest?"
}
},
"contribution": {
"edit": "Beitrag bearbeiten",
"delete": "Beitrag löschen"
},
"comment": {
"edit": "Kommentar bearbeiten",
"delete": "Kommentar löschen",
"content": {
"unavailable-placeholder": "...dieser Kommentar ist nicht mehr verfügbar"
}
},
"followButton": {
"follow": "Folgen",
"following": "Folge Ich"

View File

@ -135,19 +135,24 @@
"takeAction": {
"name": "Take action"
},
"delete": {
"submit": "Delete",
"cancel": "Cancel",
"success": "Post deleted successfully",
"title": "Delete Post",
"type": "Contribution",
"message": "Do you really want to delete the post \"<b>{name}</b>\"?"
"menu": {
"edit": "Edit Post",
"delete": "Delete Post"
},
"comment": {
"submit": "Comment",
"submitted": "Comment Submitted"
}
},
"comment": {
"content": {
"unavailable-placeholder": "...this comment is not available anymore"
},
"menu": {
"edit": "Edit Comment",
"delete": "Delete Comment"
}
},
"quotes": {
"african": {
"quote": "Many small people in many small places do many small things, that can alter the face of the world.",
@ -210,6 +215,22 @@
"message": "Do you really want to disable the comment from \"<b>{name}</b>\"?"
}
},
"delete": {
"submit": "Delete",
"cancel": "Cancel",
"contribution": {
"title": "Delete Post",
"type": "Contribution",
"message": "Do you really want to delete the post \"<b>{name}</b>\"?",
"success": "Post successfully deleted!"
},
"comment": {
"title": "Delete Comment",
"type": "Comment",
"message": "Do you really want to delete the comment from \"<b>{name}</b>\"?",
"success": "Comment successfully deleted!"
}
},
"report": {
"submit": "Report",
"cancel": "Cancel",
@ -230,17 +251,6 @@
"message": "Do you really want to report the comment from \"<b>{name}</b>\"?"
}
},
"contribution": {
"edit": "Edit Contribution",
"delete": "Delete Contribution"
},
"comment": {
"edit": "Edit Comment",
"delete": "Delete Comment",
"content": {
"unavailable-placeholder": "...this comment is not available anymore"
}
},
"followButton": {
"follow": "Follow",
"following": "Following"

View File

@ -0,0 +1,39 @@
import gql from 'graphql-tag'
export default {
methods: {
async deletePostCallback(postDisplayType = 'list') {
// console.log('inside "deletePostCallback" !!! ', this.post)
try {
var gqlMutation = gql`
mutation($id: ID!) {
DeletePost(id: $id) {
id
}
}
`
await this.$apollo.mutate({
mutation: gqlMutation,
variables: {
id: this.post.id,
},
})
this.$toast.success(this.$t('delete.contribution.success'))
// console.log('called "this.$t" !!!')
switch (postDisplayType) {
case 'list':
this.$emit('deletePost')
// console.log('emitted "deletePost" !!!')
break
default:
// case 'page'
// console.log('called "this.$router.history.push" !!!')
this.$router.history.push('/') // Single page type: Redirect to index (main) page
break
}
} catch (err) {
this.$toast.error(err.message)
}
},
},
}

View File

@ -32,8 +32,8 @@ module.exports = {
locales: require('./locales'),
},
/*
** Headers of the page
*/
** Headers of the page
*/
head: {
title: 'Human Connection',
titleTemplate: '%s - Human Connection',
@ -46,8 +46,8 @@ module.exports = {
},
/*
** Customize the progress-bar color
*/
** Customize the progress-bar color
*/
loading: {
color: '#86b31e',
height: '2px',
@ -55,20 +55,20 @@ module.exports = {
},
/*
** Global CSS
*/
** Global CSS
*/
css: ['~assets/styles/main.scss'],
/*
** Global processed styles
*/
** Global processed styles
*/
styleResources: {
scss: styleguideStyles,
},
/*
** Plugins to load before mounting the App
*/
** Plugins to load before mounting the App
*/
plugins: [
{
src: `~/plugins/styleguide${process.env.STYLEGUIDE_DEV ? '-dev' : ''}.js`,
@ -93,8 +93,8 @@ module.exports = {
},
/*
** Nuxt.js modules
*/
** Nuxt.js modules
*/
modules: [
['@nuxtjs/dotenv', { only: envWhitelist }],
['nuxt-env', { keys: envWhitelist }],
@ -105,8 +105,8 @@ module.exports = {
],
/*
** Axios module configuration
*/
** Axios module configuration
*/
axios: {
// See https://github.com/nuxt-community/axios-module#options
debug: dev,
@ -181,12 +181,12 @@ module.exports = {
},
/*
** Build configuration
*/
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
** You can extend webpack config here
*/
extend(config, ctx) {
if (process.env.STYLEGUIDE_DEV) {
const path = require('path')

View File

@ -29,7 +29,6 @@
"!**/?(*.)+(spec|test).js?(x)"
],
"coverageReporters": [
"text",
"lcov"
],
"transform": {
@ -106,7 +105,7 @@
"jest": "~24.8.0",
"node-sass": "~4.12.0",
"nodemon": "~1.19.1",
"prettier": "~1.14.3",
"prettier": "~1.17.1",
"sass-loader": "~7.1.0",
"tippy.js": "^4.3.1",
"vue-jest": "~3.0.4",

View File

@ -5,16 +5,10 @@
</ds-heading>
<ds-flex gutter="small">
<ds-flex-item :width="{ base: '100%', md: '200px' }">
<ds-menu
:routes="routes"
:is-exact="() => true"
/>
<ds-menu :routes="routes" :is-exact="() => true" />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">
<transition
name="slide-up"
appear
>
<transition name="slide-up" appear>
<nuxt-child />
</transition>
</ds-flex-item>

View File

@ -1,14 +1,7 @@
<template>
<ds-card :header="$t('admin.categories.name')">
<ds-table
:data="Category"
:fields="fields"
condensed
>
<template
slot="icon"
slot-scope="scope"
>
<ds-table :data="Category" :fields="fields" condensed>
<template slot="icon" slot-scope="scope">
<ds-icon :name="scope.row.icon" />
</template>
</ds-table>

View File

@ -5,34 +5,18 @@
<ds-flex>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.users')"
size="x-large"
uppercase
>
<ds-number :count="0" :label="$t('admin.dashboard.users')" size="x-large" uppercase>
<no-ssr slot="count">
<hc-count-to
:start-val="0"
:end-val="statistics.countUsers || 0"
/>
<hc-count-to :start-val="0" :end-val="statistics.countUsers || 0" />
</no-ssr>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.posts')"
size="x-large"
uppercase
>
<ds-number :count="0" :label="$t('admin.dashboard.posts')" size="x-large" uppercase>
<no-ssr slot="count">
<hc-count-to
:start-val="0"
:end-val="statistics.countPosts || 0"
/>
<hc-count-to :start-val="0" :end-val="statistics.countPosts || 0" />
</no-ssr>
</ds-number>
</ds-space>
@ -46,10 +30,7 @@
uppercase
>
<no-ssr slot="count">
<hc-count-to
:start-val="0"
:end-val="statistics.countComments || 0"
/>
<hc-count-to :start-val="0" :end-val="statistics.countComments || 0" />
</no-ssr>
</ds-number>
</ds-space>
@ -63,10 +44,7 @@
uppercase
>
<no-ssr slot="count">
<hc-count-to
:start-val="0"
:end-val="statistics.countNotifications || 0"
/>
<hc-count-to :start-val="0" :end-val="statistics.countNotifications || 0" />
</no-ssr>
</ds-number>
</ds-space>
@ -80,10 +58,7 @@
uppercase
>
<no-ssr slot="count">
<hc-count-to
:start-val="0"
:end-val="statistics.countOrganizations || 0"
/>
<hc-count-to :start-val="0" :end-val="statistics.countOrganizations || 0" />
</no-ssr>
</ds-number>
</ds-space>
@ -97,61 +72,34 @@
uppercase
>
<no-ssr slot="count">
<hc-count-to
:start-val="0"
:end-val="statistics.countProjects || 0"
/>
<hc-count-to :start-val="0" :end-val="statistics.countProjects || 0" />
</no-ssr>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.invites')"
size="x-large"
uppercase
>
<ds-number :count="0" :label="$t('admin.dashboard.invites')" size="x-large" uppercase>
<no-ssr slot="count">
<hc-count-to
:start-val="0"
:end-val="statistics.countInvites || 0"
/>
<hc-count-to :start-val="0" :end-val="statistics.countInvites || 0" />
</no-ssr>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.follows')"
size="x-large"
uppercase
>
<ds-number :count="0" :label="$t('admin.dashboard.follows')" size="x-large" uppercase>
<no-ssr slot="count">
<hc-count-to
:start-val="0"
:end-val="statistics.countFollows || 0"
/>
<hc-count-to :start-val="0" :end-val="statistics.countFollows || 0" />
</no-ssr>
</ds-number>
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: '50%', md: '33%' }">
<ds-space margin="small">
<ds-number
:count="0"
:label="$t('admin.dashboard.shouts')"
size="x-large"
uppercase
>
<ds-number :count="0" :label="$t('admin.dashboard.shouts')" size="x-large" uppercase>
<no-ssr slot="count">
<hc-count-to
:start-val="0"
:end-val="statistics.countShouts || 0"
/>
<hc-count-to :start-val="0" :end-val="statistics.countShouts || 0" />
</no-ssr>
</ds-number>
</ds-space>

View File

@ -1,9 +1,6 @@
<template>
<ds-card :header="$t('admin.notifications.name')">
<hc-empty
icon="tasks"
message="Coming Soon…"
/>
<hc-empty icon="tasks" message="Coming Soon…" />
</ds-card>
</template>

View File

@ -1,9 +1,6 @@
<template>
<ds-card :header="$t('admin.organizations.name')">
<hc-empty
icon="tasks"
message="Coming Soon…"
/>
<hc-empty icon="tasks" message="Coming Soon…" />
</ds-card>
</template>

View File

@ -1,9 +1,6 @@
<template>
<ds-card :header="$t('admin.pages.name')">
<hc-empty
icon="tasks"
message="Coming Soon…"
/>
<hc-empty icon="tasks" message="Coming Soon…" />
</ds-card>
</template>

View File

@ -1,9 +1,6 @@
<template>
<ds-card :header="$t('admin.settings.name')">
<hc-empty
icon="tasks"
message="Coming Soon…"
/>
<hc-empty icon="tasks" message="Coming Soon…" />
</ds-card>
</template>

View File

@ -1,14 +1,7 @@
<template>
<ds-card :header="$t('admin.tags.name')">
<ds-table
:data="Tag"
:fields="fields"
condensed
>
<template
slot="id"
slot-scope="scope"
>
<ds-table :data="Tag" :fields="fields" condensed>
<template slot="id" slot-scope="scope">
{{ scope.index + 1 }}
</template>
</ds-table>

View File

@ -1,9 +1,6 @@
<template>
<ds-card :header="$t('admin.users.name')">
<hc-empty
icon="tasks"
message="Coming Soon…"
/>
<hc-empty icon="tasks" message="Coming Soon…" />
</ds-card>
</template>

View File

@ -1,21 +1,17 @@
<template>
<div>
<ds-flex
v-if="Post && Post.length"
:width="{ base: '100%' }"
gutter="base"
>
<ds-flex-item
v-for="post in uniq(Post)"
<ds-flex v-if="Post && Post.length" :width="{ base: '100%' }" gutter="base">
<hc-post-card
v-for="(post, index) in uniq(Post)"
:key="post.id"
:post="post"
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
>
<hc-post-card :post="post" />
</ds-flex-item>
@deletePost="deletePost(index, post.id)"
/>
</ds-flex>
<no-ssr>
<ds-button
v-tooltip="{content: 'Create a new Post', placement: 'left', delay: { show: 500 }}"
v-tooltip="{ content: 'Create a new Post', placement: 'left', delay: { show: 500 } }"
:path="{ name: 'post-create' }"
class="post-add-button"
icon="plus"
@ -23,11 +19,7 @@
primary
/>
</no-ssr>
<hc-load-more
v-if="true"
:loading="$apollo.loading"
@click="showMoreContributions"
/>
<hc-load-more v-if="true" :loading="$apollo.loading" @click="showMoreContributions" />
</div>
</template>
@ -86,6 +78,14 @@ export default {
fetchPolicy: 'cache-and-network',
})
},
deletePost(_index, postId) {
this.Post = this.Post.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: {
Post: {

View File

@ -1,12 +1,6 @@
<template>
<transition
name="fade"
appear
>
<ds-container
v-if="ready"
width="small"
>
<transition name="fade" appear>
<ds-container v-if="ready" width="small">
<ds-space margin="small">
<blockquote>
<p>{{ $t('quotes.african.quote') }}</p>
@ -15,41 +9,25 @@
</ds-space>
<ds-card class="login-card">
<ds-flex gutter="small">
<ds-flex-item
:width="{ base: '100%', sm: '50%' }"
centered
>
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
<no-ssr>
<locale-switch
class="login-locale-switch"
offset="5"
/>
<locale-switch class="login-locale-switch" offset="5" />
</no-ssr>
<ds-space
margin-top="small"
margin-bottom="xxx-small"
centered
>
<ds-space margin-top="small" margin-bottom="xxx-small" centered>
<img
class="login-image"
alt="Human Connection"
src="/img/sign-up/humanconnection.svg"
>
/>
</ds-space>
</ds-flex-item>
<ds-flex-item
:width="{ base: '100%', sm: '50%' }"
centered
>
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
<ds-space margin="small">
<ds-text size="small">
{{ $t('login.copy') }}
</ds-text>
</ds-space>
<form
:disabled="pending"
@submit.prevent="onSubmit"
>
<form :disabled="pending" @submit.prevent="onSubmit">
<ds-input
v-model="form.email"
:disabled="pending"
@ -82,7 +60,9 @@
:href="$t('login.moreInfoURL')"
:title="$t('login.moreInfoHint')"
target="_blank"
>{{ $t('login.moreInfo') }}</a>
>
{{ $t('login.moreInfo') }}
</a>
</ds-space>
</form>
</ds-flex-item>

View File

@ -1,32 +1,12 @@
<template>
<ds-container width="small">
<ds-flex>
<ds-flex-item
:width="{ base: '100%' }"
centered
>
<ds-space
style="text-align: center;"
margin-top="large"
margin-bottom="xxx-small"
centered
>
<img
style="width: 200px;"
src="/img/sign-up/onourjourney.png"
alt="Human Connection"
>
<ds-flex-item :width="{ base: '100%' }" centered>
<ds-space style="text-align: center;" margin-top="large" margin-bottom="xxx-small" centered>
<img style="width: 200px;" src="/img/sign-up/onourjourney.png" alt="Human Connection" />
</ds-space>
<ds-space
style="text-align: center;"
margin-top="small"
margin-bottom="xxx-small"
centered
>
<ds-heading
tag="h3"
soft
>
<ds-space style="text-align: center;" margin-top="small" margin-bottom="xxx-small" centered>
<ds-heading tag="h3" soft>
Logging out...
</ds-heading>
</ds-space>

View File

@ -8,10 +8,7 @@
<ds-menu :routes="routes" />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">
<transition
name="slide-up"
appear
>
<transition name="slide-up" appear>
<nuxt-child />
</transition>
</ds-flex-item>

View File

@ -3,56 +3,48 @@
<ds-heading tag="h3">
{{ $t('moderation.reports.name') }}
</ds-heading>
<ds-table
v-if="Report && Report.length"
:data="Report"
:fields="fields"
condensed
>
<template
slot="name"
slot-scope="scope"
>
<ds-table v-if="Report && Report.length" :data="Report" :fields="fields" condensed>
<template slot="name" slot-scope="scope">
<div v-if="scope.row.type === 'Post'">
<nuxt-link
:to="{ name: 'post-id-slug', params: { id: scope.row.post.id, slug: scope.row.post.slug } }"
:to="{
name: 'post-id-slug',
params: { id: scope.row.post.id, slug: scope.row.post.slug },
}"
>
<b>{{ scope.row.post.title | truncate(50) }}</b>
</nuxt-link>
<br>
<ds-text
size="small"
color="soft"
>
<br />
<ds-text size="small" color="soft">
{{ scope.row.post.author.name }}
</ds-text>
</div>
<div v-else-if="scope.row.type === 'Comment'">
<nuxt-link
:to="{ name: 'post-id-slug', params: { id: scope.row.comment.post.id, slug: scope.row.comment.post.slug } }"
:to="{
name: 'post-id-slug',
params: { id: scope.row.comment.post.id, slug: scope.row.comment.post.slug },
}"
>
<b>{{ scope.row.comment.contentExcerpt | truncate(50) }}</b>
</nuxt-link>
<br>
<ds-text
size="small"
color="soft"
>
<br />
<ds-text size="small" color="soft">
{{ scope.row.comment.author.name }}
</ds-text>
</div>
<div v-else>
<nuxt-link
:to="{ name: 'profile-id-slug', params: { id: scope.row.user.id, slug: scope.row.user.slug } }"
:to="{
name: 'profile-id-slug',
params: { id: scope.row.user.id, slug: scope.row.user.slug },
}"
>
<b>{{ scope.row.user.name | truncate(50) }}</b>
</nuxt-link>
</div>
</template>
<template
slot="type"
slot-scope="scope"
>
<template slot="type" slot-scope="scope">
<ds-text color="soft">
<ds-icon
v-if="scope.row.type === 'Post'"
@ -71,45 +63,50 @@
/>
</ds-text>
</template>
<template
slot="submitter"
slot-scope="scope"
>
<template slot="submitter" slot-scope="scope">
<nuxt-link
:to="{ name: 'profile-id-slug', params: { id: scope.row.submitter.id, slug: scope.row.submitter.slug } }"
:to="{
name: 'profile-id-slug',
params: { id: scope.row.submitter.id, slug: scope.row.submitter.slug },
}"
>
{{ scope.row.submitter.name }}
</nuxt-link>
</template>
<template
slot="disabledBy"
slot-scope="scope"
>
<template slot="disabledBy" slot-scope="scope">
<nuxt-link
v-if="scope.row.type === 'Post' && scope.row.post.disabledBy"
:to="{ name: 'profile-id-slug', params: { id: scope.row.post.disabledBy.id, slug: scope.row.post.disabledBy.slug } }"
:to="{
name: 'profile-id-slug',
params: { id: scope.row.post.disabledBy.id, slug: scope.row.post.disabledBy.slug },
}"
>
<b>{{ scope.row.post.disabledBy.name | truncate(50) }}</b>
</nuxt-link>
<nuxt-link
v-else-if="scope.row.type === 'Comment' && scope.row.comment.disabledBy"
:to="{ name: 'profile-id-slug', params: { id: scope.row.comment.disabledBy.id, slug: scope.row.comment.disabledBy.slug } }"
:to="{
name: 'profile-id-slug',
params: {
id: scope.row.comment.disabledBy.id,
slug: scope.row.comment.disabledBy.slug,
},
}"
>
<b>{{ scope.row.comment.disabledBy.name | truncate(50) }}</b>
</nuxt-link>
<nuxt-link
v-else-if="scope.row.type === 'User' && scope.row.user.disabledBy"
:to="{ name: 'profile-id-slug', params: { id: scope.row.user.disabledBy.id, slug: scope.row.user.disabledBy.slug } }"
:to="{
name: 'profile-id-slug',
params: { id: scope.row.user.disabledBy.id, slug: scope.row.user.disabledBy.slug },
}"
>
<b>{{ scope.row.user.disabledBy.name | truncate(50) }}</b>
</nuxt-link>
</template>
</ds-table>
<hc-empty
v-else
icon="alert"
:message="$t('moderation.reports.empty')"
/>
<hc-empty v-else icon="alert" :message="$t('moderation.reports.empty')" />
</ds-card>
</template>

View File

@ -2,18 +2,12 @@
<div>
<ds-flex gutter="small">
<ds-flex-item :width="{ base: '100%', sm: 2, md: 2, lg: 1 }">
<transition
name="slide-up"
appear
>
<transition name="slide-up" appear>
<nuxt-child />
</transition>
</ds-flex-item>
<ds-flex-item :width="{ base: '200px' }">
<ds-menu
:routes="routes"
class="post-side-navigation"
/>
<ds-menu :routes="routes" class="post-side-navigation" />
</ds-flex-item>
</ds-flex>
</div>

View File

@ -0,0 +1,100 @@
import { config, shallowMount, createLocalVue } from '@vue/test-utils'
import PostSlug from './index.vue'
import Vuex from 'vuex'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
config.stubs['no-ssr'] = '<span><slot /></span>'
describe('PostSlug', () => {
let wrapper
let Wrapper
let store
let mocks
beforeEach(() => {
store = new Vuex.Store({
getters: {
'auth/user': () => {
return {}
},
},
})
mocks = {
$t: jest.fn(),
$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
$router: {
history: {
push: jest.fn(),
},
},
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$apollo: {
mutate: jest.fn().mockResolvedValue(),
},
}
})
describe('shallowMount', () => {
Wrapper = () => {
return shallowMount(PostSlug, {
store,
mocks,
localVue,
})
}
beforeEach(jest.useFakeTimers)
describe('test mixin "PostMutationHelpers"', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper.setData({
post: {
id: 'p23',
name: 'It is a post',
author: {
id: 'u1',
},
},
})
})
describe('deletion of Post from Page by invoking "deletePostCallback(`page`)"', () => {
beforeEach(() => {
wrapper.vm.deletePostCallback('page')
})
describe('after timeout', () => {
beforeEach(jest.runAllTimers)
it('not emits "deletePost"', () => {
expect(wrapper.emitted().deletePost).toBeFalsy()
})
it('does go to index (main) page', () => {
expect(mocks.$router.history.push).toHaveBeenCalledTimes(1)
})
it('does call mutation', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
it('mutation is successful', () => {
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
})
})
})
})
})

View File

@ -1,41 +1,30 @@
<template>
<transition
name="fade"
appear
>
<transition name="fade" appear>
<ds-card
v-if="post && ready"
:image="post.image"
:class="{'post-card': true, 'disabled-content': post.disabled}"
:class="{ 'post-card': true, 'disabled-content': post.disabled }"
>
<ds-space margin-bottom="small" />
<hc-user
:user="post.author"
:date-time="post.createdAt"
/>
<hc-user :user="post.author" :date-time="post.createdAt" />
<no-ssr>
<content-menu
placement="bottom-end"
resource-type="contribution"
:resource="post"
:callbacks="{ confirm: () => deletePostCallback('page'), cancel: null }"
:is-owner="isAuthor(post.author.id)"
/>
</no-ssr>
<ds-space margin-bottom="small" />
<ds-heading
tag="h3"
no-margin
>
<ds-heading tag="h3" no-margin>
{{ post.title }}
</ds-heading>
<ds-space margin-bottom="small" />
<!-- Content -->
<!-- eslint-disable vue/no-v-html -->
<!-- TODO: replace editor content with tiptap render view -->
<div
class="content hc-editor-content"
v-html="post.content"
/>
<div class="content hc-editor-content" v-html="post.content" />
<!-- eslint-enable vue/no-v-html -->
<ds-space margin="xx-large" />
<!-- Categories -->
@ -44,23 +33,16 @@
<hc-category
v-for="category in post.categories"
:key="category.id"
v-tooltip="{content: category.name, placement: 'top-start', delay: { show: 300 }}"
v-tooltip="{ content: category.name, placement: 'top-start', delay: { show: 300 } }"
:icon="category.icon"
:name="category.name"
/>
</div>
<ds-space margin-bottom="small" />
<!-- Tags -->
<div
v-if="post.tags && post.tags.length"
class="tags"
>
<div v-if="post.tags && post.tags.length" class="tags">
<ds-space margin="xx-small" />
<hc-tag
v-for="tag in post.tags"
:key="tag.id"
:name="tag.name"
/>
<hc-tag v-for="tag in post.tags" :key="tag.id" :name="tag.name" />
</div>
<!-- Shout Button -->
<hc-shout-button
@ -90,8 +72,10 @@ import HcUser from '~/components/User'
import HcShoutButton from '~/components/ShoutButton.vue'
import HcCommentForm from '~/components/comments/CommentForm'
import HcCommentList from '~/components/comments/CommentList'
import PostMutationHelpers from '~/mixins/PostMutationHelpers'
export default {
name: 'PostSlug',
transition: {
name: 'slide-up',
mode: 'out-in',
@ -105,6 +89,7 @@ export default {
HcCommentForm,
HcCommentList,
},
mixins: [PostMutationHelpers],
head() {
return {
title: this.title,

View File

@ -5,50 +5,47 @@
</h2>
<p>Hier findest du weitere infos zum Thema.</p>
<ds-space />
<h3><ds-icon name="compass" /> Themenkategorien</h3>
<h3>
<ds-icon name="compass" />
Themenkategorien
</h3>
<div class="tags">
<ds-icon
v-for="category in post.categories"
:key="category.id"
v-tooltip="{content: category.name, placement: 'top-start', delay: { show: 300 }}"
v-tooltip="{ content: category.name, placement: 'top-start', delay: { show: 300 } }"
:name="category.icon"
size="large"
/>&nbsp;
/>
&nbsp;
<!--<ds-tag
v-for="category in post.categories"
:key="category.id"><ds-icon :name="category.icon" /> {{ category.name }}</ds-tag>-->
:key="category.id"><ds-icon :name="category.icon" /> {{ category.name }}</ds-tag>-->
</div>
<template v-if="post.tags && post.tags.length">
<h3><ds-icon name="tags" /> Schlagwörter</h3>
<h3>
<ds-icon name="tags" />
Schlagwörter
</h3>
<div class="tags">
<ds-tag
v-for="tag in post.tags"
:key="tag.id"
>
<ds-icon name="tag" /> {{ tag.name }}
<ds-tag v-for="tag in post.tags" :key="tag.id">
<ds-icon name="tag" />
{{ tag.name }}
</ds-tag>
</div>
</template>
<h3>Verwandte Beiträge</h3>
<ds-section style="margin: 0 -1.5rem; padding: 1.5rem;">
<ds-flex
v-if="post.relatedContributions && post.relatedContributions.length"
gutter="small"
>
<ds-flex-item
v-for="relatedPost in post.relatedContributions"
<ds-flex v-if="post.relatedContributions && post.relatedContributions.length" gutter="small">
<hc-post-card
v-for="(relatedPost, index) in post.relatedContributions"
:key="relatedPost.id"
:post="relatedPost"
:width="{ base: '100%', lg: 1 }"
>
<hc-post-card :post="relatedPost" />
</ds-flex-item>
@deletePost="post.relatedContributions.splice(index, 1)"
/>
</ds-flex>
<hc-empty
v-else
margin="large"
icon="file"
message="No related Posts"
/>
<hc-empty v-else margin="large" icon="file" message="No related Posts" />
</ds-section>
<ds-space margin-bottom="large" />
</ds-card>

View File

@ -1,9 +1,6 @@
<template>
<ds-card header="Werde aktiv!">
<hc-empty
icon="tasks"
message="Coming Soon…"
/>
<hc-empty icon="tasks" message="Coming Soon…" />
</ds-card>
</template>

View File

@ -1,8 +1,5 @@
<template>
<ds-flex
:width="{ base: '100%' }"
gutter="base"
>
<ds-flex :width="{ base: '100%' }" gutter="base">
<ds-flex-item :width="{ base: '100%', md: 3 }">
<hc-contribution-form />
</ds-flex-item>

View File

@ -1,8 +1,5 @@
<template>
<ds-flex
:width="{ base: '100%' }"
gutter="base"
>
<ds-flex :width="{ base: '100%' }" gutter="base">
<ds-flex-item :width="{ base: '100%', md: 3 }">
<hc-contribution-form :contribution="contribution" />
</ds-flex-item>

View File

@ -0,0 +1,81 @@
import { shallowMount, createLocalVue } from '@vue/test-utils'
import ProfileSlug from './_slug.vue'
import Vuex from 'vuex'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
describe('ProfileSlug', () => {
let wrapper
let Wrapper
let mocks
beforeEach(() => {
mocks = {
post: {
id: 'p23',
name: 'It is a post',
},
$t: jest.fn(),
// If you mocking router, than don't use VueRouter with lacalVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
$router: {
history: {
push: jest.fn(),
},
},
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$apollo: {
mutate: jest.fn().mockResolvedValue(),
},
}
})
describe('shallowMount', () => {
Wrapper = () => {
return shallowMount(ProfileSlug, {
mocks,
localVue,
})
}
beforeEach(jest.useFakeTimers)
describe('test mixin "PostMutationHelpers"', () => {
beforeEach(() => {
wrapper = Wrapper()
})
describe('deletion of Post from List by invoking "deletePostCallback(`list`)"', () => {
beforeEach(() => {
wrapper.vm.deletePostCallback('list')
})
describe('after timeout', () => {
beforeEach(jest.runAllTimers)
it('emits "deletePost"', () => {
expect(wrapper.emitted().deletePost.length).toBe(1)
})
it('does not go to index (main) page', () => {
expect(mocks.$router.history.push).not.toHaveBeenCalled()
})
it('does call mutation', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
it('mutation is successful', () => {
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
})
})
})
})
})

View File

@ -4,84 +4,49 @@
<p>PROFILE IMAGE</p>
</ds-card>
<ds-space />
<ds-flex
v-if="user"
:width="{ base: '100%' }"
gutter="base"
>
<ds-flex v-if="user" :width="{ base: '100%' }" gutter="base">
<ds-flex-item :width="{ base: '100%', sm: 2, md: 2, lg: 1 }">
<ds-card
:class="{'disabled-content': user.disabled}"
:class="{ 'disabled-content': user.disabled }"
style="position: relative; height: auto;"
>
<hc-upload
v-if="myProfile"
:user="user"
/>
<hc-avatar
v-else
:user="user"
class="profile-avatar"
size="x-large"
/>
<hc-upload v-if="myProfile" :user="user" />
<hc-avatar v-else :user="user" class="profile-avatar" size="x-large" />
<no-ssr>
<content-menu
placement="bottom-end"
resource-type="user"
:resource="user"
:callbacks="{ confirm: deletePostCallback, cancel: null }"
:is-owner="myProfile"
class="user-content-menu"
/>
</no-ssr>
<ds-space margin="small">
<ds-heading
tag="h3"
align="center"
no-margin
>
{{ userName }}
</ds-heading>
<ds-text
v-if="user.location"
align="center"
color="soft"
size="small"
>
<ds-heading tag="h3" align="center" no-margin>{{ userName }}</ds-heading>
<ds-text v-if="user.location" align="center" color="soft" size="small">
<ds-icon name="map-marker" />
{{ user.location.name }}
</ds-text>
<ds-text
align="center"
color="soft"
size="small"
>
<ds-text align="center" color="soft" size="small">
{{ $t('profile.memberSince') }} {{ user.createdAt | date('MMMM yyyy') }}
</ds-text>
</ds-space>
<ds-space
v-if="user.badges && user.badges.length"
margin="x-small"
>
<ds-space v-if="user.badges && user.badges.length" margin="x-small">
<hc-badges :badges="user.badges" />
</ds-space>
<ds-flex>
<ds-flex-item>
<no-ssr>
<ds-number :label="$t('profile.followers')">
<hc-count-to
slot="count"
:end-val="followedByCount"
/>
<hc-count-to slot="count" :end-val="followedByCount" />
</ds-number>
</no-ssr>
</ds-flex-item>
<ds-flex-item>
<no-ssr>
<ds-number :label="$t('profile.following')">
<hc-count-to
slot="count"
:end-val="Number(user.followingCount) || 0"
/>
<hc-count-to slot="count" :end-val="Number(user.followingCount) || 0" />
</ds-number>
</no-ssr>
</ds-flex-item>
@ -91,140 +56,72 @@
v-if="!myProfile"
:follow-id="user.id"
:is-followed="user.followedByCurrentUser"
@optimistic="follow => user.followedByCurrentUser = follow"
@optimistic="follow => (user.followedByCurrentUser = follow)"
@update="follow => fetchUser()"
/>
</ds-space>
<template v-if="user.about">
<hr>
<ds-space
margin-top="small"
margin-bottom="small"
>
<ds-text
color="soft"
size="small"
>
{{ user.about }}
</ds-text>
<hr />
<ds-space margin-top="small" margin-bottom="small">
<ds-text color="soft" size="small">{{ user.about }}</ds-text>
</ds-space>
</template>
</ds-card>
<ds-space />
<ds-heading
tag="h3"
soft
style="text-align: center; margin-bottom: 10px;"
>
<ds-heading tag="h3" soft style="text-align: center; margin-bottom: 10px;">
Netzwerk
</ds-heading>
<ds-card style="position: relative; height: auto;">
<ds-space
v-if="user.following && user.following.length"
margin="x-small"
>
<ds-text
tag="h5"
color="soft"
>
Wem folgt {{ userName | truncate(15) }}?
</ds-text>
<ds-space v-if="user.following && user.following.length" margin="x-small">
<ds-text tag="h5" color="soft">Wem folgt {{ userName | truncate(15) }}?</ds-text>
</ds-space>
<template v-if="user.following && user.following.length">
<ds-space
v-for="follow in uniq(user.following)"
:key="follow.id"
margin="x-small"
>
<ds-space v-for="follow in uniq(user.following)" :key="follow.id" margin="x-small">
<!-- TODO: find better solution for rendering errors -->
<no-ssr>
<user
:user="follow"
:trunc="15"
/>
<user :user="follow" :trunc="15" />
</no-ssr>
</ds-space>
<ds-space
v-if="user.followingCount - user.following.length"
margin="small"
>
<ds-text
size="small"
color="softer"
>
<ds-space v-if="user.followingCount - user.following.length" margin="small">
<ds-text size="small" color="softer">
und {{ user.followingCount - user.following.length }} weitere
</ds-text>
</ds-space>
</template>
<template v-else>
<p style="text-align: center; opacity: .5;">
{{ userName }} folgt niemandem
</p>
<p style="text-align: center; opacity: .5;">{{ userName }} folgt niemandem</p>
</template>
</ds-card>
<ds-space />
<ds-card style="position: relative; height: auto;">
<ds-space
v-if="user.followedBy && user.followedBy.length"
margin="x-small"
>
<ds-text
tag="h5"
color="soft"
>
Wer folgt {{ userName | truncate(15) }}?
</ds-text>
<ds-space v-if="user.followedBy && user.followedBy.length" margin="x-small">
<ds-text tag="h5" color="soft">Wer folgt {{ userName | truncate(15) }}?</ds-text>
</ds-space>
<template v-if="user.followedBy && user.followedBy.length">
<ds-space
v-for="follow in uniq(user.followedBy)"
:key="follow.id"
margin="x-small"
>
<ds-space v-for="follow in uniq(user.followedBy)" :key="follow.id" margin="x-small">
<!-- TODO: find better solution for rendering errors -->
<no-ssr>
<user
:user="follow"
:trunc="15"
/>
<user :user="follow" :trunc="15" />
</no-ssr>
</ds-space>
<ds-space
v-if="user.followedByCount - user.followedBy.length"
margin="small"
>
<ds-text
size="small"
color="softer"
>
<ds-space v-if="user.followedByCount - user.followedBy.length" margin="small">
<ds-text size="small" color="softer">
und {{ user.followedByCount - user.followedBy.length }} weitere
</ds-text>
</ds-space>
</template>
<template v-else>
<p style="text-align: center; opacity: .5;">
niemand folgt {{ userName }}
</p>
<p style="text-align: center; opacity: .5;">niemand folgt {{ userName }}</p>
</template>
</ds-card>
<ds-space
v-if="user.socialMedia && user.socialMedia.length"
margin="large"
>
<ds-space v-if="user.socialMedia && user.socialMedia.length" margin="large">
<ds-card style="position: relative; height: auto;">
<ds-space margin="x-small">
<ds-text
tag="h5"
color="soft"
>
<ds-text tag="h5" color="soft">
{{ $t('profile.socialMedia') }} {{ user.name | truncate(15) }}?
</ds-text>
<template>
<ds-space
v-for="link in socialMediaLinks"
:key="link.username"
margin="x-small"
>
<ds-space v-for="link in socialMediaLinks" :key="link.username" margin="x-small">
<a :href="link.url">
<ds-avatar :image="link.favicon" />
{{ 'link.username' }}
@ -236,10 +133,7 @@
</ds-space>
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: 3, md: 5, lg: 3 }">
<ds-flex
:width="{ base: '100%' }"
gutter="small"
>
<ds-flex :width="{ base: '100%' }" gutter="small">
<ds-flex-item class="profile-top-navigation">
<ds-card class="ds-tab-nav">
<ds-flex>
@ -248,10 +142,7 @@
<!-- TODO: find better solution for rendering errors -->
<no-ssr>
<ds-number :label="$t('common.post', null, user.contributionsCount)">
<hc-count-to
slot="count"
:end-val="user.contributionsCount"
/>
<hc-count-to slot="count" :end-val="user.contributionsCount" />
</ds-number>
</no-ssr>
</ds-space>
@ -284,7 +175,7 @@
<ds-flex-item style="text-align: center">
<ds-button
v-if="myProfile"
v-tooltip="{content: 'Create a new Post', placement: 'left', delay: { show: 500 }}"
v-tooltip="{ content: 'Create a new Post', placement: 'left', delay: { show: 500 } }"
:path="{ name: 'post-create' }"
class="profile-post-add-button"
icon="plus"
@ -293,28 +184,21 @@
/>
</ds-flex-item>
<template v-if="activePosts.length">
<ds-flex-item
v-for="post in activePosts"
<hc-post-card
v-for="(post, index) in activePosts"
:key="post.id"
:post="post"
:width="{ base: '100%', md: '100%', xl: '50%' }"
>
<hc-post-card :post="post" />
</ds-flex-item>
@deletePost="user.contributions.splice(index, 1)"
/>
</template>
<template v-else>
<ds-flex-item :width="{ base: '100%' }">
<hc-empty
margin="xx-large"
icon="file"
/>
<hc-empty margin="xx-large" icon="file" />
</ds-flex-item>
</template>
</ds-flex>
<hc-load-more
v-if="hasMore"
:loading="$apollo.loading"
@click="showMoreContributions"
/>
<hc-load-more v-if="hasMore" :loading="$apollo.loading" @click="showMoreContributions" />
</ds-flex-item>
</ds-flex>
</div>
@ -333,6 +217,7 @@ import HcEmpty from '~/components/Empty.vue'
import ContentMenu from '~/components/ContentMenu'
import HcUpload from '~/components/Upload'
import HcAvatar from '~/components/Avatar/Avatar.vue'
import PostMutationHelpers from '~/mixins/PostMutationHelpers'
export default {
components: {
@ -347,6 +232,7 @@ export default {
ContentMenu,
HcUpload,
},
mixins: [PostMutationHelpers],
transition: {
name: 'slide-up',
mode: 'out-in',

View File

@ -5,16 +5,10 @@
</ds-heading>
<ds-flex gutter="small">
<ds-flex-item :width="{ base: '100%', md: '200px' }">
<ds-menu
:routes="routes"
:is-exact="() => true"
/>
<ds-menu :routes="routes" :is-exact="() => true" />
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', md: 1 }">
<transition
name="slide-up"
appear
>
<transition name="slide-up" appear>
<nuxt-child />
</transition>
</ds-flex-item>

View File

@ -1,9 +1,6 @@
<template>
<ds-card :header="$t('settings.download.name')">
<hc-empty
icon="tasks"
message="Coming Soon…"
/>
<hc-empty icon="tasks" message="Coming Soon…" />
</ds-card>
</template>

View File

@ -1,9 +1,6 @@
<template>
<ds-card :header="$t('settings.delete.name')">
<hc-empty
icon="tasks"
message="Coming Soon…"
/>
<hc-empty icon="tasks" message="Coming Soon…" />
</ds-card>
</template>

View File

@ -1,8 +1,5 @@
<template>
<ds-form
v-model="form"
@submit="submit"
>
<ds-form v-model="form" @submit="submit">
<ds-card :header="$t('settings.data.name')">
<ds-input
id="name"
@ -32,13 +29,7 @@
:placeholder="$t('settings.data.labelBio')"
/>
<template slot="footer">
<ds-button
style="float: right;"
icon="check"
type="submit"
:loading="loadingData"
primary
>
<ds-button style="float: right;" icon="check" type="submit" :loading="loadingData" primary>
{{ $t('actions.save') }}
</ds-button>
</template>

View File

@ -1,9 +1,6 @@
<template>
<ds-card :header="$t('settings.invites.name')">
<hc-empty
icon="tasks"
message="Coming Soon…"
/>
<hc-empty icon="tasks" message="Coming Soon…" />
</ds-card>
</template>

View File

@ -1,9 +1,6 @@
<template>
<ds-card :header="$t('settings.languages.name')">
<hc-empty
icon="tasks"
message="Coming Soon…"
/>
<hc-empty icon="tasks" message="Coming Soon…" />
</ds-card>
</template>

View File

@ -1,9 +1,6 @@
<template>
<ds-card :header="$t('settings.organizations.name')">
<hc-empty
icon="tasks"
message="Coming Soon…"
/>
<hc-empty icon="tasks" message="Coming Soon…" />
</ds-card>
</template>

View File

@ -1,19 +1,9 @@
<template>
<ds-card :header="$t('settings.social-media.name')">
<ds-space
v-if="socialMediaLinks"
margin-top="base"
margin="x-small"
>
<ds-space v-if="socialMediaLinks" margin-top="base" margin="x-small">
<ds-list>
<ds-list-item
v-for="link in socialMediaLinks"
:key="link.id"
>
<a
:href="link.url"
target="_blank"
>
<ds-list-item v-for="link in socialMediaLinks" :key="link.id">
<a :href="link.url" target="_blank">
<hc-image
:image-props="{ src: link.favicon }"
alt="Social Media link"
@ -23,15 +13,10 @@
{{ link.url }}
</a>
&nbsp;&nbsp;
<span class="layout-leave-active">|</span> &nbsp;&nbsp;
<ds-icon
name="edit"
class="layout-leave-active"
/>
<a
name="delete"
@click="handleDeleteSocialMedia(link)"
>
<span class="layout-leave-active">|</span>
&nbsp;&nbsp;
<ds-icon name="edit" class="layout-leave-active" />
<a name="delete" @click="handleDeleteSocialMedia(link)">
<ds-icon name="trash" />
</a>
</ds-list-item>
@ -43,15 +28,12 @@
v-model="value"
:placeholder="$t('settings.social-media.placeholder')"
name="social-media"
:schema="{type: 'url'}"
:schema="{ type: 'url' }"
/>
</div>
<ds-space margin-top="base">
<div>
<ds-button
primary
@click="handleAddSocialMedia"
>
<ds-button primary @click="handleAddSocialMedia">
{{ $t('settings.social-media.submit') }}
</ds-button>
</div>

View File

@ -8856,10 +8856,10 @@ prettier@^1.15.2:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.17.0.tgz#53b303676eed22cc14a9f0cec09b477b3026c008"
integrity sha512-sXe5lSt2WQlCbydGETgfm1YBShgOX4HxQkFPvbxkcwgDvGDeqVau8h+12+lmSVlP3rHPz0oavfddSZg/q+Szjw==
prettier@~1.14.3:
version "1.14.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.3.tgz#90238dd4c0684b7edce5f83b0fb7328e48bd0895"
integrity sha512-qZDVnCrnpsRJJq5nSsiHCE3BYMED2OtsI+cmzIzF1QIfqm5ALf8tEJcO27zV1gKNKRPdhjO0dNWnrzssDQ1tFg==
prettier@~1.17.1:
version "1.17.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.17.1.tgz#ed64b4e93e370cb8a25b9ef7fef3e4fd1c0995db"
integrity sha512-TzGRNvuUSmPgwivDqkZ9tM/qTGW9hqDKWOE9YHiyQdixlKbv7kvEqsmDPrcHJTKwthU774TQwZXVtaQ/mMsvjg==
pretty-bytes@^5.2.0:
version "5.2.0"