mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' of github.com:Human-Connection/Human-Connection into 906-maintenace-mode
This commit is contained in:
commit
733d3333c9
14
.codecov.yml
14
.codecov.yml
@ -154,16 +154,4 @@ coverage:
|
|||||||
#fixes:
|
#fixes:
|
||||||
# - "old_path::new_path"
|
# - "old_path::new_path"
|
||||||
|
|
||||||
comment:
|
comment: off
|
||||||
# layout options are quite limited in v4.x - there have been way more options in v1.0
|
|
||||||
layout: reach, diff, flags, files # mostly old options: header, diff, uncovered, reach, files, tree, changes, sunburst, flags
|
|
||||||
behavior: new # default = posts once then update, posts new if delete
|
|
||||||
# once = post once then updates
|
|
||||||
# new = delete old, post new
|
|
||||||
# spammy = post new
|
|
||||||
require_changes: false # if true: only post the comment if coverage changes
|
|
||||||
require_base: no # [yes :: must have a base report to post]
|
|
||||||
require_head: no # [yes :: must have a head report to post]
|
|
||||||
branches: null # branch names that can post comment
|
|
||||||
flags: null
|
|
||||||
paths: null
|
|
||||||
|
|||||||
@ -11,7 +11,6 @@ addons:
|
|||||||
before_install:
|
before_install:
|
||||||
- yarn global add wait-on
|
- yarn global add wait-on
|
||||||
# Install Codecov
|
# Install Codecov
|
||||||
- yarn global add codecov
|
|
||||||
- yarn install
|
- yarn install
|
||||||
- cp cypress.env.template.json cypress.env.json
|
- cp cypress.env.template.json cypress.env.json
|
||||||
|
|
||||||
@ -40,7 +39,7 @@ script:
|
|||||||
# Fullstack
|
# Fullstack
|
||||||
- yarn run cypress:run
|
- yarn run cypress:run
|
||||||
# Coverage
|
# Coverage
|
||||||
- codecov
|
- yarn run codecov
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
|
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
"dev": "nodemon --exec babel-node src/ -e js,gql",
|
"dev": "nodemon --exec babel-node src/ -e js,gql",
|
||||||
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,gql",
|
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,gql",
|
||||||
"lint": "eslint src --config .eslintrc.js",
|
"lint": "eslint src --config .eslintrc.js",
|
||||||
|
"jest": "jest --forceExit --detectOpenHandles --runInBand",
|
||||||
"test": "run-s test:jest test:cucumber",
|
"test": "run-s test:jest test:cucumber",
|
||||||
"test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 yarn run dev 2> /dev/null",
|
"test:before:server": "cross-env GRAPHQL_URI=http://localhost:4123 GRAPHQL_PORT=4123 yarn run dev 2> /dev/null",
|
||||||
"test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions,activityPub yarn run dev 2> /dev/null",
|
"test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions,activityPub yarn run dev 2> /dev/null",
|
||||||
@ -34,7 +35,6 @@
|
|||||||
"!**/src/**/?(*.)+(spec|test).js?(x)"
|
"!**/src/**/?(*.)+(spec|test).js?(x)"
|
||||||
],
|
],
|
||||||
"coverageReporters": [
|
"coverageReporters": [
|
||||||
"text",
|
|
||||||
"lcov"
|
"lcov"
|
||||||
],
|
],
|
||||||
"testMatch": [
|
"testMatch": [
|
||||||
@ -48,59 +48,75 @@
|
|||||||
"apollo-client": "~2.6.3",
|
"apollo-client": "~2.6.3",
|
||||||
"apollo-link-context": "~1.0.18",
|
"apollo-link-context": "~1.0.18",
|
||||||
"apollo-link-http": "~1.5.15",
|
"apollo-link-http": "~1.5.15",
|
||||||
"apollo-server": "~2.6.8",
|
"apollo-server": "~2.7.0",
|
||||||
|
"apollo-server-express": "^2.6.9",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"cheerio": "~1.0.0-rc.3",
|
"cheerio": "~1.0.0-rc.3",
|
||||||
"cors": "~2.8.5",
|
"cors": "~2.8.5",
|
||||||
"cross-env": "~5.2.0",
|
"cross-env": "~5.2.0",
|
||||||
"date-fns": "2.0.0-beta.1",
|
"date-fns": "2.0.0-beta.3",
|
||||||
"debug": "~4.1.1",
|
"debug": "~4.1.1",
|
||||||
"dotenv": "~8.0.0",
|
"dotenv": "~8.0.0",
|
||||||
"express": "~4.17.1",
|
"express": "^4.17.1",
|
||||||
"faker": "Marak/faker.js#master",
|
"faker": "Marak/faker.js#master",
|
||||||
"graphql": "~14.4.2",
|
"graphql": "~14.4.2",
|
||||||
"graphql-custom-directives": "~0.2.14",
|
"graphql-custom-directives": "~0.2.14",
|
||||||
"graphql-iso-date": "~3.6.1",
|
"graphql-iso-date": "~3.6.1",
|
||||||
"graphql-middleware": "~3.0.2",
|
"graphql-middleware": "~3.0.2",
|
||||||
"graphql-shield": "~6.0.3",
|
"graphql-shield": "~6.0.4",
|
||||||
"graphql-tag": "~2.10.1",
|
"graphql-tag": "~2.10.1",
|
||||||
"graphql-yoga": "~1.18.0",
|
"helmet": "~3.19.0",
|
||||||
"helmet": "~3.18.0",
|
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jsonwebtoken": "~8.5.1",
|
||||||
"linkifyjs": "~2.1.8",
|
"linkifyjs": "~2.1.8",
|
||||||
"lodash": "~4.17.14",
|
"lodash": "~4.17.14",
|
||||||
"merge-graphql-schemas": "^1.5.8",
|
"merge-graphql-schemas": "^1.6.1",
|
||||||
|
"metascraper": "^4.10.3",
|
||||||
|
"metascraper-audio": "^5.5.0",
|
||||||
|
"metascraper-author": "^5.6.3",
|
||||||
|
"metascraper-clearbit-logo": "^5.3.0",
|
||||||
|
"metascraper-date": "^5.6.3",
|
||||||
|
"metascraper-description": "^5.5.0",
|
||||||
|
"metascraper-image": "^5.6.3",
|
||||||
|
"metascraper-lang": "^5.6.3",
|
||||||
|
"metascraper-lang-detector": "^4.8.5",
|
||||||
|
"metascraper-logo": "^5.5.0",
|
||||||
|
"metascraper-publisher": "^5.6.3",
|
||||||
|
"metascraper-soundcloud": "^5.5.3",
|
||||||
|
"metascraper-title": "^5.6.3",
|
||||||
|
"metascraper-url": "^5.5.0",
|
||||||
|
"metascraper-video": "^4.8.5",
|
||||||
|
"metascraper-youtube": "^4.8.5",
|
||||||
"neo4j-driver": "~1.7.4",
|
"neo4j-driver": "~1.7.4",
|
||||||
"neo4j-graphql-js": "^2.6.3",
|
"neo4j-graphql-js": "^2.6.3",
|
||||||
"neode": "^0.2.16",
|
"neode": "^0.2.16",
|
||||||
"node-fetch": "~2.6.0",
|
"node-fetch": "~2.6.0",
|
||||||
"nodemailer": "^6.2.1",
|
"nodemailer": "^6.3.0",
|
||||||
"npm-run-all": "~4.1.5",
|
"npm-run-all": "~4.1.5",
|
||||||
"request": "~2.88.0",
|
"request": "~2.88.0",
|
||||||
"sanitize-html": "~1.20.1",
|
"sanitize-html": "~1.20.1",
|
||||||
"slug": "~1.1.0",
|
"slug": "~1.1.0",
|
||||||
"trunc-html": "~1.1.2",
|
"trunc-html": "~1.1.2",
|
||||||
"uuid": "~3.3.2",
|
"uuid": "~3.3.2",
|
||||||
"wait-on": "~3.2.0"
|
"wait-on": "~3.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "~7.5.0",
|
"@babel/cli": "~7.5.5",
|
||||||
"@babel/core": "~7.5.4",
|
"@babel/core": "~7.5.5",
|
||||||
"@babel/node": "~7.5.0",
|
"@babel/node": "~7.5.5",
|
||||||
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
|
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
|
||||||
"@babel/preset-env": "~7.5.4",
|
"@babel/preset-env": "~7.5.5",
|
||||||
"@babel/register": "~7.4.4",
|
"@babel/register": "~7.5.5",
|
||||||
"apollo-server-testing": "~2.6.8",
|
"apollo-server-testing": "~2.7.0",
|
||||||
"babel-core": "~7.0.0-0",
|
"babel-core": "~7.0.0-0",
|
||||||
"babel-eslint": "~10.0.2",
|
"babel-eslint": "~10.0.2",
|
||||||
"babel-jest": "~24.8.0",
|
"babel-jest": "~24.8.0",
|
||||||
"chai": "~4.2.0",
|
"chai": "~4.2.0",
|
||||||
"cucumber": "~5.1.0",
|
"cucumber": "~5.1.0",
|
||||||
"eslint": "~6.0.1",
|
"eslint": "~6.1.0",
|
||||||
"eslint-config-prettier": "~6.0.0",
|
"eslint-config-prettier": "~6.0.0",
|
||||||
"eslint-config-standard": "~12.0.0",
|
"eslint-config-standard": "~12.0.0",
|
||||||
"eslint-plugin-import": "~2.18.0",
|
"eslint-plugin-import": "~2.18.2",
|
||||||
"eslint-plugin-jest": "~22.7.2",
|
"eslint-plugin-jest": "~22.13.6",
|
||||||
"eslint-plugin-node": "~9.1.0",
|
"eslint-plugin-node": "~9.1.0",
|
||||||
"eslint-plugin-prettier": "~3.1.0",
|
"eslint-plugin-prettier": "~3.1.0",
|
||||||
"eslint-plugin-promise": "~4.2.1",
|
"eslint-plugin-promise": "~4.2.1",
|
||||||
|
|||||||
@ -1,18 +1,8 @@
|
|||||||
import createServer from './server'
|
import createServer from './server'
|
||||||
import ActivityPub from './activitypub/ActivityPub'
|
|
||||||
import CONFIG from './config'
|
import CONFIG from './config'
|
||||||
|
|
||||||
const serverConfig = {
|
const { app } = createServer()
|
||||||
port: CONFIG.GRAPHQL_PORT,
|
app.listen({ port: CONFIG.GRAPHQL_PORT }, () => {
|
||||||
// cors: {
|
|
||||||
// credentials: true,
|
|
||||||
// origin: [CONFIG.CLIENT_URI] // your frontend url.
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = createServer()
|
|
||||||
server.start(serverConfig, options => {
|
|
||||||
/* eslint-disable-next-line no-console */
|
/* eslint-disable-next-line no-console */
|
||||||
console.log(`GraphQLServer ready at ${CONFIG.GRAPHQL_URI} 🚀`)
|
console.log(`GraphQLServer ready at ${CONFIG.GRAPHQL_URI} 🚀`)
|
||||||
ActivityPub.init(server)
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -15,3 +15,9 @@ export async function login(variables) {
|
|||||||
authorization: `Bearer ${response.login}`,
|
authorization: `Bearer ${response.login}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//* This is a fake ES2015 template string, just to benefit of syntax
|
||||||
|
// highlighting of `gql` template strings in certain editors.
|
||||||
|
export function gql(strings) {
|
||||||
|
return strings.join('')
|
||||||
|
}
|
||||||
|
|||||||
38
backend/src/jest/snapshots/embeds/HumanConnectionOrg.html
Normal file
38
backend/src/jest/snapshots/embeds/HumanConnectionOrg.html
Normal file
File diff suppressed because one or more lines are too long
1545
backend/src/jest/snapshots/embeds/babyLovesCat.html
Normal file
1545
backend/src/jest/snapshots/embeds/babyLovesCat.html
Normal file
File diff suppressed because one or more lines are too long
10042
backend/src/jest/snapshots/embeds/pr960.html
Normal file
10042
backend/src/jest/snapshots/embeds/pr960.html
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,7 @@ export const signupTemplate = options => {
|
|||||||
} = options
|
} = options
|
||||||
const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI)
|
const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI)
|
||||||
actionUrl.searchParams.set('nonce', nonce)
|
actionUrl.searchParams.set('nonce', nonce)
|
||||||
|
actionUrl.searchParams.set('email', email)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
to: email,
|
to: email,
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
import { GraphQLClient } from 'graphql-request'
|
||||||
import gql from 'graphql-tag'
|
import { host, login, gql } from '../../jest/helpers'
|
||||||
import { host, login } from '../../jest/helpers'
|
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
import { applyMiddleware } from 'graphql-middleware'
|
||||||
import CONFIG from './../config'
|
import CONFIG from './../config'
|
||||||
|
|
||||||
import activityPub from './activityPubMiddleware'
|
import activityPub from './activityPubMiddleware'
|
||||||
import softDelete from './softDeleteMiddleware'
|
import softDelete from './softDeleteMiddleware'
|
||||||
import sluggify from './sluggifyMiddleware'
|
import sluggify from './sluggifyMiddleware'
|
||||||
@ -50,11 +52,14 @@ export default schema => {
|
|||||||
if (CONFIG.DISABLED_MIDDLEWARES) {
|
if (CONFIG.DISABLED_MIDDLEWARES) {
|
||||||
const disabledMiddlewares = CONFIG.DISABLED_MIDDLEWARES.split(',')
|
const disabledMiddlewares = CONFIG.DISABLED_MIDDLEWARES.split(',')
|
||||||
order = order.filter(key => {
|
order = order.filter(key => {
|
||||||
|
if (disabledMiddlewares.includes(key)) {
|
||||||
|
/* eslint-disable-next-line no-console */
|
||||||
|
console.log(`Warning: Disabled "${disabledMiddlewares}" middleware.`)
|
||||||
|
}
|
||||||
return !disabledMiddlewares.includes(key)
|
return !disabledMiddlewares.includes(key)
|
||||||
})
|
})
|
||||||
/* eslint-disable-next-line no-console */
|
|
||||||
console.log(`Warning: "${disabledMiddlewares}" middlewares have been disabled.`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return order.map(key => middlewares[key])
|
const appliedMiddlewares = order.map(key => middlewares[key])
|
||||||
|
return applyMiddleware(schema, ...appliedMiddlewares)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -136,6 +136,7 @@ const permissions = shield(
|
|||||||
Query: {
|
Query: {
|
||||||
'*': deny,
|
'*': deny,
|
||||||
findPosts: allow,
|
findPosts: allow,
|
||||||
|
embed: allow,
|
||||||
Category: allow,
|
Category: allow,
|
||||||
Tag: allow,
|
Tag: allow,
|
||||||
Report: isModerator,
|
Report: isModerator,
|
||||||
@ -146,6 +147,7 @@ const permissions = shield(
|
|||||||
Comment: allow,
|
Comment: allow,
|
||||||
User: or(noEmailFilter, isAdmin),
|
User: or(noEmailFilter, isAdmin),
|
||||||
isLoggedIn: allow,
|
isLoggedIn: allow,
|
||||||
|
Badge: allow,
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
'*': deny,
|
'*': deny,
|
||||||
@ -160,9 +162,6 @@ const permissions = shield(
|
|||||||
UpdatePost: isAuthor,
|
UpdatePost: isAuthor,
|
||||||
DeletePost: isAuthor,
|
DeletePost: isAuthor,
|
||||||
report: isAuthenticated,
|
report: isAuthenticated,
|
||||||
CreateBadge: isAdmin,
|
|
||||||
UpdateBadge: isAdmin,
|
|
||||||
DeleteBadge: isAdmin,
|
|
||||||
CreateSocialMedia: isAuthenticated,
|
CreateSocialMedia: isAuthenticated,
|
||||||
DeleteSocialMedia: isAuthenticated,
|
DeleteSocialMedia: isAuthenticated,
|
||||||
// AddBadgeRewarded: isAdmin,
|
// AddBadgeRewarded: isAdmin,
|
||||||
@ -178,6 +177,7 @@ const permissions = shield(
|
|||||||
enable: isModerator,
|
enable: isModerator,
|
||||||
disable: isModerator,
|
disable: isModerator,
|
||||||
CreateComment: isAuthenticated,
|
CreateComment: isAuthenticated,
|
||||||
|
UpdateComment: isAuthor,
|
||||||
DeleteComment: isAuthor,
|
DeleteComment: isAuthor,
|
||||||
DeleteUser: isDeletingOwnAccount,
|
DeleteUser: isDeletingOwnAccount,
|
||||||
requestPasswordReset: allow,
|
requestPasswordReset: allow,
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
import { UserInputError } from 'apollo-server'
|
|
||||||
|
|
||||||
const validateUrl = async (resolve, root, args, context, info) => {
|
|
||||||
const { url } = args
|
|
||||||
const isValid = url.match(/^(?:https?:\/\/)(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g)
|
|
||||||
if (isValid) {
|
|
||||||
/* eslint-disable-next-line no-return-await */
|
|
||||||
return await resolve(root, args, context, info)
|
|
||||||
} else {
|
|
||||||
throw new UserInputError('Input is not a URL')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
Mutation: {
|
|
||||||
CreateSocialMedia: validateUrl,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@ -1,6 +1,9 @@
|
|||||||
import { UserInputError } from 'apollo-server'
|
import { UserInputError } from 'apollo-server'
|
||||||
import Joi from '@hapi/joi'
|
import Joi from '@hapi/joi'
|
||||||
|
|
||||||
|
const COMMENT_MIN_LENGTH = 1
|
||||||
|
const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
|
||||||
|
|
||||||
const validate = schema => {
|
const validate = schema => {
|
||||||
return async (resolve, root, args, context, info) => {
|
return async (resolve, root, args, context, info) => {
|
||||||
const validation = schema.validate(args)
|
const validation = schema.validate(args)
|
||||||
@ -15,8 +18,47 @@ const socialMediaSchema = Joi.object().keys({
|
|||||||
.required(),
|
.required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const validateCommentCreation = async (resolve, root, args, context, info) => {
|
||||||
|
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||||
|
const { postId } = args
|
||||||
|
|
||||||
|
if (!args.content || content.length < COMMENT_MIN_LENGTH) {
|
||||||
|
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
|
||||||
|
}
|
||||||
|
const session = context.driver.session()
|
||||||
|
const postQueryRes = await session.run(
|
||||||
|
`
|
||||||
|
MATCH (post:Post {id: $postId})
|
||||||
|
RETURN post`,
|
||||||
|
{
|
||||||
|
postId,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const [post] = postQueryRes.records.map(record => {
|
||||||
|
return record.get('post')
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
throw new UserInputError(NO_POST_ERR_MESSAGE)
|
||||||
|
} else {
|
||||||
|
return resolve(root, args, context, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateUpdateComment = async (resolve, root, args, context, info) => {
|
||||||
|
const COMMENT_MIN_LENGTH = 1
|
||||||
|
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||||
|
if (!args.content || content.length < COMMENT_MIN_LENGTH) {
|
||||||
|
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(root, args, context, info)
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
CreateSocialMedia: validate(socialMediaSchema),
|
CreateSocialMedia: validate(socialMediaSchema),
|
||||||
|
CreateComment: validateCommentCreation,
|
||||||
|
UpdateComment: validateUpdateComment,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
7
backend/src/models/Badge.js
Normal file
7
backend/src/models/Badge.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
id: { type: 'string', primary: true, lowercase: true },
|
||||||
|
status: { type: 'string', valid: ['permanent', 'temporary'] },
|
||||||
|
type: { type: 'string', valid: ['role', 'crowdfunding'] },
|
||||||
|
icon: { type: 'string', required: true },
|
||||||
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
|
}
|
||||||
@ -8,5 +8,6 @@ module.exports = {
|
|||||||
relationship: 'BELONGS_TO',
|
relationship: 'BELONGS_TO',
|
||||||
target: 'User',
|
target: 'User',
|
||||||
direction: 'out',
|
direction: 'out',
|
||||||
|
eager: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ module.exports = {
|
|||||||
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
|
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
|
||||||
actorId: { type: 'string', allow: [null] },
|
actorId: { type: 'string', allow: [null] },
|
||||||
name: { type: 'string', min: 3 },
|
name: { type: 'string', min: 3 },
|
||||||
email: { type: 'string', lowercase: true, email: true },
|
|
||||||
slug: 'string',
|
slug: 'string',
|
||||||
encryptedPassword: 'string',
|
encryptedPassword: 'string',
|
||||||
avatar: { type: 'string', allow: [null] },
|
avatar: { type: 'string', allow: [null] },
|
||||||
@ -43,6 +42,12 @@ module.exports = {
|
|||||||
target: 'User',
|
target: 'User',
|
||||||
direction: 'in',
|
direction: 'in',
|
||||||
},
|
},
|
||||||
|
rewarded: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'REWARDED',
|
||||||
|
target: 'Badge',
|
||||||
|
direction: 'in',
|
||||||
|
},
|
||||||
invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' },
|
invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' },
|
||||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
updatedAt: {
|
updatedAt: {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm
|
// NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm
|
||||||
// module that is not browser-compatible. Node's `fs` module is server-side only
|
// module that is not browser-compatible. Node's `fs` module is server-side only
|
||||||
export default {
|
export default {
|
||||||
|
Badge: require('./Badge.js'),
|
||||||
User: require('./User.js'),
|
User: require('./User.js'),
|
||||||
InvitationCode: require('./InvitationCode.js'),
|
InvitationCode: require('./InvitationCode.js'),
|
||||||
EmailAddress: require('./EmailAddress.js'),
|
EmailAddress: require('./EmailAddress.js'),
|
||||||
|
|||||||
9
backend/src/schema/helpers.js
Normal file
9
backend/src/schema/helpers.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export const undefinedToNull = list => {
|
||||||
|
const resolvers = {}
|
||||||
|
list.forEach(key => {
|
||||||
|
resolvers[key] = async (parent, params, context, resolveInfo) => {
|
||||||
|
return typeof parent[key] === 'undefined' ? null : parent[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return resolvers
|
||||||
|
}
|
||||||
@ -12,11 +12,25 @@ export default applyScalars(
|
|||||||
resolvers,
|
resolvers,
|
||||||
config: {
|
config: {
|
||||||
query: {
|
query: {
|
||||||
exclude: ['InvitationCode', 'EmailAddress', 'Notfication', 'Statistics', 'LoggedInUser'],
|
exclude: [
|
||||||
|
'Badge',
|
||||||
|
'InvitationCode',
|
||||||
|
'EmailAddress',
|
||||||
|
'Notfication',
|
||||||
|
'Statistics',
|
||||||
|
'LoggedInUser',
|
||||||
|
],
|
||||||
// add 'User' here as soon as possible
|
// add 'User' here as soon as possible
|
||||||
},
|
},
|
||||||
mutation: {
|
mutation: {
|
||||||
exclude: ['InvitationCode', 'EmailAddress', 'Notfication', 'Statistics', 'LoggedInUser'],
|
exclude: [
|
||||||
|
'Badge',
|
||||||
|
'InvitationCode',
|
||||||
|
'EmailAddress',
|
||||||
|
'Notfication',
|
||||||
|
'Statistics',
|
||||||
|
'LoggedInUser',
|
||||||
|
],
|
||||||
// add 'User' here as soon as possible
|
// add 'User' here as soon as possible
|
||||||
},
|
},
|
||||||
debug: CONFIG.DEBUG,
|
debug: CONFIG.DEBUG,
|
||||||
|
|||||||
9
backend/src/schema/resolvers/badges.js
Normal file
9
backend/src/schema/resolvers/badges.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Query: {
|
||||||
|
Badge: async (object, args, context, resolveInfo) => {
|
||||||
|
return neo4jgraphql(object, args, context, resolveInfo, false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -1,200 +0,0 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
|
||||||
import Factory from '../../seed/factories'
|
|
||||||
import { host, login } from '../../jest/helpers'
|
|
||||||
|
|
||||||
const factory = Factory()
|
|
||||||
let client
|
|
||||||
|
|
||||||
describe('badges', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await factory.create('User', {
|
|
||||||
email: 'user@example.org',
|
|
||||||
role: 'user',
|
|
||||||
password: '1234',
|
|
||||||
})
|
|
||||||
await factory.create('User', {
|
|
||||||
id: 'u2',
|
|
||||||
role: 'moderator',
|
|
||||||
email: 'moderator@example.org',
|
|
||||||
})
|
|
||||||
await factory.create('User', {
|
|
||||||
id: 'u3',
|
|
||||||
role: 'admin',
|
|
||||||
email: 'admin@example.org',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await factory.cleanDatabase()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('CreateBadge', () => {
|
|
||||||
const variables = {
|
|
||||||
id: 'b1',
|
|
||||||
key: 'indiegogo_en_racoon',
|
|
||||||
type: 'crowdfunding',
|
|
||||||
status: 'permanent',
|
|
||||||
icon: '/img/badges/indiegogo_en_racoon.svg',
|
|
||||||
}
|
|
||||||
|
|
||||||
const mutation = `
|
|
||||||
mutation(
|
|
||||||
$id: ID
|
|
||||||
$key: String!
|
|
||||||
$type: BadgeType!
|
|
||||||
$status: BadgeStatus!
|
|
||||||
$icon: String!
|
|
||||||
) {
|
|
||||||
CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
|
|
||||||
id,
|
|
||||||
key,
|
|
||||||
type,
|
|
||||||
status,
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
|
||||||
it('throws authorization error', async () => {
|
|
||||||
client = new GraphQLClient(host)
|
|
||||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('authenticated admin', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const headers = await login({ email: 'admin@example.org', password: '1234' })
|
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
|
||||||
it('creates a badge', async () => {
|
|
||||||
const expected = {
|
|
||||||
CreateBadge: {
|
|
||||||
icon: '/img/badges/indiegogo_en_racoon.svg',
|
|
||||||
id: 'b1',
|
|
||||||
key: 'indiegogo_en_racoon',
|
|
||||||
status: 'permanent',
|
|
||||||
type: 'crowdfunding',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('authenticated moderator', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws authorization error', async () => {
|
|
||||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('UpdateBadge', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await factory.authenticateAs({ email: 'admin@example.org', password: '1234' })
|
|
||||||
await factory.create('Badge', { id: 'b1' })
|
|
||||||
})
|
|
||||||
const variables = {
|
|
||||||
id: 'b1',
|
|
||||||
key: 'whatever',
|
|
||||||
}
|
|
||||||
|
|
||||||
const mutation = `
|
|
||||||
mutation($id: ID!, $key: String!) {
|
|
||||||
UpdateBadge(id: $id, key: $key) {
|
|
||||||
id
|
|
||||||
key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
|
||||||
it('throws authorization error', async () => {
|
|
||||||
client = new GraphQLClient(host)
|
|
||||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('authenticated moderator', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws authorization error', async () => {
|
|
||||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('authenticated admin', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const headers = await login({ email: 'admin@example.org', password: '1234' })
|
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
|
||||||
it('updates a badge', async () => {
|
|
||||||
const expected = {
|
|
||||||
UpdateBadge: {
|
|
||||||
id: 'b1',
|
|
||||||
key: 'whatever',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('DeleteBadge', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await factory.authenticateAs({ email: 'admin@example.org', password: '1234' })
|
|
||||||
await factory.create('Badge', { id: 'b1' })
|
|
||||||
})
|
|
||||||
const variables = {
|
|
||||||
id: 'b1',
|
|
||||||
}
|
|
||||||
|
|
||||||
const mutation = `
|
|
||||||
mutation($id: ID!) {
|
|
||||||
DeleteBadge(id: $id) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
|
||||||
it('throws authorization error', async () => {
|
|
||||||
client = new GraphQLClient(host)
|
|
||||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('authenticated moderator', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws authorization error', async () => {
|
|
||||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('authenticated admin', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const headers = await login({ email: 'admin@example.org', password: '1234' })
|
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
|
||||||
it('deletes a badge', async () => {
|
|
||||||
const expected = {
|
|
||||||
DeleteBadge: {
|
|
||||||
id: 'b1',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,40 +1,15 @@
|
|||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||||
import { UserInputError } from 'apollo-server'
|
|
||||||
|
|
||||||
const COMMENT_MIN_LENGTH = 1
|
|
||||||
const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
CreateComment: async (object, params, context, resolveInfo) => {
|
CreateComment: async (object, params, context, resolveInfo) => {
|
||||||
const content = params.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
|
||||||
const { postId } = params
|
const { postId } = params
|
||||||
// Adding relationship from comment to post by passing in the postId,
|
// Adding relationship from comment to post by passing in the postId,
|
||||||
// but we do not want to create the comment with postId as an attribute
|
// but we do not want to create the comment with postId as an attribute
|
||||||
// because we use relationships for this. So, we are deleting it from params
|
// because we use relationships for this. So, we are deleting it from params
|
||||||
// before comment creation.
|
// before comment creation.
|
||||||
delete params.postId
|
delete params.postId
|
||||||
|
|
||||||
if (!params.content || content.length < COMMENT_MIN_LENGTH) {
|
|
||||||
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
const postQueryRes = await session.run(
|
|
||||||
`
|
|
||||||
MATCH (post:Post {id: $postId})
|
|
||||||
RETURN post`,
|
|
||||||
{
|
|
||||||
postId,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
const [post] = postQueryRes.records.map(record => {
|
|
||||||
return record.get('post')
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!post) {
|
|
||||||
throw new UserInputError(NO_POST_ERR_MESSAGE)
|
|
||||||
}
|
|
||||||
const commentWithoutRelationships = await neo4jgraphql(
|
const commentWithoutRelationships = await neo4jgraphql(
|
||||||
object,
|
object,
|
||||||
params,
|
params,
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import gql from 'graphql-tag'
|
|
||||||
import { GraphQLClient } from 'graphql-request'
|
import { GraphQLClient } from 'graphql-request'
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
import { host, login } from '../../jest/helpers'
|
import { host, login, gql } from '../../jest/helpers'
|
||||||
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
let client
|
let client
|
||||||
@ -10,7 +9,28 @@ let createPostVariables
|
|||||||
let createCommentVariablesSansPostId
|
let createCommentVariablesSansPostId
|
||||||
let createCommentVariablesWithNonExistentPost
|
let createCommentVariablesWithNonExistentPost
|
||||||
let userParams
|
let userParams
|
||||||
let authorParams
|
let headers
|
||||||
|
|
||||||
|
const createPostMutation = gql`
|
||||||
|
mutation($id: ID!, $title: String!, $content: String!) {
|
||||||
|
CreatePost(id: $id, title: $title, content: $content) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const createCommentMutation = gql`
|
||||||
|
mutation($id: ID, $postId: ID!, $content: String!) {
|
||||||
|
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||||
|
id
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
createPostVariables = {
|
||||||
|
id: 'p1',
|
||||||
|
title: 'post to comment on',
|
||||||
|
content: 'please comment on me',
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
userParams = {
|
userParams = {
|
||||||
@ -26,21 +46,6 @@ afterEach(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('CreateComment', () => {
|
describe('CreateComment', () => {
|
||||||
const createCommentMutation = gql`
|
|
||||||
mutation($postId: ID!, $content: String!) {
|
|
||||||
CreateComment(postId: $postId, content: $content) {
|
|
||||||
id
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
const createPostMutation = gql`
|
|
||||||
mutation($id: ID!, $title: String!, $content: String!) {
|
|
||||||
CreatePost(id: $id, title: $title, content: $content) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
createCommentVariables = {
|
createCommentVariables = {
|
||||||
@ -55,7 +60,6 @@ describe('CreateComment', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated', () => {
|
describe('authenticated', () => {
|
||||||
let headers
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
headers = await login(userParams)
|
headers = await login(userParams)
|
||||||
client = new GraphQLClient(host, {
|
client = new GraphQLClient(host, {
|
||||||
@ -65,11 +69,6 @@ describe('CreateComment', () => {
|
|||||||
postId: 'p1',
|
postId: 'p1',
|
||||||
content: "I'm authorised to comment",
|
content: "I'm authorised to comment",
|
||||||
}
|
}
|
||||||
createPostVariables = {
|
|
||||||
id: 'p1',
|
|
||||||
title: 'post to comment on',
|
|
||||||
content: 'please comment on me',
|
|
||||||
}
|
|
||||||
await client.request(createPostMutation, createPostVariables)
|
await client.request(createPostMutation, createPostVariables)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -188,19 +187,8 @@ describe('CreateComment', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('DeleteComment', () => {
|
describe('ManageComments', () => {
|
||||||
const deleteCommentMutation = gql`
|
let authorParams
|
||||||
mutation($id: ID!) {
|
|
||||||
DeleteComment(id: $id) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
let deleteCommentVariables = {
|
|
||||||
id: 'c1',
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
authorParams = {
|
authorParams = {
|
||||||
email: 'author@example.org',
|
email: 'author@example.org',
|
||||||
@ -214,51 +202,178 @@ describe('DeleteComment', () => {
|
|||||||
content: 'Post to be commented',
|
content: 'Post to be commented',
|
||||||
})
|
})
|
||||||
await asAuthor.create('Comment', {
|
await asAuthor.create('Comment', {
|
||||||
id: 'c1',
|
id: 'c456',
|
||||||
postId: 'p1',
|
postId: 'p1',
|
||||||
content: 'Comment to be deleted',
|
content: 'Comment to be deleted',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('UpdateComment', () => {
|
||||||
it('throws authorization error', async () => {
|
const updateCommentMutation = gql`
|
||||||
client = new GraphQLClient(host)
|
mutation($content: String!, $id: ID!) {
|
||||||
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow(
|
UpdateComment(content: $content, id: $id) {
|
||||||
'Not Authorised',
|
id
|
||||||
)
|
content
|
||||||
})
|
}
|
||||||
})
|
|
||||||
|
|
||||||
describe('authenticated but not the author', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
let headers
|
|
||||||
headers = await login(userParams)
|
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws authorization error', async () => {
|
|
||||||
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow(
|
|
||||||
'Not Authorised',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('authenticated as author', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
let headers
|
|
||||||
headers = await login(authorParams)
|
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('deletes the comment', async () => {
|
|
||||||
const expected = {
|
|
||||||
DeleteComment: {
|
|
||||||
id: 'c1',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).resolves.toEqual(
|
`
|
||||||
expected,
|
|
||||||
)
|
let updateCommentVariables = {
|
||||||
|
id: 'c456',
|
||||||
|
content: 'The comment is updated',
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
client = new GraphQLClient(host)
|
||||||
|
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
||||||
|
'Not Authorised',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated but not the author', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
headers = await login({
|
||||||
|
email: 'test@example.org',
|
||||||
|
password: '1234',
|
||||||
|
})
|
||||||
|
client = new GraphQLClient(host, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
||||||
|
'Not Authorised',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated as author', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
headers = await login(authorParams)
|
||||||
|
client = new GraphQLClient(host, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates the comment', async () => {
|
||||||
|
const expected = {
|
||||||
|
UpdateComment: {
|
||||||
|
id: 'c456',
|
||||||
|
content: 'The comment is updated',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
client.request(updateCommentMutation, updateCommentVariables),
|
||||||
|
).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throw an error if an empty string is sent from the editor as content', async () => {
|
||||||
|
updateCommentVariables = {
|
||||||
|
id: 'c456',
|
||||||
|
content: '<p></p>',
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
||||||
|
'Comment must be at least 1 character long!',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error if a comment sent from the editor does not contain a single letter character', async () => {
|
||||||
|
updateCommentVariables = {
|
||||||
|
id: 'c456',
|
||||||
|
content: '<p> </p>',
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
||||||
|
'Comment must be at least 1 character long!',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error if commentId is sent as an empty string', async () => {
|
||||||
|
updateCommentVariables = {
|
||||||
|
id: '',
|
||||||
|
content: '<p>Hello</p>',
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
||||||
|
'Not Authorised!',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error if the comment does not exist in the database', async () => {
|
||||||
|
updateCommentVariables = {
|
||||||
|
id: 'c1000',
|
||||||
|
content: '<p>Hello</p>',
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(client.request(updateCommentMutation, updateCommentVariables)).rejects.toThrow(
|
||||||
|
'Not Authorised!',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('DeleteComment', () => {
|
||||||
|
const deleteCommentMutation = gql`
|
||||||
|
mutation($id: ID!) {
|
||||||
|
DeleteComment(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
let deleteCommentVariables = {
|
||||||
|
id: 'c456',
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
client = new GraphQLClient(host)
|
||||||
|
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow(
|
||||||
|
'Not Authorised',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated but not the author', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
headers = await login({
|
||||||
|
email: 'test@example.org',
|
||||||
|
password: '1234',
|
||||||
|
})
|
||||||
|
client = new GraphQLClient(host, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).rejects.toThrow(
|
||||||
|
'Not Authorised',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated as author', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
headers = await login(authorParams)
|
||||||
|
client = new GraphQLClient(host, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deletes the comment', async () => {
|
||||||
|
const expected = {
|
||||||
|
DeleteComment: {
|
||||||
|
id: 'c456',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
client.request(deleteCommentMutation, deleteCommentVariables),
|
||||||
|
).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
29
backend/src/schema/resolvers/embeds.js
Normal file
29
backend/src/schema/resolvers/embeds.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import scrape from './embeds/scraper.js'
|
||||||
|
import { undefinedToNull } from '../helpers'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Query: {
|
||||||
|
embed: async (object, { url }, context, resolveInfo) => {
|
||||||
|
return scrape(url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Embed: {
|
||||||
|
...undefinedToNull([
|
||||||
|
'type',
|
||||||
|
'title',
|
||||||
|
'author',
|
||||||
|
'publisher',
|
||||||
|
'date',
|
||||||
|
'description',
|
||||||
|
'url',
|
||||||
|
'image',
|
||||||
|
'audio',
|
||||||
|
'video',
|
||||||
|
'lang',
|
||||||
|
'html',
|
||||||
|
]),
|
||||||
|
sources: async (parent, params, context, resolveInfo) => {
|
||||||
|
return typeof parent.sources === 'undefined' ? [] : parent.sources
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
216
backend/src/schema/resolvers/embeds.spec.js
Normal file
216
backend/src/schema/resolvers/embeds.spec.js
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
import fetch from 'node-fetch'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
import createServer from '../../server'
|
||||||
|
import { gql } from '../../jest/helpers'
|
||||||
|
|
||||||
|
jest.mock('node-fetch')
|
||||||
|
const { Response } = jest.requireActual('node-fetch')
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fetch.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
|
let variables = {}
|
||||||
|
|
||||||
|
const HumanConnectionOrg = fs.readFileSync(
|
||||||
|
path.join(__dirname, '../../jest/snapshots/embeds/HumanConnectionOrg.html'),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
const pr960 = fs.readFileSync(
|
||||||
|
path.join(__dirname, '../../jest/snapshots/embeds/pr960.html'),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
const babyLovesCat = fs.readFileSync(
|
||||||
|
path.join(__dirname, '../../jest/snapshots/embeds/babyLovesCat.html'),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
|
||||||
|
const babyLovesCatEmbedResponse = new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
height: 270,
|
||||||
|
provider_name: 'YouTube',
|
||||||
|
title: 'Baby Loves Cat',
|
||||||
|
type: 'video',
|
||||||
|
width: 480,
|
||||||
|
thumbnail_height: 360,
|
||||||
|
provider_url: 'https://www.youtube.com/',
|
||||||
|
thumbnail_width: 480,
|
||||||
|
html:
|
||||||
|
'<iframe width="480" height="270" src="https://www.youtube.com/embed/qkdXAtO40Fo?start=18&feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>',
|
||||||
|
thumbnail_url: 'https://i.ytimg.com/vi/qkdXAtO40Fo/hqdefault.jpg',
|
||||||
|
version: '1.0',
|
||||||
|
author_name: 'Merkley Family',
|
||||||
|
author_url: 'https://www.youtube.com/channel/UC5P8yei950tif7UmdPpkJLQ',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
describe('Query', () => {
|
||||||
|
describe('embed', () => {
|
||||||
|
let embedAction
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
embedAction = async variables => {
|
||||||
|
const { server } = createServer({
|
||||||
|
context: () => {},
|
||||||
|
})
|
||||||
|
const { query } = createTestClient(server)
|
||||||
|
const embed = gql`
|
||||||
|
query($url: String!) {
|
||||||
|
embed(url: $url) {
|
||||||
|
type
|
||||||
|
title
|
||||||
|
author
|
||||||
|
publisher
|
||||||
|
date
|
||||||
|
description
|
||||||
|
url
|
||||||
|
image
|
||||||
|
audio
|
||||||
|
video
|
||||||
|
lang
|
||||||
|
sources
|
||||||
|
html
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
return query({ query: embed, variables })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given a video link', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch
|
||||||
|
.mockReturnValueOnce(Promise.resolve(new Response('')))
|
||||||
|
.mockReturnValueOnce(Promise.resolve(JSON.stringify({})))
|
||||||
|
variables = { url: 'https://www.w3schools.com/html/mov_bbb.mp4' }
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows some default data', async () => {
|
||||||
|
const expected = expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
embed: {
|
||||||
|
audio: null,
|
||||||
|
author: null,
|
||||||
|
date: null,
|
||||||
|
description: null,
|
||||||
|
html: null,
|
||||||
|
image: null,
|
||||||
|
lang: null,
|
||||||
|
publisher: null,
|
||||||
|
sources: ['resource'],
|
||||||
|
title: null,
|
||||||
|
type: 'link',
|
||||||
|
url: 'https://www.w3schools.com/html/mov_bbb.mp4',
|
||||||
|
video: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await expect(embedAction(variables)).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given a Facebook link', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch
|
||||||
|
.mockReturnValueOnce(Promise.resolve(new Response(HumanConnectionOrg)))
|
||||||
|
.mockReturnValueOnce(Promise.resolve('invalid json'))
|
||||||
|
variables = { url: 'https://www.facebook.com/HumanConnectionOrg/' }
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not crash if embed provider returns invalid JSON', async () => {
|
||||||
|
const expected = expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
embed: {
|
||||||
|
audio: null,
|
||||||
|
author: null,
|
||||||
|
date: expect.any(String),
|
||||||
|
description:
|
||||||
|
'Human Connection, Weilheim an der Teck. Gefällt 24.407 Mal. An upcoming non-profit social network focused on local and global positive change. Twitter accounts : @hc_world (EN), @hc_deutschland (GE),...',
|
||||||
|
html: null,
|
||||||
|
image:
|
||||||
|
'https://scontent.ftxl3-1.fna.fbcdn.net/v/t1.0-1/c5.0.200.200a/p200x200/12108307_997373093648222_70057205881020137_n.jpg?_nc_cat=110&_nc_oc=AQnPPYQlR0dU556gOfl4xkXr7IPZdRIAUfQeXl3fpUv4DAsFN8T4PfgOjPwuq85GPKGZ5S5E5mWQ8IVV1UiRBAIZ&_nc_ht=scontent.ftxl3-1.fna&oh=90309adddaab38839782f16e7d4b7bcf&oe=5DEEDFE5',
|
||||||
|
lang: 'de',
|
||||||
|
publisher: 'Facebook',
|
||||||
|
sources: ['resource'],
|
||||||
|
title: 'Human Connection',
|
||||||
|
type: 'link',
|
||||||
|
url: 'https://www.facebook.com/HumanConnectionOrg/',
|
||||||
|
video: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await expect(embedAction(variables)).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given a Github link', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch
|
||||||
|
.mockReturnValueOnce(Promise.resolve(new Response(pr960)))
|
||||||
|
.mockReturnValueOnce(Promise.resolve(JSON.stringify({})))
|
||||||
|
variables = { url: 'https://github.com/Human-Connection/Human-Connection/pull/960' }
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns meta data even if no embed html can be retrieved', async () => {
|
||||||
|
const expected = expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
embed: {
|
||||||
|
type: 'link',
|
||||||
|
title:
|
||||||
|
'Editor embeds merge in nitro embed by mattwr18 · Pull Request #960 · Human-Connection/Human-Connection',
|
||||||
|
author: 'Human-Connection',
|
||||||
|
publisher: 'GitHub',
|
||||||
|
date: expect.any(String),
|
||||||
|
description: '🍰 Pullrequest Issues fixes #256',
|
||||||
|
url: 'https://github.com/Human-Connection/Human-Connection/pull/960',
|
||||||
|
image:
|
||||||
|
'https://repository-images.githubusercontent.com/112590397/52c9a000-7e11-11e9-899d-aaa55f3a3d72',
|
||||||
|
audio: null,
|
||||||
|
video: null,
|
||||||
|
lang: 'en',
|
||||||
|
sources: ['resource'],
|
||||||
|
html: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await expect(embedAction(variables)).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given a youtube link', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch
|
||||||
|
.mockReturnValueOnce(Promise.resolve(new Response(babyLovesCat)))
|
||||||
|
.mockReturnValueOnce(Promise.resolve(babyLovesCatEmbedResponse))
|
||||||
|
variables = { url: 'https://www.youtube.com/watch?v=qkdXAtO40Fo&t=18s' }
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns meta data plus youtube iframe html', async () => {
|
||||||
|
const expected = expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
embed: {
|
||||||
|
type: 'video',
|
||||||
|
title: 'Baby Loves Cat',
|
||||||
|
author: 'Merkley Family',
|
||||||
|
publisher: 'YouTube',
|
||||||
|
date: expect.any(String),
|
||||||
|
description:
|
||||||
|
'She’s incapable of controlling her limbs when her kitty is around. The obsession grows every day. Ps. That’s a sleep sack she’s in. Not a starfish outfit. Al...',
|
||||||
|
url: 'https://www.youtube.com/watch?v=qkdXAtO40Fo',
|
||||||
|
image: 'https://i.ytimg.com/vi/qkdXAtO40Fo/maxresdefault.jpg',
|
||||||
|
audio: null,
|
||||||
|
video: null,
|
||||||
|
lang: 'de',
|
||||||
|
sources: ['resource', 'oembed'],
|
||||||
|
html:
|
||||||
|
'<iframe width="480" height="270" src="https://www.youtube.com/embed/qkdXAtO40Fo?start=18&feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await expect(embedAction(variables)).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
3014
backend/src/schema/resolvers/embeds/providers.json
Normal file
3014
backend/src/schema/resolvers/embeds/providers.json
Normal file
File diff suppressed because it is too large
Load Diff
102
backend/src/schema/resolvers/embeds/scraper.js
Normal file
102
backend/src/schema/resolvers/embeds/scraper.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import Metascraper from 'metascraper'
|
||||||
|
import fetch from 'node-fetch'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import { ApolloError } from 'apollo-server'
|
||||||
|
import isEmpty from 'lodash/isEmpty'
|
||||||
|
import isArray from 'lodash/isArray'
|
||||||
|
import mergeWith from 'lodash/mergeWith'
|
||||||
|
|
||||||
|
const error = require('debug')('embed:error')
|
||||||
|
|
||||||
|
const metascraper = Metascraper([
|
||||||
|
require('metascraper-author')(),
|
||||||
|
require('metascraper-date')(),
|
||||||
|
require('metascraper-description')(),
|
||||||
|
require('metascraper-image')(),
|
||||||
|
require('metascraper-lang')(),
|
||||||
|
require('metascraper-lang-detector')(),
|
||||||
|
require('metascraper-logo')(),
|
||||||
|
// require('metascraper-clearbit-logo')(),
|
||||||
|
require('metascraper-publisher')(),
|
||||||
|
require('metascraper-title')(),
|
||||||
|
require('metascraper-url')(),
|
||||||
|
require('metascraper-audio')(),
|
||||||
|
require('metascraper-soundcloud')(),
|
||||||
|
require('metascraper-video')(),
|
||||||
|
require('metascraper-youtube')(),
|
||||||
|
|
||||||
|
// require('./rules/metascraper-embed')()
|
||||||
|
])
|
||||||
|
|
||||||
|
let oEmbedProvidersFile = fs.readFileSync(path.join(__dirname, './providers.json'), 'utf8')
|
||||||
|
|
||||||
|
// some providers allow a format parameter
|
||||||
|
// we need JSON
|
||||||
|
oEmbedProvidersFile = oEmbedProvidersFile.replace('{format}', 'json')
|
||||||
|
|
||||||
|
const oEmbedProviders = JSON.parse(oEmbedProvidersFile)
|
||||||
|
|
||||||
|
const fetchEmbed = async url => {
|
||||||
|
const provider = oEmbedProviders.find(provider => {
|
||||||
|
return provider.provider_url.includes(url.hostname)
|
||||||
|
})
|
||||||
|
if (!provider) return {}
|
||||||
|
const {
|
||||||
|
endpoints: [endpoint],
|
||||||
|
} = provider
|
||||||
|
const endpointUrl = new URL(endpoint.url)
|
||||||
|
endpointUrl.searchParams.append('url', url.href)
|
||||||
|
endpointUrl.searchParams.append('format', 'json')
|
||||||
|
let json
|
||||||
|
try {
|
||||||
|
const response = await fetch(endpointUrl)
|
||||||
|
json = await response.json()
|
||||||
|
} catch (err) {
|
||||||
|
error(`Error fetching embed data: ${err.message}`)
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: json.type,
|
||||||
|
html: json.html,
|
||||||
|
author: json.author_name,
|
||||||
|
date: json.upload_date,
|
||||||
|
sources: ['oembed'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchResource = async url => {
|
||||||
|
const response = await fetch(url)
|
||||||
|
const html = await response.text()
|
||||||
|
const resource = await metascraper({ html, url: url.href })
|
||||||
|
return {
|
||||||
|
sources: ['resource'],
|
||||||
|
...resource,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function scrape(url) {
|
||||||
|
url = new URL(url)
|
||||||
|
if (url.hostname === 'youtu.be') {
|
||||||
|
// replace youtu.be to get proper results
|
||||||
|
url.hostname = 'youtube.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
const [meta, embed] = await Promise.all([fetchResource(url), fetchEmbed(url)])
|
||||||
|
const output = mergeWith(meta, embed, (objValue, srcValue) => {
|
||||||
|
if (isArray(objValue)) {
|
||||||
|
return objValue.concat(srcValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isEmpty(output)) {
|
||||||
|
throw new ApolloError('Not found', 'NOT_FOUND')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'link',
|
||||||
|
...output,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@ export async function createPasswordReset(options) {
|
|||||||
const { driver, code, email, issuedAt = new Date() } = options
|
const { driver, code, email, issuedAt = new Date() } = options
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
const cypher = `
|
const cypher = `
|
||||||
MATCH (u:User) WHERE u.email = $email
|
MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email})
|
||||||
CREATE(pr:PasswordReset {code: $code, issuedAt: datetime($issuedAt), usedAt: NULL})
|
CREATE(pr:PasswordReset {code: $code, issuedAt: datetime($issuedAt), usedAt: NULL})
|
||||||
MERGE (u)-[:REQUESTED]->(pr)
|
MERGE (u)-[:REQUESTED]->(pr)
|
||||||
RETURN u
|
RETURN u
|
||||||
@ -35,7 +35,7 @@ export default {
|
|||||||
const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10)
|
const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10)
|
||||||
const cypher = `
|
const cypher = `
|
||||||
MATCH (pr:PasswordReset {code: $code})
|
MATCH (pr:PasswordReset {code: $code})
|
||||||
MATCH (u:User {email: $email})-[:REQUESTED]->(pr)
|
MATCH (e:EmailAddress {email: $email})<-[:PRIMARY_EMAIL]-(u:User)-[:REQUESTED]->(pr)
|
||||||
WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL
|
WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL
|
||||||
SET pr.usedAt = datetime()
|
SET pr.usedAt = datetime()
|
||||||
SET u.encryptedPassword = $encryptedNewPassword
|
SET u.encryptedPassword = $encryptedNewPassword
|
||||||
|
|||||||
@ -18,10 +18,11 @@ const createPostWithCategoriesMutation = `
|
|||||||
mutation($title: String!, $content: String!, $categoryIds: [ID]) {
|
mutation($title: String!, $content: String!, $categoryIds: [ID]) {
|
||||||
CreatePost(title: $title, content: $content, categoryIds: $categoryIds) {
|
CreatePost(title: $title, content: $content, categoryIds: $categoryIds) {
|
||||||
id
|
id
|
||||||
|
title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const creatPostWithCategoriesVariables = {
|
const createPostWithCategoriesVariables = {
|
||||||
title: postTitle,
|
title: postTitle,
|
||||||
content: postContent,
|
content: postContent,
|
||||||
categoryIds: ['cat9', 'cat4', 'cat15'],
|
categoryIds: ['cat9', 'cat4', 'cat15'],
|
||||||
@ -35,6 +36,26 @@ const postQueryWithCategories = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
const createPostWithoutCategoriesVariables = {
|
||||||
|
title: 'This is a post without categories',
|
||||||
|
content: 'I should be able to filter it out',
|
||||||
|
categoryIds: null,
|
||||||
|
}
|
||||||
|
const postQueryFilteredByCategory = `
|
||||||
|
query Post($filter: _PostFilter) {
|
||||||
|
Post(filter: $filter) {
|
||||||
|
title
|
||||||
|
id
|
||||||
|
categories {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const postCategoriesFilterParam = { categories_some: { id_in: ['cat4'] } }
|
||||||
|
const postQueryFilteredByCategoryVariables = {
|
||||||
|
filter: postCategoriesFilterParam,
|
||||||
|
}
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
userParams = {
|
userParams = {
|
||||||
name: 'TestUser',
|
name: 'TestUser',
|
||||||
@ -133,7 +154,8 @@ describe('CreatePost', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('categories', () => {
|
describe('categories', () => {
|
||||||
it('allows a user to set the categories of the post', async () => {
|
let postWithCategories
|
||||||
|
beforeEach(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
factory.create('Category', {
|
factory.create('Category', {
|
||||||
id: 'cat9',
|
id: 'cat9',
|
||||||
@ -151,18 +173,39 @@ describe('CreatePost', () => {
|
|||||||
icon: 'shopping-cart',
|
icon: 'shopping-cart',
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
const expected = [{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]
|
postWithCategories = await client.request(
|
||||||
const postWithCategories = await client.request(
|
|
||||||
createPostWithCategoriesMutation,
|
createPostWithCategoriesMutation,
|
||||||
creatPostWithCategoriesVariables,
|
createPostWithCategoriesVariables,
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows a user to set the categories of the post', async () => {
|
||||||
|
const expected = [{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]
|
||||||
const postQueryWithCategoriesVariables = {
|
const postQueryWithCategoriesVariables = {
|
||||||
id: postWithCategories.CreatePost.id,
|
id: postWithCategories.CreatePost.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
client.request(postQueryWithCategories, postQueryWithCategoriesVariables),
|
client.request(postQueryWithCategories, postQueryWithCategoriesVariables),
|
||||||
).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] })
|
).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('allows a user to filter for posts by category', async () => {
|
||||||
|
await client.request(createPostWithCategoriesMutation, createPostWithoutCategoriesVariables)
|
||||||
|
const categoryIds = [{ id: 'cat4' }, { id: 'cat15' }, { id: 'cat9' }]
|
||||||
|
const expected = {
|
||||||
|
Post: [
|
||||||
|
{
|
||||||
|
title: postTitle,
|
||||||
|
id: postWithCategories.CreatePost.id,
|
||||||
|
categories: expect.arrayContaining(categoryIds),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
client.request(postQueryFilteredByCategory, postQueryFilteredByCategoryVariables),
|
||||||
|
).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -260,7 +303,7 @@ describe('UpdatePost', () => {
|
|||||||
])
|
])
|
||||||
postWithCategories = await client.request(
|
postWithCategories = await client.request(
|
||||||
createPostWithCategoriesMutation,
|
createPostWithCategoriesMutation,
|
||||||
creatPostWithCategoriesVariables,
|
createPostWithCategoriesVariables,
|
||||||
)
|
)
|
||||||
updatePostVariables = {
|
updatePostVariables = {
|
||||||
id: postWithCategories.CreatePost.id,
|
id: postWithCategories.CreatePost.id,
|
||||||
|
|||||||
@ -12,8 +12,8 @@ const instance = neode()
|
|||||||
*/
|
*/
|
||||||
const checkEmailDoesNotExist = async ({ email }) => {
|
const checkEmailDoesNotExist = async ({ email }) => {
|
||||||
email = email.toLowerCase()
|
email = email.toLowerCase()
|
||||||
const users = await instance.all('User', { email })
|
const emails = await instance.all('EmailAddress', { email })
|
||||||
if (users.length > 0) throw new UserInputError('User account with this email already exists.')
|
if (emails.length > 0) throw new UserInputError('User account with this email already exists.')
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@ -166,11 +166,12 @@ describe('SignupByInvitation', () => {
|
|||||||
await expect(action()).rejects.toThrow('"email" must be a valid email')
|
await expect(action()).rejects.toThrow('"email" must be a valid email')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates no EmailAddress node', async done => {
|
it('creates no additional EmailAddress node', async done => {
|
||||||
try {
|
try {
|
||||||
await action()
|
await action()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const emailAddresses = await instance.all('EmailAddress')
|
let emailAddresses = await instance.all('EmailAddress')
|
||||||
|
emailAddresses = await emailAddresses.toJson
|
||||||
expect(emailAddresses).toHaveLength(0)
|
expect(emailAddresses).toHaveLength(0)
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
@ -191,16 +192,16 @@ describe('SignupByInvitation', () => {
|
|||||||
describe('creates a EmailAddress node', () => {
|
describe('creates a EmailAddress node', () => {
|
||||||
it('with a `createdAt` attribute', async () => {
|
it('with a `createdAt` attribute', async () => {
|
||||||
await action()
|
await action()
|
||||||
const emailAddresses = await instance.all('EmailAddress')
|
let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' })
|
||||||
const emailAddress = await emailAddresses.first().toJson()
|
emailAddress = await emailAddress.toJson()
|
||||||
expect(emailAddress.createdAt).toBeTruthy()
|
expect(emailAddress.createdAt).toBeTruthy()
|
||||||
expect(Date.parse(emailAddress.createdAt)).toEqual(expect.any(Number))
|
expect(Date.parse(emailAddress.createdAt)).toEqual(expect.any(Number))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('with a cryptographic `nonce`', async () => {
|
it('with a cryptographic `nonce`', async () => {
|
||||||
await action()
|
await action()
|
||||||
const emailAddresses = await instance.all('EmailAddress')
|
let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' })
|
||||||
const emailAddress = await emailAddresses.first().toJson()
|
emailAddress = await emailAddress.toJson()
|
||||||
expect(emailAddress.nonce).toEqual(expect.any(String))
|
expect(emailAddress.nonce).toEqual(expect.any(String))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -220,6 +221,7 @@ describe('SignupByInvitation', () => {
|
|||||||
it('rejects because codes can be used only once', async done => {
|
it('rejects because codes can be used only once', async done => {
|
||||||
await action()
|
await action()
|
||||||
try {
|
try {
|
||||||
|
variables.email = 'yetanotheremail@example.org'
|
||||||
await action()
|
await action()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toMatch(/Invitation code already used/)
|
expect(e.message).toMatch(/Invitation code already used/)
|
||||||
@ -282,8 +284,8 @@ describe('Signup', () => {
|
|||||||
|
|
||||||
it('creates a Signup with a cryptographic `nonce`', async () => {
|
it('creates a Signup with a cryptographic `nonce`', async () => {
|
||||||
await action()
|
await action()
|
||||||
const emailAddresses = await instance.all('EmailAddress')
|
let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' })
|
||||||
const emailAddress = await emailAddresses.first().toJson()
|
emailAddress = await emailAddress.toJson()
|
||||||
expect(emailAddress.nonce).toEqual(expect.any(String))
|
expect(emailAddress.nonce).toEqual(expect.any(String))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,47 +1,47 @@
|
|||||||
|
import { neode } from '../../bootstrap/neo4j'
|
||||||
|
import { UserInputError } from 'apollo-server'
|
||||||
|
|
||||||
|
const instance = neode()
|
||||||
|
|
||||||
|
const getUserAndBadge = async ({ badgeKey, userId }) => {
|
||||||
|
let user = await instance.first('User', 'id', userId)
|
||||||
|
const badge = await instance.first('Badge', 'id', badgeKey)
|
||||||
|
if (!user) throw new UserInputError("Couldn't find a user with that id")
|
||||||
|
if (!badge) throw new UserInputError("Couldn't find a badge with that id")
|
||||||
|
return { user, badge }
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
reward: async (_object, params, context, _resolveInfo) => {
|
reward: async (_object, params, context, _resolveInfo) => {
|
||||||
const { fromBadgeId, toUserId } = params
|
const { user, badge } = await getUserAndBadge(params)
|
||||||
const session = context.driver.session()
|
await user.relateTo(badge, 'rewarded')
|
||||||
|
return user.toJson()
|
||||||
let transactionRes = await session.run(
|
|
||||||
`MATCH (badge:Badge {id: $badgeId}), (rewardedUser:User {id: $rewardedUserId})
|
|
||||||
MERGE (badge)-[:REWARDED]->(rewardedUser)
|
|
||||||
RETURN rewardedUser {.id}`,
|
|
||||||
{
|
|
||||||
badgeId: fromBadgeId,
|
|
||||||
rewardedUserId: toUserId,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const [rewardedUser] = transactionRes.records.map(record => {
|
|
||||||
return record.get('rewardedUser')
|
|
||||||
})
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return rewardedUser.id
|
|
||||||
},
|
},
|
||||||
|
|
||||||
unreward: async (_object, params, context, _resolveInfo) => {
|
unreward: async (_object, params, context, _resolveInfo) => {
|
||||||
const { fromBadgeId, toUserId } = params
|
const { badgeKey, userId } = params
|
||||||
|
const { user } = await getUserAndBadge(params)
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
let transactionRes = await session.run(
|
// silly neode cannot remove relationships
|
||||||
`MATCH (badge:Badge {id: $badgeId})-[reward:REWARDED]->(rewardedUser:User {id: $rewardedUserId})
|
await session.run(
|
||||||
DELETE reward
|
`
|
||||||
RETURN rewardedUser {.id}`,
|
MATCH (badge:Badge {id: $badgeKey})-[reward:REWARDED]->(rewardedUser:User {id: $userId})
|
||||||
{
|
DELETE reward
|
||||||
badgeId: fromBadgeId,
|
RETURN rewardedUser
|
||||||
rewardedUserId: toUserId,
|
`,
|
||||||
},
|
{
|
||||||
)
|
badgeKey,
|
||||||
const [rewardedUser] = transactionRes.records.map(record => {
|
userId,
|
||||||
return record.get('rewardedUser')
|
},
|
||||||
})
|
)
|
||||||
session.close()
|
} catch (err) {
|
||||||
|
throw err
|
||||||
return rewardedUser.id
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
return user.toJson()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,19 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
import { GraphQLClient } from 'graphql-request'
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
import { host, login } from '../../jest/helpers'
|
import { host, login, gql } from '../../jest/helpers'
|
||||||
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
|
let user
|
||||||
|
let badge
|
||||||
|
|
||||||
describe('rewards', () => {
|
describe('rewards', () => {
|
||||||
|
const variables = {
|
||||||
|
from: 'indiegogo_en_rhino',
|
||||||
|
to: 'u1',
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await factory.create('User', {
|
user = await factory.create('User', {
|
||||||
id: 'u1',
|
id: 'u1',
|
||||||
role: 'user',
|
role: 'user',
|
||||||
email: 'user@example.org',
|
email: 'user@example.org',
|
||||||
@ -22,9 +29,8 @@ describe('rewards', () => {
|
|||||||
role: 'admin',
|
role: 'admin',
|
||||||
email: 'admin@example.org',
|
email: 'admin@example.org',
|
||||||
})
|
})
|
||||||
await factory.create('Badge', {
|
badge = await factory.create('Badge', {
|
||||||
id: 'b6',
|
id: 'indiegogo_en_rhino',
|
||||||
key: 'indiegogo_en_rhino',
|
|
||||||
type: 'crowdfunding',
|
type: 'crowdfunding',
|
||||||
status: 'permanent',
|
status: 'permanent',
|
||||||
icon: '/img/badges/indiegogo_en_rhino.svg',
|
icon: '/img/badges/indiegogo_en_rhino.svg',
|
||||||
@ -35,21 +41,19 @@ describe('rewards', () => {
|
|||||||
await factory.cleanDatabase()
|
await factory.cleanDatabase()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('RewardBadge', () => {
|
describe('reward', () => {
|
||||||
const mutation = `
|
const mutation = gql`
|
||||||
mutation(
|
mutation($from: ID!, $to: ID!) {
|
||||||
$from: ID!
|
reward(badgeKey: $from, userId: $to) {
|
||||||
$to: ID!
|
id
|
||||||
) {
|
badges {
|
||||||
reward(fromBadgeId: $from, toUserId: $to)
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
const variables = {
|
|
||||||
from: 'b6',
|
|
||||||
to: 'u1',
|
|
||||||
}
|
|
||||||
let client
|
let client
|
||||||
|
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
@ -65,74 +69,95 @@ describe('rewards', () => {
|
|||||||
client = new GraphQLClient(host, { headers })
|
client = new GraphQLClient(host, { headers })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('badge for id does not exist', () => {
|
||||||
|
it('rejects with a telling error message', async () => {
|
||||||
|
await expect(
|
||||||
|
client.request(mutation, {
|
||||||
|
...variables,
|
||||||
|
from: 'bullshit',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("Couldn't find a badge with that id")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('user for id does not exist', () => {
|
||||||
|
it('rejects with a telling error message', async () => {
|
||||||
|
await expect(
|
||||||
|
client.request(mutation, {
|
||||||
|
...variables,
|
||||||
|
to: 'bullshit',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("Couldn't find a user with that id")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('rewards a badge to user', async () => {
|
it('rewards a badge to user', async () => {
|
||||||
const variables = {
|
|
||||||
from: 'b6',
|
|
||||||
to: 'u1',
|
|
||||||
}
|
|
||||||
const expected = {
|
const expected = {
|
||||||
reward: 'u1',
|
reward: {
|
||||||
|
id: 'u1',
|
||||||
|
badges: [{ id: 'indiegogo_en_rhino' }],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('rewards a second different badge to same user', async () => {
|
it('rewards a second different badge to same user', async () => {
|
||||||
await factory.create('Badge', {
|
await factory.create('Badge', {
|
||||||
id: 'b1',
|
id: 'indiegogo_en_racoon',
|
||||||
key: 'indiegogo_en_racoon',
|
|
||||||
type: 'crowdfunding',
|
|
||||||
status: 'permanent',
|
|
||||||
icon: '/img/badges/indiegogo_en_racoon.svg',
|
icon: '/img/badges/indiegogo_en_racoon.svg',
|
||||||
})
|
})
|
||||||
const variables = {
|
const badges = [{ id: 'indiegogo_en_racoon' }, { id: 'indiegogo_en_rhino' }]
|
||||||
from: 'b1',
|
|
||||||
to: 'u1',
|
|
||||||
}
|
|
||||||
const expected = {
|
const expected = {
|
||||||
reward: 'u1',
|
reward: {
|
||||||
|
id: 'u1',
|
||||||
|
badges: expect.arrayContaining(badges),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
await client.request(mutation, variables)
|
||||||
|
await expect(
|
||||||
|
client.request(mutation, {
|
||||||
|
...variables,
|
||||||
|
from: 'indiegogo_en_racoon',
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('rewards the same badge as well to another user', async () => {
|
it('rewards the same badge as well to another user', async () => {
|
||||||
const variables1 = {
|
|
||||||
from: 'b6',
|
|
||||||
to: 'u1',
|
|
||||||
}
|
|
||||||
await client.request(mutation, variables1)
|
|
||||||
|
|
||||||
const variables2 = {
|
|
||||||
from: 'b6',
|
|
||||||
to: 'u2',
|
|
||||||
}
|
|
||||||
const expected = {
|
const expected = {
|
||||||
reward: 'u2',
|
reward: {
|
||||||
|
id: 'u2',
|
||||||
|
badges: [{ id: 'indiegogo_en_rhino' }],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
await expect(client.request(mutation, variables2)).resolves.toEqual(expected)
|
await expect(
|
||||||
|
client.request(mutation, {
|
||||||
|
...variables,
|
||||||
|
to: 'u2',
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(expected)
|
||||||
})
|
})
|
||||||
it('returns the original reward if a reward is attempted a second time', async () => {
|
|
||||||
const variables = {
|
it('creates no duplicate reward relationships', async () => {
|
||||||
from: 'b6',
|
|
||||||
to: 'u1',
|
|
||||||
}
|
|
||||||
await client.request(mutation, variables)
|
await client.request(mutation, variables)
|
||||||
await client.request(mutation, variables)
|
await client.request(mutation, variables)
|
||||||
|
|
||||||
const query = `{
|
const query = gql`
|
||||||
User( id: "u1" ) {
|
{
|
||||||
badgesCount
|
User(id: "u1") {
|
||||||
|
badgesCount
|
||||||
|
badges {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
`
|
`
|
||||||
const expected = { User: [{ badgesCount: 1 }] }
|
const expected = { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] }
|
||||||
|
|
||||||
await expect(client.request(query)).resolves.toEqual(expected)
|
await expect(client.request(query)).resolves.toEqual(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated moderator', () => {
|
describe('authenticated moderator', () => {
|
||||||
const variables = {
|
|
||||||
from: 'b6',
|
|
||||||
to: 'u1',
|
|
||||||
}
|
|
||||||
let client
|
let client
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
const headers = await login({ email: 'moderator@example.org', password: '1234' })
|
||||||
@ -147,27 +172,41 @@ describe('rewards', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('RemoveReward', () => {
|
describe('unreward', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await factory.relate('User', 'Badges', { from: 'b6', to: 'u1' })
|
await user.relateTo(badge, 'rewarded')
|
||||||
})
|
})
|
||||||
const variables = {
|
const expected = { unreward: { id: 'u1', badges: [] } }
|
||||||
from: 'b6',
|
|
||||||
to: 'u1',
|
|
||||||
}
|
|
||||||
const expected = {
|
|
||||||
unreward: 'u1',
|
|
||||||
}
|
|
||||||
|
|
||||||
const mutation = `
|
const mutation = gql`
|
||||||
mutation(
|
mutation($from: ID!, $to: ID!) {
|
||||||
$from: ID!
|
unreward(badgeKey: $from, userId: $to) {
|
||||||
$to: ID!
|
id
|
||||||
) {
|
badges {
|
||||||
unreward(fromBadgeId: $from, toUserId: $to)
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
describe('check test setup', () => {
|
||||||
|
it('user has one badge', async () => {
|
||||||
|
const query = gql`
|
||||||
|
{
|
||||||
|
User(id: "u1") {
|
||||||
|
badgesCount
|
||||||
|
badges {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const expected = { User: [{ badgesCount: 1, badges: [{ id: 'indiegogo_en_rhino' }] }] }
|
||||||
|
const client = new GraphQLClient(host)
|
||||||
|
await expect(client.request(query)).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
let client
|
let client
|
||||||
|
|
||||||
@ -188,12 +227,9 @@ describe('rewards', () => {
|
|||||||
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('fails to remove a not existing badge from user', async () => {
|
it('does not crash when unrewarding multiple times', async () => {
|
||||||
await client.request(mutation, variables)
|
await client.request(mutation, variables)
|
||||||
|
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
|
||||||
await expect(client.request(mutation, variables)).rejects.toThrow(
|
|
||||||
"Cannot read property 'id' of undefined",
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import gql from 'graphql-tag'
|
|
||||||
import { GraphQLClient } from 'graphql-request'
|
import { GraphQLClient } from 'graphql-request'
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
import { host, login } from '../../jest/helpers'
|
import { host, login, gql } from '../../jest/helpers'
|
||||||
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,9 @@ import encode from '../../jwt/encode'
|
|||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
import { AuthenticationError } from 'apollo-server'
|
import { AuthenticationError } from 'apollo-server'
|
||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||||
|
import { neode } from '../../bootstrap/neo4j'
|
||||||
|
|
||||||
|
const instance = neode()
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
@ -21,8 +24,8 @@ export default {
|
|||||||
// }
|
// }
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
const result = await session.run(
|
const result = await session.run(
|
||||||
'MATCH (user:User {email: $userEmail}) ' +
|
'MATCH (user:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})' +
|
||||||
'RETURN user {.id, .slug, .name, .avatar, .email, .encryptedPassword, .role, .disabled} as user LIMIT 1',
|
'RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1',
|
||||||
{
|
{
|
||||||
userEmail: email,
|
userEmail: email,
|
||||||
},
|
},
|
||||||
@ -46,41 +49,24 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
|
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
|
||||||
const session = driver.session()
|
let currentUser = await instance.find('User', user.id)
|
||||||
let result = await session.run(
|
|
||||||
`MATCH (user:User {email: $userEmail})
|
|
||||||
RETURN user {.id, .email, .encryptedPassword}`,
|
|
||||||
{
|
|
||||||
userEmail: user.email,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const [currentUser] = result.records.map(function(record) {
|
const encryptedPassword = currentUser.get('encryptedPassword')
|
||||||
return record.get('user')
|
if (!(await bcrypt.compareSync(oldPassword, encryptedPassword))) {
|
||||||
})
|
|
||||||
|
|
||||||
if (!(await bcrypt.compareSync(oldPassword, currentUser.encryptedPassword))) {
|
|
||||||
throw new AuthenticationError('Old password is not correct')
|
throw new AuthenticationError('Old password is not correct')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await bcrypt.compareSync(newPassword, currentUser.encryptedPassword)) {
|
if (await bcrypt.compareSync(newPassword, encryptedPassword)) {
|
||||||
throw new AuthenticationError('Old password and new password should be different')
|
throw new AuthenticationError('Old password and new password should be different')
|
||||||
} else {
|
|
||||||
const newEncryptedPassword = await bcrypt.hashSync(newPassword, 10)
|
|
||||||
session.run(
|
|
||||||
`MATCH (user:User {email: $userEmail})
|
|
||||||
SET user.encryptedPassword = $newEncryptedPassword
|
|
||||||
RETURN user
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
userEmail: user.email,
|
|
||||||
newEncryptedPassword,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return encode(currentUser)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newEncryptedPassword = await bcrypt.hashSync(newPassword, 10)
|
||||||
|
await currentUser.update({
|
||||||
|
encryptedPassword: newEncryptedPassword,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return encode(await currentUser.toJson())
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
|
|||||||
import fileUpload from './fileUpload'
|
import fileUpload from './fileUpload'
|
||||||
import { neode } from '../../bootstrap/neo4j'
|
import { neode } from '../../bootstrap/neo4j'
|
||||||
import { UserInputError } from 'apollo-server'
|
import { UserInputError } from 'apollo-server'
|
||||||
|
import { undefinedToNull } from '../helpers'
|
||||||
|
|
||||||
const instance = neode()
|
const instance = neode()
|
||||||
|
|
||||||
@ -36,16 +37,6 @@ const count = obj => {
|
|||||||
return resolvers
|
return resolvers
|
||||||
}
|
}
|
||||||
|
|
||||||
const undefinedToNull = list => {
|
|
||||||
const resolvers = {}
|
|
||||||
list.forEach(key => {
|
|
||||||
resolvers[key] = async (parent, params, context, resolveInfo) => {
|
|
||||||
return typeof parent[key] === 'undefined' ? null : parent[key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return resolvers
|
|
||||||
}
|
|
||||||
|
|
||||||
export const hasMany = obj => {
|
export const hasMany = obj => {
|
||||||
const resolvers = {}
|
const resolvers = {}
|
||||||
for (const [key, connection] of Object.entries(obj)) {
|
for (const [key, connection] of Object.entries(obj)) {
|
||||||
@ -65,6 +56,13 @@ export const hasOne = obj => {
|
|||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
User: async (object, args, context, resolveInfo) => {
|
User: async (object, args, context, resolveInfo) => {
|
||||||
|
const { email } = args
|
||||||
|
if (email) {
|
||||||
|
const e = await instance.first('EmailAddress', { email })
|
||||||
|
let user = e.get('belongsTo')
|
||||||
|
user = await user.toJson()
|
||||||
|
return [user.node]
|
||||||
|
}
|
||||||
return neo4jgraphql(object, args, context, resolveInfo, false)
|
return neo4jgraphql(object, args, context, resolveInfo, false)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -104,6 +102,14 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
|
email: async (parent, params, context, resolveInfo) => {
|
||||||
|
if (typeof parent.email !== 'undefined') return parent.email
|
||||||
|
const { id } = parent
|
||||||
|
const statement = `MATCH(u:User {id: {id}})-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e`
|
||||||
|
const result = await instance.cypher(statement, { id })
|
||||||
|
let [{ email }] = result.records.map(r => r.get('e').properties)
|
||||||
|
return email
|
||||||
|
},
|
||||||
...undefinedToNull([
|
...undefinedToNull([
|
||||||
'actorId',
|
'actorId',
|
||||||
'avatar',
|
'avatar',
|
||||||
@ -139,7 +145,7 @@ export default {
|
|||||||
organizationsCreated: '-[:CREATED_ORGA]->(related:Organization)',
|
organizationsCreated: '-[:CREATED_ORGA]->(related:Organization)',
|
||||||
organizationsOwned: '-[:OWNING_ORGA]->(related:Organization)',
|
organizationsOwned: '-[:OWNING_ORGA]->(related:Organization)',
|
||||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||||
badges: '-[:REWARDED]->(related:Badge)',
|
badges: '<-[:REWARDED]-(related:Badge)',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
import { GraphQLClient } from 'graphql-request'
|
||||||
import { login, host } from '../../jest/helpers'
|
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
import gql from 'graphql-tag'
|
import { host, login, gql } from '../../jest/helpers'
|
||||||
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
let client
|
let client
|
||||||
@ -147,7 +146,7 @@ describe('users', () => {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
asAuthor = await factory.create('User', {
|
await factory.create('User', {
|
||||||
email: 'test@example.org',
|
email: 'test@example.org',
|
||||||
password: '1234',
|
password: '1234',
|
||||||
id: 'u343',
|
id: 'u343',
|
||||||
@ -191,6 +190,7 @@ describe('users', () => {
|
|||||||
describe('attempting to delete my own account', () => {
|
describe('attempting to delete my own account', () => {
|
||||||
let expectedResponse
|
let expectedResponse
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
asAuthor = Factory()
|
||||||
await asAuthor.authenticateAs({
|
await asAuthor.authenticateAs({
|
||||||
email: 'test@example.org',
|
email: 'test@example.org',
|
||||||
password: '1234',
|
password: '1234',
|
||||||
|
|||||||
19
backend/src/schema/types/embed.gql
Normal file
19
backend/src/schema/types/embed.gql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
type Embed {
|
||||||
|
type: String
|
||||||
|
title: String
|
||||||
|
author: String
|
||||||
|
publisher: String
|
||||||
|
date: String
|
||||||
|
description: String
|
||||||
|
url: String
|
||||||
|
image: String
|
||||||
|
audio: String
|
||||||
|
video: String
|
||||||
|
lang: String
|
||||||
|
html: String
|
||||||
|
sources: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
embed(url: String!): Embed
|
||||||
|
}
|
||||||
@ -1,4 +0,0 @@
|
|||||||
enum BadgeStatus {
|
|
||||||
permanent
|
|
||||||
temporary
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
enum BadgeType {
|
|
||||||
role
|
|
||||||
crowdfunding
|
|
||||||
}
|
|
||||||
@ -28,8 +28,6 @@ type Mutation {
|
|||||||
report(id: ID!, description: String): Report
|
report(id: ID!, description: String): Report
|
||||||
disable(id: ID!): ID
|
disable(id: ID!): ID
|
||||||
enable(id: ID!): ID
|
enable(id: ID!): ID
|
||||||
reward(fromBadgeId: ID!, toUserId: ID!): ID
|
|
||||||
unreward(fromBadgeId: ID!, toUserId: ID!): ID
|
|
||||||
# Shout the given Type and ID
|
# Shout the given Type and ID
|
||||||
shout(id: ID!, type: ShoutTypeEnum): Boolean!
|
shout(id: ID!, type: ShoutTypeEnum): Boolean!
|
||||||
# Unshout the given Type and ID
|
# Unshout the given Type and ID
|
||||||
|
|||||||
@ -1,324 +0,0 @@
|
|||||||
scalar Upload
|
|
||||||
|
|
||||||
type Query {
|
|
||||||
isLoggedIn: Boolean!
|
|
||||||
# Get the currently logged in User based on the given JWT Token
|
|
||||||
currentUser: User
|
|
||||||
# Get the latest Network Statistics
|
|
||||||
statistics: Statistics!
|
|
||||||
findPosts(filter: String!, limit: Int = 10): [Post]! @cypher(
|
|
||||||
statement: """
|
|
||||||
CALL db.index.fulltext.queryNodes('full_text_search', $filter)
|
|
||||||
YIELD node as post, score
|
|
||||||
MATCH (post)<-[:WROTE]-(user:User)
|
|
||||||
WHERE score >= 0.2
|
|
||||||
AND NOT user.deleted = true AND NOT user.disabled = true
|
|
||||||
AND NOT post.deleted = true AND NOT post.disabled = true
|
|
||||||
RETURN post
|
|
||||||
LIMIT $limit
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
CommentByPost(postId: ID!): [Comment]!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Mutation {
|
|
||||||
# Get a JWT Token for the given Email and password
|
|
||||||
login(email: String!, password: String!): String!
|
|
||||||
signup(email: String!, password: String!): Boolean!
|
|
||||||
changePassword(oldPassword:String!, newPassword: String!): String!
|
|
||||||
report(id: ID!, description: String): Report
|
|
||||||
disable(id: ID!): ID
|
|
||||||
enable(id: ID!): ID
|
|
||||||
reward(fromBadgeId: ID!, toUserId: ID!): ID
|
|
||||||
unreward(fromBadgeId: ID!, toUserId: ID!): ID
|
|
||||||
# Shout the given Type and ID
|
|
||||||
shout(id: ID!, type: ShoutTypeEnum): Boolean!
|
|
||||||
# Unshout the given Type and ID
|
|
||||||
unshout(id: ID!, type: ShoutTypeEnum): Boolean!
|
|
||||||
# Follow the given Type and ID
|
|
||||||
follow(id: ID!, type: FollowTypeEnum): Boolean!
|
|
||||||
# Unfollow the given Type and ID
|
|
||||||
unfollow(id: ID!, type: FollowTypeEnum): Boolean!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Statistics {
|
|
||||||
countUsers: Int!
|
|
||||||
countPosts: Int!
|
|
||||||
countComments: Int!
|
|
||||||
countNotifications: Int!
|
|
||||||
countOrganizations: Int!
|
|
||||||
countProjects: Int!
|
|
||||||
countInvites: Int!
|
|
||||||
countFollows: Int!
|
|
||||||
countShouts: Int!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Notification {
|
|
||||||
id: ID!
|
|
||||||
read: Boolean,
|
|
||||||
user: User @relation(name: "NOTIFIED", direction: "OUT")
|
|
||||||
post: Post @relation(name: "NOTIFIED", direction: "IN")
|
|
||||||
createdAt: String
|
|
||||||
}
|
|
||||||
|
|
||||||
scalar Date
|
|
||||||
scalar Time
|
|
||||||
scalar DateTime
|
|
||||||
|
|
||||||
enum VisibilityEnum {
|
|
||||||
public
|
|
||||||
friends
|
|
||||||
private
|
|
||||||
}
|
|
||||||
|
|
||||||
enum UserGroupEnum {
|
|
||||||
admin
|
|
||||||
moderator
|
|
||||||
user
|
|
||||||
}
|
|
||||||
|
|
||||||
type Location {
|
|
||||||
id: ID!
|
|
||||||
name: String!
|
|
||||||
nameEN: String
|
|
||||||
nameDE: String
|
|
||||||
nameFR: String
|
|
||||||
nameNL: String
|
|
||||||
nameIT: String
|
|
||||||
nameES: String
|
|
||||||
namePT: String
|
|
||||||
namePL: String
|
|
||||||
type: String!
|
|
||||||
lat: Float
|
|
||||||
lng: Float
|
|
||||||
parent: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
|
||||||
}
|
|
||||||
|
|
||||||
type User {
|
|
||||||
id: ID!
|
|
||||||
actorId: String
|
|
||||||
name: String
|
|
||||||
email: String!
|
|
||||||
slug: String
|
|
||||||
password: String!
|
|
||||||
avatar: String
|
|
||||||
avatarUpload: Upload
|
|
||||||
deleted: Boolean
|
|
||||||
disabled: Boolean
|
|
||||||
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
|
||||||
role: UserGroupEnum
|
|
||||||
publicKey: String
|
|
||||||
privateKey: String
|
|
||||||
|
|
||||||
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
|
||||||
locationName: String
|
|
||||||
about: String
|
|
||||||
socialMedia: [SocialMedia]! @relation(name: "OWNED", direction: "OUT")
|
|
||||||
|
|
||||||
createdAt: String
|
|
||||||
updatedAt: String
|
|
||||||
|
|
||||||
notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN")
|
|
||||||
|
|
||||||
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
|
|
||||||
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
|
|
||||||
|
|
||||||
following: [User]! @relation(name: "FOLLOWS", direction: "OUT")
|
|
||||||
followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWS]->(r:User) RETURN COUNT(DISTINCT r)")
|
|
||||||
|
|
||||||
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
|
|
||||||
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
|
|
||||||
|
|
||||||
# Is the currently logged in user following that user?
|
|
||||||
followedByCurrentUser: Boolean! @cypher(
|
|
||||||
statement: """
|
|
||||||
MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId})
|
|
||||||
RETURN COUNT(u) >= 1
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
#contributions: [WrittenPost]!
|
|
||||||
#contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
|
|
||||||
# @cypher(
|
|
||||||
# statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp"
|
|
||||||
# )
|
|
||||||
contributions: [Post]! @relation(name: "WROTE", direction: "OUT")
|
|
||||||
contributionsCount: Int! @cypher(
|
|
||||||
statement: """
|
|
||||||
MATCH (this)-[:WROTE]->(r:Post)
|
|
||||||
WHERE (NOT exists(r.deleted) OR r.deleted = false)
|
|
||||||
AND (NOT exists(r.disabled) OR r.disabled = false)
|
|
||||||
RETURN COUNT(r)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
comments: [Comment]! @relation(name: "WROTE", direction: "OUT")
|
|
||||||
commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
|
|
||||||
|
|
||||||
shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
|
|
||||||
shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
|
|
||||||
|
|
||||||
organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT")
|
|
||||||
organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT")
|
|
||||||
|
|
||||||
blacklisted: [User]! @relation(name: "BLACKLISTED", direction: "OUT")
|
|
||||||
|
|
||||||
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
|
||||||
|
|
||||||
badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
|
|
||||||
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Post {
|
|
||||||
id: ID!
|
|
||||||
activityId: String
|
|
||||||
objectId: String
|
|
||||||
author: User @relation(name: "WROTE", direction: "IN")
|
|
||||||
title: String!
|
|
||||||
slug: String
|
|
||||||
content: String!
|
|
||||||
contentExcerpt: String
|
|
||||||
image: String
|
|
||||||
imageUpload: Upload
|
|
||||||
visibility: VisibilityEnum
|
|
||||||
deleted: Boolean
|
|
||||||
disabled: Boolean
|
|
||||||
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
|
||||||
createdAt: String
|
|
||||||
updatedAt: String
|
|
||||||
|
|
||||||
relatedContributions: [Post]! @cypher(
|
|
||||||
statement: """
|
|
||||||
MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
|
||||||
RETURN DISTINCT post
|
|
||||||
LIMIT 10
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
|
|
||||||
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
|
||||||
|
|
||||||
comments: [Comment]! @relation(name: "COMMENTS", direction: "IN")
|
|
||||||
commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
|
|
||||||
|
|
||||||
shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN")
|
|
||||||
shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(DISTINCT r)")
|
|
||||||
|
|
||||||
# Has the currently logged in user shouted that post?
|
|
||||||
shoutedByCurrentUser: Boolean! @cypher(
|
|
||||||
statement: """
|
|
||||||
MATCH (this)<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId})
|
|
||||||
RETURN COUNT(u) >= 1
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Comment {
|
|
||||||
id: ID!
|
|
||||||
activityId: String
|
|
||||||
postId: ID
|
|
||||||
author: User @relation(name: "WROTE", direction: "IN")
|
|
||||||
content: String!
|
|
||||||
contentExcerpt: String
|
|
||||||
post: Post @relation(name: "COMMENTS", direction: "OUT")
|
|
||||||
createdAt: String
|
|
||||||
updatedAt: String
|
|
||||||
deleted: Boolean
|
|
||||||
disabled: Boolean
|
|
||||||
disabledBy: User @relation(name: "DISABLED", direction: "IN")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Report {
|
|
||||||
id: ID!
|
|
||||||
submitter: User @relation(name: "REPORTED", direction: "IN")
|
|
||||||
description: String
|
|
||||||
type: String! @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]")
|
|
||||||
createdAt: String
|
|
||||||
comment: Comment @relation(name: "REPORTED", direction: "OUT")
|
|
||||||
post: Post @relation(name: "REPORTED", direction: "OUT")
|
|
||||||
user: User @relation(name: "REPORTED", direction: "OUT")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Category {
|
|
||||||
id: ID!
|
|
||||||
name: String!
|
|
||||||
slug: String
|
|
||||||
icon: String!
|
|
||||||
posts: [Post]! @relation(name: "CATEGORIZED", direction: "IN")
|
|
||||||
postCount: Int! @cypher(statement: "MATCH (this)<-[:CATEGORIZED]-(r:Post) RETURN COUNT(r)")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Badge {
|
|
||||||
id: ID!
|
|
||||||
key: String!
|
|
||||||
type: BadgeTypeEnum!
|
|
||||||
status: BadgeStatusEnum!
|
|
||||||
icon: String!
|
|
||||||
|
|
||||||
rewarded: [User]! @relation(name: "REWARDED", direction: "OUT")
|
|
||||||
}
|
|
||||||
|
|
||||||
enum BadgeTypeEnum {
|
|
||||||
role
|
|
||||||
crowdfunding
|
|
||||||
}
|
|
||||||
enum BadgeStatusEnum {
|
|
||||||
permanent
|
|
||||||
temporary
|
|
||||||
}
|
|
||||||
enum ShoutTypeEnum {
|
|
||||||
Post
|
|
||||||
Organization
|
|
||||||
Project
|
|
||||||
}
|
|
||||||
enum FollowTypeEnum {
|
|
||||||
User
|
|
||||||
Organization
|
|
||||||
Project
|
|
||||||
}
|
|
||||||
|
|
||||||
type Reward {
|
|
||||||
id: ID!
|
|
||||||
user: User @relation(name: "REWARDED", direction: "IN")
|
|
||||||
rewarderId: ID
|
|
||||||
createdAt: String
|
|
||||||
badge: Badge @relation(name: "REWARDED", direction: "OUT")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Organization {
|
|
||||||
id: ID!
|
|
||||||
createdBy: User @relation(name: "CREATED_ORGA", direction: "IN")
|
|
||||||
ownedBy: [User] @relation(name: "OWNING_ORGA", direction: "IN")
|
|
||||||
name: String!
|
|
||||||
slug: String
|
|
||||||
description: String!
|
|
||||||
descriptionExcerpt: String
|
|
||||||
deleted: Boolean
|
|
||||||
disabled: Boolean
|
|
||||||
|
|
||||||
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
|
|
||||||
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Tag {
|
|
||||||
id: ID!
|
|
||||||
name: String!
|
|
||||||
taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN")
|
|
||||||
taggedOrganizations: [Organization]! @relation(name: "TAGGED", direction: "IN")
|
|
||||||
taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p) RETURN COUNT(DISTINCT p)")
|
|
||||||
taggedCountUnique: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(p)<-[:WROTE]-(u:User) RETURN COUNT(DISTINCT u)")
|
|
||||||
deleted: Boolean
|
|
||||||
disabled: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type SharedInboxEndpoint {
|
|
||||||
id: ID!
|
|
||||||
uri: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type SocialMedia {
|
|
||||||
id: ID!
|
|
||||||
url: String
|
|
||||||
ownedBy: [User]! @relation(name: "OWNED", direction: "IN")
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
type Badge {
|
type Badge {
|
||||||
id: ID!
|
id: ID!
|
||||||
key: String!
|
|
||||||
type: BadgeType!
|
type: BadgeType!
|
||||||
status: BadgeStatus!
|
status: BadgeStatus!
|
||||||
icon: String!
|
icon: String!
|
||||||
@ -11,3 +10,22 @@ type Badge {
|
|||||||
|
|
||||||
rewarded: [User]! @relation(name: "REWARDED", direction: "OUT")
|
rewarded: [User]! @relation(name: "REWARDED", direction: "OUT")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum BadgeStatus {
|
||||||
|
permanent
|
||||||
|
temporary
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BadgeType {
|
||||||
|
role
|
||||||
|
crowdfunding
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
Badge: [Badge]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
reward(badgeKey: ID!, userId: ID!): User
|
||||||
|
unreward(badgeKey: ID!, userId: ID!): User
|
||||||
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ type Mutation {
|
|||||||
): Comment
|
): Comment
|
||||||
UpdateComment(
|
UpdateComment(
|
||||||
id: ID!
|
id: ID!
|
||||||
content: String
|
content: String!
|
||||||
contentExcerpt: String
|
contentExcerpt: String
|
||||||
deleted: Boolean
|
deleted: Boolean
|
||||||
disabled: Boolean
|
disabled: Boolean
|
||||||
|
|||||||
@ -2,7 +2,7 @@ type User {
|
|||||||
id: ID!
|
id: ID!
|
||||||
actorId: String
|
actorId: String
|
||||||
name: String
|
name: String
|
||||||
email: String!
|
email: String! @cypher(statement: "MATCH (this)-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e.email")
|
||||||
slug: String!
|
slug: String!
|
||||||
avatar: String
|
avatar: String
|
||||||
coverImg: String
|
coverImg: String
|
||||||
@ -81,6 +81,9 @@ type User {
|
|||||||
input _UserFilter {
|
input _UserFilter {
|
||||||
AND: [_UserFilter!]
|
AND: [_UserFilter!]
|
||||||
OR: [_UserFilter!]
|
OR: [_UserFilter!]
|
||||||
|
name_contains: String
|
||||||
|
about_contains: String
|
||||||
|
slug_contains: String
|
||||||
id: ID
|
id: ID
|
||||||
id_not: ID
|
id_not: ID
|
||||||
id_in: [ID!]
|
id_in: [ID!]
|
||||||
|
|||||||
@ -1,28 +1,15 @@
|
|||||||
import uuid from 'uuid/v4'
|
export default function create() {
|
||||||
|
|
||||||
export default function(params) {
|
|
||||||
const {
|
|
||||||
id = uuid(),
|
|
||||||
key = '',
|
|
||||||
type = 'crowdfunding',
|
|
||||||
status = 'permanent',
|
|
||||||
icon = '/img/badges/indiegogo_en_panda.svg',
|
|
||||||
} = params
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mutation: `
|
factory: async ({ args, neodeInstance }) => {
|
||||||
mutation(
|
const defaults = {
|
||||||
$id: ID
|
type: 'crowdfunding',
|
||||||
$key: String!
|
status: 'permanent',
|
||||||
$type: BadgeType!
|
|
||||||
$status: BadgeStatus!
|
|
||||||
$icon: String!
|
|
||||||
) {
|
|
||||||
CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`,
|
args = {
|
||||||
variables: { id, key, type, status, icon },
|
...defaults,
|
||||||
|
...args,
|
||||||
|
}
|
||||||
|
return neodeInstance.create('Badge', args)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,6 +73,7 @@ export default function Factory(options = {}) {
|
|||||||
const { factory, mutation, variables } = this.factories[node](args)
|
const { factory, mutation, variables } = this.factories[node](args)
|
||||||
if (factory) {
|
if (factory) {
|
||||||
this.lastResponse = await factory({ args, neodeInstance })
|
this.lastResponse = await factory({ args, neodeInstance })
|
||||||
|
return this.lastResponse
|
||||||
} else {
|
} else {
|
||||||
this.lastResponse = await this.graphQLClient.request(mutation, variables)
|
this.lastResponse = await this.graphQLClient.request(mutation, variables)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import uuid from 'uuid/v4'
|
|||||||
import encryptPassword from '../../helpers/encryptPassword'
|
import encryptPassword from '../../helpers/encryptPassword'
|
||||||
import slugify from 'slug'
|
import slugify from 'slug'
|
||||||
|
|
||||||
export default function create(params) {
|
export default function create() {
|
||||||
return {
|
return {
|
||||||
factory: async ({ args, neodeInstance }) => {
|
factory: async ({ args, neodeInstance }) => {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
@ -22,7 +22,10 @@ export default function create(params) {
|
|||||||
}
|
}
|
||||||
args = await encryptPassword(args)
|
args = await encryptPassword(args)
|
||||||
const user = await neodeInstance.create('User', args)
|
const user = await neodeInstance.create('User', args)
|
||||||
return user.toJson()
|
const email = await neodeInstance.create('EmailAddress', { email: args.email })
|
||||||
|
await user.relateTo(email, 'primaryEmail')
|
||||||
|
await email.relateTo(user, 'belongsTo')
|
||||||
|
return user
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,52 +5,42 @@ import Factory from './factories'
|
|||||||
;(async function() {
|
;(async function() {
|
||||||
try {
|
try {
|
||||||
const f = Factory()
|
const f = Factory()
|
||||||
await Promise.all([
|
const [racoon, rabbit, wolf, bear, turtle, rhino] = await Promise.all([
|
||||||
f.create('Badge', {
|
f.create('Badge', {
|
||||||
id: 'b1',
|
id: 'indiegogo_en_racoon',
|
||||||
key: 'indiegogo_en_racoon',
|
|
||||||
type: 'crowdfunding',
|
|
||||||
status: 'permanent',
|
|
||||||
icon: '/img/badges/indiegogo_en_racoon.svg',
|
icon: '/img/badges/indiegogo_en_racoon.svg',
|
||||||
}),
|
}),
|
||||||
f.create('Badge', {
|
f.create('Badge', {
|
||||||
id: 'b2',
|
id: 'indiegogo_en_rabbit',
|
||||||
key: 'indiegogo_en_rabbit',
|
|
||||||
type: 'crowdfunding',
|
|
||||||
status: 'permanent',
|
|
||||||
icon: '/img/badges/indiegogo_en_rabbit.svg',
|
icon: '/img/badges/indiegogo_en_rabbit.svg',
|
||||||
}),
|
}),
|
||||||
f.create('Badge', {
|
f.create('Badge', {
|
||||||
id: 'b3',
|
id: 'indiegogo_en_wolf',
|
||||||
key: 'indiegogo_en_wolf',
|
|
||||||
type: 'crowdfunding',
|
|
||||||
status: 'permanent',
|
|
||||||
icon: '/img/badges/indiegogo_en_wolf.svg',
|
icon: '/img/badges/indiegogo_en_wolf.svg',
|
||||||
}),
|
}),
|
||||||
f.create('Badge', {
|
f.create('Badge', {
|
||||||
id: 'b4',
|
id: 'indiegogo_en_bear',
|
||||||
key: 'indiegogo_en_bear',
|
|
||||||
type: 'crowdfunding',
|
|
||||||
status: 'permanent',
|
|
||||||
icon: '/img/badges/indiegogo_en_bear.svg',
|
icon: '/img/badges/indiegogo_en_bear.svg',
|
||||||
}),
|
}),
|
||||||
f.create('Badge', {
|
f.create('Badge', {
|
||||||
id: 'b5',
|
id: 'indiegogo_en_turtle',
|
||||||
key: 'indiegogo_en_turtle',
|
|
||||||
type: 'crowdfunding',
|
|
||||||
status: 'permanent',
|
|
||||||
icon: '/img/badges/indiegogo_en_turtle.svg',
|
icon: '/img/badges/indiegogo_en_turtle.svg',
|
||||||
}),
|
}),
|
||||||
f.create('Badge', {
|
f.create('Badge', {
|
||||||
id: 'b6',
|
id: 'indiegogo_en_rhino',
|
||||||
key: 'indiegogo_en_rhino',
|
|
||||||
type: 'crowdfunding',
|
|
||||||
status: 'permanent',
|
|
||||||
icon: '/img/badges/indiegogo_en_rhino.svg',
|
icon: '/img/badges/indiegogo_en_rhino.svg',
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
const [
|
||||||
|
peterLustig,
|
||||||
|
bobDerBaumeister,
|
||||||
|
jennyRostock,
|
||||||
|
tick, // eslint-disable-line no-unused-vars
|
||||||
|
trick, // eslint-disable-line no-unused-vars
|
||||||
|
track, // eslint-disable-line no-unused-vars
|
||||||
|
dagobert,
|
||||||
|
] = await Promise.all([
|
||||||
f.create('User', {
|
f.create('User', {
|
||||||
id: 'u1',
|
id: 'u1',
|
||||||
name: 'Peter Lustig',
|
name: 'Peter Lustig',
|
||||||
@ -123,30 +113,16 @@ import Factory from './factories'
|
|||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
f.relate('User', 'Badges', {
|
peterLustig.relateTo(racoon, 'rewarded'),
|
||||||
from: 'b6',
|
peterLustig.relateTo(rhino, 'rewarded'),
|
||||||
to: 'u1',
|
peterLustig.relateTo(wolf, 'rewarded'),
|
||||||
}),
|
bobDerBaumeister.relateTo(racoon, 'rewarded'),
|
||||||
f.relate('User', 'Badges', {
|
bobDerBaumeister.relateTo(turtle, 'rewarded'),
|
||||||
from: 'b5',
|
jennyRostock.relateTo(bear, 'rewarded'),
|
||||||
to: 'u2',
|
dagobert.relateTo(rabbit, 'rewarded'),
|
||||||
}),
|
])
|
||||||
f.relate('User', 'Badges', {
|
|
||||||
from: 'b4',
|
await Promise.all([
|
||||||
to: 'u3',
|
|
||||||
}),
|
|
||||||
f.relate('User', 'Badges', {
|
|
||||||
from: 'b3',
|
|
||||||
to: 'u4',
|
|
||||||
}),
|
|
||||||
f.relate('User', 'Badges', {
|
|
||||||
from: 'b2',
|
|
||||||
to: 'u5',
|
|
||||||
}),
|
|
||||||
f.relate('User', 'Badges', {
|
|
||||||
from: 'b1',
|
|
||||||
to: 'u6',
|
|
||||||
}),
|
|
||||||
f.relate('User', 'Friends', {
|
f.relate('User', 'Friends', {
|
||||||
from: 'u1',
|
from: 'u1',
|
||||||
to: 'u2',
|
to: 'u2',
|
||||||
@ -707,6 +683,13 @@ import Factory from './factories'
|
|||||||
to: 'o3',
|
to: 'o3',
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[...Array(30).keys()].map(i => {
|
||||||
|
return f.create('User')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
/* eslint-disable-next-line no-console */
|
/* eslint-disable-next-line no-console */
|
||||||
console.log('Seeded Data...')
|
console.log('Seeded Data...')
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import helmet from 'helmet'
|
import helmet from 'helmet'
|
||||||
import { GraphQLServer } from 'graphql-yoga'
|
import { ApolloServer } from 'apollo-server-express'
|
||||||
import CONFIG, { requiredConfigs } from './config'
|
import CONFIG, { requiredConfigs } from './config'
|
||||||
import mocks from './mocks'
|
import mocks from './mocks'
|
||||||
import middleware from './middleware'
|
import middleware from './middleware'
|
||||||
@ -20,28 +20,30 @@ const driver = getDriver()
|
|||||||
|
|
||||||
const createServer = options => {
|
const createServer = options => {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
context: async ({ request }) => {
|
context: async ({ req }) => {
|
||||||
const user = await decode(driver, request.headers.authorization)
|
const user = await decode(driver, req.headers.authorization)
|
||||||
return {
|
return {
|
||||||
driver,
|
driver,
|
||||||
user,
|
user,
|
||||||
req: request,
|
req,
|
||||||
cypherParams: {
|
cypherParams: {
|
||||||
currentUserId: user ? user.id : null,
|
currentUserId: user ? user.id : null,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
schema,
|
schema: middleware(schema),
|
||||||
debug: CONFIG.DEBUG,
|
debug: CONFIG.DEBUG,
|
||||||
tracing: CONFIG.DEBUG,
|
tracing: CONFIG.DEBUG,
|
||||||
middlewares: middleware(schema),
|
|
||||||
mocks: CONFIG.MOCKS ? mocks : false,
|
mocks: CONFIG.MOCKS ? mocks : false,
|
||||||
}
|
}
|
||||||
const server = new GraphQLServer(Object.assign({}, defaults, options))
|
const server = new ApolloServer(Object.assign({}, defaults, options))
|
||||||
|
|
||||||
server.express.use(helmet())
|
const app = express()
|
||||||
server.express.use(express.static('public'))
|
app.use(helmet())
|
||||||
return server
|
app.use(express.static('public'))
|
||||||
|
server.applyMiddleware({ app, path: '/' })
|
||||||
|
|
||||||
|
return { server, app }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createServer
|
export default createServer
|
||||||
|
|||||||
43
backend/src/server.spec.js
Normal file
43
backend/src/server.spec.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
import createServer from './server'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is for demonstration purposes. It does not really test the
|
||||||
|
* `isLoggedIn` query but demonstrates how we can use `apollo-server-testing`.
|
||||||
|
* All we need to do is to get an instance of `ApolloServer` and maybe we want
|
||||||
|
* stub out `context` as shown below.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
let user
|
||||||
|
let action
|
||||||
|
describe('isLoggedIn', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
action = async () => {
|
||||||
|
const { server } = createServer({
|
||||||
|
context: () => {
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { query } = createTestClient(server)
|
||||||
|
|
||||||
|
const isLoggedIn = `{ isLoggedIn }`
|
||||||
|
return query({ query: isLoggedIn })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false', async () => {
|
||||||
|
const expected = expect.objectContaining({ data: { isLoggedIn: false } })
|
||||||
|
await expect(action()).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when authenticated', () => {
|
||||||
|
it('returns true', async () => {
|
||||||
|
user = { id: '123' }
|
||||||
|
const expected = expect.objectContaining({ data: { isLoggedIn: true } })
|
||||||
|
await expect(action()).resolves.toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
1942
backend/yarn.lock
1942
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -22,16 +22,16 @@ Feature: Tags and Categories
|
|||||||
When I navigate to the administration dashboard
|
When I navigate to the administration dashboard
|
||||||
And I click on the menu item "Categories"
|
And I click on the menu item "Categories"
|
||||||
Then I can see the following table:
|
Then I can see the following table:
|
||||||
| | Name | Posts |
|
| | Name | Posts |
|
||||||
| | Just For Fun | 2 |
|
| | Just For Fun | 2 |
|
||||||
| | Happyness & Values | 1 |
|
| | Happyness & Values | 1 |
|
||||||
| | Health & Wellbeing | 0 |
|
| | Health & Wellbeing | 0 |
|
||||||
|
|
||||||
Scenario: See an overview of tags
|
Scenario: See an overview of tags
|
||||||
When I navigate to the administration dashboard
|
When I navigate to the administration dashboard
|
||||||
And I click on the menu item "Tags"
|
And I click on the menu item "Tags"
|
||||||
Then I can see the following table:
|
Then I can see the following table:
|
||||||
| | Name | Users | Posts |
|
| | Name | Users | Posts |
|
||||||
| 1 | Democracy | 3 | 4 |
|
| 1 | Democracy | 3 | 4 |
|
||||||
| 2 | Nature | 2 | 3 |
|
| 2 | Nature | 2 | 3 |
|
||||||
| 3 | Ecology | 1 | 1 |
|
| 3 | Ecology | 1 | 1 |
|
||||||
|
|||||||
@ -1,36 +1,36 @@
|
|||||||
import { When, Then } from 'cypress-cucumber-preprocessor/steps'
|
import { When, Then } from "cypress-cucumber-preprocessor/steps";
|
||||||
|
|
||||||
/* global cy */
|
/* global cy */
|
||||||
|
|
||||||
When('I visit my profile page', () => {
|
When("I visit my profile page", () => {
|
||||||
cy.openPage('profile/peter-pan')
|
cy.openPage("profile/peter-pan");
|
||||||
})
|
});
|
||||||
|
|
||||||
Then('I should be able to change my profile picture', () => {
|
Then("I should be able to change my profile picture", () => {
|
||||||
const avatarUpload = 'onourjourney.png'
|
const avatarUpload = "onourjourney.png";
|
||||||
|
|
||||||
cy.fixture(avatarUpload, 'base64').then(fileContent => {
|
cy.fixture(avatarUpload, "base64").then(fileContent => {
|
||||||
cy.get('#customdropzone').upload(
|
cy.get("#customdropzone").upload(
|
||||||
{ fileContent, fileName: avatarUpload, mimeType: 'image/png' },
|
{ fileContent, fileName: avatarUpload, mimeType: "image/png" },
|
||||||
{ subjectType: 'drag-n-drop' }
|
{ subjectType: "drag-n-drop", force: true }
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
cy.get('.profile-avatar img')
|
cy.get(".profile-avatar img")
|
||||||
.should('have.attr', 'src')
|
.should("have.attr", "src")
|
||||||
.and('contains', 'onourjourney')
|
.and("contains", "onourjourney");
|
||||||
cy.contains('.iziToast-message', 'Upload successful').should(
|
cy.contains(".iziToast-message", "Upload successful").should(
|
||||||
'have.length',
|
"have.length",
|
||||||
1
|
1
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
When("I visit another user's profile page", () => {
|
When("I visit another user's profile page", () => {
|
||||||
cy.openPage('profile/peter-pan')
|
cy.openPage("profile/peter-pan");
|
||||||
})
|
});
|
||||||
|
|
||||||
Then('I cannot upload a picture', () => {
|
Then("I cannot upload a picture", () => {
|
||||||
cy.get('.ds-card-content')
|
cy.get(".ds-card-content")
|
||||||
.children()
|
.children()
|
||||||
.should('not.have.id', 'customdropzone')
|
.should("not.have.id", "customdropzone")
|
||||||
.should('have.class', 'ds-avatar')
|
.should("have.class", "ds-avatar");
|
||||||
})
|
});
|
||||||
|
|||||||
@ -23,24 +23,27 @@ Cypress.Commands.add('factory', () => {
|
|||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
'create',
|
'create',
|
||||||
{ prevSubject: true },
|
{ prevSubject: true },
|
||||||
(factory, node, properties) => {
|
async (factory, node, properties) => {
|
||||||
return factory.create(node, properties)
|
await factory.create(node, properties)
|
||||||
|
return factory
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
'relate',
|
'relate',
|
||||||
{ prevSubject: true },
|
{ prevSubject: true },
|
||||||
(factory, node, relationship, properties) => {
|
async (factory, node, relationship, properties) => {
|
||||||
return factory.relate(node, relationship, properties)
|
await factory.relate(node, relationship, properties)
|
||||||
|
return factory
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
'mutate',
|
'mutate',
|
||||||
{ prevSubject: true },
|
{ prevSubject: true },
|
||||||
(factory, mutation, variables) => {
|
async (factory, mutation, variables) => {
|
||||||
return factory.mutate(mutation, variables)
|
await factory.mutate(mutation, variables)
|
||||||
|
return factory
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,10 @@
|
|||||||
data:
|
data:
|
||||||
SMTP_HOST: "mailserver.human-connection"
|
SMTP_HOST: "mailserver.human-connection"
|
||||||
SMTP_PORT: "25"
|
SMTP_PORT: "25"
|
||||||
SMTP_USERNAME: ""
|
|
||||||
SMTP_PASSWORD: ""
|
|
||||||
GRAPHQL_PORT: "4000"
|
GRAPHQL_PORT: "4000"
|
||||||
GRAPHQL_URI: "http://nitro-backend.human-connection:4000"
|
GRAPHQL_URI: "http://nitro-backend.human-connection:4000"
|
||||||
MOCKS: "false"
|
MOCKS: "false"
|
||||||
NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687"
|
NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687"
|
||||||
NEO4J_USERNAME: "neo4j"
|
|
||||||
NEO4J_PASSWORD: "neo4j"
|
|
||||||
NEO4J_AUTH: "none"
|
NEO4J_AUTH: "none"
|
||||||
CLIENT_URI: "https://nitro-staging.human-connection.org"
|
CLIENT_URI: "https://nitro-staging.human-connection.org"
|
||||||
metadata:
|
metadata:
|
||||||
|
|||||||
@ -5,11 +5,10 @@ data:
|
|||||||
MONGODB_PASSWORD: "TU9OR09EQl9QQVNTV09SRA=="
|
MONGODB_PASSWORD: "TU9OR09EQl9QQVNTV09SRA=="
|
||||||
PRIVATE_KEY_PASSPHRASE: "YTdkc2Y3OHNhZGc4N2FkODdzZmFnc2FkZzc4"
|
PRIVATE_KEY_PASSPHRASE: "YTdkc2Y3OHNhZGc4N2FkODdzZmFnc2FkZzc4"
|
||||||
MAPBOX_TOKEN: "cGsuZXlKMUlqb2lhSFZ0WVc0dFkyOXVibVZqZEdsdmJpSXNJbUVpT2lKamFqbDBjbkJ1Ykdvd2VUVmxNM1Z3WjJsek5UTnVkM1p0SW4wLktaOEtLOWw3MG9talhiRWtrYkhHc1EK"
|
MAPBOX_TOKEN: "cGsuZXlKMUlqb2lhSFZ0WVc0dFkyOXVibVZqZEdsdmJpSXNJbUVpT2lKamFqbDBjbkJ1Ykdvd2VUVmxNM1Z3WjJsek5UTnVkM1p0SW4wLktaOEtLOWw3MG9talhiRWtrYkhHc1EK"
|
||||||
SMTP_HOST:
|
|
||||||
SMTP_PORT: 587
|
|
||||||
SMTP_USERNAME:
|
SMTP_USERNAME:
|
||||||
SMTP_PASSWORD:
|
SMTP_PASSWORD:
|
||||||
SMTP_IGNORE_TLS:
|
NEO4J_USERNAME:
|
||||||
|
NEO4J_PASSWORD:
|
||||||
metadata:
|
metadata:
|
||||||
name: human-connection
|
name: human-connection
|
||||||
namespace: human-connection
|
namespace: human-connection
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
[?] type: String, // in nitro this is a defined enum - seems good for now
|
[?] type: String, // in nitro this is a defined enum - seems good for now
|
||||||
[X] required: true
|
[X] required: true
|
||||||
},
|
},
|
||||||
[X] key: {
|
[X] id: {
|
||||||
[X] type: String,
|
[X] type: String,
|
||||||
[X] required: true
|
[X] required: true
|
||||||
},
|
},
|
||||||
@ -43,7 +43,7 @@
|
|||||||
CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as badge
|
CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as badge
|
||||||
MERGE(b:Badge {id: badge._id["$oid"]})
|
MERGE(b:Badge {id: badge._id["$oid"]})
|
||||||
ON CREATE SET
|
ON CREATE SET
|
||||||
b.key = badge.key,
|
b.id = badge.key,
|
||||||
b.type = badge.type,
|
b.type = badge.type,
|
||||||
b.icon = replace(badge.image.path, 'https://api-alpha.human-connection.org', ''),
|
b.icon = replace(badge.image.path, 'https://api-alpha.human-connection.org', ''),
|
||||||
b.status = badge.status,
|
b.status = badge.status,
|
||||||
|
|||||||
@ -101,7 +101,7 @@ ON CREATE SET
|
|||||||
u.name = user.name,
|
u.name = user.name,
|
||||||
u.slug = user.slug,
|
u.slug = user.slug,
|
||||||
u.email = user.email,
|
u.email = user.email,
|
||||||
u.password = user.password,
|
u.encryptedPassword = user.password,
|
||||||
u.avatar = replace(user.avatar, 'https://api-alpha.human-connection.org', ''),
|
u.avatar = replace(user.avatar, 'https://api-alpha.human-connection.org', ''),
|
||||||
u.coverImg = replace(user.coverImg, 'https://api-alpha.human-connection.org', ''),
|
u.coverImg = replace(user.coverImg, 'https://api-alpha.human-connection.org', ''),
|
||||||
u.wasInvited = user.wasInvited,
|
u.wasInvited = user.wasInvited,
|
||||||
@ -111,6 +111,13 @@ u.createdAt = user.createdAt.`$date`,
|
|||||||
u.updatedAt = user.updatedAt.`$date`,
|
u.updatedAt = user.updatedAt.`$date`,
|
||||||
u.deleted = user.deletedAt IS NOT NULL,
|
u.deleted = user.deletedAt IS NOT NULL,
|
||||||
u.disabled = false
|
u.disabled = false
|
||||||
|
MERGE (e:EmailAddress {
|
||||||
|
email: user.email,
|
||||||
|
createdAt: toString(datetime()),
|
||||||
|
verifiedAt: toString(datetime())
|
||||||
|
})
|
||||||
|
MERGE (e)-[:BELONGS_TO]->(u)
|
||||||
|
MERGE (u)<-[:PRIMARY_EMAIL]-(e)
|
||||||
WITH u, user, user.badgeIds AS badgeIds
|
WITH u, user, user.badgeIds AS badgeIds
|
||||||
UNWIND badgeIds AS badgeId
|
UNWIND badgeIds AS badgeId
|
||||||
MATCH (b:Badge {id: badgeId})
|
MATCH (b:Badge {id: badgeId})
|
||||||
|
|||||||
@ -26,9 +26,4 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 4001:4001
|
- 4001:4001
|
||||||
- 4123:4123
|
- 4123:4123
|
||||||
neo4j:
|
|
||||||
environment:
|
|
||||||
- NEO4J_AUTH=none
|
|
||||||
ports:
|
|
||||||
- 7687:7687
|
|
||||||
- 7474:7474
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- hc-network
|
- hc-network
|
||||||
environment:
|
environment:
|
||||||
|
- NUXT_BUILD=.nuxt-dist
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
- GRAPHQL_URI=http://backend:4000
|
- GRAPHQL_URI=http://backend:4000
|
||||||
- MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.bZ8KK9l70omjXbEkkbHGsQ"
|
- MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.bZ8KK9l70omjXbEkkbHGsQ"
|
||||||
|
|||||||
@ -34,6 +34,8 @@ CREATE CONSTRAINT ON (p:Post) ASSERT p.slug IS UNIQUE;
|
|||||||
CREATE CONSTRAINT ON (c:Category) ASSERT c.slug IS UNIQUE;
|
CREATE CONSTRAINT ON (c:Category) ASSERT c.slug IS UNIQUE;
|
||||||
CREATE CONSTRAINT ON (u:User) ASSERT u.slug IS UNIQUE;
|
CREATE CONSTRAINT ON (u:User) ASSERT u.slug IS UNIQUE;
|
||||||
CREATE CONSTRAINT ON (o:Organization) ASSERT o.slug IS UNIQUE;
|
CREATE CONSTRAINT ON (o:Organization) ASSERT o.slug IS UNIQUE;
|
||||||
|
|
||||||
|
CREATE CONSTRAINT ON (e:EmailAddress) ASSERT e.email IS UNIQUE;
|
||||||
' | cypher-shell
|
' | cypher-shell
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
|
|||||||
@ -23,14 +23,14 @@
|
|||||||
"codecov": "^3.5.0",
|
"codecov": "^3.5.0",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"cypress": "^3.4.0",
|
"cypress": "^3.4.0",
|
||||||
"cypress-cucumber-preprocessor": "^1.12.0",
|
"cypress-cucumber-preprocessor": "^1.13.0",
|
||||||
"cypress-file-upload": "^3.2.0",
|
"cypress-file-upload": "^3.3.2",
|
||||||
"cypress-plugin-retries": "^1.2.2",
|
"cypress-plugin-retries": "^1.2.2",
|
||||||
"dotenv": "^8.0.0",
|
"dotenv": "^8.0.0",
|
||||||
"faker": "Marak/faker.js#master",
|
"faker": "Marak/faker.js#master",
|
||||||
"graphql-request": "^1.8.2",
|
"graphql-request": "^1.8.2",
|
||||||
"neo4j-driver": "^1.7.5",
|
"neo4j-driver": "^1.7.5",
|
||||||
"neode": "^0.2.16",
|
"neode": "^0.2.18",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"slug": "^1.1.0"
|
"slug": "^1.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
2
webapp/.gitignore
vendored
2
webapp/.gitignore
vendored
@ -61,6 +61,8 @@ typings/
|
|||||||
|
|
||||||
# nuxt.js build output
|
# nuxt.js build output
|
||||||
.nuxt
|
.nuxt
|
||||||
|
# also the build output in docker container
|
||||||
|
.nuxt-dist
|
||||||
|
|
||||||
# Nuxt generate
|
# Nuxt generate
|
||||||
dist
|
dist
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="[badges.length === 2 && 'hc-badges-dual']" class="hc-badges">
|
<div :class="[badges.length === 2 && 'hc-badges-dual']" class="hc-badges">
|
||||||
<div v-for="badge in badges" :key="badge.key" class="hc-badge-container">
|
<div v-for="badge in badges" :key="badge.id" class="hc-badge-container">
|
||||||
<img :title="badge.key" :src="badge.icon | proxyApiUrl" class="hc-badge" />
|
<img :title="badge.key" :src="badge.icon | proxyApiUrl" class="hc-badge" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import gql from 'graphql-tag'
|
import CategoryQuery from '~/graphql/CategoryQuery.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
@ -85,13 +85,7 @@ export default {
|
|||||||
apollo: {
|
apollo: {
|
||||||
Category: {
|
Category: {
|
||||||
query() {
|
query() {
|
||||||
return gql(`{
|
return CategoryQuery()
|
||||||
Category {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
},
|
},
|
||||||
result(result) {
|
result(result) {
|
||||||
this.categories = result.data.Category
|
this.categories = result.data.Category
|
||||||
|
|||||||
@ -23,49 +23,61 @@
|
|||||||
:modalsData="menuModalsData"
|
:modalsData="menuModalsData"
|
||||||
style="float-right"
|
style="float-right"
|
||||||
:is-owner="isAuthor(author.id)"
|
:is-owner="isAuthor(author.id)"
|
||||||
|
@showEditCommentMenu="editCommentMenu"
|
||||||
/>
|
/>
|
||||||
</no-ssr>
|
</no-ssr>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
|
||||||
<!-- TODO: replace editor content with tiptap render view -->
|
|
||||||
|
|
||||||
<div v-if="isCollapsed" v-html="comment.contentExcerpt" style="padding-left: 40px;" />
|
<ds-space margin-bottom="small" />
|
||||||
<div
|
<div v-if="openEditCommentMenu">
|
||||||
v-show="comment.content !== comment.contentExcerpt"
|
<hc-edit-comment-form
|
||||||
style="text-align: right; margin-right: 20px; margin-top: -12px;"
|
:comment="comment"
|
||||||
>
|
:post="post"
|
||||||
<a v-if="isCollapsed" style="padding-left: 40px;" @click="isCollapsed = !isCollapsed">
|
@showEditCommentMenu="editCommentMenu"
|
||||||
{{ $t('comment.show.more') }}
|
/>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isCollapsed" v-html="comment.content" style="padding-left: 40px;" />
|
<div v-show="!openEditCommentMenu">
|
||||||
<div style="text-align: right; margin-right: 20px; margin-top: -12px;">
|
<div v-if="isCollapsed" v-html="comment.contentExcerpt" style="padding-left: 40px;" />
|
||||||
<a v-if="!isCollapsed" @click="isCollapsed = !isCollapsed" style="padding-left: 40px; ">
|
<div
|
||||||
{{ $t('comment.show.less') }}
|
v-show="comment.content !== comment.contentExcerpt"
|
||||||
</a>
|
style="text-align: right; margin-right: 20px; margin-top: -12px;"
|
||||||
|
>
|
||||||
|
<a v-if="isCollapsed" style="padding-left: 40px;" @click="isCollapsed = !isCollapsed">
|
||||||
|
{{ $t('comment.show.more') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-if="!isCollapsed" v-html="comment.content" style="padding-left: 40px;" />
|
||||||
|
<div style="text-align: right; margin-right: 20px; margin-top: -12px;">
|
||||||
|
<a v-if="!isCollapsed" @click="isCollapsed = !isCollapsed" style="padding-left: 40px; ">
|
||||||
|
{{ $t('comment.show.less') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-space margin-bottom="small" />
|
<ds-space margin-bottom="small" />
|
||||||
</ds-card>
|
</ds-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
import HcUser from '~/components/User'
|
import HcUser from '~/components/User'
|
||||||
import ContentMenu from '~/components/ContentMenu'
|
import ContentMenu from '~/components/ContentMenu'
|
||||||
|
import HcEditCommentForm from '~/components/comments/EditCommentForm/EditCommentForm'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: function() {
|
data: function() {
|
||||||
return {
|
return {
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
|
openEditCommentMenu: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
HcUser,
|
HcUser,
|
||||||
ContentMenu,
|
ContentMenu,
|
||||||
|
HcEditCommentForm,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
post: { type: Object, default: () => {} },
|
||||||
comment: {
|
comment: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
@ -112,9 +124,16 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapMutations({
|
||||||
|
setEditPending: 'editor/SET_EDIT_PENDING',
|
||||||
|
}),
|
||||||
isAuthor(id) {
|
isAuthor(id) {
|
||||||
return this.user.id === id
|
return this.user.id === id
|
||||||
},
|
},
|
||||||
|
editCommentMenu(showMenu) {
|
||||||
|
this.openEditCommentMenu = showMenu
|
||||||
|
this.setEditPending(showMenu)
|
||||||
|
},
|
||||||
async deleteCommentCallback() {
|
async deleteCommentCallback() {
|
||||||
try {
|
try {
|
||||||
var gqlMutation = gql`
|
var gqlMutation = gql`
|
||||||
|
|||||||
@ -76,14 +76,13 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.isOwner && this.resourceType === 'comment') {
|
if (this.isOwner && this.resourceType === 'comment') {
|
||||||
// routes.push({
|
routes.push({
|
||||||
// name: this.$t(`comment.menu.edit`),
|
name: this.$t(`comment.menu.edit`),
|
||||||
// callback: () => {
|
callback: () => {
|
||||||
// /* eslint-disable-next-line no-console */
|
this.$emit('showEditCommentMenu', true)
|
||||||
// console.log('EDIT COMMENT')
|
},
|
||||||
// },
|
icon: 'edit',
|
||||||
// icon: 'edit'
|
})
|
||||||
// })
|
|
||||||
routes.push({
|
routes.push({
|
||||||
name: this.$t(`comment.menu.delete`),
|
name: this.$t(`comment.menu.delete`),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
|
|||||||
134
webapp/components/FilterPosts/FilterPosts.spec.js
Normal file
134
webapp/components/FilterPosts/FilterPosts.spec.js
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { mount, createLocalVue } from '@vue/test-utils'
|
||||||
|
import VTooltip from 'v-tooltip'
|
||||||
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import FilterPosts from './FilterPosts.vue'
|
||||||
|
import FilterPostsMenuItem from './FilterPostsMenuItems.vue'
|
||||||
|
import { mutations } from '~/store/posts'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
|
localVue.use(Styleguide)
|
||||||
|
localVue.use(VTooltip)
|
||||||
|
localVue.use(Vuex)
|
||||||
|
|
||||||
|
describe('FilterPosts.vue', () => {
|
||||||
|
let wrapper
|
||||||
|
let mocks
|
||||||
|
let propsData
|
||||||
|
let menuToggle
|
||||||
|
let allCategoriesButton
|
||||||
|
let environmentAndNatureButton
|
||||||
|
let consumptionAndSustainabiltyButton
|
||||||
|
let democracyAndPoliticsButton
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks = {
|
||||||
|
$apollo: {
|
||||||
|
query: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
data: { Post: { title: 'Post with Category', category: [{ id: 'cat4' }] } },
|
||||||
|
})
|
||||||
|
.mockRejectedValue({ message: 'We were unable to filter' }),
|
||||||
|
},
|
||||||
|
$t: jest.fn(),
|
||||||
|
$i18n: {
|
||||||
|
locale: () => 'en',
|
||||||
|
},
|
||||||
|
$toast: {
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
propsData = {
|
||||||
|
categories: [
|
||||||
|
{ id: 'cat4', name: 'Environment & Nature', icon: 'tree' },
|
||||||
|
{ id: 'cat15', name: 'Consumption & Sustainability', icon: 'shopping-cart' },
|
||||||
|
{ id: 'cat9', name: 'Democracy & Politics', icon: 'university' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
mutations: {
|
||||||
|
'posts/SET_POSTS': mutations.SET_POSTS,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(FilterPosts, { mocks, localVue, propsData, store })
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
menuToggle = wrapper.findAll('a').at(0)
|
||||||
|
menuToggle.trigger('click')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('groups the categories by pair', () => {
|
||||||
|
expect(wrapper.vm.chunk).toEqual([
|
||||||
|
[
|
||||||
|
{ id: 'cat4', name: 'Environment & Nature', icon: 'tree' },
|
||||||
|
{ id: 'cat15', name: 'Consumption & Sustainability', icon: 'shopping-cart' },
|
||||||
|
],
|
||||||
|
[{ id: 'cat9', name: 'Democracy & Politics', icon: 'university' }],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('starts with all categories button active', () => {
|
||||||
|
allCategoriesButton = wrapper.findAll('button').at(0)
|
||||||
|
expect(allCategoriesButton.attributes().class).toContain('ds-button-primary')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a categories id to selectedCategoryIds when clicked', () => {
|
||||||
|
environmentAndNatureButton = wrapper.findAll('button').at(1)
|
||||||
|
environmentAndNatureButton.trigger('click')
|
||||||
|
const filterPostsMenuItem = wrapper.find(FilterPostsMenuItem)
|
||||||
|
expect(filterPostsMenuItem.vm.selectedCategoryIds).toEqual(['cat4'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets primary to true when the button is clicked', () => {
|
||||||
|
democracyAndPoliticsButton = wrapper.findAll('button').at(3)
|
||||||
|
democracyAndPoliticsButton.trigger('click')
|
||||||
|
expect(democracyAndPoliticsButton.attributes().class).toContain('ds-button-primary')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('queries a post by its categories', () => {
|
||||||
|
consumptionAndSustainabiltyButton = wrapper.findAll('button').at(2)
|
||||||
|
consumptionAndSustainabiltyButton.trigger('click')
|
||||||
|
expect(mocks.$apollo.query).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
variables: {
|
||||||
|
filter: { categories_some: { id_in: ['cat15'] } },
|
||||||
|
first: expect.any(Number),
|
||||||
|
offset: expect.any(Number),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('supports a query of multiple categories', () => {
|
||||||
|
environmentAndNatureButton = wrapper.findAll('button').at(1)
|
||||||
|
environmentAndNatureButton.trigger('click')
|
||||||
|
consumptionAndSustainabiltyButton = wrapper.findAll('button').at(2)
|
||||||
|
consumptionAndSustainabiltyButton.trigger('click')
|
||||||
|
expect(mocks.$apollo.query).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
variables: {
|
||||||
|
filter: { categories_some: { id_in: ['cat4', 'cat15'] } },
|
||||||
|
first: expect.any(Number),
|
||||||
|
offset: expect.any(Number),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggles the categoryIds when clicked more than once', () => {
|
||||||
|
environmentAndNatureButton = wrapper.findAll('button').at(1)
|
||||||
|
environmentAndNatureButton.trigger('click')
|
||||||
|
environmentAndNatureButton.trigger('click')
|
||||||
|
const filterPostsMenuItem = wrapper.find(FilterPostsMenuItem)
|
||||||
|
expect(filterPostsMenuItem.vm.selectedCategoryIds).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
61
webapp/components/FilterPosts/FilterPosts.vue
Normal file
61
webapp/components/FilterPosts/FilterPosts.vue
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<dropdown ref="menu" :placement="placement" :offset="offset">
|
||||||
|
<a slot="default" slot-scope="{ toggleMenu }" href="#" @click.prevent="toggleMenu()">
|
||||||
|
<ds-icon style="margin: 12px 0px 0px 10px;" name="filter" size="large" />
|
||||||
|
<ds-icon style="margin: 7px 0px 0px 2px" size="xx-small" name="angle-down" />
|
||||||
|
</a>
|
||||||
|
<template slot="popover">
|
||||||
|
<filter-posts-menu-items :chunk="chunk" @filterPosts="filterPosts" />
|
||||||
|
</template>
|
||||||
|
</dropdown>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import _ from 'lodash'
|
||||||
|
import Dropdown from '~/components/Dropdown'
|
||||||
|
import { filterPosts } from '~/graphql/PostQuery.js'
|
||||||
|
import { mapMutations } from 'vuex'
|
||||||
|
import FilterPostsMenuItems from '~/components/FilterPosts/FilterPostsMenuItems'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Dropdown,
|
||||||
|
FilterPostsMenuItems,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
placement: { type: String },
|
||||||
|
offset: { type: [String, Number] },
|
||||||
|
categories: { type: Array, default: () => [] },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
pageSize: 12,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
chunk() {
|
||||||
|
return _.chunk(this.categories, 2)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations({
|
||||||
|
setPosts: 'posts/SET_POSTS',
|
||||||
|
}),
|
||||||
|
filterPosts(categoryIds) {
|
||||||
|
const filter = categoryIds.length ? { categories_some: { id_in: categoryIds } } : {}
|
||||||
|
this.$apollo
|
||||||
|
.query({
|
||||||
|
query: filterPosts(this.$i18n),
|
||||||
|
variables: {
|
||||||
|
filter: filter,
|
||||||
|
first: this.pageSize,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(({ data: { Post } }) => {
|
||||||
|
this.setPosts(Post)
|
||||||
|
})
|
||||||
|
.catch(error => this.$toast.error(error.message))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
126
webapp/components/FilterPosts/FilterPostsMenuItems.vue
Normal file
126
webapp/components/FilterPosts/FilterPostsMenuItems.vue
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<ds-container>
|
||||||
|
<ds-space />
|
||||||
|
<ds-flex id="filter-posts-header">
|
||||||
|
<ds-heading tag="h4">{{ $t('filter-posts.header') }}</ds-heading>
|
||||||
|
<ds-space margin-bottom="large" />
|
||||||
|
</ds-flex>
|
||||||
|
<ds-flex>
|
||||||
|
<ds-flex-item
|
||||||
|
:width="{ base: '100%', sm: '100%', md: '100%', lg: '5%' }"
|
||||||
|
class="categories-menu-item"
|
||||||
|
>
|
||||||
|
<ds-flex>
|
||||||
|
<ds-flex-item width="10%" />
|
||||||
|
<ds-flex-item width="100%">
|
||||||
|
<ds-button
|
||||||
|
icon="check"
|
||||||
|
@click.stop.prevent="toggleCategory()"
|
||||||
|
:primary="allCategories"
|
||||||
|
/>
|
||||||
|
<ds-flex-item>
|
||||||
|
<label class="category-labels">{{ $t('filter-posts.all') }}</label>
|
||||||
|
</ds-flex-item>
|
||||||
|
<ds-space />
|
||||||
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
|
</ds-flex-item>
|
||||||
|
<ds-flex-item :width="{ base: '0%', sm: '0%', md: '0%', lg: '4%' }" />
|
||||||
|
<ds-flex-item
|
||||||
|
:width="{ base: '0%', sm: '0%', md: '0%', lg: '3%' }"
|
||||||
|
id="categories-menu-divider"
|
||||||
|
/>
|
||||||
|
<ds-flex-item
|
||||||
|
:width="{ base: '50%', sm: '50%', md: '50%', lg: '11%' }"
|
||||||
|
v-for="index in chunk.length"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<ds-flex v-for="category in chunk[index - 1]" :key="category.id" class="categories-menu">
|
||||||
|
<ds-flex class="categories-menu">
|
||||||
|
<ds-flex-item width="100%" class="categories-menu-item">
|
||||||
|
<ds-button
|
||||||
|
:icon="category.icon"
|
||||||
|
:primary="isActive(category.id)"
|
||||||
|
@click.stop.prevent="toggleCategory(category.id)"
|
||||||
|
/>
|
||||||
|
<ds-space margin-bottom="small" />
|
||||||
|
</ds-flex-item>
|
||||||
|
<ds-flex>
|
||||||
|
<ds-flex-item class="categories-menu-item">
|
||||||
|
<label class="category-labels">{{ category.name }}</label>
|
||||||
|
</ds-flex-item>
|
||||||
|
<ds-space margin-bottom="xx-large" />
|
||||||
|
</ds-flex>
|
||||||
|
</ds-flex>
|
||||||
|
</ds-flex>
|
||||||
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
|
</ds-container>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
chunk: { type: Array, default: () => [] },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedCategoryIds: [],
|
||||||
|
allCategories: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isActive(id) {
|
||||||
|
const index = this.selectedCategoryIds.indexOf(id)
|
||||||
|
if (index > -1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
toggleCategory(id) {
|
||||||
|
if (!id) {
|
||||||
|
this.selectedCategoryIds = []
|
||||||
|
this.allCategories = true
|
||||||
|
} else {
|
||||||
|
const index = this.selectedCategoryIds.indexOf(id)
|
||||||
|
if (index > -1) {
|
||||||
|
this.selectedCategoryIds.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
this.selectedCategoryIds.push(id)
|
||||||
|
}
|
||||||
|
this.allCategories = false
|
||||||
|
}
|
||||||
|
this.$emit('filterPosts', this.selectedCategoryIds)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
#filter-posts-header {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-menu-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-menu {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-labels {
|
||||||
|
font-size: $font-size-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 960px) {
|
||||||
|
#categories-menu-divider {
|
||||||
|
border-left: 1px solid $border-color-soft;
|
||||||
|
margin: 9px 0px 40px 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 960px) {
|
||||||
|
#filter-posts-header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-form v-model="form" @submit="handleSubmit">
|
<ds-form v-show="!editPending" v-model="form" @submit="handleSubmit">
|
||||||
<template slot-scope="{ errors }">
|
<template slot-scope="{ errors }">
|
||||||
<ds-card>
|
<ds-card>
|
||||||
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
|
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
|
||||||
@ -27,6 +27,7 @@ import gql from 'graphql-tag'
|
|||||||
import HcEditor from '~/components/Editor/Editor'
|
import HcEditor from '~/components/Editor/Editor'
|
||||||
import PostCommentsQuery from '~/graphql/PostCommentsQuery.js'
|
import PostCommentsQuery from '~/graphql/PostCommentsQuery.js'
|
||||||
import CommentMutations from '~/graphql/CommentMutations.js'
|
import CommentMutations from '~/graphql/CommentMutations.js'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -46,6 +47,11 @@ export default {
|
|||||||
users: [],
|
users: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
editPending: 'editor/editPending',
|
||||||
|
}),
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateEditorContent(value) {
|
updateEditorContent(value) {
|
||||||
const content = value.replace(/<(?:.|\n)*?>/gm, '').trim()
|
const content = value.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||||
|
|||||||
@ -40,6 +40,7 @@ describe('CommentForm.vue', () => {
|
|||||||
'editor/placeholder': () => {
|
'editor/placeholder': () => {
|
||||||
return 'some cool placeholder'
|
return 'some cool placeholder'
|
||||||
},
|
},
|
||||||
|
'editor/editPending': () => false,
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
getters,
|
getters,
|
||||||
|
|||||||
@ -16,11 +16,12 @@
|
|||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<ds-space margin-bottom="large" />
|
<ds-space margin-bottom="large" />
|
||||||
<div v-if="comments && comments.length" class="comments">
|
<div v-if="comments && comments.length" id="comments" class="comments">
|
||||||
<comment
|
<comment
|
||||||
v-for="(comment, index) in comments"
|
v-for="(comment, index) in comments"
|
||||||
:key="comment.id"
|
:key="comment.id"
|
||||||
:comment="comment"
|
:comment="comment"
|
||||||
|
:post="post"
|
||||||
@deleteComment="comments.splice(index, 1)"
|
@deleteComment="comments.splice(index, 1)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -47,7 +48,8 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
Post(post) {
|
Post(post) {
|
||||||
this.comments = post[0].comments || []
|
const [first] = post
|
||||||
|
this.comments = (first && first.comments) || []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
|
|||||||
108
webapp/components/comments/EditCommentForm/EditCommentForm.vue
Normal file
108
webapp/components/comments/EditCommentForm/EditCommentForm.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<ds-form v-model="form" @submit="handleSubmit">
|
||||||
|
<template slot-scope="{ errors }">
|
||||||
|
<ds-card>
|
||||||
|
<!-- with no-ssr the content is not shown -->
|
||||||
|
<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 ghost class="cancelBtn" @click.prevent="closeEditWindow">
|
||||||
|
{{ $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>
|
||||||
|
{{ $t('post.comment.submit') }}
|
||||||
|
</ds-button>
|
||||||
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
|
</ds-card>
|
||||||
|
</template>
|
||||||
|
</ds-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import HcEditor from '~/components/Editor/Editor'
|
||||||
|
import { mapMutations } from 'vuex'
|
||||||
|
import CommentMutations from '~/graphql/CommentMutations.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HcEditor,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
comment: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
disabled: true,
|
||||||
|
loading: false,
|
||||||
|
form: {
|
||||||
|
content: this.comment.content,
|
||||||
|
},
|
||||||
|
users: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations({
|
||||||
|
setEditPending: 'editor/SET_EDIT_PENDING',
|
||||||
|
}),
|
||||||
|
updateEditorContent(value) {
|
||||||
|
const sanitizedContent = value.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||||
|
this.disabled = value === this.comment.content || sanitizedContent.length < 1
|
||||||
|
this.form.content = value
|
||||||
|
},
|
||||||
|
closeEditWindow() {
|
||||||
|
this.$emit('showEditCommentMenu', false)
|
||||||
|
},
|
||||||
|
handleSubmit() {
|
||||||
|
this.loading = true
|
||||||
|
this.disabled = true
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: CommentMutations().UpdateComment,
|
||||||
|
variables: {
|
||||||
|
content: this.form.content,
|
||||||
|
id: this.comment.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.loading = false
|
||||||
|
|
||||||
|
this.$toast.success(this.$t('post.comment.updated'))
|
||||||
|
this.disabled = false
|
||||||
|
this.$emit('showEditCommentMenu', false)
|
||||||
|
this.setEditPending(false)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.$toast.error(err.message)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
User: {
|
||||||
|
query() {
|
||||||
|
return gql`
|
||||||
|
{
|
||||||
|
User(orderBy: slug_asc) {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
result({ data: { User } }) {
|
||||||
|
this.users = User
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -2,7 +2,7 @@
|
|||||||
<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 }}
|
{{ totalNotifications }}
|
||||||
</ds-button>
|
</ds-button>
|
||||||
<dropdown v-else class="notifications-menu">
|
<dropdown v-else class="notifications-menu" :placement="placement">
|
||||||
<template slot="default" slot-scope="{ toggleMenu }">
|
<template slot="default" slot-scope="{ toggleMenu }">
|
||||||
<ds-button primary icon="bell" @click.prevent="toggleMenu">
|
<ds-button primary icon="bell" @click.prevent="toggleMenu">
|
||||||
{{ totalNotifications }}
|
{{ totalNotifications }}
|
||||||
@ -48,6 +48,9 @@ export default {
|
|||||||
NotificationList,
|
NotificationList,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
placement: { type: String },
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
totalNotifications() {
|
totalNotifications() {
|
||||||
return (this.notifications || []).length
|
return (this.notifications || []).length
|
||||||
|
|||||||
11
webapp/graphql/CategoryQuery.js
Normal file
11
webapp/graphql/CategoryQuery.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return gql(`{
|
||||||
|
Category {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
icon
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
@ -20,5 +20,14 @@ export default () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
UpdateComment: gql`
|
||||||
|
mutation($content: String!, $id: ID!) {
|
||||||
|
UpdateComment(content: $content, id: $id) {
|
||||||
|
id
|
||||||
|
content
|
||||||
|
contentExcerpt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import gql from 'graphql-tag'
|
|||||||
|
|
||||||
export default app => {
|
export default app => {
|
||||||
const lang = app.$i18n.locale().toUpperCase()
|
const lang = app.$i18n.locale().toUpperCase()
|
||||||
return gql(`
|
return gql`
|
||||||
query Comment($postId: ID) {
|
query Comment($postId: ID) {
|
||||||
Comment(postId: $postId) {
|
Comment(postId: $postId) {
|
||||||
id
|
id
|
||||||
@ -25,11 +25,10 @@ export default app => {
|
|||||||
}
|
}
|
||||||
badges {
|
badges {
|
||||||
id
|
id
|
||||||
key
|
|
||||||
icon
|
icon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`)
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,6 @@ export default i18n => {
|
|||||||
}
|
}
|
||||||
badges {
|
badges {
|
||||||
id
|
id
|
||||||
key
|
|
||||||
icon
|
icon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import gql from 'graphql-tag'
|
|||||||
|
|
||||||
export default i18n => {
|
export default i18n => {
|
||||||
const lang = i18n.locale().toUpperCase()
|
const lang = i18n.locale().toUpperCase()
|
||||||
return gql(`
|
return gql`
|
||||||
query Post($slug: String!) {
|
query Post($slug: String!) {
|
||||||
Post(slug: $slug) {
|
Post(slug: $slug) {
|
||||||
id
|
id
|
||||||
@ -30,7 +30,6 @@ export default i18n => {
|
|||||||
}
|
}
|
||||||
badges {
|
badges {
|
||||||
id
|
id
|
||||||
key
|
|
||||||
icon
|
icon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,7 +60,6 @@ export default i18n => {
|
|||||||
}
|
}
|
||||||
badges {
|
badges {
|
||||||
id
|
id
|
||||||
key
|
|
||||||
icon
|
icon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,5 +73,50 @@ export default i18n => {
|
|||||||
shoutedByCurrentUser
|
shoutedByCurrentUser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`)
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const filterPosts = i18n => {
|
||||||
|
const lang = i18n.locale().toUpperCase()
|
||||||
|
return gql`
|
||||||
|
query Post($filter: _PostFilter, $first: Int, $offset: Int) {
|
||||||
|
Post(filter: $filter, first: $first, offset: $offset) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
contentExcerpt
|
||||||
|
createdAt
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
slug
|
||||||
|
image
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
avatar
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
contributionsCount
|
||||||
|
shoutedCount
|
||||||
|
commentsCount
|
||||||
|
followedByCount
|
||||||
|
followedByCurrentUser
|
||||||
|
location {
|
||||||
|
name: name${lang}
|
||||||
|
}
|
||||||
|
badges {
|
||||||
|
id
|
||||||
|
icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commentsCount
|
||||||
|
categories {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
icon
|
||||||
|
}
|
||||||
|
shoutedCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,6 @@ export default i18n => {
|
|||||||
createdAt
|
createdAt
|
||||||
badges {
|
badges {
|
||||||
id
|
id
|
||||||
key
|
|
||||||
icon
|
icon
|
||||||
}
|
}
|
||||||
badgesCount
|
badgesCount
|
||||||
@ -39,7 +38,6 @@ export default i18n => {
|
|||||||
commentsCount
|
commentsCount
|
||||||
badges {
|
badges {
|
||||||
id
|
id
|
||||||
key
|
|
||||||
icon
|
icon
|
||||||
}
|
}
|
||||||
location {
|
location {
|
||||||
@ -61,7 +59,6 @@ export default i18n => {
|
|||||||
commentsCount
|
commentsCount
|
||||||
badges {
|
badges {
|
||||||
id
|
id
|
||||||
key
|
|
||||||
icon
|
icon
|
||||||
}
|
}
|
||||||
location {
|
location {
|
||||||
|
|||||||
@ -3,14 +3,24 @@
|
|||||||
<div class="main-navigation">
|
<div class="main-navigation">
|
||||||
<ds-container class="main-navigation-container" style="padding: 10px 10px;">
|
<ds-container class="main-navigation-container" style="padding: 10px 10px;">
|
||||||
<div>
|
<div>
|
||||||
<ds-flex>
|
<ds-flex class="main-navigation-flex">
|
||||||
<ds-flex-item :width="{ base: '49px', md: '150px' }">
|
<ds-flex-item :width="{ lg: '3.5%' }" />
|
||||||
<nuxt-link to="/">
|
<ds-flex-item :width="{ base: '80%', sm: '80%', md: '80%', lg: '15%' }">
|
||||||
|
<a @click="redirectToRoot">
|
||||||
<ds-logo />
|
<ds-logo />
|
||||||
</nuxt-link>
|
</a>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
<ds-flex-item>
|
<ds-flex-item
|
||||||
<div id="nav-search-box" v-on:click="unfolded" @blur.capture="foldedup">
|
:width="{ base: '20%', sm: '20%', md: '20%', lg: '0%' }"
|
||||||
|
class="mobile-hamburger-menu"
|
||||||
|
>
|
||||||
|
<ds-button icon="bars" @click="toggleMobileMenuView" right />
|
||||||
|
</ds-flex-item>
|
||||||
|
<ds-flex-item
|
||||||
|
:width="{ base: '85%', sm: '85%', md: '50%', lg: '50%' }"
|
||||||
|
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||||
|
>
|
||||||
|
<div id="nav-search-box">
|
||||||
<search-input
|
<search-input
|
||||||
id="nav-search"
|
id="nav-search"
|
||||||
:delay="300"
|
:delay="300"
|
||||||
@ -22,17 +32,36 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
<ds-flex-item width="200px" style="background-color:white">
|
<ds-flex-item
|
||||||
<div class="main-navigation-right" style="float:right">
|
:width="{ base: '15%', sm: '15%', md: '10%', lg: '10%' }"
|
||||||
|
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||||
|
>
|
||||||
|
<no-ssr>
|
||||||
|
<filter-posts placement="top-start" offset="8" :categories="categories" />
|
||||||
|
</no-ssr>
|
||||||
|
</ds-flex-item>
|
||||||
|
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '10%', lg: '2%' }" />
|
||||||
|
<ds-flex-item
|
||||||
|
:width="{ base: '100%', sm: '100%', md: '100%', lg: '13%' }"
|
||||||
|
style="background-color:white"
|
||||||
|
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="main-navigation-right"
|
||||||
|
:class="{
|
||||||
|
'desktop-view': !toggleMobileMenu,
|
||||||
|
'hide-mobile-menu': !toggleMobileMenu,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<no-ssr>
|
<no-ssr>
|
||||||
<locale-switch class="topbar-locale-switch" placement="bottom" offset="23" />
|
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
|
||||||
</no-ssr>
|
</no-ssr>
|
||||||
<template v-if="isLoggedIn">
|
<template v-if="isLoggedIn">
|
||||||
<no-ssr>
|
<no-ssr>
|
||||||
<notification-menu />
|
<notification-menu placement="top" />
|
||||||
</no-ssr>
|
</no-ssr>
|
||||||
<no-ssr>
|
<no-ssr>
|
||||||
<dropdown class="avatar-menu">
|
<dropdown class="avatar-menu" offset="8">
|
||||||
<template slot="default" slot-scope="{ toggleMenu }">
|
<template slot="default" slot-scope="{ toggleMenu }">
|
||||||
<a
|
<a
|
||||||
class="avatar-menu-trigger"
|
class="avatar-menu-trigger"
|
||||||
@ -118,6 +147,8 @@ import NotificationMenu from '~/components/notifications/NotificationMenu'
|
|||||||
import Dropdown from '~/components/Dropdown'
|
import Dropdown from '~/components/Dropdown'
|
||||||
import HcAvatar from '~/components/Avatar/Avatar.vue'
|
import HcAvatar from '~/components/Avatar/Avatar.vue'
|
||||||
import seo from '~/mixins/seo'
|
import seo from '~/mixins/seo'
|
||||||
|
import FilterPosts from '~/components/FilterPosts/FilterPosts.vue'
|
||||||
|
import CategoryQuery from '~/graphql/CategoryQuery.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -127,11 +158,14 @@ export default {
|
|||||||
Modal,
|
Modal,
|
||||||
NotificationMenu,
|
NotificationMenu,
|
||||||
HcAvatar,
|
HcAvatar,
|
||||||
|
FilterPosts,
|
||||||
},
|
},
|
||||||
mixins: [seo],
|
mixins: [seo],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
mobileSearchVisible: false,
|
mobileSearchVisible: false,
|
||||||
|
toggleMobileMenu: false,
|
||||||
|
categories: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -180,10 +214,16 @@ export default {
|
|||||||
return routes
|
return routes
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
Category(category) {
|
||||||
|
this.categories = category || []
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
quickSearchClear: 'search/quickClear',
|
quickSearchClear: 'search/quickClear',
|
||||||
quickSearch: 'search/quickSearch',
|
quickSearch: 'search/quickSearch',
|
||||||
|
fetchPosts: 'posts/fetchPosts',
|
||||||
}),
|
}),
|
||||||
goToPost(item) {
|
goToPost(item) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@ -200,23 +240,24 @@ export default {
|
|||||||
}
|
}
|
||||||
return this.$route.path.indexOf(url) === 0
|
return this.$route.path.indexOf(url) === 0
|
||||||
},
|
},
|
||||||
unfolded: function() {
|
toggleMobileMenuView() {
|
||||||
document.getElementById('nav-search-box').classList.add('unfolded')
|
this.toggleMobileMenu = !this.toggleMobileMenu
|
||||||
},
|
},
|
||||||
foldedup: function() {
|
redirectToRoot() {
|
||||||
document.getElementById('nav-search-box').classList.remove('unfolded')
|
this.$router.replace('/')
|
||||||
|
this.fetchPosts({ i18n: this.$i18n, filter: {} })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
Category: {
|
||||||
|
query() {
|
||||||
|
return CategoryQuery()
|
||||||
|
},
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
|
||||||
.unfolded {
|
|
||||||
position: absolute;
|
|
||||||
right: 0px;
|
|
||||||
left: 0px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.topbar-locale-switch {
|
.topbar-locale-switch {
|
||||||
@ -228,7 +269,7 @@ export default {
|
|||||||
|
|
||||||
.main-container {
|
.main-container {
|
||||||
padding-top: 6rem;
|
padding-top: 6rem;
|
||||||
padding-botton: 5rem;
|
padding-bottom: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-navigation {
|
.main-navigation {
|
||||||
@ -242,6 +283,14 @@ export default {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-navigation-right .desktop-view {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-menu {
|
||||||
|
margin: 2px 0px 0px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.avatar-menu-trigger {
|
.avatar-menu-trigger {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -285,6 +334,24 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 960px) {
|
||||||
|
.mobile-hamburger-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 960px) {
|
||||||
|
#nav-search-box,
|
||||||
|
.main-navigation-right {
|
||||||
|
margin: 10px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-mobile-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ds-footer {
|
.ds-footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@ -4,6 +4,10 @@
|
|||||||
"hashtag-search": "Suche nach #{hashtag}",
|
"hashtag-search": "Suche nach #{hashtag}",
|
||||||
"clearSearch": "Suche löschen"
|
"clearSearch": "Suche löschen"
|
||||||
},
|
},
|
||||||
|
"filter-posts": {
|
||||||
|
"header": "Themenkategorien",
|
||||||
|
"all": "Alle"
|
||||||
|
},
|
||||||
"site": {
|
"site": {
|
||||||
"made": "Mit ❤ gemacht",
|
"made": "Mit ❤ gemacht",
|
||||||
"imprint": "Impressum",
|
"imprint": "Impressum",
|
||||||
@ -49,8 +53,8 @@
|
|||||||
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail Adresse!",
|
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail Adresse!",
|
||||||
"invalid-invitation-token": "Es sieht so aus, als ob der Einladungscode schon eingelöst wurde. Jeder Code kann nur einmalig benutzt werden."
|
"invalid-invitation-token": "Es sieht so aus, als ob der Einladungscode schon eingelöst wurde. Jeder Code kann nur einmalig benutzt werden."
|
||||||
},
|
},
|
||||||
"submit": "Konto erstellen",
|
"submit": "Konto erstellen",
|
||||||
"success": "Eine Mail mit einem Bestätigungslink für die Registrierung wurde an <b>{email}</b> geschickt"
|
"success": "Eine Mail mit einem Bestätigungslink für die Registrierung wurde an <b>{email}</b> geschickt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"create-user-account": {
|
"create-user-account": {
|
||||||
@ -188,7 +192,19 @@
|
|||||||
"name": "Organisationen"
|
"name": "Organisationen"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"name": "Benutzer"
|
"name": "Benutzer",
|
||||||
|
"form": {
|
||||||
|
"placeholder": "E-Mail, Name oder Beschreibung"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"columns": {
|
||||||
|
"name": "Name",
|
||||||
|
"slug": "Slug",
|
||||||
|
"role": "Rolle",
|
||||||
|
"createdAt": "Erstellt am"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"empty": "Keine Benutzer gefunden"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"name": "Seiten"
|
"name": "Seiten"
|
||||||
@ -396,5 +412,4 @@
|
|||||||
"terms": {
|
"terms": {
|
||||||
"text": "<div ><ol><li><strong>UNFALLGEFAHR: </strong>Das ist eine Testversion! Alle Daten, Dein Profil und die Server können jederzeit komplett vernichtet, verloren, verbrannt und vielleicht auch in der Nähe von Alpha Centauri synchronisiert werden. Die Benutzung läuft auf eigene Gefahr. Mit kommerziellen Nebenwirkungen ist jedoch nicht zu rechnen.</li><br><li><strong>DU UND DEINE DATEN: </strong>Bitte beachte, dass wir die Inhalte der Alphaversion zu Werbezwecken, Webpräsentationen usw. verwenden, aber wir glauben, dass das auch in Deinem Interesse ist. Am besten keinen Nachnamen eingeben und bei noch mehr Datensparsamkeit ein Profilfoto ohne Identität verwenden. Mehr in unserer <a href='/pages/privacy' target='_blank'>Datenschutzerklärung</a>.</li><br><li><strong>BAUSTELLEN: </strong>Das ist immer noch eine Testversion. Wenn etwas nicht funktioniert, blockiert, irritiert, falsch angezeigt, verbogen oder nicht anklickbar ist, bitten wir dies zu entschuldigen. Fehler, Käfer und Bugs bitte melden! <a href='http://localhost:3000/%22https://human-connection.org/alpha/#bugreport%5C%22' target='_blank'>https://human-connection.org/support</a></li><br><li><strong>VERHALTENSCODEX</strong>: Die Verhaltensregeln dienen als Leitsätze für den persönlichen Auftritt und den Umgang untereinander. Wer als Nutzer im Human Connection Netzwerk aktiv ist, Beiträge verfasst, kommentiert oder mit anderen Nutzern, auch außerhalb des Netzwerkes, Kontakt aufnimmt, erkennt diese Verhaltensregeln als verbindlich an: <a href='https://alpha.human-connection.org/pages/code-of-conduct' target='_blank'>https://alpha.human-connection.org/pages/code-of-conduct</a></li><br><li><strong>MODERATION: </strong>Solange kein Community-Moderationssystem lauffähig ist, entscheidet ein Regenbogen-Einhorn darüber, ob Du körperlich und psychisch stabil genug bist, unsere Testversion zu bedienen. Das Einhorn kann Dich jederzeit von der Alpha entfernen. Also sei nett und lass Regenbogenfutter da!</li><br><li><strong>FAIRNESS: </strong>Sollte Dir die Alphaversion unseres Netzwerks wider Erwarten, egal aus welchen Gründen, nicht gefallen, überweisen wir Dir Deine gespendeten Monatsbeiträge innerhalb der ersten 2 Monate gerne zurück. Einfach Mail an: <a href='mailto:info@human-connection.org' target='_blank'>info@human-connection.org </a><strong>Achtung: Viele Funktionen werden erst nach und nach eingebaut. </strong></li><br><li><strong>FRAGEN?</strong> Die Termine und Links zu den Zoom-Räumen findest Du hier: <a href='http://localhost:3000/%22https://human-connection.org/events-und-news//%22' target='_blank'>https://human-connection.org/veranstaltungen/</a></li><br><li><strong>VON MENSCHEN FÜR MENSCHEN: </strong>Bitte hilf uns weitere monatlichen Spender für Human Connection zu bekommen, damit das Netzwerk so schnell wie möglich offiziell an den Start gehen kann. <a href='http://localhost:3000/%22https://human-connection.org/alpha/#bugreport%5C%22' target='_blank'>https://human-connection.org</a></li></ol><p>Jetzt aber viel Spaß mit der Alpha von Human Connection! Für den ersten Weltfrieden. ♥︎</p><br><p><strong>Herzlichst,</strong></p><p><strong>Euer Human Connection Team</strong></p></div>"
|
"text": "<div ><ol><li><strong>UNFALLGEFAHR: </strong>Das ist eine Testversion! Alle Daten, Dein Profil und die Server können jederzeit komplett vernichtet, verloren, verbrannt und vielleicht auch in der Nähe von Alpha Centauri synchronisiert werden. Die Benutzung läuft auf eigene Gefahr. Mit kommerziellen Nebenwirkungen ist jedoch nicht zu rechnen.</li><br><li><strong>DU UND DEINE DATEN: </strong>Bitte beachte, dass wir die Inhalte der Alphaversion zu Werbezwecken, Webpräsentationen usw. verwenden, aber wir glauben, dass das auch in Deinem Interesse ist. Am besten keinen Nachnamen eingeben und bei noch mehr Datensparsamkeit ein Profilfoto ohne Identität verwenden. Mehr in unserer <a href='/pages/privacy' target='_blank'>Datenschutzerklärung</a>.</li><br><li><strong>BAUSTELLEN: </strong>Das ist immer noch eine Testversion. Wenn etwas nicht funktioniert, blockiert, irritiert, falsch angezeigt, verbogen oder nicht anklickbar ist, bitten wir dies zu entschuldigen. Fehler, Käfer und Bugs bitte melden! <a href='http://localhost:3000/%22https://human-connection.org/alpha/#bugreport%5C%22' target='_blank'>https://human-connection.org/support</a></li><br><li><strong>VERHALTENSCODEX</strong>: Die Verhaltensregeln dienen als Leitsätze für den persönlichen Auftritt und den Umgang untereinander. Wer als Nutzer im Human Connection Netzwerk aktiv ist, Beiträge verfasst, kommentiert oder mit anderen Nutzern, auch außerhalb des Netzwerkes, Kontakt aufnimmt, erkennt diese Verhaltensregeln als verbindlich an: <a href='https://alpha.human-connection.org/pages/code-of-conduct' target='_blank'>https://alpha.human-connection.org/pages/code-of-conduct</a></li><br><li><strong>MODERATION: </strong>Solange kein Community-Moderationssystem lauffähig ist, entscheidet ein Regenbogen-Einhorn darüber, ob Du körperlich und psychisch stabil genug bist, unsere Testversion zu bedienen. Das Einhorn kann Dich jederzeit von der Alpha entfernen. Also sei nett und lass Regenbogenfutter da!</li><br><li><strong>FAIRNESS: </strong>Sollte Dir die Alphaversion unseres Netzwerks wider Erwarten, egal aus welchen Gründen, nicht gefallen, überweisen wir Dir Deine gespendeten Monatsbeiträge innerhalb der ersten 2 Monate gerne zurück. Einfach Mail an: <a href='mailto:info@human-connection.org' target='_blank'>info@human-connection.org </a><strong>Achtung: Viele Funktionen werden erst nach und nach eingebaut. </strong></li><br><li><strong>FRAGEN?</strong> Die Termine und Links zu den Zoom-Räumen findest Du hier: <a href='http://localhost:3000/%22https://human-connection.org/events-und-news//%22' target='_blank'>https://human-connection.org/veranstaltungen/</a></li><br><li><strong>VON MENSCHEN FÜR MENSCHEN: </strong>Bitte hilf uns weitere monatlichen Spender für Human Connection zu bekommen, damit das Netzwerk so schnell wie möglich offiziell an den Start gehen kann. <a href='http://localhost:3000/%22https://human-connection.org/alpha/#bugreport%5C%22' target='_blank'>https://human-connection.org</a></li></ol><p>Jetzt aber viel Spaß mit der Alpha von Human Connection! Für den ersten Weltfrieden. ♥︎</p><br><p><strong>Herzlichst,</strong></p><p><strong>Euer Human Connection Team</strong></p></div>"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,10 @@
|
|||||||
"hashtag-search": "Searching for #{hashtag}",
|
"hashtag-search": "Searching for #{hashtag}",
|
||||||
"clearSearch": "Clear search"
|
"clearSearch": "Clear search"
|
||||||
},
|
},
|
||||||
|
"filter-posts": {
|
||||||
|
"header": "Categories of Content",
|
||||||
|
"all": "All"
|
||||||
|
},
|
||||||
"site": {
|
"site": {
|
||||||
"made": "Made with ❤",
|
"made": "Made with ❤",
|
||||||
"imprint": "Imprint",
|
"imprint": "Imprint",
|
||||||
@ -14,8 +18,8 @@
|
|||||||
"tribunal": "Registry court",
|
"tribunal": "Registry court",
|
||||||
"register": "Registry number",
|
"register": "Registry number",
|
||||||
"director": "Managing Director",
|
"director": "Managing Director",
|
||||||
"taxident": "Value added tax identification number according to § 27 a Value Added Tax Act (Germany)",
|
"taxident": "USt-ID. according to §27a of the German Sales Tax Law:",
|
||||||
"responsible": "Responsible according to § 55 Abs. 2 RStV (Germany) ",
|
"responsible": "responsible for contents of this page (§ 55 Abs. 2 RStV)",
|
||||||
"bank": "bank account",
|
"bank": "bank account",
|
||||||
"germany": "Germany"
|
"germany": "Germany"
|
||||||
},
|
},
|
||||||
@ -50,8 +54,8 @@
|
|||||||
"email-exists": "There is already a user account with this email address!",
|
"email-exists": "There is already a user account with this email address!",
|
||||||
"invalid-invitation-token": "It looks like as if the invitation has been used already. Invitation links can only be used once."
|
"invalid-invitation-token": "It looks like as if the invitation has been used already. Invitation links can only be used once."
|
||||||
},
|
},
|
||||||
"submit": "Create an account",
|
"submit": "Create an account",
|
||||||
"success": "A mail with a link to complete your registration has been sent to <b>{email}</b>"
|
"success": "A mail with a link to complete your registration has been sent to <b>{email}</b>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"create-user-account": {
|
"create-user-account": {
|
||||||
@ -189,7 +193,19 @@
|
|||||||
"name": "Organizations"
|
"name": "Organizations"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"name": "Users"
|
"name": "Users",
|
||||||
|
"form": {
|
||||||
|
"placeholder": "E-Mail, name or description"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"columns": {
|
||||||
|
"name": "Name",
|
||||||
|
"slug": "Slug",
|
||||||
|
"role": "Role",
|
||||||
|
"createdAt": "Created at"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"empty": "No users found"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"name": "Pages"
|
"name": "Pages"
|
||||||
@ -230,7 +246,8 @@
|
|||||||
},
|
},
|
||||||
"comment": {
|
"comment": {
|
||||||
"submit": "Comment",
|
"submit": "Comment",
|
||||||
"submitted": "Comment Submitted"
|
"submitted": "Comment Submitted",
|
||||||
|
"updated": "Changes Saved"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"comment": {
|
"comment": {
|
||||||
@ -245,7 +262,6 @@
|
|||||||
"more": "show more",
|
"more": "show more",
|
||||||
"less": "show less"
|
"less": "show less"
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
"quotes": {
|
"quotes": {
|
||||||
"african": {
|
"african": {
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"filter-menu": {
|
||||||
|
"title": "Twoja bańka filtrująca"
|
||||||
|
},
|
||||||
"site": {
|
"site": {
|
||||||
"made": "Z ❤ zrobiony",
|
"made": "Z ❤ zrobiony",
|
||||||
"imprint": "Nadruk",
|
"imprint": "Nadruk",
|
||||||
@ -13,6 +16,7 @@
|
|||||||
"responsible": "Odpowiedzialny zgodnie z § 55 Abs. 2 RStV (Niemcy)",
|
"responsible": "Odpowiedzialny zgodnie z § 55 Abs. 2 RStV (Niemcy)",
|
||||||
"bank": "rachunek bankowy",
|
"bank": "rachunek bankowy",
|
||||||
"germany": "Niemcy"
|
"germany": "Niemcy"
|
||||||
|
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"copy": "Jeśli masz już konto Human Connection, zaloguj się tutaj.",
|
"copy": "Jeśli masz już konto Human Connection, zaloguj się tutaj.",
|
||||||
@ -20,9 +24,35 @@
|
|||||||
"logout": "Wyloguj się",
|
"logout": "Wyloguj się",
|
||||||
"email": "Twój adres e-mail",
|
"email": "Twój adres e-mail",
|
||||||
"password": "Twoje hasło",
|
"password": "Twoje hasło",
|
||||||
|
"forgotPassword": "Zapomniałeś hasła?",
|
||||||
"moreInfo": "Co to jest Human Connection?",
|
"moreInfo": "Co to jest Human Connection?",
|
||||||
|
"moreInfoURL": "https://human-connection.org/pl/",
|
||||||
|
"moreInfoHint": "na stronę prezentacji",
|
||||||
"hello": "Cześć"
|
"hello": "Cześć"
|
||||||
},
|
},
|
||||||
|
"password-reset": {
|
||||||
|
"title": "Zresetuj hasło",
|
||||||
|
"form": {
|
||||||
|
"description": "Na podany adres e-mail zostanie wysłany email z resetem hasła.",
|
||||||
|
"submit": "Poproś o wiadomość e-mail",
|
||||||
|
"submitted": "Na adres <b>{email}</b> została wysłana wiadomość z dalszymi instrukcjami."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"verify-code": {
|
||||||
|
"form": {
|
||||||
|
"code": "Wprowadź swój kod",
|
||||||
|
"description": "Otwórz swoją skrzynkę odbiorczą i wpisz kod, który do Ciebie wysłaliśmy.",
|
||||||
|
"next": "Kontynuuj",
|
||||||
|
"change-password": {
|
||||||
|
"success": "Zmiana hasła zakończyła się sukcesem!",
|
||||||
|
"error": "Zmiana hasła nie powiodła się. Może kod bezpieczeństwa nie był poprawny?",
|
||||||
|
"help": "W przypadku problemów, zachęcamy do zwrócenia się o pomoc, wysyłając do nas wiadomość e-mail:"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editor": {
|
||||||
|
"placeholder": "Zostaw swoje inspirujące myśli...."
|
||||||
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"name": "Mój profil",
|
"name": "Mój profil",
|
||||||
"memberSince": "Członek od",
|
"memberSince": "Członek od",
|
||||||
@ -31,7 +61,27 @@
|
|||||||
"following": "Obserwowani",
|
"following": "Obserwowani",
|
||||||
"shouted": "Krzyknij",
|
"shouted": "Krzyknij",
|
||||||
"commented": "Skomentuj",
|
"commented": "Skomentuj",
|
||||||
"userAnonym": "Anonymous"
|
"userAnonym": "Anonimowy",
|
||||||
|
"socialMedia": "Gdzie indziej mogę znaleźć",
|
||||||
|
"network": {
|
||||||
|
"title": "Sieć",
|
||||||
|
"following": "jest następująca:",
|
||||||
|
"followingNobody": "nie podąża za nikim.",
|
||||||
|
"followedBy": "po którym następuje:",
|
||||||
|
"followedByNobody": "nie jest śledzona przez nikogo.",
|
||||||
|
"and": "i",
|
||||||
|
"more": "więcej"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"menu": {
|
||||||
|
"mentioned": "wspomniała o tobie na posterunku."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Wyszukiwanie",
|
||||||
|
"hint": "Czego szukasz?",
|
||||||
|
"failed": "Nic nie znaleziono"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"name": "Ustawienia",
|
"name": "Ustawienia",
|
||||||
@ -40,10 +90,28 @@
|
|||||||
"labelName": "Twoje dane",
|
"labelName": "Twoje dane",
|
||||||
"namePlaceholder": "Anonymous",
|
"namePlaceholder": "Anonymous",
|
||||||
"labelCity": "Twoje miasto lub region",
|
"labelCity": "Twoje miasto lub region",
|
||||||
"labelBio": "O Tobie"
|
"labelBio": "O Tobie",
|
||||||
|
"success": "Twoje dane zostały pomyślnie zaktualizowane!"
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"name": "Bezpieczeństwo"
|
"name": "Bezpieczeństwo",
|
||||||
|
"change-password": {
|
||||||
|
"button": "Zmień hasło",
|
||||||
|
"success": "Hasło zostało pomyślnie zmienione!",
|
||||||
|
"label-old-password": "Twoje stare hasło",
|
||||||
|
"label-new-password": "Twoje nowe hasło",
|
||||||
|
"label-new-password-confirm": "Potwierdź nowe hasło",
|
||||||
|
"message-old-password-required": "Wprowadź swoje stare hasło",
|
||||||
|
"message-new-password-required": "Wprowadź nowe hasło",
|
||||||
|
"message-new-password-confirm-required": "Potwierdź nowe hasło.",
|
||||||
|
"message-new-password-missmatch": "Wpisz ponownie to samo hasło.",
|
||||||
|
"passwordSecurity": "Zabezpieczenie hasłem",
|
||||||
|
"passwordStrength0": "Bardzo niepewne hasło",
|
||||||
|
"passwordStrength1": "Niepewne hasło",
|
||||||
|
"passwordStrength2": "Hasło pośredniczące",
|
||||||
|
"passwordStrength3": "Silne hasło",
|
||||||
|
"passwordStrength4": "Bardzo mocne hasło"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"invites": {
|
"invites": {
|
||||||
"name": "Zaproszenia"
|
"name": "Zaproszenia"
|
||||||
@ -51,29 +119,42 @@
|
|||||||
"download": {
|
"download": {
|
||||||
"name": "Pobierz dane"
|
"name": "Pobierz dane"
|
||||||
},
|
},
|
||||||
"delete": {
|
"deleteUserAccount": {
|
||||||
"name": "Usuń konto"
|
"name": "Usuwanie danych",
|
||||||
|
"contributionsCount": "Usuń moje stanowiska.",
|
||||||
|
"commentsCount": "Usuń moje komentarze {liczba}.",
|
||||||
|
"accountDescription": "Bądź świadomy, że Twój post i komentarze są ważne dla naszej społeczności. Jeśli nadal chcesz je usunąć, musisz zaznaczyć je poniżej.",
|
||||||
|
"accountWarning": "<b>Nie możesz zarządzać</b> i <b>Nie możesz REKOVER</b> swoje konto, posty lub komentarze po usunięciu konta!",
|
||||||
|
"success": "Konto zostało pomyślnie usunięte",
|
||||||
|
"pleaseConfirm": "<b class='is-danger'>Niszczycielskie działanie!</b> Typ <b>{potwierdź}</b> aby potwierdzić"
|
||||||
},
|
},
|
||||||
"organizations": {
|
"organizations": {
|
||||||
"name": "Moje organizacje"
|
"name": "My Organizations"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"name": "Języki"
|
"name": "Languages"
|
||||||
|
},
|
||||||
|
"social-media": {
|
||||||
|
"name": "Social media",
|
||||||
|
"placeholder": "Add social media url",
|
||||||
|
"submit": "Add link",
|
||||||
|
"successAdd": "Added social media. Updated user profile!",
|
||||||
|
"successDelete": "Deleted social media. Updated user profile!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"name": "Administrator",
|
"name": "Admin",
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"name": "Tablica rozdzielcza",
|
"name": "Tablica rozdzielcza",
|
||||||
"users": "Użytkownicy",
|
"users": "Użytkownicy",
|
||||||
"posts": "Posty",
|
"posts": "Stanowiska",
|
||||||
"comments": "Komentarze",
|
"comments": "Komentarze",
|
||||||
"notifications": "Powiadomienia",
|
"notifications": "Powiadomienia",
|
||||||
"organizations": "Organizacje",
|
"organizations": "Organizacje",
|
||||||
"projects": "Projekty",
|
"projects": "Projekty",
|
||||||
"invites": "Zaproszenia",
|
"invites": "Zaprasza",
|
||||||
"follows": "Obserwowań",
|
"follows": "Podąża za",
|
||||||
"shouts": "Okrzyk"
|
"shouts": "Zalecane"
|
||||||
},
|
},
|
||||||
"organizations": {
|
"organizations": {
|
||||||
"name": "Organizacje"
|
"name": "Organizacje"
|
||||||
@ -90,116 +171,193 @@
|
|||||||
"categories": {
|
"categories": {
|
||||||
"name": "Kategorie",
|
"name": "Kategorie",
|
||||||
"categoryName": "Nazwa",
|
"categoryName": "Nazwa",
|
||||||
"postCount": "Posty"
|
"postCount": "Stanowiska"
|
||||||
},
|
},
|
||||||
"tags": {
|
"tags": {
|
||||||
"name": "Tagi",
|
"name": "Znaczniki",
|
||||||
"tagCountUnique": "Użytkownicy",
|
"tagCountUnique": "Użytkownicy",
|
||||||
"tagCount": "Posty"
|
"tagCount": "Stanowiska"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"name": "Ustawienia"
|
"name": "Ustawienia"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"name": "Post",
|
"name": "Poczta",
|
||||||
"moreInfo": {
|
"moreInfo": {
|
||||||
"name": "Więcej informacji"
|
"name": "Więcej informacji"
|
||||||
},
|
},
|
||||||
"takeAction": {
|
"takeAction": {
|
||||||
"name": "Podejmij działanie"
|
"name": "Podejmij działania"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"edit": "Edytuj Post",
|
||||||
|
"delete": "Usuń wpis"
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"submit": "Komentarz",
|
||||||
|
"submitted": "Przedłożony komentarz"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"comment": {
|
||||||
|
"content": {
|
||||||
|
"unavailable-placeholder": " ...ten komentarz nie jest już dostępny."
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"edit": "Edytuj komentarz",
|
||||||
|
"delete": "Usuń komentarz"
|
||||||
|
},
|
||||||
|
"show": {
|
||||||
|
"more": "Pokaż więcej",
|
||||||
|
"less": "Pokaż mniej"
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
"quotes": {
|
"quotes": {
|
||||||
"african": {
|
"african": {
|
||||||
"quote": "Wielu małych ludzi w wielu małych miejscach robi wiele małych rzeczy, które mogą zmienić oblicze świata.",
|
"quote": "Wielu małych ludzi w wielu małych miejscowościach robi wiele małych rzeczy, które mogą zmienić oblicze świata.",
|
||||||
"author": "Afrykańskie przysłowie"
|
"author": "Afrykańskie przysłowie"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"post": "Post ::: Posty",
|
"post": "Poczta ::: Posty",
|
||||||
"comment": "Komentarz ::: Komentarze",
|
"comment": "Komentarz ::: Komentarze",
|
||||||
"letsTalk": "Porozmawiajmy",
|
"letsTalk": "Porozmawiajmy",
|
||||||
"versus": "Versus",
|
"versus": "werset",
|
||||||
"moreInfo": "Więcej informacji",
|
"moreInfo": "Więcej informacji",
|
||||||
"takeAction": "Podejmij działanie",
|
"takeAction": "Podejmij działania",
|
||||||
"shout": "okrzyk okrzyki",
|
"shout": "przekazanie sprawy ::: Polecam tę stronę",
|
||||||
"user": "Użytkownik ::: Użytkownicy",
|
"user": "Użytkownik ::: Użytkownicy",
|
||||||
"category": "kategoria kategorie",
|
"category": "Kategoria ::: Kategorie",
|
||||||
"organization": "Organizacja ::: Organizacje",
|
"organization": "Organization ::: Organizations",
|
||||||
"project": "Projekt ::: Projekty",
|
"project": "Projekt ::: Projekty",
|
||||||
"tag": "Tag ::: Tagi",
|
"tag": "Znacznik ::: Znaczniki",
|
||||||
"name": "imię",
|
"name": "Nazwa",
|
||||||
"loadMore": "załaduj więcej",
|
"loadMore": "Obciążenie więcej",
|
||||||
"loading": "ładowanie",
|
"loading": "załadunek",
|
||||||
"reportContent": "Raport"
|
"reportContent": "Sprawozdanie",
|
||||||
|
"validations": {
|
||||||
|
"email": "musi być ważny adres e-mail.",
|
||||||
|
"verification-code": "musi mieć długość 6 znaków."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"loading": "ładowanie",
|
"loading": "załadunek",
|
||||||
"loadMore": "załaduj więcej",
|
"loadMore": "Obciążenie więcej",
|
||||||
"create": "Stwórz",
|
"create": "Tworzenie",
|
||||||
"save": "Zapisz",
|
"save": "Oszczędzaj",
|
||||||
"edit": "Edytuj",
|
"edit": "Edycja",
|
||||||
"delete": "Usuń",
|
"delete": "Usuń",
|
||||||
"cancel": "Anuluj"
|
"cancel": "Odwołaj"
|
||||||
},
|
},
|
||||||
"moderation": {
|
"moderation": {
|
||||||
"name": "Moderacja",
|
"name": "Umiarkowanie",
|
||||||
"reports": {
|
"reports": {
|
||||||
"empty": "Gratulacje, moderacja nie jest potrzebna",
|
"empty": "Gratulacje, nic do umiarkowanego.",
|
||||||
"name": "Raporty",
|
"name": "Sprawozdania",
|
||||||
"reporter": "zgłoszone przez"
|
"submitter": "zgłaszane przez",
|
||||||
|
"disabledBy": "niepełnosprawni przez"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"disable": {
|
"disable": {
|
||||||
|
"submit": "Niepełnosprawność",
|
||||||
|
"cancel": "Odwołaj",
|
||||||
|
"success": "Niepełnosprawni skutecznie",
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Ukryj użytkownika",
|
"title": "Wyłączenie użytkownika",
|
||||||
"type": "Użytkownik",
|
"type": "Użytkownik",
|
||||||
"message": "Czy na pewno chcesz wyłączyć użytkownika \" <b> {name} </b> \"?"
|
"message": "Czy naprawdę chcesz wyłączyć użytkownika \"<b>{name}</b>\"?"
|
||||||
},
|
},
|
||||||
"contribution": {
|
"contribution": {
|
||||||
"title": "Ukryj wpis",
|
"title": "Wyłącz Wkład",
|
||||||
"type": "Wpis / Post",
|
"type": "Wkład",
|
||||||
"message": "Czy na pewno chcesz ukryć wpis \" <b> tytuł} </b> \"?"
|
"message": "Naprawdę chcesz unieszkodliwić ten wkład \"<b>{name}</b>\"?"
|
||||||
},
|
},
|
||||||
"comment": {
|
"comment": {
|
||||||
"title": "Ukryj wpis",
|
"title": "Wyłącz komentarz",
|
||||||
"type": "Komentarz",
|
"type": "Komentarz",
|
||||||
"message": "Czy na pewno chcesz ukryć komentarz użytkownika\"<b>(Imie/Avatar</b>\"?"
|
"message": "Naprawdę chcesz wyłączyć komentarz \"<b>{name}</b>\"?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"submit": "Usuń",
|
||||||
|
"cancel": "Odwołaj",
|
||||||
|
"contribution": {
|
||||||
|
"title": "Usuń Post",
|
||||||
|
"type": "Wkład",
|
||||||
|
"message": "Naprawdę chcesz usunąć post \"<b>{name}</b>\"?",
|
||||||
|
"success": "Wyślij pomyślnie usunięty!"
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"title": "Usuń komentarz",
|
||||||
|
"type": "Komentarz",
|
||||||
|
"message": "Czy naprawdę chcesz usunąć komentarz \"<b>{name}</b>\"?",
|
||||||
|
"success": "Komentarz został pomyślnie usunięty!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"report": {
|
"report": {
|
||||||
"submit": "Wyślij raport",
|
"submit": "Sprawozdanie",
|
||||||
"cancel": "Anuluj",
|
"cancel": "Odwołaj",
|
||||||
|
"success": "Dzięki za zgłoszenie!",
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Zgłoś użytkownika",
|
"title": "Raport Użytkownik",
|
||||||
"type": "Użytkownik",
|
"type": "Użytkownik",
|
||||||
"message": "Czy na pewno chcesz zgłosić użytkownika \" <b> {Imie} </b> \"?"
|
"message": "Naprawdę chcesz zgłosić użytkownika \"<b>{name}</b>\"?",
|
||||||
|
"error": "Zgłosiłeś już użytkownika!"
|
||||||
},
|
},
|
||||||
"contribution": {
|
"contribution": {
|
||||||
"title": "Zgłoś wpis",
|
"title": "Wkład w raport",
|
||||||
"type": "Wpis / Post",
|
"type": "Wkład",
|
||||||
"message": "Czy na pewno chcesz zgłosić ten wpis użytkownika \" <b> {Imie} </b> \"?"
|
"message": "Naprawdę chcesz zgłosić wkład, jaki wniosłaś do programu \"<b>{name}</b>\"?",
|
||||||
|
"error": "Zgłosiłeś już ten wkład!"
|
||||||
},
|
},
|
||||||
"comment": {
|
"comment": {
|
||||||
"title": "Zgłoś komentarz",
|
"title": "Sprawozdanie Komentarz",
|
||||||
"type": "Komentarz",
|
"type": "Komentarz",
|
||||||
"message": "Czy na pewno chcesz zgłosić komentarz użytkownika\"<b>(Imie/Avatar</b>\"?"
|
"message": "Naprawdę chcesz zgłosić komentarz od \"<b>{name}</b>\"?",
|
||||||
|
"error": "Zgłosiłeś już komentarz!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"followButton": {
|
||||||
|
"follow": "naśladować",
|
||||||
|
"following": "w skutek"
|
||||||
|
},
|
||||||
|
"shoutButton": {
|
||||||
|
"shouted": "wykrzyczany"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"submit": "Zwolnienie",
|
||||||
|
"cancel": "Odwołaj",
|
||||||
|
"success": "Wydany z powodzeniem!",
|
||||||
|
"user": {
|
||||||
|
"title": "Zwolnienie użytkownika",
|
||||||
|
"type": "Użytkownik",
|
||||||
|
"message": "Naprawdę chcesz uwolnić użytkownika \"<b>{name}</b>\"?"
|
||||||
|
},
|
||||||
|
"contribution": {
|
||||||
|
"title": "Zwolnienie Wkład",
|
||||||
|
"type": "Wkład",
|
||||||
|
"message": "Naprawdę chcesz uwolnić swój wkład \"<b>{name}</b>\"?"
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"title": "Zwolnienie komentarz",
|
||||||
|
"type": "komentarz",
|
||||||
|
"message": "Czy naprawdę chcesz opublikować komentarz od \"<b>{name}</b>\"?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"avatar": {
|
||||||
|
"submitted": "Przesłanie udane"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contribution": {
|
"contribution": {
|
||||||
"edit": "Edytuj wpis",
|
"newPost": "Utwórz nowy post",
|
||||||
"delete": "Usuń wpis"
|
"filterFollow": "Filtrowanie wkładu użytkowników, za którymi podążam",
|
||||||
},
|
"filterALL": "Wyświetl wszystkie wkłady",
|
||||||
"comment": {
|
"success": "Zachowany!",
|
||||||
"edit": "Edytuj komentarz",
|
"languageSelectLabel": "Język",
|
||||||
"delete": "Usuń komentarz"
|
"categories": {
|
||||||
},
|
"infoSelectedNoOfMaxCategories": "{chosen} z {max} wybrane kategorie"
|
||||||
"followButton": {
|
}
|
||||||
"follow": "Obserwuj",
|
|
||||||
"following": "Obserwowani"
|
|
||||||
},
|
|
||||||
"shoutButton": {
|
|
||||||
"shouted": "krzyczeć"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,10 @@ const styleguideStyles = process.env.STYLEGUIDE_DEV
|
|||||||
]
|
]
|
||||||
: '@human-connection/styleguide/dist/shared.scss'
|
: '@human-connection/styleguide/dist/shared.scss'
|
||||||
|
|
||||||
|
const buildDir = process.env.NUXT_BUILD || '.nuxt'
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
buildDir,
|
||||||
mode: 'universal',
|
mode: 'universal',
|
||||||
|
|
||||||
dev: dev,
|
dev: dev,
|
||||||
|
|||||||
@ -29,7 +29,6 @@
|
|||||||
"!**/?(*.)+(spec|test).js?(x)"
|
"!**/?(*.)+(spec|test).js?(x)"
|
||||||
],
|
],
|
||||||
"coverageReporters": [
|
"coverageReporters": [
|
||||||
"text",
|
|
||||||
"lcov"
|
"lcov"
|
||||||
],
|
],
|
||||||
"transform": {
|
"transform": {
|
||||||
@ -60,11 +59,12 @@
|
|||||||
"apollo-client": "~2.6.3",
|
"apollo-client": "~2.6.3",
|
||||||
"cookie-universal-nuxt": "~2.0.16",
|
"cookie-universal-nuxt": "~2.0.16",
|
||||||
"cross-env": "~5.2.0",
|
"cross-env": "~5.2.0",
|
||||||
"date-fns": "2.0.0-beta.2",
|
"date-fns": "2.0.0-beta.3",
|
||||||
"express": "~4.17.1",
|
"express": "~4.17.1",
|
||||||
"graphql": "~14.4.2",
|
"graphql": "~14.4.2",
|
||||||
|
"isemail": "^3.2.0",
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jsonwebtoken": "~8.5.1",
|
||||||
"linkify-it": "~2.1.0",
|
"linkify-it": "~2.2.0",
|
||||||
"nuxt": "~2.8.1",
|
"nuxt": "~2.8.1",
|
||||||
"nuxt-dropzone": "^1.0.2",
|
"nuxt-dropzone": "^1.0.2",
|
||||||
"nuxt-env": "~0.1.0",
|
"nuxt-env": "~0.1.0",
|
||||||
@ -80,11 +80,11 @@
|
|||||||
"zxcvbn": "^4.4.2"
|
"zxcvbn": "^4.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "~7.5.4",
|
"@babel/core": "~7.5.5",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||||
"@babel/preset-env": "~7.5.4",
|
"@babel/preset-env": "~7.5.5",
|
||||||
"@vue/cli-shared-utils": "~3.9.0",
|
"@vue/cli-shared-utils": "~3.9.0",
|
||||||
"@vue/eslint-config-prettier": "~4.0.1",
|
"@vue/eslint-config-prettier": "~5.0.0",
|
||||||
"@vue/server-test-utils": "~1.0.0-beta.29",
|
"@vue/server-test-utils": "~1.0.0-beta.29",
|
||||||
"@vue/test-utils": "~1.0.0-beta.29",
|
"@vue/test-utils": "~1.0.0-beta.29",
|
||||||
"babel-core": "~7.0.0-bridge.0",
|
"babel-core": "~7.0.0-bridge.0",
|
||||||
@ -94,8 +94,8 @@
|
|||||||
"eslint-config-prettier": "~6.0.0",
|
"eslint-config-prettier": "~6.0.0",
|
||||||
"eslint-config-standard": "~12.0.0",
|
"eslint-config-standard": "~12.0.0",
|
||||||
"eslint-loader": "~2.2.1",
|
"eslint-loader": "~2.2.1",
|
||||||
"eslint-plugin-import": "~2.18.0",
|
"eslint-plugin-import": "~2.18.2",
|
||||||
"eslint-plugin-jest": "~22.7.2",
|
"eslint-plugin-jest": "~22.13.6",
|
||||||
"eslint-plugin-node": "~9.1.0",
|
"eslint-plugin-node": "~9.1.0",
|
||||||
"eslint-plugin-prettier": "~3.1.0",
|
"eslint-plugin-prettier": "~3.1.0",
|
||||||
"eslint-plugin-promise": "~4.2.1",
|
"eslint-plugin-promise": "~4.2.1",
|
||||||
|
|||||||
@ -24,11 +24,10 @@ export default {
|
|||||||
name: this.$t('admin.dashboard.name'),
|
name: this.$t('admin.dashboard.name'),
|
||||||
path: `/admin`,
|
path: `/admin`,
|
||||||
},
|
},
|
||||||
// TODO implement
|
{
|
||||||
/* {
|
|
||||||
name: this.$t('admin.users.name'),
|
name: this.$t('admin.users.name'),
|
||||||
path: `/admin/users`
|
path: `/admin/users`,
|
||||||
}, */
|
},
|
||||||
// TODO implement
|
// TODO implement
|
||||||
/* {
|
/* {
|
||||||
name: this.$t('admin.organizations.name'),
|
name: this.$t('admin.organizations.name'),
|
||||||
|
|||||||
70
webapp/pages/admin/users.spec.js
Normal file
70
webapp/pages/admin/users.spec.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { mount, createLocalVue } from '@vue/test-utils'
|
||||||
|
import Users from './users.vue'
|
||||||
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
|
localVue.use(Styleguide)
|
||||||
|
|
||||||
|
describe('Users', () => {
|
||||||
|
let wrapper
|
||||||
|
let Wrapper
|
||||||
|
let mocks
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks = {
|
||||||
|
$t: jest.fn(),
|
||||||
|
$apollo: {
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
Wrapper = () => {
|
||||||
|
return mount(Users, {
|
||||||
|
mocks,
|
||||||
|
localVue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it('renders', () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
expect(wrapper.is('div')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('search', () => {
|
||||||
|
let searchAction
|
||||||
|
beforeEach(() => {
|
||||||
|
searchAction = (wrapper, { query }) => {
|
||||||
|
wrapper.find('input').setValue(query)
|
||||||
|
wrapper.find('form').trigger('submit')
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('query looks like an email address', () => {
|
||||||
|
it('searches users for exact email address', async () => {
|
||||||
|
const wrapper = await searchAction(Wrapper(), { query: 'email@example.org' })
|
||||||
|
expect(wrapper.vm.email).toEqual('email@example.org')
|
||||||
|
expect(wrapper.vm.filter).toBe(null)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('query is just text', () => {
|
||||||
|
it('tries to find matching users by `name`, `slug` or `about`', async () => {
|
||||||
|
const wrapper = await searchAction(await Wrapper(), { query: 'Find me' })
|
||||||
|
const expected = {
|
||||||
|
OR: [
|
||||||
|
{ name_contains: 'Find me' },
|
||||||
|
{ slug_contains: 'Find me' },
|
||||||
|
{ about_contains: 'Find me' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
expect(wrapper.vm.email).toBe(null)
|
||||||
|
expect(wrapper.vm.filter).toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,15 +1,173 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-card :header="$t('admin.users.name')">
|
<div>
|
||||||
<hc-empty icon="tasks" message="Coming Soon…" />
|
<ds-space>
|
||||||
</ds-card>
|
<ds-card :header="$t('admin.users.name')">
|
||||||
|
<ds-form v-model="form" @submit="submit">
|
||||||
|
<ds-flex gutter="small">
|
||||||
|
<ds-flex-item width="90%">
|
||||||
|
<ds-input
|
||||||
|
model="query"
|
||||||
|
:placeholder="$t('admin.users.form.placeholder')"
|
||||||
|
icon="search"
|
||||||
|
/>
|
||||||
|
</ds-flex-item>
|
||||||
|
<ds-flex-item width="30px">
|
||||||
|
<ds-button primary type="submit" icon="search" :loading="$apollo.loading" />
|
||||||
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
|
</ds-form>
|
||||||
|
</ds-card>
|
||||||
|
</ds-space>
|
||||||
|
<ds-card v-if="User && User.length">
|
||||||
|
<ds-table :data="User" :fields="fields" condensed>
|
||||||
|
<template slot="index" slot-scope="scope">
|
||||||
|
{{ scope.row.index }}.
|
||||||
|
</template>
|
||||||
|
<template slot="name" slot-scope="scope">
|
||||||
|
<nuxt-link
|
||||||
|
:to="{
|
||||||
|
name: 'profile-id-slug',
|
||||||
|
params: { id: scope.row.id, slug: scope.row.slug },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<b>{{ scope.row.name | truncate(20) }}</b>
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
<template slot="slug" slot-scope="scope">
|
||||||
|
<nuxt-link
|
||||||
|
:to="{
|
||||||
|
name: 'profile-id-slug',
|
||||||
|
params: { id: scope.row.id, slug: scope.row.slug },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<b>{{ scope.row.slug | truncate(20) }}</b>
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
<template slot="createdAt" slot-scope="scope">
|
||||||
|
{{ scope.row.createdAt | dateTime }}
|
||||||
|
</template>
|
||||||
|
</ds-table>
|
||||||
|
<ds-flex direction="row-reverse">
|
||||||
|
<ds-flex-item width="50px">
|
||||||
|
<ds-button @click="next" :disabled="!hasNext" icon="arrow-right" primary />
|
||||||
|
</ds-flex-item>
|
||||||
|
<ds-flex-item width="50px">
|
||||||
|
<ds-button @click="back" :disabled="!hasPrevious" icon="arrow-left" primary />
|
||||||
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
|
</ds-card>
|
||||||
|
<ds-card v-else>
|
||||||
|
<ds-placeholder>
|
||||||
|
{{ $t('admin.users.empty') }}
|
||||||
|
</ds-placeholder>
|
||||||
|
</ds-card>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import HcEmpty from '~/components/Empty.vue'
|
import gql from 'graphql-tag'
|
||||||
|
import isemail from 'isemail'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
data() {
|
||||||
HcEmpty,
|
const pageSize = 15
|
||||||
|
return {
|
||||||
|
offset: 0,
|
||||||
|
pageSize,
|
||||||
|
first: pageSize,
|
||||||
|
User: [],
|
||||||
|
hasNext: false,
|
||||||
|
email: null,
|
||||||
|
filter: null,
|
||||||
|
form: {
|
||||||
|
formData: {
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasPrevious() {
|
||||||
|
return this.offset > 0
|
||||||
|
},
|
||||||
|
fields() {
|
||||||
|
return {
|
||||||
|
index: '#',
|
||||||
|
name: this.$t('admin.users.table.columns.name'),
|
||||||
|
slug: this.$t('admin.users.table.columns.slug'),
|
||||||
|
createdAt: this.$t('admin.users.table.columns.createdAt'),
|
||||||
|
contributionsCount: {
|
||||||
|
label: '🖉',
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
commentedCount: {
|
||||||
|
label: '🗨',
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
shoutedCount: {
|
||||||
|
label: '❤',
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
label: this.$t('admin.users.table.columns.role'),
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
User: {
|
||||||
|
query() {
|
||||||
|
return gql(`
|
||||||
|
query($filter: _UserFilter, $first: Int, $offset: Int, $email: String) {
|
||||||
|
User(email: $email, filter: $filter, first: $first, offset: $offset, orderBy: createdAt_desc) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
role
|
||||||
|
createdAt
|
||||||
|
contributionsCount
|
||||||
|
commentedCount
|
||||||
|
shoutedCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
},
|
||||||
|
variables() {
|
||||||
|
const { offset, first, email, filter } = this
|
||||||
|
const variables = { first, offset }
|
||||||
|
if (email) variables.email = email
|
||||||
|
if (filter) variables.filter = filter
|
||||||
|
return variables
|
||||||
|
},
|
||||||
|
update({ User }) {
|
||||||
|
if (!User) return []
|
||||||
|
this.hasNext = User.length >= this.pageSize
|
||||||
|
if (User.length <= 0 && this.offset > 0) return this.User // edge case, avoid a blank page
|
||||||
|
return User.map((u, i) => Object.assign({}, u, { index: this.offset + i }))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
back() {
|
||||||
|
this.offset = Math.max(this.offset - this.pageSize, 0)
|
||||||
|
},
|
||||||
|
next() {
|
||||||
|
this.offset += this.pageSize
|
||||||
|
},
|
||||||
|
submit(formData) {
|
||||||
|
this.offset = 0
|
||||||
|
const { query } = formData
|
||||||
|
if (isemail.validate(query)) {
|
||||||
|
this.email = query
|
||||||
|
this.filter = null
|
||||||
|
} else {
|
||||||
|
this.email = null
|
||||||
|
this.filter = {
|
||||||
|
OR: [{ name_contains: query }, { slug_contains: query }, { about_contains: query }],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
<hc-post-card
|
<hc-post-card
|
||||||
v-for="(post, index) in uniq(Post)"
|
v-for="(post, index) in posts"
|
||||||
:key="post.id"
|
:key="post.id"
|
||||||
:post="post"
|
:post="post"
|
||||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||||
@ -33,11 +33,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
|
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
|
||||||
import gql from 'graphql-tag'
|
|
||||||
import uniqBy from 'lodash/uniqBy'
|
import uniqBy from 'lodash/uniqBy'
|
||||||
import HcPostCard from '~/components/PostCard'
|
import HcPostCard from '~/components/PostCard'
|
||||||
import HcLoadMore from '~/components/LoadMore.vue'
|
import HcLoadMore from '~/components/LoadMore.vue'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
|
import { filterPosts } from '~/graphql/PostQuery.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -49,7 +49,6 @@ export default {
|
|||||||
const { hashtag = null } = this.$route.query
|
const { hashtag = null } = this.$route.query
|
||||||
return {
|
return {
|
||||||
// Initialize your apollo data
|
// Initialize your apollo data
|
||||||
Post: [],
|
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
filter: {},
|
filter: {},
|
||||||
@ -61,18 +60,27 @@ export default {
|
|||||||
this.changeFilterBubble({ tags_some: { name: this.hashtag } })
|
this.changeFilterBubble({ tags_some: { name: this.hashtag } })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
Post(post) {
|
||||||
|
this.setPosts(this.Post)
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentUser: 'auth/user',
|
currentUser: 'auth/user',
|
||||||
|
posts: 'posts/posts',
|
||||||
}),
|
}),
|
||||||
tags() {
|
tags() {
|
||||||
return this.Post ? this.Post[0].tags.map(tag => tag.name) : '-'
|
return this.posts ? this.posts.tags.map(tag => tag.name) : '-'
|
||||||
},
|
},
|
||||||
offset() {
|
offset() {
|
||||||
return (this.page - 1) * this.pageSize
|
return (this.page - 1) * this.pageSize
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapMutations({
|
||||||
|
setPosts: 'posts/SET_POSTS',
|
||||||
|
}),
|
||||||
changeFilterBubble(filter) {
|
changeFilterBubble(filter) {
|
||||||
if (this.hashtag) {
|
if (this.hashtag) {
|
||||||
filter = {
|
filter = {
|
||||||
@ -129,48 +137,7 @@ export default {
|
|||||||
apollo: {
|
apollo: {
|
||||||
Post: {
|
Post: {
|
||||||
query() {
|
query() {
|
||||||
return gql(`
|
return filterPosts(this.$i18n)
|
||||||
query Post($filter: _PostFilter, $first: Int, $offset: Int) {
|
|
||||||
Post(filter: $filter, first: $first, offset: $offset) {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
contentExcerpt
|
|
||||||
createdAt
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
slug
|
|
||||||
image
|
|
||||||
author {
|
|
||||||
id
|
|
||||||
avatar
|
|
||||||
slug
|
|
||||||
name
|
|
||||||
disabled
|
|
||||||
deleted
|
|
||||||
contributionsCount
|
|
||||||
shoutedCount
|
|
||||||
commentsCount
|
|
||||||
followedByCount
|
|
||||||
followedByCurrentUser
|
|
||||||
location {
|
|
||||||
name: name${this.$i18n.locale().toUpperCase()}
|
|
||||||
}
|
|
||||||
badges {
|
|
||||||
id
|
|
||||||
key
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
commentsCount
|
|
||||||
categories {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
shoutedCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
},
|
},
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -111,7 +111,6 @@ export default {
|
|||||||
}
|
}
|
||||||
badges {
|
badges {
|
||||||
id
|
id
|
||||||
key
|
|
||||||
icon
|
icon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -206,7 +206,7 @@
|
|||||||
:key="post.id"
|
:key="post.id"
|
||||||
:post="post"
|
:post="post"
|
||||||
:width="{ base: '100%', md: '100%', xl: '50%' }"
|
:width="{ base: '100%', md: '100%', xl: '50%' }"
|
||||||
@removePostFromList="activePosts.splice(index, 1)"
|
@removePostFromList="removePostFromList(index)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="$apollo.loading">
|
<template v-else-if="$apollo.loading">
|
||||||
@ -331,6 +331,10 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
removePostFromList(index) {
|
||||||
|
this.activePosts.splice(index, 1)
|
||||||
|
this.$apollo.queries.User.refetch()
|
||||||
|
},
|
||||||
handleTab(tab) {
|
handleTab(tab) {
|
||||||
this.tabActive = tab
|
this.tabActive = tab
|
||||||
this.Post = null
|
this.Post = null
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
export const state = () => {
|
export const state = () => {
|
||||||
return {
|
return {
|
||||||
placeholder: null,
|
placeholder: null,
|
||||||
|
editPending: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -8,10 +9,16 @@ export const getters = {
|
|||||||
placeholder(state) {
|
placeholder(state) {
|
||||||
return state.placeholder
|
return state.placeholder
|
||||||
},
|
},
|
||||||
|
editPending(state) {
|
||||||
|
return state.editPending
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
SET_PLACEHOLDER_TEXT(state, text) {
|
SET_PLACEHOLDER_TEXT(state, text) {
|
||||||
state.placeholder = text
|
state.placeholder = text
|
||||||
},
|
},
|
||||||
|
SET_EDIT_PENDING(state, boolean) {
|
||||||
|
state.editPending = boolean
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
76
webapp/store/posts.js
Normal file
76
webapp/store/posts.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const state = () => {
|
||||||
|
return {
|
||||||
|
posts: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
SET_POSTS(state, posts) {
|
||||||
|
state.posts = posts || null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
posts(state) {
|
||||||
|
return state.posts || []
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
async fetchPosts({ commit, dispatch }, { i18n, filter }) {
|
||||||
|
const client = this.app.apolloProvider.defaultClient
|
||||||
|
const {
|
||||||
|
data: { Post },
|
||||||
|
} = await client.query({
|
||||||
|
query: gql(`
|
||||||
|
query Post($filter: _PostFilter, $first: Int, $offset: Int) {
|
||||||
|
Post(filter: $filter, first: $first, offset: $offset) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
contentExcerpt
|
||||||
|
createdAt
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
slug
|
||||||
|
image
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
avatar
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
contributionsCount
|
||||||
|
shoutedCount
|
||||||
|
commentsCount
|
||||||
|
followedByCount
|
||||||
|
followedByCurrentUser
|
||||||
|
location {
|
||||||
|
name: name${i18n.locale().toUpperCase()}
|
||||||
|
}
|
||||||
|
badges {
|
||||||
|
id
|
||||||
|
icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commentsCount
|
||||||
|
categories {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
icon
|
||||||
|
}
|
||||||
|
shoutedCount
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
variables: {
|
||||||
|
filter,
|
||||||
|
first: 12,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
commit('SET_POSTS', Post)
|
||||||
|
return Post
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -1,5 +0,0 @@
|
|||||||
<!-- Generated by IcoMoon.io -->
|
|
||||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
|
||||||
<title>bold</title>
|
|
||||||
<path d="M16 7h-9v18h11c2.8 0 5-2.2 5-5 0-2.2-1.4-4-3.3-4.7 0.8-0.9 1.3-2 1.3-3.3 0-2.8-2.2-5-5-5zM9 15v-6h7c1.7 0 3 1.3 3 3s-1.3 3-3 3h-7zM9 23v-6h9c1.7 0 3 1.3 3 3s-1.3 3-3 3h-9zM16 5v0c3.9 0 7 3.1 7 7 0 0.9-0.2 1.8-0.5 2.6 1.5 1.3 2.5 3.3 2.5 5.4 0 3.9-3.1 7-7 7h-13v-22h11zM11 11v0 2h5c0.6 0 1-0.4 1-1s-0.4-1-1-1h-5zM11 19v0 2h7c0.6 0 1-0.4 1-1s-0.4-1-1-1h-7z"></path>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 531 B |
@ -1,5 +0,0 @@
|
|||||||
<!-- Generated by IcoMoon.io -->
|
|
||||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
|
||||||
<title>italic</title>
|
|
||||||
<path d="M11.75 5h10.031l-0.094 1.063-0.188 3-0.063 0.938h-2l-0.875 12h2l-0.063 1.063-0.188 3-0.063 0.938h-10.031l0.094-1.063 0.188-3 0.063-0.938h2l0.875-12h-2l0.063-1.063 0.188-3zM13.625 7l-0.063 1h2l-0.063 1.063-1 14-0.063 0.938h-2l-0.063 1h6l0.063-1h-2l0.063-1.063 1-14 0.063-0.938h2l0.063-1h-6z"></path>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 468 B |
@ -1,5 +0,0 @@
|
|||||||
<!-- Generated by IcoMoon.io -->
|
|
||||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
|
||||||
<title>list-ol</title>
|
|
||||||
<path d="M5.969 3h2.031v7h-2v-4.531c-0.444 0.255-0.913 0.531-1.594 0.531v-2c0.494 0 1.25-0.656 1.25-0.656zM11 6h17v2h-17v-2zM6.5 12c1.383 0 2.5 1.117 2.5 2.5 0 0.481-0.248 1.090-0.75 1.5l0.031 0.031-0.125 0.094-0.875 0.875h1.719v2h-5v-1.625l0.313-0.281 2.688-2.594c0-0.217-0.283-0.5-0.5-0.5s-0.5 0.283-0.5 0.5v0.5h-2v-0.5c0-1.383 1.117-2.5 2.5-2.5zM11 15h17v2h-17v-2zM4 21h4v1.469l-0.125 0.25-0.406 0.688c0.853 0.398 1.531 1.089 1.531 2.094 0 1.383-1.117 2.5-2.5 2.5h-2.5v-2h2.5c0.217 0 0.5-0.283 0.5-0.5s-0.283-0.5-0.5-0.5h-1.5v-1.375l0.125-0.219 0.25-0.406h-1.375v-2zM11 24h17v2h-17v-2z"></path>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 759 B |
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user