Merge branch 'master' of github.com:Human-Connection/Human-Connection into 722_comment-date

This commit is contained in:
aonomike 2019-06-04 13:54:41 +03:00
commit 55b3764258
99 changed files with 1165 additions and 570 deletions

View File

@ -4,7 +4,7 @@ NEO4J_PASSWORD=letmein
GRAPHQL_PORT=4000 GRAPHQL_PORT=4000
GRAPHQL_URI=http://localhost:4000 GRAPHQL_URI=http://localhost:4000
CLIENT_URI=http://localhost:3000 CLIENT_URI=http://localhost:3000
MOCK=false MOCKS=false
JWT_SECRET="b/&&7b78BF&fv/Vd" JWT_SECRET="b/&&7b78BF&fv/Vd"
MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ" MAPBOX_TOKEN="pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ"

View File

@ -11,7 +11,7 @@
"lint": "eslint src --config .eslintrc.js", "lint": "eslint src --config .eslintrc.js",
"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 DEBUG=true DISABLED_MIDDLEWARES=permissions,activityPub yarn run dev 2> /dev/null",
"test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand", "test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand",
"test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/", "test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/",
"test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand", "test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand",
@ -19,8 +19,8 @@
"test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --", "test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --",
"test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --", "test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --",
"db:script:seed": "wait-on tcp:4001 && babel-node src/seed/seed-db.js", "db:script:seed": "wait-on tcp:4001 && babel-node src/seed/seed-db.js",
"db:reset": "babel-node src/seed/reset-db.js", "db:reset": "cross-env DEBUG=true babel-node src/seed/reset-db.js",
"db:seed": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions run-p --race dev db:script:seed" "db:seed": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DEBUG=true DISABLED_MIDDLEWARES=permissions run-p --race dev db:script:seed"
}, },
"author": "Human Connection gGmbH", "author": "Human Connection gGmbH",
"license": "MIT", "license": "MIT",
@ -47,7 +47,7 @@
"apollo-client": "~2.5.1", "apollo-client": "~2.5.1",
"apollo-link-context": "~1.0.14", "apollo-link-context": "~1.0.14",
"apollo-link-http": "~1.5.14", "apollo-link-http": "~1.5.14",
"apollo-server": "~2.5.1", "apollo-server": "~2.6.1",
"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",
@ -109,4 +109,4 @@
"prettier": "~1.17.1", "prettier": "~1.17.1",
"supertest": "~4.0.2" "supertest": "~4.0.2"
} }
} }

View File

