diff --git a/backend/neo4j/Dockerfile b/backend/neo4j/Dockerfile index f6e71811b..a02d41f38 100644 --- a/backend/neo4j/Dockerfile +++ b/backend/neo4j/Dockerfile @@ -1,3 +1,3 @@ -FROM neo4j:3.5.0 +FROM neo4j:3.5.4 RUN wget https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/3.5.0.1/apoc-3.5.0.1-all.jar -P plugins/ COPY migrate.sh /usr/local/bin/migrate diff --git a/backend/package.json b/backend/package.json index 2457b9dee..a3f03f1fb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,14 +10,13 @@ "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,graphql", "lint": "eslint src --config .eslintrc.js", "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", "test:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions,activityPub yarn run dev", "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:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand", "test:jest": "run-p --race test:before:* 'test:jest:cmd {@}' --", - "test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:server test:cucumber:before:seeder 'test:cucumber:cmd {@}' --", - "test:cucumber:before:seeder": "cross-env GRAPHQL_URI=http://localhost:4001 GRAPHQL_PORT=4001 DISABLED_MIDDLEWARES=permissions yarn run dev", + "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 {@}' --", "db:script:seed": "wait-on tcp:4001 && babel-node src/seed/seed-db.js", "db:reset": "babel-node src/seed/reset-db.js", @@ -39,7 +38,7 @@ "apollo-link-http": "~1.5.14", "apollo-server": "~2.4.8", "bcryptjs": "~2.4.3", - "cheerio": "~1.0.0-rc.2", + "cheerio": "~1.0.0-rc.3", "cors": "~2.8.5", "cross-env": "~5.2.0", "date-fns": "2.0.0-alpha.27", @@ -51,7 +50,7 @@ "graphql-custom-directives": "~0.2.14", "graphql-iso-date": "~3.6.1", "graphql-middleware": "~3.0.2", - "graphql-shield": "~5.3.1", + "graphql-shield": "~5.3.2", "graphql-tag": "~2.10.1", "graphql-yoga": "~1.17.4", "helmet": "~3.16.0", @@ -71,7 +70,7 @@ "wait-on": "~3.2.0" }, "devDependencies": { - "@babel/cli": "~7.2.3", + "@babel/cli": "~7.4.3", "@babel/core": "~7.4.3", "@babel/node": "~7.2.2", "@babel/plugin-proposal-throw-expressions": "^7.2.0", @@ -92,7 +91,7 @@ "eslint-plugin-standard": "~4.0.0", "graphql-request": "~1.8.2", "jest": "~24.7.1", - "nodemon": "~1.18.10", + "nodemon": "~1.18.11", "supertest": "~4.0.2" } -} +} \ No newline at end of file diff --git a/backend/src/graphql-schema.js b/backend/src/graphql-schema.js index 57b2ffb6c..c17b967d2 100644 --- a/backend/src/graphql-schema.js +++ b/backend/src/graphql-schema.js @@ -7,6 +7,7 @@ import reports from './resolvers/reports.js' import posts from './resolvers/posts.js' import moderation from './resolvers/moderation.js' import rewards from './resolvers/rewards.js' +import notifications from './resolvers/notifications' export const typeDefs = fs .readFileSync( @@ -17,13 +18,15 @@ export const typeDefs = fs export const resolvers = { Query: { ...statistics.Query, - ...userManagement.Query + ...userManagement.Query, + ...notifications.Query }, Mutation: { ...userManagement.Mutation, ...reports.Mutation, ...posts.Mutation, ...moderation.Mutation, - ...rewards.Mutation + ...rewards.Mutation, + ...notifications.Mutation } } diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 8f86a88e6..8d893a78b 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -10,6 +10,7 @@ import permissionsMiddleware from './permissionsMiddleware' import userMiddleware from './userMiddleware' import includedFieldsMiddleware from './includedFieldsMiddleware' import orderByMiddleware from './orderByMiddleware' +import notificationsMiddleware from './notificationsMiddleware' export default schema => { let middleware = [ @@ -19,6 +20,7 @@ export default schema => { excerptMiddleware, xssMiddleware, fixImageUrlsMiddleware, + notificationsMiddleware, softDeleteMiddleware, userMiddleware, includedFieldsMiddleware, diff --git a/backend/src/middleware/notifications/mentions.js b/backend/src/middleware/notifications/mentions.js new file mode 100644 index 000000000..137c23f1c --- /dev/null +++ b/backend/src/middleware/notifications/mentions.js @@ -0,0 +1,10 @@ +const MENTION_REGEX = /\s@([\w_-]+)/g + +export function extractSlugs (content) { + let slugs = [] + let match + while ((match = MENTION_REGEX.exec(content)) != null) { + slugs.push(match[1]) + } + return slugs +} diff --git a/backend/src/middleware/notifications/mentions.spec.js b/backend/src/middleware/notifications/mentions.spec.js new file mode 100644 index 000000000..f12df7f07 --- /dev/null +++ b/backend/src/middleware/notifications/mentions.spec.js @@ -0,0 +1,30 @@ +import { extractSlugs } from './mentions' + +describe('extract', () => { + describe('finds mentions in the form of', () => { + it('@user', () => { + const content = 'Hello @user' + expect(extractSlugs(content)).toEqual(['user']) + }) + + it('@user-with-dash', () => { + const content = 'Hello @user-with-dash' + expect(extractSlugs(content)).toEqual(['user-with-dash']) + }) + + it('@user.', () => { + const content = 'Hello @user.' + expect(extractSlugs(content)).toEqual(['user']) + }) + + it('@user-With-Capital-LETTERS', () => { + const content = 'Hello @user-With-Capital-LETTERS' + expect(extractSlugs(content)).toEqual(['user-With-Capital-LETTERS']) + }) + }) + + it('ignores email addresses', () => { + const content = 'Hello somebody@example.org' + expect(extractSlugs(content)).toEqual([]) + }) +}) diff --git a/backend/src/middleware/notificationsMiddleware.js b/backend/src/middleware/notificationsMiddleware.js new file mode 100644 index 000000000..30205278b --- /dev/null +++ b/backend/src/middleware/notificationsMiddleware.js @@ -0,0 +1,27 @@ +import { extractSlugs } from './notifications/mentions' + +const notify = async (resolve, root, args, context, resolveInfo) => { + const post = await resolve(root, args, context, resolveInfo) + + const session = context.driver.session() + const { content, id: postId } = post + const slugs = extractSlugs(content) + const createdAt = (new Date()).toISOString() + const cypher = ` + match(u:User) where u.slug in $slugs + match(p:Post) where p.id = $postId + create(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt}) + merge (n)-[:NOTIFIED]->(u) + merge (p)-[:NOTIFIED]->(n) + ` + await session.run(cypher, { slugs, createdAt, postId }) + session.close() + + return post +} + +export default { + Mutation: { + CreatePost: notify + } +} diff --git a/backend/src/middleware/notificationsMiddleware.spec.js b/backend/src/middleware/notificationsMiddleware.spec.js new file mode 100644 index 000000000..e6fc78c52 --- /dev/null +++ b/backend/src/middleware/notificationsMiddleware.spec.js @@ -0,0 +1,85 @@ +import Factory from '../seed/factories' +import { GraphQLClient } from 'graphql-request' +import { host, login } from '../jest/helpers' + +const factory = Factory() +let client + +beforeEach(async () => { + await factory.create('User', { + id: 'you', + name: 'Al Capone', + slug: 'al-capone', + email: 'test@example.org', + password: '1234' + }) +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('currentUser { notifications }', () => { + const query = `query($read: Boolean) { + currentUser { + notifications(read: $read, orderBy: createdAt_desc) { + read + post { + content + } + } + } + }` + + describe('authenticated', () => { + let headers + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + describe('given another user', () => { + let authorClient + let authorParams + let authorHeaders + + beforeEach(async () => { + authorParams = { + email: 'author@example.org', + password: '1234', + id: 'author' + } + await factory.create('User', authorParams) + authorHeaders = await login(authorParams) + }) + + describe('who mentions me in a post', () => { + beforeEach(async () => { + const content = 'Hey @al-capone how do you do?' + const title = 'Mentioning Al Capone' + const createPostMutation = ` + mutation($title: String!, $content: String!) { + CreatePost(title: $title, content: $content) { + title + content + } + } + ` + authorClient = new GraphQLClient(host, { headers: authorHeaders }) + await authorClient.request(createPostMutation, { title, content }) + }) + + it('sends you a notification', async () => { + const expected = { + currentUser: { + notifications: [ + { read: false, post: { content: 'Hey @al-capone how do you do?' } } + ] + } + } + await expect(client.request(query, { read: false })).resolves.toEqual(expected) + }) + }) + }) + }) +}) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 495bc9145..4ff334806 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -20,6 +20,21 @@ const isMyOwn = rule({ cache: 'no_cache' })(async (parent, args, context, info) return context.user.id === parent.id }) +const belongsToMe = rule({ cache: 'no_cache' })(async (_, args, context) => { + const { driver, user: { id: userId } } = context + const { id: notificationId } = args + const session = driver.session() + const result = await session.run(` + MATCH (u:User {id: $userId})<-[:NOTIFIED]-(n:Notification {id: $notificationId}) + RETURN n + `, { userId, notificationId }) + const [notification] = result.records.map((record) => { + return record.get('n') + }) + session.close() + return Boolean(notification) +}) + const onlyEnabledContent = rule({ cache: 'strict' })(async (parent, args, ctx, info) => { const { disabled, deleted } = args return !(disabled || deleted) @@ -50,6 +65,7 @@ const permissions = shield({ Post: or(onlyEnabledContent, isModerator) }, Mutation: { + UpdateNotification: belongsToMe, CreatePost: isAuthenticated, UpdatePost: isAuthor, DeletePost: isAuthor, diff --git a/backend/src/resolvers/notifications.js b/backend/src/resolvers/notifications.js new file mode 100644 index 000000000..bc3da0acf --- /dev/null +++ b/backend/src/resolvers/notifications.js @@ -0,0 +1,14 @@ +import { neo4jgraphql } from 'neo4j-graphql-js' + +export default { + Query: { + Notification: (object, params, context, resolveInfo) => { + return neo4jgraphql(object, params, context, resolveInfo, false) + } + }, + Mutation: { + UpdateNotification: (object, params, context, resolveInfo) => { + return neo4jgraphql(object, params, context, resolveInfo, false) + } + } +} diff --git a/backend/src/resolvers/notifications.spec.js b/backend/src/resolvers/notifications.spec.js index 50ded7bc4..799bc1594 100644 --- a/backend/src/resolvers/notifications.spec.js +++ b/backend/src/resolvers/notifications.spec.js @@ -5,13 +5,14 @@ import { host, login } from '../jest/helpers' const factory = Factory() let client +let userParams = { + id: 'you', + email: 'test@example.org', + password: '1234' +} beforeEach(async () => { - await factory.create('User', { - id: 'you', - email: 'test@example.org', - password: '1234' - }) + await factory.create('User', userParams) }) afterEach(async () => { @@ -118,3 +119,63 @@ describe('currentUser { notifications }', () => { }) }) }) + +describe('UpdateNotification', () => { + const mutation = `mutation($id: ID!, $read: Boolean){ + UpdateNotification(id: $id, read: $read) { + id read + } + }` + const variables = { id: 'to-be-updated', read: true } + + describe('given a notifications', () => { + let headers + + beforeEach(async () => { + const mentionedParams = { + id: 'mentioned-1', + email: 'mentioned@example.org', + password: '1234', + slug: 'mentioned' + } + await factory.create('User', mentionedParams) + await factory.create('Notification', { id: 'to-be-updated' }) + await factory.authenticateAs(userParams) + await factory.create('Post', { id: 'p1' }) + await Promise.all([ + factory.relate('Notification', 'User', { from: 'to-be-updated', to: 'mentioned-1' }), + factory.relate('Notification', 'Post', { from: 'p1', to: 'to-be-updated' }) + ]) + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorization error', async () => { + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + }) + + describe('and owner', () => { + beforeEach(async () => { + headers = await login({ email: 'mentioned@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('updates notification', async () => { + const expected = { UpdateNotification: { id: 'to-be-updated', read: true } } + await expect(client.request(mutation, variables)).resolves.toEqual(expected) + }) + }) + }) + }) +}) diff --git a/backend/src/server.js b/backend/src/server.js index efa9a17c0..fe0d4ee1d 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -28,10 +28,10 @@ let schema = makeAugmentedSchema({ resolvers, config: { query: { - exclude: ['Statistics', 'LoggedInUser'] + exclude: ['Notfication', 'Statistics', 'LoggedInUser'] }, mutation: { - exclude: ['Statistics', 'LoggedInUser'] + exclude: ['Notfication', 'Statistics', 'LoggedInUser'] }, debug: debug } diff --git a/backend/yarn.lock b/backend/yarn.lock index 68e6cb931..29de5d201 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -14,22 +14,22 @@ resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.6.tgz#022209e28a2b547dcde15b219f0c50f47aa5beb3" integrity sha512-lqK94b+caNtmKFs5oUVXlSpN3sm5IXZ+KfhMxOtr0LR2SqErzkoJilitjDvJ1WbjHlxLI7WtCjRmOLdOGJqtMQ== -"@babel/cli@~7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.2.3.tgz#1b262e42a3e959d28ab3d205ba2718e1923cfee6" - integrity sha512-bfna97nmJV6nDJhXNPeEfxyMjWnt6+IjUAaDPiYRTBlm8L41n8nvw6UAqUCbvpFfU246gHPxW7sfWwqtF4FcYA== +"@babel/cli@~7.4.3": + version "7.4.3" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.4.3.tgz#353048551306ff42e5855b788b6ccd9477289774" + integrity sha512-cbC5H9iTDV9H7sMxK5rUm18UbdVPNTPqgdzmQAkOUP3YLysgDWLZaysVAfylK49rgTlzL01a6tXyq9rCb3yLhQ== dependencies: commander "^2.8.1" convert-source-map "^1.1.0" fs-readdir-recursive "^1.1.0" glob "^7.0.0" - lodash "^4.17.10" + lodash "^4.17.11" mkdirp "^0.5.1" output-file-sync "^2.0.0" slash "^2.0.0" source-map "^0.5.0" optionalDependencies: - chokidar "^2.0.3" + chokidar "^2.0.4" "@babel/code-frame@^7.0.0": version "7.0.0" @@ -1104,6 +1104,11 @@ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.9.tgz#693e76a52f61a2f1e7fb48c0eef167b95ea4ffd0" integrity sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA== +"@types/yup@0.26.9": + version "0.26.9" + resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.9.tgz#8a619ac4d2b8dcacb0d81345746018303b479919" + integrity sha512-C7HdLLs1ZNPbYeNsSX++fMosxWAwzVeUs9wc76XlKJrKvLEyNwXMDUjag75EVAPxlZ36YiRJ6iTy4zc5Dbtndw== + "@types/zen-observable@^0.5.3": version "0.5.4" resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.5.4.tgz#b863a4191e525206819e008097ebf0fb2e3a1cdc" @@ -2132,22 +2137,22 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= -cheerio@~1.0.0-rc.2: - version "1.0.0-rc.2" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db" - integrity sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs= +cheerio@~1.0.0-rc.3: + version "1.0.0-rc.3" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6" + integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA== dependencies: css-select "~1.2.0" - dom-serializer "~0.1.0" + dom-serializer "~0.1.1" entities "~1.1.1" htmlparser2 "^3.9.1" lodash "^4.15.0" parse5 "^3.0.1" -chokidar@^2.0.3, chokidar@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.0.tgz#5fcb70d0b28ebe0867eb0f09d5f6a08f29a1efa0" - integrity sha512-5t6G2SH8eO6lCvYOoUpaRnF5Qfd//gd7qJAkwRUw9qlGVkiQ13uwQngqbWWaurOsaAm9+kUGbITADxt6H0XFNQ== +chokidar@^2.0.4, chokidar@^2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.5.tgz#0ae8434d962281a5f56c72869e79cb6d9d86ad4d" + integrity sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A== dependencies: anymatch "^2.0.0" async-each "^1.0.1" @@ -2159,7 +2164,7 @@ chokidar@^2.0.3, chokidar@^2.1.0: normalize-path "^3.0.0" path-is-absolute "^1.0.0" readdirp "^2.2.1" - upath "^1.1.0" + upath "^1.1.1" optionalDependencies: fsevents "^1.2.7" @@ -2727,24 +2732,19 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-serializer@0, dom-serializer@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" - integrity sha1-BzxpdUbOB4DOI75KKOKT5AvDDII= +dom-serializer@0, dom-serializer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== dependencies: - domelementtype "~1.1.1" - entities "~1.1.1" + domelementtype "^1.3.0" + entities "^1.1.1" domelementtype@1, domelementtype@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== -domelementtype@~1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" - integrity sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs= - domexception@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" @@ -3743,11 +3743,12 @@ graphql-request@~1.8.2: dependencies: cross-fetch "2.2.2" -graphql-shield@~5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.3.1.tgz#34cff4d1bfdcc3caa6fc348afb11503dde1893cd" - integrity sha512-vVJ7rjkR7miWi/Zspr7/ibmtdL2gEHagCtpsJY534DyRE70r+PurCp2kR/e1fZhb4JdmTYCS+sokyYfH974/+w== +graphql-shield@~5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-5.3.2.tgz#2d47907ed9882a0636cb8ade6087123309d215ef" + integrity sha512-fib7rSr5aS/WHL3+Aa5LXhcCuPGEIDXmzfGtFjUXkUiZ6E5u+bDSL+9KRXo/p14A28GkJF+1Vu1hlg9H/QFG1w== dependencies: + "@types/yup" "0.26.9" lightercollective "^0.2.0" object-hash "^1.3.1" yup "^0.27.0" @@ -5677,12 +5678,12 @@ node-releases@^1.1.13: dependencies: semver "^5.3.0" -nodemon@~1.18.10: - version "1.18.10" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.18.10.tgz#3ba63f64eb4c283cf3e4f75f30817e9d4f393afe" - integrity sha512-we51yBb1TfEvZamFchRgcfLbVYgg0xlGbyXmOtbBzDwxwgewYS/YbZ5tnlnsH51+AoSTTsT3A2E/FloUbtH8cQ== +nodemon@~1.18.11: + version "1.18.11" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.18.11.tgz#d836ab663776e7995570b963da5bfc807e53f6b8" + integrity sha512-KdN3tm1zkarlqNo4+W9raU3ihM4H15MVMSE/f9rYDZmFgDHAfAJsomYrHhApAkuUemYjFyEeXlpCOQ2v5gtBEw== dependencies: - chokidar "^2.1.0" + chokidar "^2.1.5" debug "^3.1.0" ignore-by-default "^1.0.1" minimatch "^3.0.4" @@ -7643,10 +7644,10 @@ unzip-response@^2.0.1: resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c= -upath@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" - integrity sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw== +upath@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" + integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== update-notifier@^2.5.0: version "2.5.0" diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 8944b7c25..9478d8d4e 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -228,7 +228,7 @@ Then('I get redirected to {string}', route => { }) Then('the post was saved successfully', () => { - cy.get('.ds-card-header > .ds-heading').should('contain', lastPost.title) + cy.get('.ds-card-content > .ds-heading').should('contain', lastPost.title) cy.get('.content').should('contain', lastPost.content) }) diff --git a/package.json b/package.json index be1e4a90d..b22dd158c 100644 --- a/package.json +++ b/package.json @@ -25,4 +25,4 @@ "neo4j-driver": "^1.7.3", "npm-run-all": "^4.1.5" } -} +} \ No newline at end of file diff --git a/webapp/components/Category/Readme.md b/webapp/components/Category/Readme.md new file mode 100644 index 000000000..50e07f966 --- /dev/null +++ b/webapp/components/Category/Readme.md @@ -0,0 +1,7 @@ +### Example + +Category "IT, Internet & Data Privacy" with icon "mouse-cursor" + +``` + +``` \ No newline at end of file diff --git a/webapp/components/Category/index.spec.js b/webapp/components/Category/index.spec.js new file mode 100644 index 000000000..149f96189 --- /dev/null +++ b/webapp/components/Category/index.spec.js @@ -0,0 +1,35 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils' +import Styleguide from '@human-connection/styleguide' +import Category from './index' + +const localVue = createLocalVue() +localVue.use(Styleguide) + +describe('Category', () => { + let icon + let name + + let Wrapper = () => { + return shallowMount(Category, { + localVue, + propsData: { + icon, + name + } + }) + } + + describe('given Strings for Icon and Name', () => { + beforeEach(() => { + icon = 'mouse-cursor' + name = 'Peter' + }) + + it('shows Name', () => { + expect(Wrapper().text()).toContain('Peter') + }) + it('shows Icon Svg', () => { + expect(Wrapper().contains('svg')) + }) + }) +}) diff --git a/webapp/components/Category/index.vue b/webapp/components/Category/index.vue new file mode 100644 index 000000000..af602d4d0 --- /dev/null +++ b/webapp/components/Category/index.vue @@ -0,0 +1,19 @@ + + + diff --git a/webapp/components/Comment.vue b/webapp/components/Comment.vue index 71043e90d..13edc9c0d 100644 --- a/webapp/components/Comment.vue +++ b/webapp/components/Comment.vue @@ -4,16 +4,15 @@ style="padding-left: 40px; font-weight: bold;" color="soft" > - {{ this.$t('comment.content.unavailable-placeholder') }} + + {{ this.$t('comment.content.unavailable-placeholder') }}
- + @@ -32,13 +31,13 @@ style="padding-left: 40px;" v-html="comment.contentExcerpt" /> - +
diff --git a/webapp/components/Tag/Readme.md b/webapp/components/Tag/Readme.md new file mode 100644 index 000000000..359f2487d --- /dev/null +++ b/webapp/components/Tag/Readme.md @@ -0,0 +1,7 @@ +### Example + +Tag "Liebe" + +``` + +``` \ No newline at end of file diff --git a/webapp/components/Tag/index.spec.js b/webapp/components/Tag/index.spec.js new file mode 100644 index 000000000..20267e375 --- /dev/null +++ b/webapp/components/Tag/index.spec.js @@ -0,0 +1,29 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils' +import Styleguide from '@human-connection/styleguide' +import Tag from './index' + +const localVue = createLocalVue() +localVue.use(Styleguide) + +describe('Tag', () => { + let name + + let Wrapper = () => { + return shallowMount(Tag, { + localVue, + propsData: { + name + } + }) + } + + describe('given a String for Name', () => { + beforeEach(() => { + name = 'Liebe' + }) + + it('shows Name', () => { + expect(Wrapper().text()).toContain('Liebe') + }) + }) +}) diff --git a/webapp/components/Tag/index.vue b/webapp/components/Tag/index.vue new file mode 100644 index 000000000..70e1cf2a2 --- /dev/null +++ b/webapp/components/Tag/index.vue @@ -0,0 +1,15 @@ + + + diff --git a/webapp/components/User.spec.js b/webapp/components/User/index.spec.js similarity index 97% rename from webapp/components/User.spec.js rename to webapp/components/User/index.spec.js index 0bbf13529..9f45ae83a 100644 --- a/webapp/components/User.spec.js +++ b/webapp/components/User/index.spec.js @@ -1,5 +1,5 @@ import { config, mount, createLocalVue, RouterLinkStub } from '@vue/test-utils' -import User from './User.vue' +import User from './index' import Vue from 'vue' import Vuex from 'vuex' import VTooltip from 'v-tooltip' @@ -15,7 +15,7 @@ localVue.use(Styleguide) localVue.filter('truncate', filter) -describe('User.vue', () => { +describe('User', () => { let wrapper let Wrapper let propsData diff --git a/webapp/components/User.vue b/webapp/components/User/index.vue similarity index 80% rename from webapp/components/User.vue rename to webapp/components/User/index.vue index dd176a67d..6b0731981 100644 --- a/webapp/components/User.vue +++ b/webapp/components/User/index.vue @@ -1,6 +1,8 @@ -