mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
Merge remote-tracking branch 'origin/master' into 342-merge_documentation
This commit is contained in:
commit
d3ff3ca241
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
10
backend/src/middleware/notifications/mentions.js
Normal file
10
backend/src/middleware/notifications/mentions.js
Normal file
@ -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
|
||||
}
|
||||
30
backend/src/middleware/notifications/mentions.spec.js
Normal file
30
backend/src/middleware/notifications/mentions.spec.js
Normal file
@ -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([])
|
||||
})
|
||||
})
|
||||
27
backend/src/middleware/notificationsMiddleware.js
Normal file
27
backend/src/middleware/notificationsMiddleware.js
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
85
backend/src/middleware/notificationsMiddleware.spec.js
Normal file
85
backend/src/middleware/notificationsMiddleware.spec.js
Normal file
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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,
|
||||
|
||||
14
backend/src/resolvers/notifications.js
Normal file
14
backend/src/resolvers/notifications.js
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
|
||||
@ -25,4 +25,4 @@
|
||||
"neo4j-driver": "^1.7.3",
|
||||
"npm-run-all": "^4.1.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
webapp/components/Category/Readme.md
Normal file
7
webapp/components/Category/Readme.md
Normal file
@ -0,0 +1,7 @@
|
||||
### Example
|
||||
|
||||
Category "IT, Internet & Data Privacy" with icon "mouse-cursor"
|
||||
|
||||
```
|
||||
<hc-category icon="mouse-pointer" name="IT, Internet & Data Privacy" />
|
||||
```
|
||||
35
webapp/components/Category/index.spec.js
Normal file
35
webapp/components/Category/index.spec.js
Normal file
@ -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'))
|
||||
})
|
||||
})
|
||||
})
|
||||
19
webapp/components/Category/index.vue
Normal file
19
webapp/components/Category/index.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<ds-tag>
|
||||
<ds-icon
|
||||
size="large"
|
||||
:name="icon"
|
||||
/>
|
||||
{{ name }}
|
||||
</ds-tag>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HcCategory',
|
||||
props: {
|
||||
icon: { type: String, required: true },
|
||||
name: { type: String, required: true }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -4,16 +4,15 @@
|
||||
style="padding-left: 40px; font-weight: bold;"
|
||||
color="soft"
|
||||
>
|
||||
<ds-icon name="ban" /> {{ this.$t('comment.content.unavailable-placeholder') }}
|
||||
<ds-icon name="ban" />
|
||||
{{ this.$t('comment.content.unavailable-placeholder') }}
|
||||
</ds-text>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="{'comment': true, 'disabled-content': (comment.deleted || comment.disabled)}"
|
||||
>
|
||||
<ds-space
|
||||
margin-bottom="x-small"
|
||||
>
|
||||
<ds-space margin-bottom="x-small">
|
||||
<hc-user :user="author" />
|
||||
</ds-space>
|
||||
<no-ssr>
|
||||
@ -32,13 +31,13 @@
|
||||
style="padding-left: 40px;"
|
||||
v-html="comment.contentExcerpt"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import HcUser from '~/components/User.vue'
|
||||
import HcUser from '~/components/User'
|
||||
import ContentMenu from '~/components/ContentMenu'
|
||||
|
||||
export default {
|
||||
|
||||
@ -111,9 +111,8 @@ export default {
|
||||
|
||||
if (this.isOwner && this.resourceType === 'user') {
|
||||
routes.push({
|
||||
name: this.$t(`settings.data.name`),
|
||||
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||
callback: () => this.$router.push('/settings'),
|
||||
name: this.$t(`settings.name`),
|
||||
path: '/settings',
|
||||
icon: 'edit'
|
||||
})
|
||||
}
|
||||
|
||||
@ -8,9 +8,7 @@
|
||||
v-router-link
|
||||
class="post-link"
|
||||
:href="href(post)"
|
||||
>
|
||||
{{ post.title }}
|
||||
</a>
|
||||
>{{ post.title }}</a>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<!-- TODO: replace editor content with tiptap render view -->
|
||||
<ds-space margin-bottom="large">
|
||||
@ -32,7 +30,7 @@
|
||||
</ds-space>
|
||||
<ds-space
|
||||
margin="small"
|
||||
style="position: absolute; bottom: 44px; z-index: 1;"
|
||||
style="position: absolute; bottom: 44px;"
|
||||
>
|
||||
<!-- TODO: find better solution for rendering errors -->
|
||||
<no-ssr>
|
||||
@ -53,11 +51,13 @@
|
||||
</div>
|
||||
<div style="display: inline-block; float: right">
|
||||
<span :style="{ opacity: post.shoutedCount ? 1 : .5 }">
|
||||
<ds-icon name="bullhorn" /> <small>{{ post.shoutedCount }}</small>
|
||||
<ds-icon name="bullhorn" />
|
||||
<small>{{ post.shoutedCount }}</small>
|
||||
</span>
|
||||
|
||||
<span :style="{ opacity: post.commentsCount ? 1 : .5 }">
|
||||
<ds-icon name="comments" /> <small>{{ post.commentsCount }}</small>
|
||||
<ds-icon name="comments" />
|
||||
<small>{{ post.commentsCount }}</small>
|
||||
</span>
|
||||
<no-ssr>
|
||||
<content-menu
|
||||
@ -72,7 +72,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HcUser from '~/components/User.vue'
|
||||
import HcUser from '~/components/User'
|
||||
import ContentMenu from '~/components/ContentMenu'
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
@ -118,27 +118,25 @@ export default {
|
||||
.post-card {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.ds-card-footer {
|
||||
z-index: 1;
|
||||
}
|
||||
/*.ds-card-footer {
|
||||
}*/
|
||||
|
||||
.content-menu {
|
||||
display: inline-block;
|
||||
margin-left: $space-xx-small;
|
||||
margin-right: -$space-x-small;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.post-link {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-indent: -999999px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-link {
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-indent: -999999px;
|
||||
}
|
||||
</style>
|
||||
|
||||
7
webapp/components/RelativeDateTime/Readme.md
Normal file
7
webapp/components/RelativeDateTime/Readme.md
Normal file
@ -0,0 +1,7 @@
|
||||
### Example
|
||||
|
||||
Relative time from 08.03.2017
|
||||
|
||||
```
|
||||
<hc-relative-date-time dateTime="03.08.2017" />
|
||||
```
|
||||
79
webapp/components/RelativeDateTime/index.spec.js
Normal file
79
webapp/components/RelativeDateTime/index.spec.js
Normal file
@ -0,0 +1,79 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils'
|
||||
import RelativeDateTime from './index'
|
||||
|
||||
const localVue = createLocalVue()
|
||||
|
||||
describe('RelativeDateTime', () => {
|
||||
let wrapper
|
||||
let mocks
|
||||
let locale
|
||||
let dateTime
|
||||
|
||||
beforeEach(() => {
|
||||
mocks = {
|
||||
$i18n: {
|
||||
locale: () => locale
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let Wrapper = () => {
|
||||
return shallowMount(RelativeDateTime, {
|
||||
mocks,
|
||||
localVue,
|
||||
propsData: {
|
||||
dateTime
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('given a String as dateTime', () => {
|
||||
beforeEach(() => {
|
||||
dateTime = '08.03.2017'
|
||||
})
|
||||
|
||||
it('translates', () => {
|
||||
expect(Wrapper().text()).toContain('08/03/2017')
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a Date object as dateTime', () => {
|
||||
beforeEach(() => {
|
||||
dateTime = new Date()
|
||||
})
|
||||
|
||||
it('renders', () => {
|
||||
expect(Wrapper().is('span')).toBe(true)
|
||||
})
|
||||
|
||||
describe("locale == 'en'", () => {
|
||||
beforeEach(() => {
|
||||
locale = 'en'
|
||||
})
|
||||
|
||||
it('translates', () => {
|
||||
expect(Wrapper().text()).toContain('today at')
|
||||
})
|
||||
})
|
||||
|
||||
describe("locale == 'gibberish'", () => {
|
||||
beforeEach(() => {
|
||||
locale = 'gibberish'
|
||||
})
|
||||
|
||||
it('translates', () => {
|
||||
expect(Wrapper().text()).toContain('today at')
|
||||
})
|
||||
})
|
||||
|
||||
describe("locale == 'de'", () => {
|
||||
beforeEach(() => {
|
||||
locale = 'de'
|
||||
})
|
||||
|
||||
it('translates', () => {
|
||||
expect(Wrapper().text()).toContain('heute um')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
33
webapp/components/RelativeDateTime/index.vue
Normal file
33
webapp/components/RelativeDateTime/index.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<span>{{ relativeDateTime }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import formatRelative from 'date-fns/formatRelative'
|
||||
import { enUS, de, nl, fr, pt, es /*, pl*/ } from 'date-fns/locale'
|
||||
const locales = {
|
||||
en: enUS,
|
||||
de,
|
||||
nl,
|
||||
fr,
|
||||
es,
|
||||
pt
|
||||
// pl
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'HcRelativeDateTime',
|
||||
props: {
|
||||
dateTime: {
|
||||
type: [Date, String],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
relativeDateTime() {
|
||||
let locale = locales[this.$i18n.locale() || 'en']
|
||||
return formatRelative(new Date(this.dateTime), new Date(), { locale })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
7
webapp/components/Tag/Readme.md
Normal file
7
webapp/components/Tag/Readme.md
Normal file
@ -0,0 +1,7 @@
|
||||
### Example
|
||||
|
||||
Tag "Liebe"
|
||||
|
||||
```
|
||||
<hc-tag name="Liebe" />
|
||||
```
|
||||
29
webapp/components/Tag/index.spec.js
Normal file
29
webapp/components/Tag/index.spec.js
Normal file
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
15
webapp/components/Tag/index.vue
Normal file
15
webapp/components/Tag/index.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<ds-tag>
|
||||
<ds-icon name="tag" />
|
||||
{{ name }}
|
||||
</ds-tag>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HcTag',
|
||||
props: {
|
||||
name: { type: String, required: true }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -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
|
||||
@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div v-if="!user || ((user.disabled || user.deleted) && !isModerator)">
|
||||
<div style="display: inline-block; float: left; margin-right: 4px; height: 100%; vertical-align: middle;">
|
||||
<div
|
||||
style="display: inline-block; float: left; margin-right: 4px; height: 100%; vertical-align: middle;"
|
||||
>
|
||||
<ds-avatar
|
||||
style="display: inline-block; vertical-align: middle;"
|
||||
size="32px"
|
||||
@ -10,9 +12,7 @@
|
||||
<b
|
||||
class="username"
|
||||
style="vertical-align: middle;"
|
||||
>
|
||||
Anonymus
|
||||
</b>
|
||||
>Anonymus</b>
|
||||
</div>
|
||||
</div>
|
||||
<dropdown
|
||||
@ -33,7 +33,9 @@
|
||||
@mouseover="openMenu(true)"
|
||||
@mouseleave="closeMenu(true)"
|
||||
>
|
||||
<div style="display: inline-block; float: left; margin-right: 4px; height: 100%; vertical-align: middle;">
|
||||
<div
|
||||
style="display: inline-block; float: left; margin-right: 4px; height: 100%; vertical-align: middle;"
|
||||
>
|
||||
<ds-avatar
|
||||
:image="user.avatar"
|
||||
:name="user.name"
|
||||
@ -45,16 +47,28 @@
|
||||
<b
|
||||
class="username"
|
||||
style="vertical-align: middle;"
|
||||
>{{ user.name | truncate(trunc, 18) }}</b>
|
||||
</div>
|
||||
<!-- Time -->
|
||||
<div
|
||||
v-if="dateTime"
|
||||
style="display: inline;"
|
||||
>
|
||||
<ds-text
|
||||
align="right"
|
||||
size="small"
|
||||
color="soft"
|
||||
>
|
||||
{{ user.name | truncate(trunc, 18) }}
|
||||
</b>
|
||||
<ds-icon name="clock" />
|
||||
<no-ssr>
|
||||
<hc-relative-date-time :date-time="dateTime" />
|
||||
</no-ssr>
|
||||
</ds-text>
|
||||
</div>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<template
|
||||
slot="popover"
|
||||
>
|
||||
<template slot="popover">
|
||||
<div style="min-width: 250px">
|
||||
<hc-badges
|
||||
v-if="user.badges && user.badges.length"
|
||||
@ -68,11 +82,10 @@
|
||||
style="margin-top: 5px"
|
||||
bold
|
||||
>
|
||||
<ds-icon name="map-marker" /> {{ user.location.name }}
|
||||
<ds-icon name="map-marker" />
|
||||
{{ user.location.name }}
|
||||
</ds-text>
|
||||
<ds-flex
|
||||
style="margin-top: -10px"
|
||||
>
|
||||
<ds-flex style="margin-top: -10px">
|
||||
<ds-flex-item class="ds-tab-nav-item">
|
||||
<ds-space margin="small">
|
||||
<ds-number
|
||||
@ -125,21 +138,25 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import HcRelativeDateTime from '~/components/RelativeDateTime'
|
||||
import HcFollowButton from '~/components/FollowButton.vue'
|
||||
import HcBadges from '~/components/Badges.vue'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'HcUser',
|
||||
components: {
|
||||
HcRelativeDateTime,
|
||||
HcFollowButton,
|
||||
HcBadges,
|
||||
Dropdown
|
||||
},
|
||||
props: {
|
||||
user: { type: Object, default: null },
|
||||
trunc: { type: Number, default: null }
|
||||
trunc: { type: Number, default: null },
|
||||
dateTime: { type: [Date, String], default: null }
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
@ -47,12 +47,13 @@
|
||||
"graphql": "~14.2.1",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"linkify-it": "~2.1.0",
|
||||
"nuxt": "~2.4.5",
|
||||
"nuxt": "~2.6.1",
|
||||
"nuxt-env": "~0.1.0",
|
||||
"stack-utils": "^1.0.2",
|
||||
"string-hash": "^1.1.3",
|
||||
"tiptap": "^1.14.0",
|
||||
"tiptap-extensions": "^1.14.0",
|
||||
"v-tooltip": "~2.0.0-rc.33",
|
||||
"v-tooltip": "~2.0.0",
|
||||
"vue-count-to": "~1.0.13",
|
||||
"vue-izitoast": "1.1.2",
|
||||
"vue-sweetalert-icons": "~3.2.0",
|
||||
@ -75,7 +76,7 @@
|
||||
"eslint-plugin-vue": "~5.2.2",
|
||||
"jest": "~24.7.1",
|
||||
"node-sass": "~4.11.0",
|
||||
"nodemon": "~1.18.10",
|
||||
"nodemon": "~1.18.11",
|
||||
"prettier": "~1.14.3",
|
||||
"sass-loader": "~7.1.0",
|
||||
"vue-jest": "~3.0.4",
|
||||
|
||||
@ -32,22 +32,26 @@ export default {
|
||||
name: this.$t('admin.dashboard.name'),
|
||||
path: `/admin`
|
||||
},
|
||||
{
|
||||
// TODO implement
|
||||
/* {
|
||||
name: this.$t('admin.users.name'),
|
||||
path: `/admin/users`
|
||||
},
|
||||
{
|
||||
}, */
|
||||
// TODO implement
|
||||
/* {
|
||||
name: this.$t('admin.organizations.name'),
|
||||
path: `/admin/organizations`
|
||||
},
|
||||
{
|
||||
}, */
|
||||
// TODO implement
|
||||
/* {
|
||||
name: this.$t('admin.pages.name'),
|
||||
path: `/admin/pages`
|
||||
},
|
||||
{
|
||||
}, */
|
||||
// TODO implement
|
||||
/* {
|
||||
name: this.$t('admin.notifications.name'),
|
||||
path: `/admin/notifications`
|
||||
},
|
||||
}, */
|
||||
{
|
||||
name: this.$t('admin.categories.name'),
|
||||
path: `/admin/categories`
|
||||
@ -55,11 +59,12 @@ export default {
|
||||
{
|
||||
name: this.$t('admin.tags.name'),
|
||||
path: `/admin/tags`
|
||||
},
|
||||
{
|
||||
}
|
||||
// TODO implement
|
||||
/* {
|
||||
name: this.$t('admin.settings.name'),
|
||||
path: `/admin/settings`
|
||||
}
|
||||
} */
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,8 +149,8 @@ export default {
|
||||
.post-add-button {
|
||||
z-index: 100;
|
||||
position: fixed;
|
||||
top: 100vh;
|
||||
left: 100vw;
|
||||
top: 98vh;
|
||||
left: 98vw;
|
||||
transform: translate(-120%, -120%);
|
||||
box-shadow: $box-shadow-x-large;
|
||||
}
|
||||
|
||||
@ -10,7 +10,10 @@
|
||||
</transition>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item :width="{ base: '200px' }">
|
||||
<ds-menu :routes="routes" />
|
||||
<ds-menu
|
||||
:routes="routes"
|
||||
class="post-side-navigation"
|
||||
/>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</div>
|
||||
@ -55,27 +58,38 @@ export default {
|
||||
{
|
||||
name: this.$t('common.comment', null, 2),
|
||||
path: `/post/${id}/${slug}#comments`
|
||||
},
|
||||
{
|
||||
}
|
||||
// TODO implement
|
||||
/* {
|
||||
name: this.$t('common.letsTalk'),
|
||||
path: `/post/${id}/${slug}#lets-talk`
|
||||
},
|
||||
{
|
||||
}, */
|
||||
// TODO implement
|
||||
/* {
|
||||
name: this.$t('common.versus'),
|
||||
path: `/post/${id}/${slug}#versus`
|
||||
}
|
||||
} */
|
||||
]
|
||||
},
|
||||
{
|
||||
name: this.$t('common.moreInfo'),
|
||||
path: `/post/${id}/${slug}/more-info`
|
||||
},
|
||||
{
|
||||
}
|
||||
// TODO implement
|
||||
/* {
|
||||
name: this.$t('common.takeAction'),
|
||||
path: `/post/${id}/${slug}/take-action`
|
||||
}
|
||||
} */
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.post-side-navigation {
|
||||
position: sticky;
|
||||
top: 65px;
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -6,10 +6,13 @@
|
||||
<ds-card
|
||||
v-if="post && ready"
|
||||
:image="post.image"
|
||||
:header="post.title"
|
||||
:class="{'post-card': true, 'disabled-content': post.disabled}"
|
||||
>
|
||||
<hc-user :user="post.author" />
|
||||
<ds-space margin-bottom="small" />
|
||||
<hc-user
|
||||
:user="post.author"
|
||||
:date-time="post.createdAt"
|
||||
/>
|
||||
<no-ssr>
|
||||
<content-menu
|
||||
placement="bottom-end"
|
||||
@ -19,6 +22,13 @@
|
||||
/>
|
||||
</no-ssr>
|
||||
<ds-space margin-bottom="small" />
|
||||
<ds-heading
|
||||
tag="h3"
|
||||
no-margin
|
||||
>
|
||||
{{ post.title }}
|
||||
</ds-heading>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!-- Content -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<!-- TODO: replace editor content with tiptap render view -->
|
||||
@ -26,19 +36,33 @@
|
||||
class="content hc-editor-content"
|
||||
v-html="post.content"
|
||||
/>
|
||||
<ds-space>
|
||||
<ds-text
|
||||
v-if="post.createdAt"
|
||||
align="right"
|
||||
size="small"
|
||||
color="soft"
|
||||
>
|
||||
{{ post.createdAt | dateTime('dd. MMMM yyyy HH:mm') }}
|
||||
</ds-text>
|
||||
</ds-space>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<!-- Shout Button -->
|
||||
<ds-space margin="xx-large" />
|
||||
<!-- Categories -->
|
||||
<div class="categories">
|
||||
<ds-space margin="xx-small" />
|
||||
<hc-category
|
||||
v-for="category in post.categories"
|
||||
:key="category.id"
|
||||
v-tooltip="{content: category.name, placement: 'top-start', delay: { show: 300 }}"
|
||||
:icon="category.icon"
|
||||
:name="category.name"
|
||||
/>
|
||||
</div>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!-- Tags -->
|
||||
<div
|
||||
v-if="post.tags && post.tags.length"
|
||||
class="tags"
|
||||
>
|
||||
<ds-space margin="xx-small" />
|
||||
<hc-tag
|
||||
v-for="tag in post.tags"
|
||||
:key="tag.id"
|
||||
:name="tag.name"
|
||||
/>
|
||||
</div>
|
||||
<!-- Shout Button -->
|
||||
<hc-shout-button
|
||||
v-if="post.author"
|
||||
:disabled="isAuthor(post.author.id)"
|
||||
@ -55,23 +79,17 @@
|
||||
size="large"
|
||||
/>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!--<div class="tags">
|
||||
<ds-icon name="compass" /> <ds-tag
|
||||
v-for="category in post.categories"
|
||||
:key="category.id"
|
||||
>
|
||||
{{ category.name }}
|
||||
</ds-tag>
|
||||
</div>-->
|
||||
<!-- Tags -->
|
||||
<template v-if="post.tags && post.tags.length">
|
||||
<ds-space margin="xx-small" />
|
||||
<div class="tags">
|
||||
<ds-icon name="tags" /> <ds-tag
|
||||
<ds-icon name="tags" />
|
||||
<ds-tag
|
||||
v-for="tag in post.tags"
|
||||
:key="tag.id"
|
||||
>
|
||||
<ds-icon name="tag" /> {{ tag.name }}
|
||||
<ds-icon name="tag" />
|
||||
{{ tag.name }}
|
||||
</ds-tag>
|
||||
</div>
|
||||
</template>
|
||||
@ -87,9 +105,7 @@
|
||||
color="primary"
|
||||
size="small"
|
||||
round
|
||||
>
|
||||
{{ post.commentsCount }}
|
||||
</ds-tag> Comments
|
||||
>{{ post.commentsCount }}</ds-tag> Comments
|
||||
</span>
|
||||
</h3>
|
||||
<ds-space margin-bottom="large" />
|
||||
@ -115,8 +131,11 @@
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import HcCategory from '~/components/Category'
|
||||
import HcTag from '~/components/Tag'
|
||||
import ContentMenu from '~/components/ContentMenu'
|
||||
import HcUser from '~/components/User.vue'
|
||||
import HcUser from '~/components/User'
|
||||
import HcShoutButton from '~/components/ShoutButton.vue'
|
||||
import HcEmpty from '~/components/Empty.vue'
|
||||
import Comment from '~/components/Comment.vue'
|
||||
@ -127,6 +146,8 @@ export default {
|
||||
mode: 'out-in'
|
||||
},
|
||||
components: {
|
||||
HcTag,
|
||||
HcCategory,
|
||||
HcUser,
|
||||
HcShoutButton,
|
||||
HcEmpty,
|
||||
|
||||
@ -42,7 +42,8 @@
|
||||
color="soft"
|
||||
size="small"
|
||||
>
|
||||
<ds-icon name="map-marker" /> {{ user.location.name }}
|
||||
<ds-icon name="map-marker" />
|
||||
{{ user.location.name }}
|
||||
</ds-text>
|
||||
<ds-text
|
||||
align="center"
|
||||
@ -56,9 +57,7 @@
|
||||
v-if="user.badges && user.badges.length"
|
||||
margin="x-small"
|
||||
>
|
||||
<hc-badges
|
||||
:badges="user.badges"
|
||||
/>
|
||||
<hc-badges :badges="user.badges" />
|
||||
</ds-space>
|
||||
<ds-flex>
|
||||
<ds-flex-item>
|
||||
@ -82,9 +81,7 @@
|
||||
</no-ssr>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
<ds-space
|
||||
margin="small"
|
||||
>
|
||||
<ds-space margin="small">
|
||||
<hc-follow-button
|
||||
v-if="!myProfile"
|
||||
:follow-id="user.id"
|
||||
@ -227,32 +224,28 @@
|
||||
</no-ssr>
|
||||
</ds-space>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item class="ds-tab-nav-item">
|
||||
<ds-space margin="small">
|
||||
<!-- TODO: find better solution for rendering errors -->
|
||||
<!--<ds-flex-item class="ds-tab-nav-item">
|
||||
<ds-space margin="small">-->
|
||||
<!-- TODO: find better solution for rendering errors -->
|
||||
<!--
|
||||
<no-ssr>
|
||||
<ds-number :label="$t('profile.commented')">
|
||||
<hc-count-to
|
||||
slot="count"
|
||||
:end-val="user.commentsCount"
|
||||
/>
|
||||
<hc-count-to slot="count" :end-val="user.commentsCount"/>
|
||||
</ds-number>
|
||||
</no-ssr>
|
||||
</ds-space>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item class="ds-tab-nav-item">
|
||||
<ds-space margin="small">
|
||||
<!-- TODO: find better solution for rendering errors -->
|
||||
<no-ssr>
|
||||
-->
|
||||
<!--<ds-flex-item class="ds-tab-nav-item">
|
||||
<ds-space margin="small">-->
|
||||
<!-- TODO: find better solution for rendering errors -->
|
||||
<!--<no-ssr>
|
||||
<ds-number :label="$t('profile.shouted')">
|
||||
<hc-count-to
|
||||
slot="count"
|
||||
:end-val="user.shoutedCount"
|
||||
/>
|
||||
<hc-count-to slot="count" :end-val="user.shoutedCount"/>
|
||||
</ds-number>
|
||||
</no-ssr>
|
||||
</ds-space>
|
||||
</ds-flex-item>
|
||||
</ds-flex-item>-->
|
||||
</ds-flex>
|
||||
</ds-card>
|
||||
</ds-flex-item>
|
||||
@ -273,9 +266,7 @@
|
||||
:key="post.id"
|
||||
:width="{ base: '100%', md: '100%', xl: '50%' }"
|
||||
>
|
||||
<hc-post-card
|
||||
:post="post"
|
||||
/>
|
||||
<hc-post-card :post="post" />
|
||||
</ds-flex-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
@ -300,7 +291,7 @@
|
||||
<script>
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
|
||||
import User from '~/components/User.vue'
|
||||
import User from '~/components/User'
|
||||
import HcPostCard from '~/components/PostCard.vue'
|
||||
import HcFollowButton from '~/components/FollowButton.vue'
|
||||
import HcCountTo from '~/components/CountTo.vue'
|
||||
|
||||
@ -34,27 +34,32 @@ export default {
|
||||
{
|
||||
name: this.$t('settings.security.name'),
|
||||
path: `/settings/security`
|
||||
},
|
||||
{
|
||||
}
|
||||
// TODO implement
|
||||
/* {
|
||||
name: this.$t('settings.invites.name'),
|
||||
path: `/settings/invites`
|
||||
},
|
||||
{
|
||||
}, */
|
||||
// TODO implement
|
||||
/* {
|
||||
name: this.$t('settings.download.name'),
|
||||
path: `/settings/data-download`
|
||||
},
|
||||
{
|
||||
}, */
|
||||
// TODO implement
|
||||
/* {
|
||||
name: this.$t('settings.delete.name'),
|
||||
path: `/settings/delete-account`
|
||||
},
|
||||
{
|
||||
}, */
|
||||
// TODO implement
|
||||
/* {
|
||||
name: this.$t('settings.organizations.name'),
|
||||
path: `/settings/my-organizations`
|
||||
},
|
||||
{
|
||||
}, */
|
||||
// TODO implement
|
||||
/* {
|
||||
name: this.$t('settings.languages.name'),
|
||||
path: `/settings/languages`
|
||||
}
|
||||
} */
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,12 +34,6 @@ export default ({ app }) => {
|
||||
locale: getLocalizedFormat()
|
||||
})
|
||||
},
|
||||
relativeDateTime: value => {
|
||||
if (!value) return ''
|
||||
return formatRelative(new Date(value), new Date(), {
|
||||
locale: getLocalizedFormat()
|
||||
})
|
||||
},
|
||||
number: (
|
||||
value,
|
||||
precision = 2,
|
||||
|
||||
920
webapp/yarn.lock
920
webapp/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user