@ -4,9 +4,9 @@ import request from 'request'
import as from 'activitystrea.ms' import as from 'activitystrea.ms'
import NitroDataSource from './NitroDataSource' import NitroDataSource from './NitroDataSource'
import router from './routes' import router from './routes'
import dotenv from 'dotenv'
import Collections from './Collections' import Collections from './Collections'
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
import CONFIG from '../config'
const debug = require('debug')('ea') const debug = require('debug')('ea')
let activityPub = null let activityPub = null
@ -22,11 +22,7 @@ export default class ActivityPub {
static init(server) { static init(server) {
if (!activityPub) { if (!activityPub) {
dotenv.config() activityPub = new ActivityPub(CONFIG.CLIENT_URI, CONFIG.GRAPHQL_URI)
activityPub = new ActivityPub(
process.env.CLIENT_URI || 'http://localhost:3000',
process.env.GRAPHQL_URI || 'http://localhost:4000',
)
// integrate into running graphql express server // integrate into running graphql express server
server.express.set('ap', activityPub) server.express.set('ap', activityPub)

View File

@ -1,13 +1,15 @@
import dotenv from 'dotenv' // import dotenv from 'dotenv'
import { resolve } from 'path' // import { resolve } from 'path'
import crypto from 'crypto' import crypto from 'crypto'
import request from 'request' import request from 'request'
import CONFIG from './../../config'
const debug = require('debug')('ea:security') const debug = require('debug')('ea:security')
dotenv.config({ path: resolve('src', 'activitypub', '.env') }) // TODO Does this reference a local config? Why?
// dotenv.config({ path: resolve('src', 'activitypub', '.env') })
export function generateRsaKeyPair(options = {}) { export function generateRsaKeyPair(options = {}) {
const { passphrase = process.env.PRIVATE_KEY_PASSPHRASE } = options const { passphrase = CONFIG.PRIVATE_KEY_PASSPHRASE } = options
return crypto.generateKeyPairSync('rsa', { return crypto.generateKeyPairSync('rsa', {
modulusLength: 4096, modulusLength: 4096,
publicKeyEncoding: { publicKeyEncoding: {
@ -31,7 +33,7 @@ export function createSignature(options) {
url, url,
headers = {}, headers = {},
algorithm = 'rsa-sha256', algorithm = 'rsa-sha256',
passphrase = process.env.PRIVATE_KEY_PASSPHRASE, passphrase = CONFIG.PRIVATE_KEY_PASSPHRASE,
} = options } = options
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) {
throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`)

View File

@ -2,6 +2,7 @@ import { activityPub } from '../ActivityPub'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { createSignature } from '../security' import { createSignature } from '../security'
import request from 'request' import request from 'request'
import CONFIG from './../../config'
const debug = require('debug')('ea:utils') const debug = require('debug')('ea:utils')
export function extractNameFromId(uri) { export function extractNameFromId(uri) {
@ -38,7 +39,7 @@ export function throwErrorIfApolloErrorOccurred(result) {
export function signAndSend(activity, fromName, targetDomain, url) { export function signAndSend(activity, fromName, targetDomain, url) {
// fix for development: replace with http // fix for development: replace with http
url = url.indexOf('localhost') > -1 ? url.replace('https', 'http') : url url = url.indexOf('localhost') > -1 ? url.replace('https', 'http') : url
debug(`passhprase = ${process.env.PRIVATE_KEY_PASSPHRASE}`) debug(`passhprase = ${CONFIG.PRIVATE_KEY_PASSPHRASE}`)
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
debug('inside signAndSend') debug('inside signAndSend')
// get the private key // get the private key

View File

@ -1,15 +1,13 @@
import { v1 as neo4j } from 'neo4j-driver' import { v1 as neo4j } from 'neo4j-driver'
import dotenv from 'dotenv' import CONFIG from './../config'
dotenv.config()
let driver let driver
export function getDriver(options = {}) { export function getDriver(options = {}) {
const { const {
uri = process.env.NEO4J_URI || 'bolt://localhost:7687', uri = CONFIG.NEO4J_URI,
username = process.env.NEO4J_USERNAME || 'neo4j', username = CONFIG.NEO4J_USERNAME,
password = process.env.NEO4J_PASSWORD || 'neo4j', password = CONFIG.NEO4J_PASSWORD,
} = options } = options
if (!driver) { if (!driver) {
driver = neo4j.driver(uri, neo4j.auth.basic(username, password)) driver = neo4j.driver(uri, neo4j.auth.basic(username, password))

View File

@ -0,0 +1,34 @@
import dotenv from 'dotenv'
dotenv.config()
export const requiredConfigs = {
MAPBOX_TOKEN: process.env.MAPBOX_TOKEN,
JWT_SECRET: process.env.JWT_SECRET,
PRIVATE_KEY_PASSPHRASE: process.env.PRIVATE_KEY_PASSPHRASE,
}
export const neo4jConfigs = {
NEO4J_URI: process.env.NEO4J_URI || 'bolt://localhost:7687',
NEO4J_USERNAME: process.env.NEO4J_USERNAME || 'neo4j',
NEO4J_PASSWORD: process.env.NEO4J_PASSWORD || 'neo4j',
}
export const serverConfigs = {
GRAPHQL_PORT: process.env.GRAPHQL_PORT || 4000,
CLIENT_URI: process.env.CLIENT_URI || 'http://localhost:3000',
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000',
}
export const developmentConfigs = {
DEBUG: process.env.NODE_ENV !== 'production' && process.env.DEBUG === 'true',
MOCKS: process.env.MOCKS === 'true',
DISABLED_MIDDLEWARES: process.env.DISABLED_MIDDLEWARES || '',
}
export default {
...requiredConfigs,
...neo4jConfigs,
...serverConfigs,
...developmentConfigs,
}

View File

@ -1,2 +0,0 @@
export { default as typeDefs } from './types'
export { default as resolvers } from './resolvers'

View File

@ -1,17 +1,18 @@
import createServer from './server' import createServer from './server'
import ActivityPub from './activitypub/ActivityPub' import ActivityPub from './activitypub/ActivityPub'
import CONFIG from './config'
const serverConfig = { const serverConfig = {
port: process.env.GRAPHQL_PORT || 4000, port: CONFIG.GRAPHQL_PORT,
// cors: { // cors: {
// credentials: true, // credentials: true,
// origin: [process.env.CLIENT_URI] // your frontend url. // origin: [CONFIG.CLIENT_URI] // your frontend url.
// } // }
} }
const server = createServer() const server = createServer()
server.start(serverConfig, options => { server.start(serverConfig, options => {
/* eslint-disable-next-line no-console */ /* eslint-disable-next-line no-console */
console.log(`GraphQLServer ready at ${process.env.GRAPHQL_URI} 🚀`) console.log(`GraphQLServer ready at ${CONFIG.GRAPHQL_URI} 🚀`)
ActivityPub.init(server) ActivityPub.init(server)
}) })

View File

@ -1,11 +1,12 @@
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import CONFIG from './../config'
export default async (driver, authorizationHeader) => { export default async (driver, authorizationHeader) => {
if (!authorizationHeader) return null if (!authorizationHeader) return null
const token = authorizationHeader.replace('Bearer ', '') const token = authorizationHeader.replace('Bearer ', '')
let id = null let id = null
try { try {
const decoded = await jwt.verify(token, process.env.JWT_SECRET) const decoded = await jwt.verify(token, CONFIG.JWT_SECRET)
id = decoded.sub id = decoded.sub
} catch (err) { } catch (err) {
return null return null

View File

@ -1,15 +1,16 @@
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import ms from 'ms' import ms from 'ms'
import CONFIG from './../config'
// Generate an Access Token for the given User ID // Generate an Access Token for the given User ID
export default function encode(user) { export default function encode(user) {
const token = jwt.sign(user, process.env.JWT_SECRET, { const token = jwt.sign(user, CONFIG.JWT_SECRET, {
expiresIn: ms('1d'), expiresIn: ms('1d'),
issuer: process.env.GRAPHQL_URI, issuer: CONFIG.GRAPHQL_URI,
audience: process.env.CLIENT_URI, audience: CONFIG.CLIENT_URI,
subject: user.id.toString(), subject: user.id.toString(),
}) })
// jwt.verifySignature(token, process.env.JWT_SECRET, (err, data) => { // jwt.verifySignature(token, CONFIG.JWT_SECRET, (err, data) => {
// console.log('token verification:', err, data) // console.log('token verification:', err, data)
// }) // })
return token return token

View File

@ -1,10 +1,8 @@
import { generateRsaKeyPair } from '../activitypub/security' import { generateRsaKeyPair } from '../activitypub/security'
import { activityPub } from '../activitypub/ActivityPub' import { activityPub } from '../activitypub/ActivityPub'
import as from 'activitystrea.ms' import as from 'activitystrea.ms'
import dotenv from 'dotenv'
const debug = require('debug')('backend:schema') const debug = require('debug')('backend:schema')
dotenv.config()
export default { export default {
Mutation: { Mutation: {

View File

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

View File

@ -2,6 +2,7 @@ import request from 'request'
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
import isEmpty from 'lodash/isEmpty' import isEmpty from 'lodash/isEmpty'
import asyncForEach from '../../helpers/asyncForEach' import asyncForEach from '../../helpers/asyncForEach'
import CONFIG from './../../config'
const fetch = url => { const fetch = url => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -58,11 +59,12 @@ const createOrUpdateLocations = async (userId, locationName, driver) => {
if (isEmpty(locationName)) { if (isEmpty(locationName)) {
return return
} }
const mapboxToken = process.env.MAPBOX_TOKEN
const res = await fetch( const res = await fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent( `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(
locationName, locationName,
)}.json?access_token=${mapboxToken}&types=region,place,country&language=${locales.join(',')}`, )}.json?access_token=${CONFIG.MAPBOX_TOKEN}&types=region,place,country&language=${locales.join(
',',
)}`,
) )
if (!res || !res.features || !res.features[0]) { if (!res || !res.features || !res.features[0]) {

View File

@ -1,6 +1,6 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../seed/factories' import Factory from '../seed/factories'
import { host } from '../jest/helpers' import { host } from '../jest/helpers'
import { GraphQLClient } from 'graphql-request'
let client let client
let headers let headers

View File

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

View File

@ -1,6 +1,6 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../seed/factories' import Factory from '../seed/factories'
import { host, login } from '../jest/helpers' import { host, login } from '../jest/helpers'
import { GraphQLClient } from 'graphql-request'
const factory = Factory() const factory = Factory()

View File

@ -1,6 +1,6 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../seed/factories' import Factory from '../seed/factories'
import { host, login } from '../jest/helpers' import { host, login } from '../jest/helpers'
import { GraphQLClient } from 'graphql-request'
let authenticatedClient let authenticatedClient
let headers let headers

View File

@ -1,6 +1,6 @@
import { GraphQLClient } from 'graphql-request'
import Factory from '../seed/factories' import Factory from '../seed/factories'
import { host, login } from '../jest/helpers' import { host, login } from '../jest/helpers'
import { GraphQLClient } from 'graphql-request'
const factory = Factory() const factory = Factory()
let client let client

View File

@ -1,9 +1,5 @@
import dotenv from 'dotenv'
import createOrUpdateLocations from './nodes/locations' import createOrUpdateLocations from './nodes/locations'
dotenv.config()
export default { export default {
Mutation: { Mutation: {
CreateUser: async (resolve, root, args, context, info) => { CreateUser: async (resolve, root, args, context, info) => {

View File

@ -0,0 +1,24 @@
import { makeAugmentedSchema } from 'neo4j-graphql-js'
import CONFIG from './../config'
import applyScalars from './../bootstrap/scalars'
import applyDirectives from './../bootstrap/directives'
import typeDefs from './types'
import resolvers from './resolvers'
export default applyScalars(
applyDirectives(
makeAugmentedSchema({
typeDefs,
resolvers,
config: {
query: {
exclude: ['Notfication', 'Statistics', 'LoggedInUser'],
},
mutation: {
exclude: ['Notfication', 'Statistics', 'LoggedInUser'],
},
debug: CONFIG.DEBUG,
},
}),
),
)

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers' import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory() const factory = Factory()
let client let client

View File

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

View File

@ -1,6 +1,7 @@
import Factory from '../seed/factories' import gql from 'graphql-tag'
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers' import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory() const factory = Factory()
let client let client
@ -21,22 +22,22 @@ afterEach(async () => {
}) })
describe('CreateComment', () => { describe('CreateComment', () => {
const createCommentMutation = ` const createCommentMutation = gql`
mutation($postId: ID, $content: String!) { mutation($postId: ID, $content: String!) {
CreateComment(postId: $postId, content: $content) { CreateComment(postId: $postId, content: $content) {
id id
content content
}
} }
}
` `
const createPostMutation = ` const createPostMutation = gql`
mutation($id: ID!, $title: String!, $content: String!) { mutation($id: ID!, $title: String!, $content: String!) {
CreatePost(id: $id, title: $title, content: $content) { CreatePost(id: $id, title: $title, content: $content) {
id id
}
} }
}
` `
const commentQueryForPostId = ` const commentQueryForPostId = gql`
query($content: String) { query($content: String) {
Comment(content: $content) { Comment(content: $content) {
postId postId
@ -59,8 +60,13 @@ describe('CreateComment', () => {
describe('authenticated', () => { describe('authenticated', () => {
let headers let headers
beforeEach(async () => { beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' }) headers = await login({
client = new GraphQLClient(host, { headers }) email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
createCommentVariables = { createCommentVariables = {
postId: 'p1', postId: 'p1',
content: "I'm authorised to comment", content: "I'm authorised to comment",
@ -88,15 +94,25 @@ describe('CreateComment', () => {
it('assigns the authenticated user as author', async () => { it('assigns the authenticated user as author', async () => {
await client.request(createCommentMutation, createCommentVariables) await client.request(createCommentMutation, createCommentVariables)
const { User } = await client.request(`{ const { User } = await client.request(gql`
{
User(email: "test@example.org") { User(email: "test@example.org") {
comments { comments {
content content
} }
} }
}`) }
`)
expect(User).toEqual([{ comments: [{ content: "I'm authorised to comment" }] }]) expect(User).toEqual([
{
comments: [
{
content: "I'm authorised to comment",
},
],
},
])
}) })
it('throw an error if an empty string is sent from the editor as content', async () => { it('throw an error if an empty string is sent from the editor as content', async () => {
@ -186,7 +202,98 @@ describe('CreateComment', () => {
commentQueryForPostId, commentQueryForPostId,
commentQueryVariablesByContent, commentQueryVariablesByContent,
) )
expect(Comment).toEqual([{ postId: null }]) expect(Comment).toEqual([
{
postId: null,
},
])
})
})
})
describe('DeleteComment', () => {
const deleteCommentMutation = gql`
mutation($id: ID!) {
DeleteComment(id: $id) {
id
}
}
`
let deleteCommentVariables = {
id: 'c1',
}
beforeEach(async () => {
const asAuthor = Factory()
await asAuthor.create('User', {
email: 'author@example.org',
password: '1234',
})
await asAuthor.authenticateAs({
email: 'author@example.org',
password: '1234',
})
await asAuthor.create('Post', {
id: 'p1',
content: 'Post to be commented',
})
await asAuthor.create('Comment', {
id: 'c1',
postId: 'p1',
content: 'Comment to be deleted',
})
})
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 () => {
let headers
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 () => {
let headers
headers = await login({
email: 'author@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
})
it('deletes the comment', async () => {
const expected = {
DeleteComment: {
id: 'c1',
},
}
await expect(client.request(deleteCommentMutation, deleteCommentVariables)).resolves.toEqual(
expected,
)
}) })
}) })
}) })

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers' import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory() const factory = Factory()
let clientUser1 let clientUser1

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers' import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory() const factory = Factory()
let client let client

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers' import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory() const factory = Factory()
let client let client

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers' import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory() const factory = Factory()
let client let client

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers' import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory() const factory = Factory()

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers' import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory() const factory = Factory()

View File

@ -1,6 +1,6 @@
import Factory from '../seed/factories'
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { host, login } from '../jest/helpers' import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory() const factory = Factory()
let clientUser1, clientUser2 let clientUser1, clientUser2

View File

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

View File

@ -1,4 +1,4 @@
import encode from '../jwt/encode' 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'

View File

@ -1,8 +1,9 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
import Factory from '../seed/factories'
import { GraphQLClient, request } from 'graphql-request' import { GraphQLClient, request } from 'graphql-request'
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import { host, login } from '../jest/helpers' import CONFIG from './../../config'
import Factory from '../../seed/factories'
import { host, login } from '../../jest/helpers'
const factory = Factory() const factory = Factory()
@ -185,7 +186,7 @@ describe('login', () => {
}), }),
) )
const token = data.login const token = data.login
jwt.verify(token, process.env.JWT_SECRET, (err, data) => { jwt.verify(token, CONFIG.JWT_SECRET, (err, data) => {
expect(data.email).toEqual('test@example.org') expect(data.email).toEqual('test@example.org')
expect(err).toBeNull() expect(err).toBeNull()
}) })

View File

@ -1,6 +1,6 @@
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { host } from '../jest/helpers' import { host } from '../../jest/helpers'
import Factory from '../seed/factories' import Factory from '../../seed/factories'
const factory = Factory() const factory = Factory()
let client let client

View File

@ -1,10 +1,8 @@
import { cleanDatabase } from './factories' import { cleanDatabase } from './factories'
import dotenv from 'dotenv' import CONFIG from './../config'
dotenv.config() if (!CONFIG.DEBUG) {
throw new Error(`YOU CAN'T CLEAN THE DATABASE WITH DEBUG=${CONFIG.DEBUG}`)
if (process.env.NODE_ENV === 'production') {
throw new Error(`YOU CAN'T CLEAN THE DATABASE WITH NODE_ENV=${process.env.NODE_ENV}`)
} }
;(async function() { ;(async function() {

View File

@ -1,48 +1,27 @@
import { GraphQLServer } from 'graphql-yoga'
import { makeAugmentedSchema } from 'neo4j-graphql-js'
import { typeDefs, resolvers } from './graphql-schema'
import express from 'express' import express from 'express'
import dotenv from 'dotenv' import helmet from 'helmet'
import { GraphQLServer } from 'graphql-yoga'
import CONFIG, { requiredConfigs } from './config'
import mocks from './mocks' import mocks from './mocks'
import middleware from './middleware' import middleware from './middleware'
import applyDirectives from './bootstrap/directives'
import applyScalars from './bootstrap/scalars'
import { getDriver } from './bootstrap/neo4j' import { getDriver } from './bootstrap/neo4j'
import helmet from 'helmet'
import decode from './jwt/decode' import decode from './jwt/decode'
import schema from './schema'
dotenv.config() // check required configs and throw error
// check env and warn // TODO check this directly in config file - currently not possible due to testsetup
const requiredEnvVars = ['MAPBOX_TOKEN', 'JWT_SECRET', 'PRIVATE_KEY_PASSPHRASE'] Object.entries(requiredConfigs).map(entry => {
requiredEnvVars.forEach(env => { if (!entry[1]) {
if (!process.env[env]) { throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
throw new Error(`ERROR: "${env}" env variable is missing.`)
} }
}) })
const driver = getDriver() const driver = getDriver()
const debug = process.env.NODE_ENV !== 'production' && process.env.DEBUG === 'true'
let schema = makeAugmentedSchema({
typeDefs,
resolvers,
config: {
query: {
exclude: ['Notfication', 'Statistics', 'LoggedInUser'],
},
mutation: {
exclude: ['Notfication', 'Statistics', 'LoggedInUser'],
},
debug: debug,
},
})
schema = applyScalars(applyDirectives(schema))
const createServer = options => { const createServer = options => {
const defaults = { const defaults = {
context: async ({ request }) => { context: async ({ request }) => {
const authorizationHeader = request.headers.authorization || '' const user = await decode(driver, request.headers.authorization)
const user = await decode(driver, authorizationHeader)
return { return {
driver, driver,
user, user,
@ -52,11 +31,11 @@ const createServer = options => {
}, },
} }
}, },
schema: schema, schema,
debug: debug, debug: CONFIG.DEBUG,
tracing: debug, tracing: CONFIG.DEBUG,
middlewares: middleware(schema), middlewares: middleware(schema),
mocks: process.env.MOCK === 'true' ? mocks : false, mocks: CONFIG.MOCKS ? mocks : false,
} }
const server = new GraphQLServer(Object.assign({}, defaults, options)) const server = new GraphQLServer(Object.assign({}, defaults, options))

View File

@ -9,13 +9,6 @@
dependencies: dependencies:
apollo-env "0.5.1" apollo-env "0.5.1"
"@apollographql/apollo-tools@^0.3.6-alpha.1":
version "0.3.6-alpha.1"
resolved "https://registry.yarnpkg.com/@apollographql/apollo-tools/-/apollo-tools-0.3.6-alpha.1.tgz#5199b36c65c2fddc4f8bc8bb97642f74e9fb00c5"
integrity sha512-fq74In3Vw9OmtKHze0L5/Ns/pdTZOqUeFVC6Um9NRgziVehXz/qswsh2r3+dsn82uqoa/AlvckHtd6aPPPYj9g==
dependencies:
apollo-env "0.4.1-alpha.1"
"@apollographql/graphql-playground-html@1.6.20": "@apollographql/graphql-playground-html@1.6.20":
version "1.6.20" version "1.6.20"
resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.20.tgz#bf9f2acdf319c0959fad8ec1239741dd2ead4e8d" resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.20.tgz#bf9f2acdf319c0959fad8ec1239741dd2ead4e8d"
@ -1288,14 +1281,6 @@ anymatch@^2.0.0:
micromatch "^3.1.4" micromatch "^3.1.4"
normalize-path "^2.1.1" normalize-path "^2.1.1"
apollo-cache-control@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.6.1.tgz#c73ff521fe606faf18edcbd3463c421a966f3e5d"
integrity sha512-M3cDeQDXtRxYPQ/sL4pu3IVE5Q/9jpBlENB2IjwxTDir+WFZbJV1CAnvVwyJdL1DvS6ESR35DFOurJH4Ws/OPA==
dependencies:
apollo-server-env "2.3.0"
graphql-extensions "0.6.1"
apollo-cache-control@0.7.1: apollo-cache-control@0.7.1:
version "0.7.1" version "0.7.1"
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.7.1.tgz#3d4fba232f561f096f61051e103bf58ee4bf8b54" resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.7.1.tgz#3d4fba232f561f096f61051e103bf58ee4bf8b54"
@ -1353,14 +1338,6 @@ apollo-client@~2.5.1:
tslib "^1.9.3" tslib "^1.9.3"
zen-observable "^0.8.0" zen-observable "^0.8.0"
apollo-datasource@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.4.0.tgz#f042641fd2593fa5f4f002fc30d1fb1a20284df8"
integrity sha512-6QkgnLYwQrW0qv+yXIf617DojJbGmza2XJXUlgnzrGGhxzfAynzEjaLyYkc8rYS1m82vjrl9EOmLHTcnVkvZAQ==
dependencies:
apollo-server-caching "0.4.0"
apollo-server-env "2.3.0"
apollo-datasource@0.5.0: apollo-datasource@0.5.0:
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.5.0.tgz#7a8c97e23da7b9c15cb65103d63178ab19eca5e9" resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.5.0.tgz#7a8c97e23da7b9c15cb65103d63178ab19eca5e9"
@ -1376,18 +1353,6 @@ apollo-engine-reporting-protobuf@0.3.0:
dependencies: dependencies:
protobufjs "^6.8.6" protobufjs "^6.8.6"
apollo-engine-reporting@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.1.1.tgz#f5a3240bc5c5afb210ff8c45d72995de7b0d2a13"
integrity sha512-K7BDsj99jr8ftd9NIuHL4oF/S7CBFcgMGjL0ChhfxpkgUv80FPxJ+9Fs+9ZkKIVylV3PCi2WnihpDeEO10eZAw==
dependencies:
apollo-engine-reporting-protobuf "0.3.0"
apollo-graphql "^0.2.1-alpha.1"
apollo-server-core "2.5.1"
apollo-server-env "2.3.0"
async-retry "^1.2.1"
graphql-extensions "0.6.1"
apollo-engine-reporting@1.2.1: apollo-engine-reporting@1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.2.1.tgz#0b77fad2e9221d62f4a29b8b4fab8f7f47dcc1d6" resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.2.1.tgz#0b77fad2e9221d62f4a29b8b4fab8f7f47dcc1d6"
@ -1400,15 +1365,6 @@ apollo-engine-reporting@1.2.1:
async-retry "^1.2.1" async-retry "^1.2.1"
graphql-extensions "0.7.1" graphql-extensions "0.7.1"
apollo-env@0.4.1-alpha.1:
version "0.4.1-alpha.1"
resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.4.1-alpha.1.tgz#10d3ea508b8f3ba03939ef4e6ec4b2b5db77e8f1"
integrity sha512-4qWiaUKWh92jvKxxRsiZSjmW9YH9GWSG1W6X+S1BcC1uqtPiHsem7ExG9MMTt+UrzHsbpQLctj12xk8lI4lgCg==
dependencies:
core-js "3.0.0-beta.13"
node-fetch "^2.2.0"
sha.js "^2.4.11"
apollo-env@0.5.1: apollo-env@0.5.1:
version "0.5.1" version "0.5.1"
resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.5.1.tgz#b9b0195c16feadf0fe9fd5563edb0b9b7d9e97d3" resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.5.1.tgz#b9b0195c16feadf0fe9fd5563edb0b9b7d9e97d3"
@ -1426,14 +1382,6 @@ apollo-errors@^1.9.0:
assert "^1.4.1" assert "^1.4.1"
extendable-error "^0.1.5" extendable-error "^0.1.5"
apollo-graphql@^0.2.1-alpha.1:
version "0.2.1-alpha.1"
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.2.1-alpha.1.tgz#a0cc0bd65e03c7e887c96c9f53421f3c6dd7b599"
integrity sha512-kObCSpYRHEf4IozJV+TZAXEL2Yni2DpzQckohJNYXg5/KRAF20jJ7lHxuJz+kMQrc7QO4wYGSa29HuFZH2AtQA==
dependencies:
apollo-env "0.4.1-alpha.1"
lodash.sortby "^4.7.0"
apollo-graphql@^0.3.0: apollo-graphql@^0.3.0:
version "0.3.1" version "0.3.1"
resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.1.tgz#d13b80cc0cae3fe7066b81b80914c6f983fac8d7" resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.1.tgz#d13b80cc0cae3fe7066b81b80914c6f983fac8d7"
@ -1492,32 +1440,6 @@ apollo-server-caching@0.4.0:
dependencies: dependencies:
lru-cache "^5.0.0" lru-cache "^5.0.0"
apollo-server-core@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.5.1.tgz#0fdb6cfca56a0f5b5b3aecffb48db17b3c8e1d71"
integrity sha512-4QNrW1AUM3M/p0+hbBX/MsjSjZTy+2rt7JpiKKkG9RmeEIzd/VG7hwwwloAZSLjYx3twz0+BnASJ9y+rGEPC8A==
dependencies:
"@apollographql/apollo-tools" "^0.3.6"
"@apollographql/graphql-playground-html" "1.6.20"
"@types/ws" "^6.0.0"
apollo-cache-control "0.6.1"
apollo-datasource "0.4.0"
apollo-engine-reporting "1.1.1"
apollo-server-caching "0.4.0"
apollo-server-env "2.3.0"
apollo-server-errors "2.3.0"
apollo-server-plugin-base "0.4.1"
apollo-tracing "0.6.1"
fast-json-stable-stringify "^2.0.0"
graphql-extensions "0.6.1"
graphql-subscriptions "^1.0.0"
graphql-tag "^2.9.2"
graphql-tools "^4.0.0"
graphql-upload "^8.0.2"
sha.js "^2.4.11"
subscriptions-transport-ws "^0.9.11"
ws "^6.0.0"
apollo-server-core@2.6.1: apollo-server-core@2.6.1:
version "2.6.1" version "2.6.1"
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.1.tgz#d0d878b0a4959b6c661fc43300ce45b29996176a" resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.6.1.tgz#d0d878b0a4959b6c661fc43300ce45b29996176a"
@ -1553,14 +1475,6 @@ apollo-server-core@^1.3.6, apollo-server-core@^1.4.0:
apollo-tracing "^0.1.0" apollo-tracing "^0.1.0"
graphql-extensions "^0.0.x" graphql-extensions "^0.0.x"
apollo-server-env@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.3.0.tgz#f0bf4484a6cc331a8c13763ded56e91beb16ba17"
integrity sha512-WIwlkCM/gir0CkoYWPMTCH8uGCCKB/aM074U1bKayvkFOBVO2VgG5x2kgsfkyF05IMQq2/GOTsKhNY7RnUEhTA==
dependencies:
node-fetch "^2.1.2"
util.promisify "^1.0.0"
apollo-server-env@2.4.0: apollo-server-env@2.4.0:
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.0.tgz#6611556c6b627a1636eed31317d4f7ea30705872" resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.0.tgz#6611556c6b627a1636eed31317d4f7ea30705872"
@ -1574,10 +1488,10 @@ apollo-server-errors@2.3.0:
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.0.tgz#700622b66a16dffcad3b017e4796749814edc061" resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.0.tgz#700622b66a16dffcad3b017e4796749814edc061"
integrity sha512-rUvzwMo2ZQgzzPh2kcJyfbRSfVKRMhfIlhY7BzUfM4x6ZT0aijlgsf714Ll3Mbf5Fxii32kD0A/DmKsTecpccw== integrity sha512-rUvzwMo2ZQgzzPh2kcJyfbRSfVKRMhfIlhY7BzUfM4x6ZT0aijlgsf714Ll3Mbf5Fxii32kD0A/DmKsTecpccw==
apollo-server-express@2.5.1: apollo-server-express@2.6.1:
version "2.5.1" version "2.6.1"
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.5.1.tgz#b112d9795f2fb39076d9cbc109f5eeb7835bed6b" resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.6.1.tgz#1e2649d3fd38c0c0a2c830090fd41e086b259c9f"
integrity sha512-528wDQnOMIenDaICkYPFWQykdXQZwpygxd+Ar0PmZiaST042NSVExV4iRWI09p1THqfsuyHygqpkK+K94bUtBA== integrity sha512-TVu68LVp+COMGOXuxc0OFeCUQiPApxy7Isv2Vk85nikZV4t4FXlODB6PrRKf5rfvP31dvGsfE6GHPJTLLbKfyg==
dependencies: dependencies:
"@apollographql/graphql-playground-html" "1.6.20" "@apollographql/graphql-playground-html" "1.6.20"
"@types/accepts" "^1.3.5" "@types/accepts" "^1.3.5"
@ -1585,7 +1499,7 @@ apollo-server-express@2.5.1:
"@types/cors" "^2.8.4" "@types/cors" "^2.8.4"
"@types/express" "4.16.1" "@types/express" "4.16.1"
accepts "^1.3.5" accepts "^1.3.5"
apollo-server-core "2.5.1" apollo-server-core "2.6.1"
body-parser "^1.18.3" body-parser "^1.18.3"
cors "^2.8.4" cors "^2.8.4"
graphql-subscriptions "^1.0.0" graphql-subscriptions "^1.0.0"
@ -1613,11 +1527,6 @@ apollo-server-module-graphiql@^1.3.4, apollo-server-module-graphiql@^1.4.0:
resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz#c559efa285578820709f1769bb85d3b3eed3d8ec" resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz#c559efa285578820709f1769bb85d3b3eed3d8ec"
integrity sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA== integrity sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA==
apollo-server-plugin-base@0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.4.1.tgz#be380b28d71ad3b6b146d0d6a8f7ebf5675b07ff"
integrity sha512-D2G6Ca/KBdQgEbmSfYqZqYbdVJnk/rrSv7Vj2NntwjfL7WJf0TjufxYJlrTH5jF6xCbsszDNGqfmt2Nm8x/o4g==
apollo-server-plugin-base@0.5.1: apollo-server-plugin-base@0.5.1:
version "0.5.1" version "0.5.1"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.1.tgz#b81056666763879bdc98d8d58f3c4c43cbb30da6" resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.5.1.tgz#b81056666763879bdc98d8d58f3c4c43cbb30da6"
@ -1630,25 +1539,17 @@ apollo-server-testing@~2.6.1:
dependencies: dependencies:
apollo-server-core "2.6.1" apollo-server-core "2.6.1"
apollo-server@~2.5.1: apollo-server@~2.6.1:
version "2.5.1" version "2.6.1"
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.5.1.tgz#bfcfbebc123f692c0e6d85b0c56739646bd1bb7e" resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.6.1.tgz#1b1fc6020b75c0913550da5fa0f2005c62f1bc53"
integrity sha512-eH3ubq300xhpFAxek28kb+5WZINXpWcwzyNqBQDbuasTlW8qSsqY7xrV6IIz6WUYKdX+ET0mx+Ta1DdaYQPrqw== integrity sha512-Ed0zZjluRYPMC3Yr6oXQjcR11izu86nkjiS2MhjJA1mF8IXJfxbPp2hnX4Jf4vXPSkOP2e5ZHw0cdaIcu9GnRw==
dependencies: dependencies:
apollo-server-core "2.5.1" apollo-server-core "2.6.1"
apollo-server-express "2.5.1" apollo-server-express "2.6.1"
express "^4.0.0" express "^4.0.0"
graphql-subscriptions "^1.0.0" graphql-subscriptions "^1.0.0"
graphql-tools "^4.0.0" graphql-tools "^4.0.0"
apollo-tracing@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.6.1.tgz#48a6d6040f9b2f2b4365a890c2e97cb763eb2392"
integrity sha512-rrDBgTHa9GDA3wY8O7rDsFwC6ePIVzRGxpUsThgmLvIVkkCr0KS4wJJ4C01c+v4xsOXNuQwx0IyYhxZt4twwcA==
dependencies:
apollo-server-env "2.3.0"
graphql-extensions "0.6.1"
apollo-tracing@0.7.1: apollo-tracing@0.7.1:
version "0.7.1" version "0.7.1"
resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.7.1.tgz#6a7356b619f3aa0ca22c623b5d8bb1af5ca1c74c" resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.7.1.tgz#6a7356b619f3aa0ca22c623b5d8bb1af5ca1c74c"
@ -2524,11 +2425,6 @@ core-js-pure@3.1.2:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.1.2.tgz#62fc435f35b7374b9b782013cdcb2f97e9f6dffa" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.1.2.tgz#62fc435f35b7374b9b782013cdcb2f97e9f6dffa"
integrity sha512-5ckIdBF26B3ldK9PM177y2ZcATP2oweam9RskHSoqfZCrJ2As6wVg8zJ1zTriFsZf6clj/N1ThDFRGaomMsh9w== integrity sha512-5ckIdBF26B3ldK9PM177y2ZcATP2oweam9RskHSoqfZCrJ2As6wVg8zJ1zTriFsZf6clj/N1ThDFRGaomMsh9w==
core-js@3.0.0-beta.13:
version "3.0.0-beta.13"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.0-beta.13.tgz#7732c69be5e4758887917235fe7c0352c4cb42a1"
integrity sha512-16Q43c/3LT9NyePUJKL8nRIQgYWjcBhjJSMWg96PVSxoS0PeE0NHitPI3opBrs9MGGHjte1KoEVr9W63YKlTXQ==
core-js@^2.4.0, core-js@^2.5.3, core-js@^2.5.7: core-js@^2.4.0, core-js@^2.5.3, core-js@^2.5.7:
version "2.6.2" version "2.6.2"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.2.tgz#267988d7268323b349e20b4588211655f0e83944" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.2.tgz#267988d7268323b349e20b4588211655f0e83944"
@ -3832,13 +3728,6 @@ graphql-deduplicator@^2.0.1:
resolved "https://registry.yarnpkg.com/graphql-deduplicator/-/graphql-deduplicator-2.0.2.tgz#d8608161cf6be97725e178df0c41f6a1f9f778f3" resolved "https://registry.yarnpkg.com/graphql-deduplicator/-/graphql-deduplicator-2.0.2.tgz#d8608161cf6be97725e178df0c41f6a1f9f778f3"
integrity sha512-0CGmTmQh4UvJfsaTPppJAcHwHln8Ayat7yXXxdnuWT+Mb1dBzkbErabCWzjXyKh/RefqlGTTA7EQOZHofMaKJA== integrity sha512-0CGmTmQh4UvJfsaTPppJAcHwHln8Ayat7yXXxdnuWT+Mb1dBzkbErabCWzjXyKh/RefqlGTTA7EQOZHofMaKJA==
graphql-extensions@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.6.1.tgz#e61c4cb901e336dc5993a61093a8678a021dda59"
integrity sha512-vB2WNQJn99pncHfvxgcdyVoazmG3cD8XzkgcaDrHTvV+xJGJEBP6056EWi0mNt1d6ukYyRS2zexdekmMCjcq0w==
dependencies:
"@apollographql/apollo-tools" "^0.3.6-alpha.1"
graphql-extensions@0.7.1: graphql-extensions@0.7.1:
version "0.7.1" version "0.7.1"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.1.tgz#f55b01ac8ddf09a215e21f34caeee3ae66a88f21" resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.7.1.tgz#f55b01ac8ddf09a215e21f34caeee3ae66a88f21"

View File

@ -4,7 +4,7 @@
data: data:
GRAPHQL_PORT: "4000" GRAPHQL_PORT: "4000"
GRAPHQL_URI: "http://nitro-backend.human-connection:4000" GRAPHQL_URI: "http://nitro-backend.human-connection:4000"
MOCK: "false" MOCKS: "false"
NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687" NEO4J_URI: "bolt://nitro-neo4j.human-connection:7687"
NEO4J_USER: "neo4j" NEO4J_USER: "neo4j"
NEO4J_AUTH: "none" NEO4J_AUTH: "none"

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ services:
- GRAPHQL_URI=http://localhost:4000 - GRAPHQL_URI=http://localhost:4000
- CLIENT_URI=http://localhost:3000 - CLIENT_URI=http://localhost:3000
- JWT_SECRET=b/&&7b78BF&fv/Vd - JWT_SECRET=b/&&7b78BF&fv/Vd
- MOCK=false - MOCKS=false
- MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ - MAPBOX_TOKEN=pk.eyJ1IjoiaHVtYW4tY29ubmVjdGlvbiIsImEiOiJjajl0cnBubGoweTVlM3VwZ2lzNTNud3ZtIn0.KZ8KK9l70omjXbEkkbHGsQ
- PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78 - PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78
- NEO4J_apoc_import_file_enabled=true - NEO4J_apoc_import_file_enabled=true
@ -30,6 +30,7 @@ services:
- "MONGODB_AUTH_DB=${MONGODB_AUTH_DB}" - "MONGODB_AUTH_DB=${MONGODB_AUTH_DB}"
- "MONGODB_DATABASE=${MONGODB_DATABASE}" - "MONGODB_DATABASE=${MONGODB_DATABASE}"
- "UPLOADS_DIRECTORY=${UPLOADS_DIRECTORY}" - "UPLOADS_DIRECTORY=${UPLOADS_DIRECTORY}"
- "MONGO_EXPORT_SPLIT_SIZE=${MONGO_EXPORT_SPLIT_SIZE}"
ports: ports:
- 7687:7687 - 7687:7687
- 7474:7474 - 7474:7474

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@
placement="bottom-end" placement="bottom-end"
resource-type="comment" resource-type="comment"
:resource="comment" :resource="comment"
:callbacks="{ confirm: deleteCommentCallback, cancel: null }"
style="float-right" style="float-right"
:is-owner="isAuthor(author.id)" :is-owner="isAuthor(author.id)"
/> />
@ -27,6 +28,7 @@
</template> </template>
<script> <script>
import gql from 'graphql-tag'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import HcUser from '~/components/User' import HcUser from '~/components/User'
import ContentMenu from '~/components/ContentMenu' import ContentMenu from '~/components/ContentMenu'
@ -62,6 +64,25 @@ export default {
isAuthor(id) { isAuthor(id) {
return this.user.id === id return this.user.id === id
}, },
async deleteCommentCallback() {
try {
var gqlMutation = gql`
mutation($id: ID!) {
DeleteComment(id: $id) {
id
}
}
`
await this.$apollo.mutate({
mutation: gqlMutation,
variables: { id: this.comment.id },
})
this.$toast.success(this.$t(`delete.comment.success`))
this.$emit('deleteComment')
} catch (err) {
this.$toast.error(err.message)
}
},
}, },
} }
</script> </script>

View File

@ -28,6 +28,7 @@
import Dropdown from '~/components/Dropdown' import Dropdown from '~/components/Dropdown'
export default { export default {
name: 'ContentMenu',
components: { components: {
Dropdown, Dropdown,
}, },
@ -42,6 +43,7 @@ export default {
return value.match(/(contribution|comment|organization|user)/) return value.match(/(contribution|comment|organization|user)/)
}, },
}, },
callbacks: { type: Object, required: true },
}, },
computed: { computed: {
routes() { routes() {
@ -49,7 +51,7 @@ export default {
if (this.isOwner && this.resourceType === 'contribution') { if (this.isOwner && this.resourceType === 'contribution') {
routes.push({ routes.push({
name: this.$t(`contribution.edit`), name: this.$t(`post.menu.edit`),
path: this.$router.resolve({ path: this.$router.resolve({
name: 'post-edit-id', name: 'post-edit-id',
params: { params: {
@ -59,21 +61,29 @@ export default {
icon: 'edit', icon: 'edit',
}) })
routes.push({ routes.push({
name: this.$t(`post.delete.title`), name: this.$t(`post.menu.delete`),
callback: () => { callback: () => {
this.openModal('delete') this.openModal('delete')
}, },
icon: 'trash', icon: 'trash',
}) })
} }
if (this.isOwner && this.resourceType === 'comment') { if (this.isOwner && this.resourceType === 'comment') {
// routes.push({
// name: this.$t(`comment.menu.edit`),
// callback: () => {
// /* eslint-disable-next-line no-console */
// console.log('EDIT COMMENT')
// },
// icon: 'edit'
// })
routes.push({ routes.push({
name: this.$t(`comment.edit`), name: this.$t(`comment.menu.delete`),
callback: () => { callback: () => {
/* eslint-disable-next-line no-console */ this.openModal('delete')
console.log('EDIT COMMENT')
}, },
icon: 'edit', icon: 'trash',
}) })
} }
@ -125,6 +135,7 @@ export default {
data: { data: {
type: this.resourceType, type: this.resourceType,
resource: this.resource, resource: this.resource,
callbacks: this.callbacks,
}, },
}) })
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,9 +19,11 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
export default { export default {
name: 'DisableModal',
props: { props: {
name: { type: String, default: '' }, name: { type: String, default: '' },
type: { type: String, required: true }, type: { type: String, required: true },
callbacks: { type: Object, required: true },
id: { type: String, required: true }, id: { type: String, required: true },
}, },
data() { data() {
@ -41,7 +43,10 @@ export default {
}, },
}, },
methods: { methods: {
cancel() { async cancel() {
if (this.callbacks.cancel) {
await this.callbacks.cancel()
}
this.isOpen = false this.isOpen = false
setTimeout(() => { setTimeout(() => {
this.$emit('close') this.$emit('close')
@ -49,6 +54,9 @@ export default {
}, },
async confirm() { async confirm() {
try { try {
if (this.callbacks.confirm) {
await this.callbacks.confirm()
}
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: gql` mutation: gql`
mutation($id: ID!) { mutation($id: ID!) {

View File

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

View File

@ -39,6 +39,7 @@ export default {
props: { props: {
name: { type: String, default: '' }, name: { type: String, default: '' },
type: { type: String, required: true }, type: { type: String, required: true },
callbacks: { type: Object, required: true },
id: { type: String, required: true }, id: { type: String, required: true },
}, },
data() { data() {
@ -59,6 +60,9 @@ export default {
}, },
methods: { methods: {
async cancel() { async cancel() {
if (this.callbacks.cancel) {
await this.callbacks.cancel()
}
this.isOpen = false this.isOpen = false
setTimeout(() => { setTimeout(() => {
this.$emit('close') this.$emit('close')
@ -67,6 +71,9 @@ export default {
async confirm() { async confirm() {
this.loading = true this.loading = true
try { try {
if (this.callbacks.confirm) {
await this.callbacks.confirm()
}
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: gql` mutation: gql`
mutation($id: ID!) { mutation($id: ID!) {

View File

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

View File

@ -17,7 +17,12 @@
</h3> </h3>
<ds-space margin-bottom="large" /> <ds-space margin-bottom="large" />
<div v-if="comments && comments.length" id="comments" class="comments"> <div v-if="comments && comments.length" id="comments" class="comments">
<comment v-for="comment in comments" :key="comment.id" :comment="comment" /> <comment
v-for="(comment, index) in comments"
:key="comment.id"
:comment="comment"
@deleteComment="comments.splice(index, 1)"
/>
</div> </div>
<hc-empty v-else name="empty" icon="messages" /> <hc-empty v-else name="empty" icon="messages" />
</div> </div>

View File

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

View File

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

View File

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

View File

@ -29,7 +29,6 @@
"!**/?(*.)+(spec|test).js?(x)" "!**/?(*.)+(spec|test).js?(x)"
], ],
"coverageReporters": [ "coverageReporters": [
"text",
"lcov" "lcov"
], ],
"transform": { "transform": {
@ -71,7 +70,7 @@
"stack-utils": "^1.0.2", "stack-utils": "^1.0.2",
"string-hash": "^1.1.3", "string-hash": "^1.1.3",
"tiptap": "1.20.1", "tiptap": "1.20.1",
"tiptap-extensions": "1.20.1", "tiptap-extensions": "1.20.2",
"v-tooltip": "~2.0.2", "v-tooltip": "~2.0.2",
"vue-count-to": "~1.0.13", "vue-count-to": "~1.0.13",
"vue-izitoast": "1.1.2", "vue-izitoast": "1.1.2",
@ -108,7 +107,7 @@
"nodemon": "~1.19.1", "nodemon": "~1.19.1",
"prettier": "~1.17.1", "prettier": "~1.17.1",
"sass-loader": "~7.1.0", "sass-loader": "~7.1.0",
"tippy.js": "^4.3.1", "tippy.js": "^4.3.2",
"vue-jest": "~3.0.4", "vue-jest": "~3.0.4",
"vue-svg-loader": "~0.12.0" "vue-svg-loader": "~0.12.0"
} }

View File

@ -1,13 +1,13 @@
<template> <template>
<div> <div>
<ds-flex v-if="Post && Post.length" :width="{ base: '100%' }" gutter="base"> <ds-flex v-if="Post && Post.length" :width="{ base: '100%' }" gutter="base">
<ds-flex-item <hc-post-card
v-for="post in uniq(Post)" v-for="(post, index) in uniq(Post)"
:key="post.id" :key="post.id"
:post="post"
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }" :width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
> @deletePost="deletePost(index, post.id)"
<hc-post-card :post="post" /> />
</ds-flex-item>
</ds-flex> </ds-flex>
<no-ssr> <no-ssr>
<ds-button <ds-button
@ -78,6 +78,14 @@ export default {
fetchPolicy: 'cache-and-network', fetchPolicy: 'cache-and-network',
}) })
}, },
deletePost(_index, postId) {
this.Post = this.Post.filter(post => {
return post.id !== postId
})
// Why "uniq(Post)" is used in the array for list creation?
// Ideal solution here:
// this.Post.splice(index, 1)
},
}, },
apollo: { apollo: {
Post: { Post: {

View File

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

View File

@ -12,6 +12,7 @@
placement="bottom-end" placement="bottom-end"
resource-type="contribution" resource-type="contribution"
:resource="post" :resource="post"
:callbacks="{ confirm: () => deletePostCallback('page'), cancel: null }"
:is-owner="isAuthor(post.author.id)" :is-owner="isAuthor(post.author.id)"
/> />
</no-ssr> </no-ssr>
@ -71,8 +72,10 @@ import HcUser from '~/components/User'
import HcShoutButton from '~/components/ShoutButton.vue' import HcShoutButton from '~/components/ShoutButton.vue'
import HcCommentForm from '~/components/comments/CommentForm' import HcCommentForm from '~/components/comments/CommentForm'
import HcCommentList from '~/components/comments/CommentList' import HcCommentList from '~/components/comments/CommentList'
import PostMutationHelpers from '~/mixins/PostMutationHelpers'
export default { export default {
name: 'PostSlug',
transition: { transition: {
name: 'slide-up', name: 'slide-up',
mode: 'out-in', mode: 'out-in',
@ -86,6 +89,7 @@ export default {
HcCommentForm, HcCommentForm,
HcCommentList, HcCommentList,
}, },
mixins: [PostMutationHelpers],
head() { head() {
return { return {
title: this.title, title: this.title,

View File

@ -20,7 +20,7 @@
&nbsp; &nbsp;
<!--<ds-tag <!--<ds-tag
v-for="category in post.categories" v-for="category in post.categories"
:key="category.id"><ds-icon :name="category.icon" /> {{ category.name }}</ds-tag>--> :key="category.id"><ds-icon :name="category.icon" /> {{ category.name }}</ds-tag>-->
</div> </div>
<template v-if="post.tags && post.tags.length"> <template v-if="post.tags && post.tags.length">
<h3> <h3>
@ -37,13 +37,13 @@
<h3>Verwandte Beiträge</h3> <h3>Verwandte Beiträge</h3>
<ds-section style="margin: 0 -1.5rem; padding: 1.5rem;"> <ds-section style="margin: 0 -1.5rem; padding: 1.5rem;">
<ds-flex v-if="post.relatedContributions && post.relatedContributions.length" gutter="small"> <ds-flex v-if="post.relatedContributions && post.relatedContributions.length" gutter="small">
<ds-flex-item <hc-post-card
v-for="relatedPost in post.relatedContributions" v-for="(relatedPost, index) in post.relatedContributions"
:key="relatedPost.id" :key="relatedPost.id"
:post="relatedPost"
:width="{ base: '100%', lg: 1 }" :width="{ base: '100%', lg: 1 }"
> @deletePost="post.relatedContributions.splice(index, 1)"
<hc-post-card :post="relatedPost" /> />
</ds-flex-item>
</ds-flex> </ds-flex>
<hc-empty v-else margin="large" icon="file" message="No related Posts" /> <hc-empty v-else margin="large" icon="file" message="No related Posts" />
</ds-section> </ds-section>

View File

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

View File

@ -17,6 +17,7 @@
placement="bottom-end" placement="bottom-end"
resource-type="user" resource-type="user"
:resource="user" :resource="user"
:callbacks="{ confirm: deletePostCallback, cancel: null }"
:is-owner="myProfile" :is-owner="myProfile"
class="user-content-menu" class="user-content-menu"
/> />
@ -183,13 +184,13 @@
/> />
</ds-flex-item> </ds-flex-item>
<template v-if="activePosts.length"> <template v-if="activePosts.length">
<ds-flex-item <hc-post-card
v-for="post in activePosts" v-for="(post, index) in activePosts"
:key="post.id" :key="post.id"
:post="post"
:width="{ base: '100%', md: '100%', xl: '50%' }" :width="{ base: '100%', md: '100%', xl: '50%' }"
> @deletePost="user.contributions.splice(index, 1)"
<hc-post-card :post="post" /> />
</ds-flex-item>
</template> </template>
<template v-else> <template v-else>
<ds-flex-item :width="{ base: '100%' }"> <ds-flex-item :width="{ base: '100%' }">
@ -216,6 +217,7 @@ import HcEmpty from '~/components/Empty.vue'
import ContentMenu from '~/components/ContentMenu' import ContentMenu from '~/components/ContentMenu'
import HcUpload from '~/components/Upload' import HcUpload from '~/components/Upload'
import HcAvatar from '~/components/Avatar/Avatar.vue' import HcAvatar from '~/components/Avatar/Avatar.vue'
import PostMutationHelpers from '~/mixins/PostMutationHelpers'
export default { export default {
components: { components: {
@ -230,6 +232,7 @@ export default {
ContentMenu, ContentMenu,
HcUpload, HcUpload,
}, },
mixins: [PostMutationHelpers],
transition: { transition: {
name: 'slide-up', name: 'slide-up',
mode: 'out-in', mode: 'out-in',

View File

@ -8931,10 +8931,10 @@ proper-lockfile@^4.1.1:
retry "^0.12.0" retry "^0.12.0"
signal-exit "^3.0.2" signal-exit "^3.0.2"
prosemirror-collab@^1.1.1: prosemirror-collab@^1.1.2:
version "1.1.1" version "1.1.2"
resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.1.1.tgz#c8f5d951abaeac8a80818b6bd960f5a392b35b3f" resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.1.2.tgz#622fdc52692a83045ba6914c01a0416ff35f646a"
integrity sha512-BpXIB3WBD7UvgxuiasKOxlAZ78TTOdW+SQN4bbJan995tVx/wM/OZXtRJebS+tSWWAbRisHaO3ciFo732vuvdA== integrity sha512-ew0p0XWlp3PYc4h20hOfix8UPSRaJMRHQQCMoIUzxoiBgGGij/N4pXIY+w/iw5JISgP8QYyOy5arOYnCzFJQlQ==
dependencies: dependencies:
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
@ -9014,10 +9014,10 @@ prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.2.3:
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0" prosemirror-transform "^1.0.0"
prosemirror-tables@^0.8.0: prosemirror-tables@^0.8.0, prosemirror-tables@^0.8.1:
version "0.8.0" version "0.8.1"
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-0.8.0.tgz#30d3adb388b3310b47605e615753521a2a4ca77c" resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-0.8.1.tgz#ea99ad4effec99dd4e2fdb0b33cce4d2547eed83"
integrity sha512-D4okaUqh9wQPkuimkgbNExy1aG2z+BtsUDtacZHPL8tDB5rbFuE+cotjJPIUSK3Ky5gvI8it4gL1+Xo//nqguw== integrity sha512-6eY8I+NkyrXAQ1gmYkKo7XDLZaj0iGutdc/zT0+VMY15IzgBINwcRP62+miaCTuneLTKufMYzfUB37NjGJaetw==
dependencies: dependencies:
prosemirror-keymap "^1.0.0" prosemirror-keymap "^1.0.0"
prosemirror-model "^1.0.0" prosemirror-model "^1.0.0"
@ -9037,10 +9037,15 @@ prosemirror-utils@^0.8.2:
resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.8.2.tgz#e0e4a47cd45b1cff3d84464446d9f92adf4a558b" resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.8.2.tgz#e0e4a47cd45b1cff3d84464446d9f92adf4a558b"
integrity sha512-jNIj3/eREx4x2FU6pFEUDmdVmtoRBuLA6JTjgNum/84Nf+Ns+Y9l0Q//R8EL/Qm/5J5xTg5/s+hmQkXaHY+ilA== integrity sha512-jNIj3/eREx4x2FU6pFEUDmdVmtoRBuLA6JTjgNum/84Nf+Ns+Y9l0Q//R8EL/Qm/5J5xTg5/s+hmQkXaHY+ilA==
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.9.6: prosemirror-utils@^0.9.0:
version "1.9.6" version "0.9.0"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.9.6.tgz#8bf508b0e2d176cb7a80a2a7882d3cb3749ab4e9" resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.9.0.tgz#3ab616c94ccd61fcb18968f0d5aa273a9f1f28e4"
integrity sha512-b4yBGeq9jvmW+rvC48fjHL4+9zyT/6NSuaACT4bcL8LuLHYzb6KSSZaZ5v27oUPt2DXIYv3Nl/YoUxJa7r8ciA== integrity sha512-YcvmHcq7phbn+OagJSvmne92qZG9dOVfb3zfuA1HuyWUif3hUDt2Yfu299BHqVkEkUCF6FN7Gi9folDQntMhxA==
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.9.6, prosemirror-view@^1.9.8:
version "1.9.8"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.9.8.tgz#47b961204a0b2e8ff87370c270d4f82598e81273"
integrity sha512-yS4yrqxydvi7ddz9VFLeJgbfVd5g3/bMcRxb1PbWtG0i9OrPSsiHaEBJHLVeTbraGqRlAu+tbNLakO7RhUhp1w==
dependencies: dependencies:
prosemirror-model "^1.1.0" prosemirror-model "^1.1.0"
prosemirror-state "^1.0.0" prosemirror-state "^1.0.0"
@ -10550,43 +10555,43 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tippy.js@^4.3.1: tippy.js@^4.3.2:
version "4.3.1" version "4.3.2"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-4.3.1.tgz#ea38fa7e1a2e3448ac35faa5115ccbb8414f50aa" resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-4.3.2.tgz#f785d96fd03d890aa118646e1a873f851bd1c8b4"
integrity sha512-H09joePakSu6eDSL1wj5LjEWwvpEELyJQlgsts4wVH7223t4DlyzGCaZNDO8/MQAnSuic4JhKpXtgzSYGlobvg== integrity sha512-vSdVU8zkhsdCFegwtKq7WJfF29xo4Qiq5GWPZEjKbW4knogI43HJHPAOCUkxbi28gKTTgiWF+GveZgTqhS9QOw==
dependencies: dependencies:
popper.js "^1.14.7" popper.js "^1.14.7"
tiptap-commands@^1.10.5: tiptap-commands@^1.10.5, tiptap-commands@^1.10.6:
version "1.10.5" version "1.10.6"
resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.10.5.tgz#e897b59debdddcbc20f8289c92f9e39c5d22e19a" resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.10.6.tgz#46f972aacbc8d175248ab7ed7e6183ebc6cc72ed"
integrity sha512-4OTdcf8IIDyd/CrRvJLfQpYaw+SGIuUJmwDDpxNCbcqnpHFsO1IkWdmV3loRab6Adg1UBNxeUKA8//nzpdJcUQ== integrity sha512-62GrTo3Mmev3AmN0rFDa0gzUFQyN9yTjpuH6xMTo0OqMx6iTluqxdiROB2Hc+9qVCHj6qFwJIG4t8jPrYiuKuw==
dependencies: dependencies:
prosemirror-commands "^1.0.8" prosemirror-commands "^1.0.8"
prosemirror-inputrules "^1.0.4" prosemirror-inputrules "^1.0.4"
prosemirror-model "^1.7.0" prosemirror-model "^1.7.0"
prosemirror-schema-list "^1.0.3" prosemirror-schema-list "^1.0.3"
prosemirror-state "^1.2.3" prosemirror-state "^1.2.3"
prosemirror-tables "^0.8.0" prosemirror-tables "^0.8.1"
prosemirror-utils "^0.8.2" prosemirror-utils "^0.9.0"
tiptap-utils "^1.5.3" tiptap-utils "^1.5.4"
tiptap-extensions@1.20.1: tiptap-extensions@1.20.2:
version "1.20.1" version "1.20.2"
resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.20.1.tgz#fee879f27d2016176dda57d7d0ecef0e457ad3bf" resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.20.2.tgz#820c8b1c0c3d2f2176a634c428c22b425f7bbc5f"
integrity sha512-YyWKvZe6AMU6PeyKWqHg545/8OYbNWYOWmnDm3DWhQICBOnx0Oj7rYnuXyfDFLPeqD38KtSlDXGOvcpzENygXg== integrity sha512-DG5ba2DRKJS11T9B8RMtR/YMkU0PNM25pgDVJ/9MRWBFdD6WFZjyi+fEB9u0uaXRlnyJnYUiV3BScj97UsWl0g==
dependencies: dependencies:
lowlight "^1.12.1" lowlight "^1.12.1"
prosemirror-collab "^1.1.1" prosemirror-collab "^1.1.2"
prosemirror-history "^1.0.4" prosemirror-history "^1.0.4"
prosemirror-model "^1.7.0" prosemirror-model "^1.7.0"
prosemirror-state "^1.2.3" prosemirror-state "^1.2.3"
prosemirror-tables "^0.8.0" prosemirror-tables "^0.8.1"
prosemirror-transform "^1.1.3" prosemirror-transform "^1.1.3"
prosemirror-utils "^0.8.2" prosemirror-utils "^0.9.0"
prosemirror-view "^1.9.6" prosemirror-view "^1.9.8"
tiptap "^1.20.1" tiptap "^1.21.0"
tiptap-commands "^1.10.5" tiptap-commands "^1.10.6"
tiptap-utils@^1.5.3: tiptap-utils@^1.5.3:
version "1.5.3" version "1.5.3"
@ -10598,7 +10603,17 @@ tiptap-utils@^1.5.3:
prosemirror-tables "^0.8.0" prosemirror-tables "^0.8.0"
prosemirror-utils "^0.8.2" prosemirror-utils "^0.8.2"
tiptap@1.20.1, tiptap@^1.20.1: tiptap-utils@^1.5.4:
version "1.5.4"
resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.5.4.tgz#c64b65d305ee70793376c9cec1da242ebf6e1884"
integrity sha512-Tl74RM7HZ3v9Eut+cE3DKb+uWM6k0sGvurs9eyCgokrgYmxMMa3CzH1e2c2cmyMB4ErLiY/5v5eMgBKmIZK5Vg==
dependencies:
prosemirror-model "^1.7.0"
prosemirror-state "^1.2.3"
prosemirror-tables "^0.8.1"
prosemirror-utils "^0.9.0"
tiptap@1.20.1:
version "1.20.1" version "1.20.1"
resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.20.1.tgz#d10fd0cd73a96bbb1f2d581da02ceda38fa8695b" resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.20.1.tgz#d10fd0cd73a96bbb1f2d581da02ceda38fa8695b"
integrity sha512-uVGxPknq+cQH0G8yyCHvo8p3jPMLZMnkLeFjcrTyiY9PXl6XsSJwOjtIg4GXnIyCcfz2jWI5mhJGzCD26cdJGA== integrity sha512-uVGxPknq+cQH0G8yyCHvo8p3jPMLZMnkLeFjcrTyiY9PXl6XsSJwOjtIg4GXnIyCcfz2jWI5mhJGzCD26cdJGA==
@ -10614,6 +10629,22 @@ tiptap@1.20.1, tiptap@^1.20.1:
tiptap-commands "^1.10.5" tiptap-commands "^1.10.5"
tiptap-utils "^1.5.3" tiptap-utils "^1.5.3"
tiptap@^1.21.0:
version "1.21.0"
resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.21.0.tgz#4d8c1365c611e41c8d4f3d7aa195ddaf891e605b"
integrity sha512-MoOj/8OPMlmoAotIZjAIlUZ59yMMR83xReOw2rGjqbFOooncoY1rLEBp0xz5oe5FLYqoe8dKb+kzOoFERqckVQ==
dependencies:
prosemirror-commands "^1.0.8"
prosemirror-dropcursor "^1.1.1"
prosemirror-gapcursor "^1.0.3"
prosemirror-inputrules "^1.0.4"
prosemirror-keymap "^1.0.1"
prosemirror-model "^1.7.0"
prosemirror-state "^1.2.3"
prosemirror-view "^1.9.8"
tiptap-commands "^1.10.6"
tiptap-utils "^1.5.4"
tmp@^0.0.33: tmp@^0.0.33:
version "0.0.33" version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"