mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' into lokalise-2020-02-19_20-53-55
This commit is contained in:
commit
f17cf9db14
@ -6,7 +6,8 @@ addons:
|
||||
- libgconf-2-4
|
||||
snaps:
|
||||
- docker
|
||||
|
||||
firefox: "latest-esr"
|
||||
|
||||
install:
|
||||
- yarn global add wait-on
|
||||
# Install Codecov
|
||||
|
||||
63
CHANGELOG.md
63
CHANGELOG.md
@ -4,6 +4,63 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v0.4.0](https://github.com/Human-Connection/Human-Connection/compare/v0.3.1...v0.4.0)
|
||||
|
||||
> 21 February 2020
|
||||
|
||||
- fix: Favor Cypress.Promise over async/await in e2e tests [`#3115`](https://github.com/Human-Connection/Human-Connection/pull/3115)
|
||||
- docs(setup): Fix links in tests [`#3120`](https://github.com/Human-Connection/Human-Connection/pull/3120)
|
||||
- feat: 🍰 Expose sensitive report type to moderators only [`#3075`](https://github.com/Human-Connection/Human-Connection/pull/3075)
|
||||
- refactor: migrate card component [`#2870`](https://github.com/Human-Connection/Human-Connection/pull/2870)
|
||||
- build(deps): bump metascraper-youtube from 5.10.7 to 5.11.1 in /backend [`#3114`](https://github.com/Human-Connection/Human-Connection/pull/3114)
|
||||
- fix(cypress): Upgrade cypress, remove log out step [`#3119`](https://github.com/Human-Connection/Human-Connection/pull/3119)
|
||||
- build(deps): bump metascraper-date from 5.10.7 to 5.11.1 in /backend [`#3069`](https://github.com/Human-Connection/Human-Connection/pull/3069)
|
||||
- build(deps): bump metascraper-author from 5.10.7 to 5.11.1 in /backend [`#3070`](https://github.com/Human-Connection/Human-Connection/pull/3070)
|
||||
- build(deps): bump xregexp from 4.2.4 to 4.3.0 in /webapp [`#3047`](https://github.com/Human-Connection/Human-Connection/pull/3047)
|
||||
- build(deps): bump metascraper-publisher from 5.10.7 to 5.11.1 in /backend [`#3068`](https://github.com/Human-Connection/Human-Connection/pull/3068)
|
||||
- build(deps): bump @sentry/node from 5.12.3 to 5.12.4 in /backend [`#3113`](https://github.com/Human-Connection/Human-Connection/pull/3113)
|
||||
- feat: German Translations Update By Andreas Plank [`#3109`](https://github.com/Human-Connection/Human-Connection/pull/3109)
|
||||
- fix(frontend): Remove Hover Menu from User Teaser [`#3093`](https://github.com/Human-Connection/Human-Connection/pull/3093)
|
||||
- build(deps-dev): bump eslint-plugin-jest from 23.6.0 to 23.7.0 in /webapp [`#3030`](https://github.com/Human-Connection/Human-Connection/pull/3030)
|
||||
- fix(frontend): Post page won't crash on anonymous user [`#2981`](https://github.com/Human-Connection/Human-Connection/pull/2981)
|
||||
- chore(cypress): Remove debug statements [`#3110`](https://github.com/Human-Connection/Human-Connection/pull/3110)
|
||||
- build(deps): bump metascraper-audio from 5.10.7 to 5.11.1 in /backend [`#3066`](https://github.com/Human-Connection/Human-Connection/pull/3066)
|
||||
- build(deps): bump @nuxtjs/sentry from 3.2.3 to 3.2.4 in /webapp [`#3081`](https://github.com/Human-Connection/Human-Connection/pull/3081)
|
||||
- build(deps-dev): bump apollo-server-testing from 2.10.0 to 2.10.1 in /backend [`#3078`](https://github.com/Human-Connection/Human-Connection/pull/3078)
|
||||
- fix(subscriptions): Don't publish undefined [`#3101`](https://github.com/Human-Connection/Human-Connection/pull/3101)
|
||||
- build(deps): [security] bump yarn from 1.17.3 to 1.22.0 in /webapp [`#3077`](https://github.com/Human-Connection/Human-Connection/pull/3077)
|
||||
- feat: Normalize locales/json files [`#3003`](https://github.com/Human-Connection/Human-Connection/pull/3003)
|
||||
- 🍰feat: Delete teaser image [`#2585`](https://github.com/Human-Connection/Human-Connection/pull/2585)
|
||||
- fix: swap lat and lng [`#2589`](https://github.com/Human-Connection/Human-Connection/pull/2589)
|
||||
- fix(frontend): avatar image covers full circle [`#3102`](https://github.com/Human-Connection/Human-Connection/pull/3102)
|
||||
- fix(jwt): Whitelist encoded JWT attributes [`#3090`](https://github.com/Human-Connection/Human-Connection/pull/3090)
|
||||
- test: Write cypress tests for ImageUploader [`#3056`](https://github.com/Human-Connection/Human-Connection/pull/3056)
|
||||
- build(deps-dev): bump eslint-plugin-vue from 6.1.2 to 6.2.1 in /webapp [`#3092`](https://github.com/Human-Connection/Human-Connection/pull/3092)
|
||||
- build: Fix intermittent failing tests [`#3087`](https://github.com/Human-Connection/Human-Connection/pull/3087)
|
||||
- fix(nuxt-env): Configuration issue with websockets [`#3089`](https://github.com/Human-Connection/Human-Connection/pull/3089)
|
||||
- build(deps-dev): bump eslint-plugin-jest from 23.6.0 to 23.7.0 in /backend [`#3029`](https://github.com/Human-Connection/Human-Connection/pull/3029)
|
||||
- build(deps): bump cookie-universal-nuxt from 2.1.1 to 2.1.2 in /webapp [`#3073`](https://github.com/Human-Connection/Human-Connection/pull/3073)
|
||||
- build(deps): bump @nuxtjs/sentry from 3.2.2 to 3.2.3 in /webapp [`#3072`](https://github.com/Human-Connection/Human-Connection/pull/3072)
|
||||
- build(deps): bump metascraper-image from 5.10.7 to 5.11.1 in /backend [`#3067`](https://github.com/Human-Connection/Human-Connection/pull/3067)
|
||||
- build(deps-dev): bump vue-loader from 15.8.3 to 15.9.0 in /webapp [`#3060`](https://github.com/Human-Connection/Human-Connection/pull/3060)
|
||||
- build(deps-dev): bump @storybook/addon-actions from 5.3.12 to 5.3.13 in /webapp [`#3049`](https://github.com/Human-Connection/Human-Connection/pull/3049)
|
||||
- refactor(cypress): Speed up builds, avoid login through UI [`#3042`](https://github.com/Human-Connection/Human-Connection/pull/3042)
|
||||
- feat: 🍰 Set up Vue-Apollo Subscriptions [`#1705`](https://github.com/Human-Connection/Human-Connection/pull/1705)
|
||||
- fix: Update devops_ticket.md [`#3053`](https://github.com/Human-Connection/Human-Connection/pull/3053)
|
||||
- build(deps-dev): bump @storybook/addon-notes from 5.3.12 to 5.3.13 in /webapp [`#3048`](https://github.com/Human-Connection/Human-Connection/pull/3048)
|
||||
- build(deps-dev): bump @storybook/addon-a11y from 5.3.12 to 5.3.13 in /webapp [`#3050`](https://github.com/Human-Connection/Human-Connection/pull/3050)
|
||||
- build(deps): Node v13 compatbility [`#3041`](https://github.com/Human-Connection/Human-Connection/pull/3041)
|
||||
- build(deps): bump request from 2.88.0 to 2.88.2 in /backend [`#3045`](https://github.com/Human-Connection/Human-Connection/pull/3045)
|
||||
- build(deps-dev): bump @storybook/vue from 5.3.12 to 5.3.13 in /webapp [`#3046`](https://github.com/Human-Connection/Human-Connection/pull/3046)
|
||||
- feat(deployment): Add helm charts for deploy [`#1613`](https://github.com/Human-Connection/Human-Connection/pull/1613)
|
||||
- build(deps-dev): bump vue-svg-loader from 0.15.0 to 0.16.0 in /webapp [`#3039`](https://github.com/Human-Connection/Human-Connection/pull/3039)
|
||||
- fix: Increase body parser limit [`#3037`](https://github.com/Human-Connection/Human-Connection/pull/3037)
|
||||
- chore: Update to v0.3.1 [`#3035`](https://github.com/Human-Connection/Human-Connection/pull/3035)
|
||||
- fix(subscriptions): Don't publish undefined [`#3088`](https://github.com/Human-Connection/Human-Connection/issues/3088)
|
||||
- locales sorted. [`fa906ef`](https://github.com/Human-Connection/Human-Connection/commit/fa906efb1f40dc5bd80c9678f33c7b607a320099)
|
||||
- Upgrade cypress, remove log out step [`0df4038`](https://github.com/Human-Connection/Human-Connection/commit/0df40386dd866c6b9ce540b966dfe00089507d31)
|
||||
- Refactor GQL and tests, first approach [`f380915`](https://github.com/Human-Connection/Human-Connection/commit/f380915b2c679d42e5db136ea1d923cf00bbcf10)
|
||||
|
||||
#### [v0.3.1](https://github.com/Human-Connection/Human-Connection/compare/v0.3.0...v0.3.1)
|
||||
|
||||
> 10 February 2020
|
||||
@ -57,8 +114,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- Fix typo [`#2966`](https://github.com/Human-Connection/Human-Connection/pull/2966)
|
||||
- chore: Update to v0.3.0 [`#2941`](https://github.com/Human-Connection/Human-Connection/pull/2941)
|
||||
- Replace buildList with array of Promises [`46edc3f`](https://github.com/Human-Connection/Human-Connection/commit/46edc3fdd5b83c2f00506f595b1254d7597767e0)
|
||||
- build(deps-dev): bump @storybook/addon-notes in /webapp [`75137ce`](https://github.com/Human-Connection/Human-Connection/commit/75137ce716dadcc6f0ceeed6a2b0fe5c50fa7b8f)
|
||||
- Update to v0.3.0 [`dbe2c4c`](https://github.com/Human-Connection/Human-Connection/commit/dbe2c4cdd5bab2195c6369b84989507b9f7da768)
|
||||
- refactor TeaserImage component [`e14cbf8`](https://github.com/Human-Connection/Human-Connection/commit/e14cbf8173e3040b5285ba6a5c73e2d2d2a47860)
|
||||
- refactor DeleteData template and CSS [`509892b`](https://github.com/Human-Connection/Human-Connection/commit/509892b6caee6c4ca8384fb0090122ced98edfd4)
|
||||
|
||||
#### [v0.3.0](https://github.com/Human-Connection/Human-Connection/compare/v0.2.1...v0.3.0)
|
||||
|
||||
@ -349,7 +406,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- fixes #2659 [`#2659`](https://github.com/Human-Connection/Human-Connection/issues/2659)
|
||||
- Convert block/unblock to blacklist/whitelist [`c297b83`](https://github.com/Human-Connection/Human-Connection/commit/c297b83f873edc61ddec370633b9b65896c56591)
|
||||
- Rename blacklist/whitelist to mute/unmute [`ba3e9e1`](https://github.com/Human-Connection/Human-Connection/commit/ba3e9e1025bf432151c9bf1002045179b338ff7f)
|
||||
- build(deps-dev): bump storybook-design-token in /webapp [`88d39c4`](https://github.com/Human-Connection/Human-Connection/commit/88d39c4a427cb86527b06201f3f5e96d53ac09a0)
|
||||
- manage button states and color schemes with mixin [`1b9249c`](https://github.com/Human-Connection/Human-Connection/commit/1b9249c685e34eb2e94b31ee0ec22421c6aa6a73)
|
||||
|
||||
#### [v0.2.0](https://github.com/Human-Connection/Human-Connection/compare/v0.1.13...v0.2.0)
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "human-connection-backend",
|
||||
"version": "0.3.1",
|
||||
"version": "0.4.0",
|
||||
"description": "GraphQL Backend for Human Connection",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
@ -38,19 +38,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/joi": "^17.1.0",
|
||||
"@sentry/node": "^5.12.3",
|
||||
"@sentry/node": "^5.13.1",
|
||||
"apollo-cache-inmemory": "~1.6.5",
|
||||
"apollo-client": "~2.6.8",
|
||||
"apollo-link-context": "~1.0.19",
|
||||
"apollo-link-http": "~1.5.16",
|
||||
"apollo-server": "~2.10.0",
|
||||
"apollo-server": "~2.10.1",
|
||||
"apollo-server-express": "^2.9.16",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"cheerio": "~1.0.0-rc.3",
|
||||
"cors": "~2.8.5",
|
||||
"cross-env": "~7.0.0",
|
||||
"date-fns": "2.9.0",
|
||||
"date-fns": "2.10.0",
|
||||
"debug": "~4.1.1",
|
||||
"dotenv": "~8.2.0",
|
||||
"express": "^4.17.1",
|
||||
@ -61,30 +61,30 @@
|
||||
"graphql-middleware": "~4.0.2",
|
||||
"graphql-middleware-sentry": "^3.2.1",
|
||||
"graphql-redis-subscriptions": "^2.1.2",
|
||||
"graphql-shield": "~7.0.11",
|
||||
"graphql-shield": "~7.0.14",
|
||||
"graphql-tag": "~2.10.3",
|
||||
"helmet": "~3.21.2",
|
||||
"ioredis": "^4.14.1",
|
||||
"helmet": "~3.21.3",
|
||||
"ioredis": "^4.16.0",
|
||||
"jsonwebtoken": "~8.5.1",
|
||||
"linkifyjs": "~2.1.8",
|
||||
"lodash": "~4.17.14",
|
||||
"merge-graphql-schemas": "^1.7.6",
|
||||
"metascraper": "^5.11.0",
|
||||
"metascraper": "^5.11.4",
|
||||
"metascraper-audio": "^5.11.1",
|
||||
"metascraper-author": "^5.10.7",
|
||||
"metascraper-author": "^5.11.1",
|
||||
"metascraper-clearbit-logo": "^5.3.0",
|
||||
"metascraper-date": "^5.10.7",
|
||||
"metascraper-description": "^5.11.0",
|
||||
"metascraper-date": "^5.11.1",
|
||||
"metascraper-description": "^5.11.1",
|
||||
"metascraper-image": "^5.11.1",
|
||||
"metascraper-lang": "^5.10.7",
|
||||
"metascraper-lang": "^5.11.1",
|
||||
"metascraper-lang-detector": "^4.10.2",
|
||||
"metascraper-logo": "^5.10.7",
|
||||
"metascraper-publisher": "^5.10.7",
|
||||
"metascraper-soundcloud": "^5.10.7",
|
||||
"metascraper-title": "^5.10.7",
|
||||
"metascraper-url": "^5.10.7",
|
||||
"metascraper-video": "^5.10.7",
|
||||
"metascraper-youtube": "^5.10.7",
|
||||
"metascraper-logo": "^5.11.1",
|
||||
"metascraper-publisher": "^5.11.1",
|
||||
"metascraper-soundcloud": "^5.11.4",
|
||||
"metascraper-title": "^5.11.1",
|
||||
"metascraper-url": "^5.11.1",
|
||||
"metascraper-video": "^5.11.1",
|
||||
"metascraper-youtube": "^5.11.1",
|
||||
"migrate": "^1.6.2",
|
||||
"minimatch": "^3.0.4",
|
||||
"mustache": "^4.0.0",
|
||||
@ -92,29 +92,29 @@
|
||||
"neo4j-graphql-js": "^2.11.5",
|
||||
"neode": "^0.3.7",
|
||||
"node-fetch": "~2.6.0",
|
||||
"nodemailer": "^6.4.2",
|
||||
"nodemailer": "^6.4.4",
|
||||
"nodemailer-html-to-text": "^3.1.0",
|
||||
"npm-run-all": "~4.1.5",
|
||||
"request": "~2.88.2",
|
||||
"sanitize-html": "~1.21.1",
|
||||
"sanitize-html": "~1.22.0",
|
||||
"slug": "~2.1.1",
|
||||
"subscriptions-transport-ws": "^0.9.16",
|
||||
"trunc-html": "~1.1.2",
|
||||
"uuid": "~3.4.0",
|
||||
"uuid": "~7.0.1",
|
||||
"validator": "^12.2.0",
|
||||
"wait-on": "~4.0.0",
|
||||
"xregexp": "^4.2.4"
|
||||
"wait-on": "~4.0.1",
|
||||
"xregexp": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "~7.8.4",
|
||||
"@babel/core": "~7.8.4",
|
||||
"@babel/core": "~7.8.6",
|
||||
"@babel/node": "~7.8.4",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
|
||||
"@babel/preset-env": "~7.8.4",
|
||||
"@babel/register": "^7.8.3",
|
||||
"@babel/preset-env": "~7.8.6",
|
||||
"@babel/register": "^7.8.6",
|
||||
"apollo-server-testing": "~2.10.1",
|
||||
"babel-core": "~7.0.0-0",
|
||||
"babel-eslint": "~10.0.3",
|
||||
"babel-eslint": "~10.1.0",
|
||||
"babel-jest": "~25.1.0",
|
||||
"chai": "~4.2.0",
|
||||
"cucumber": "~6.0.5",
|
||||
@ -122,7 +122,7 @@
|
||||
"eslint-config-prettier": "~6.10.0",
|
||||
"eslint-config-standard": "~14.1.0",
|
||||
"eslint-plugin-import": "~2.20.1",
|
||||
"eslint-plugin-jest": "~23.7.0",
|
||||
"eslint-plugin-jest": "~23.8.1",
|
||||
"eslint-plugin-node": "~11.0.0",
|
||||
"eslint-plugin-prettier": "~3.1.2",
|
||||
"eslint-plugin-promise": "~4.2.1",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import uuid from 'uuid/v4'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import faker from 'faker'
|
||||
import slugify from 'slug'
|
||||
import { hashSync } from 'bcryptjs'
|
||||
|
||||
@ -152,6 +152,7 @@ export default shield(
|
||||
User: {
|
||||
email: or(isMyOwn, isAdmin),
|
||||
},
|
||||
Report: isModerator,
|
||||
},
|
||||
{
|
||||
debug,
|
||||
|
||||
@ -9,14 +9,6 @@ const driver = getDriver()
|
||||
|
||||
let query, authenticatedUser, owner, anotherRegularUser, administrator, variables, moderator
|
||||
|
||||
const userQuery = gql`
|
||||
query($name: String) {
|
||||
User(name: $name) {
|
||||
email
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('authorization', () => {
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
@ -30,7 +22,11 @@ describe('authorization', () => {
|
||||
query = createTestClient(server).query
|
||||
})
|
||||
|
||||
describe('given two existing users', () => {
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
describe('given an owner, an other user, an admin, a moderator', () => {
|
||||
beforeEach(async () => {
|
||||
;[owner, anotherRegularUser, administrator, moderator] = await Promise.all([
|
||||
Factory.build(
|
||||
@ -79,15 +75,20 @@ describe('authorization', () => {
|
||||
variables = {}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
describe('access email address', () => {
|
||||
const userQuery = gql`
|
||||
query($name: String) {
|
||||
User(name: $name) {
|
||||
email
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
beforeEach(() => {
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
it("throws an error and does not expose the owner's email address", async () => {
|
||||
await expect(
|
||||
query({ query: userQuery, variables: { name: 'Owner' } }),
|
||||
@ -143,7 +144,7 @@ describe('authorization', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('administrator', () => {
|
||||
describe('as an administrator', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await administrator.toJson()
|
||||
})
|
||||
|
||||
@ -153,7 +153,7 @@ describe('slugifyMiddleware', () => {
|
||||
\`\`\`
|
||||
|
||||
Learn how to setup the database here:
|
||||
https://docs.human-connection.org/human-connection/neo4j
|
||||
https://docs.human-connection.org/human-connection/backend#database-indices-and-constraints
|
||||
`)
|
||||
}
|
||||
})
|
||||
|
||||
@ -58,7 +58,7 @@ const reportMutation = gql`
|
||||
reasonCategory: $reasonCategory
|
||||
reasonDescription: $reasonDescription
|
||||
) {
|
||||
id
|
||||
reportId
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import uuid from 'uuid/v4'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
export default {
|
||||
id: { type: 'string', primary: true, default: uuid },
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import uuid from 'uuid/v4'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
export default {
|
||||
id: { type: 'string', primary: true, default: uuid },
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import uuid from 'uuid/v4'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
export default {
|
||||
id: { type: 'string', primary: true, default: uuid },
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import uuid from 'uuid/v4'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
export default {
|
||||
id: { type: 'string', primary: true, default: uuid },
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import uuid from 'uuid/v4'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
export default {
|
||||
id: { type: 'string', primary: true, default: uuid },
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import uuid from 'uuid/v4'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
export default {
|
||||
id: { type: 'string', primary: true, default: uuid },
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import uuid from 'uuid/v4'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
export default {
|
||||
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
|
||||
|
||||
@ -46,7 +46,7 @@ describe('slug', () => {
|
||||
\`\`\`
|
||||
|
||||
Learn how to setup the database here:
|
||||
https://docs.human-connection.org/human-connection/neo4j
|
||||
https://docs.human-connection.org/human-connection/backend#database-indices-and-constraints
|
||||
`)
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import uuid from 'uuid/v4'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import Resolver from './helpers/Resolver'
|
||||
|
||||
export default {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { createWriteStream } from 'fs'
|
||||
import path from 'path'
|
||||
import slug from 'slug'
|
||||
import uuid from 'uuid/v4'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
const localFileUpload = async ({ createReadStream, uniqueFilename }) => {
|
||||
await new Promise((resolve, reject) =>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import uuid from 'uuid/v4'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
export default function generateNonce() {
|
||||
return uuid().substring(0, 6)
|
||||
}
|
||||
|
||||
@ -1,20 +1,10 @@
|
||||
const transformReturnType = record => {
|
||||
return {
|
||||
...record.get('review').properties,
|
||||
report: record.get('report').properties,
|
||||
resource: {
|
||||
__typename: record.get('type'),
|
||||
...record.get('resource').properties,
|
||||
},
|
||||
}
|
||||
}
|
||||
import log from './helpers/databaseLogger'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
review: async (_object, params, context, _resolveInfo) => {
|
||||
const { user: moderator, driver } = context
|
||||
|
||||
let createdRelationshipWithNestedAttributes = null // return value
|
||||
const session = driver.session()
|
||||
try {
|
||||
const cypher = `
|
||||
@ -25,10 +15,11 @@ export default {
|
||||
ON CREATE SET review.createdAt = $dateTime, review.updatedAt = review.createdAt
|
||||
ON MATCH SET review.updatedAt = $dateTime
|
||||
SET review.disable = $params.disable
|
||||
SET report.updatedAt = $dateTime, report.closed = $params.closed
|
||||
SET resource.disabled = review.disable
|
||||
SET report.updatedAt = $dateTime, report.disable = review.disable, report.closed = $params.closed
|
||||
SET resource.disabled = report.disable
|
||||
|
||||
RETURN review, report, resource, labels(resource)[0] AS type
|
||||
WITH review, report, resource {.*, __typename: labels(resource)[0]} AS finalResource
|
||||
RETURN review {.*, report: properties(report), resource: properties(finalResource)}
|
||||
`
|
||||
const reviewWriteTxResultPromise = session.writeTransaction(async txc => {
|
||||
const reviewTransactionResponse = await txc.run(cypher, {
|
||||
@ -36,16 +27,14 @@ export default {
|
||||
moderatorId: moderator.id,
|
||||
dateTime: new Date().toISOString(),
|
||||
})
|
||||
return reviewTransactionResponse.records.map(transformReturnType)
|
||||
log(reviewTransactionResponse)
|
||||
return reviewTransactionResponse.records.map(record => record.get('review'))
|
||||
})
|
||||
const txResult = await reviewWriteTxResultPromise
|
||||
if (!txResult[0]) return null
|
||||
createdRelationshipWithNestedAttributes = txResult[0]
|
||||
const [reviewed] = await reviewWriteTxResultPromise
|
||||
return reviewed || null
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
return createdRelationshipWithNestedAttributes
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import uuid from 'uuid/v4'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import createPasswordReset from './helpers/createPasswordReset'
|
||||
|
||||
@ -22,6 +22,7 @@ export default {
|
||||
WHERE duration.between(passwordReset.issuedAt, datetime()).days <= 0 AND passwordReset.usedAt IS NULL
|
||||
SET passwordReset.usedAt = datetime()
|
||||
SET user.encryptedPassword = $encryptedNewPassword
|
||||
SET user.updatedAt = toString(datetime())
|
||||
RETURN passwordReset
|
||||
`,
|
||||
{
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import uuid from 'uuid/v4'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { UserInputError } from 'apollo-server'
|
||||
|
||||
@ -1,23 +1,13 @@
|
||||
import log from './helpers/databaseLogger'
|
||||
|
||||
const transformReturnType = record => {
|
||||
return {
|
||||
...record.get('report').properties,
|
||||
resource: {
|
||||
__typename: record.get('type'),
|
||||
...record.get('resource').properties,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
fileReport: async (_parent, params, context, _resolveInfo) => {
|
||||
const { resourceId, reasonCategory, reasonDescription } = params
|
||||
const { driver, user } = context
|
||||
const session = driver.session()
|
||||
const reportWriteTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const reportTransactionResponse = await transaction.run(
|
||||
const fileReportWriteTxResultPromise = session.writeTransaction(async transaction => {
|
||||
const fileReportTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (submitter:User {id: $submitterId})
|
||||
MATCH (resource {id: $resourceId})
|
||||
@ -27,7 +17,8 @@ export default {
|
||||
WITH submitter, resource, report
|
||||
CREATE (report)<-[filed:FILED {createdAt: $createdAt, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription}]-(submitter)
|
||||
|
||||
RETURN report, resource, labels(resource)[0] AS type
|
||||
WITH filed, report, resource {.*, __typename: labels(resource)[0]} AS finalResource
|
||||
RETURN filed {.*, reportId: report.id, resource: properties(finalResource)} AS filedReport
|
||||
`,
|
||||
{
|
||||
resourceId,
|
||||
@ -37,13 +28,12 @@ export default {
|
||||
reasonDescription,
|
||||
},
|
||||
)
|
||||
log(reportTransactionResponse)
|
||||
return reportTransactionResponse.records.map(transformReturnType)
|
||||
log(fileReportTransactionResponse)
|
||||
return fileReportTransactionResponse.records.map(record => record.get('filedReport'))
|
||||
})
|
||||
try {
|
||||
const [createdRelationshipWithNestedAttributes] = await reportWriteTxResultPromise
|
||||
if (!createdRelationshipWithNestedAttributes) return null
|
||||
return createdRelationshipWithNestedAttributes
|
||||
const [filedReport] = await fileReportWriteTxResultPromise
|
||||
return filedReport || null
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
@ -76,14 +66,24 @@ export default {
|
||||
filterClause = ''
|
||||
}
|
||||
|
||||
if (params.closed) filterClause = 'AND report.closed = true'
|
||||
switch (params.closed) {
|
||||
case true:
|
||||
filterClause = 'AND report.closed = true'
|
||||
break
|
||||
case false:
|
||||
filterClause = 'AND report.closed = false'
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
const offset =
|
||||
params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : ''
|
||||
const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : ''
|
||||
|
||||
const reportReadTxPromise = session.readTransaction(async transaction => {
|
||||
const allReportsTransactionResponse = await transaction.run(
|
||||
const reportsReadTxPromise = session.readTransaction(async transaction => {
|
||||
const reportsTransactionResponse = await transaction.run(
|
||||
// !!! this Cypher query returns multiple reports on the same resource! i will create an issue for refactoring (bug fixing)
|
||||
`
|
||||
MATCH (report:Report)-[:BELONGS_TO]->(resource)
|
||||
WHERE (resource:User OR resource:Post OR resource:Comment)
|
||||
@ -101,11 +101,11 @@ export default {
|
||||
${offset} ${limit}
|
||||
`,
|
||||
)
|
||||
log(allReportsTransactionResponse)
|
||||
return allReportsTransactionResponse.records.map(record => record.get('report'))
|
||||
log(reportsTransactionResponse)
|
||||
return reportsTransactionResponse.records.map(record => record.get('report'))
|
||||
})
|
||||
try {
|
||||
const reports = await reportReadTxPromise
|
||||
const reports = await reportsReadTxPromise
|
||||
return reports
|
||||
} finally {
|
||||
session.close()
|
||||
|
||||
@ -10,18 +10,17 @@ const driver = getDriver()
|
||||
describe('file a report on a resource', () => {
|
||||
let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser, otherReportingUser
|
||||
const categoryIds = ['cat9']
|
||||
const reportMutation = gql`
|
||||
const fileReportMutation = gql`
|
||||
mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||
fileReport(
|
||||
resourceId: $resourceId
|
||||
reasonCategory: $reasonCategory
|
||||
reasonDescription: $reasonDescription
|
||||
) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
closed
|
||||
rule
|
||||
reasonCategory
|
||||
reasonDescription
|
||||
reportId
|
||||
resource {
|
||||
__typename
|
||||
... on User {
|
||||
@ -34,6 +33,35 @@ describe('file a report on a resource', () => {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const variables = {
|
||||
resourceId: 'invalid',
|
||||
reasonCategory: 'other',
|
||||
reasonDescription: 'Violates code of conduct !!!',
|
||||
}
|
||||
const reportsQuery = gql`
|
||||
query($closed: Boolean) {
|
||||
reports(orderBy: createdAt_desc, closed: $closed) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
rule
|
||||
disable
|
||||
closed
|
||||
resource {
|
||||
__typename
|
||||
... on User {
|
||||
id
|
||||
}
|
||||
... on Post {
|
||||
id
|
||||
}
|
||||
... on Comment {
|
||||
id
|
||||
}
|
||||
}
|
||||
filed {
|
||||
submitter {
|
||||
id
|
||||
@ -45,11 +73,31 @@ describe('file a report on a resource', () => {
|
||||
}
|
||||
}
|
||||
`
|
||||
const variables = {
|
||||
resourceId: 'whatever',
|
||||
reasonCategory: 'other',
|
||||
reasonDescription: 'Violates code of conduct !!!',
|
||||
}
|
||||
const reviewMutation = gql`
|
||||
mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
|
||||
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
|
||||
createdAt
|
||||
resource {
|
||||
__typename
|
||||
... on User {
|
||||
id
|
||||
disabled
|
||||
}
|
||||
... on Post {
|
||||
id
|
||||
disabled
|
||||
}
|
||||
... on Comment {
|
||||
id
|
||||
disabled
|
||||
}
|
||||
}
|
||||
report {
|
||||
disable
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
@ -74,7 +122,7 @@ describe('file a report on a resource', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({
|
||||
await expect(mutate({ mutation: fileReportMutation, variables })).resolves.toMatchObject({
|
||||
data: { fileReport: null },
|
||||
errors: [{ message: 'Not Authorised!' }],
|
||||
})
|
||||
@ -94,6 +142,17 @@ describe('file a report on a resource', () => {
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
moderator = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'moderator-id',
|
||||
role: 'moderator',
|
||||
},
|
||||
{
|
||||
email: 'moderator@example.org',
|
||||
password: '1234',
|
||||
},
|
||||
)
|
||||
otherReportingUser = await Factory.build(
|
||||
'user',
|
||||
{
|
||||
@ -127,7 +186,7 @@ describe('file a report on a resource', () => {
|
||||
|
||||
describe('invalid resource id', () => {
|
||||
it('returns null', async () => {
|
||||
await expect(mutate({ mutation: reportMutation, variables })).resolves.toMatchObject({
|
||||
await expect(mutate({ mutation: fileReportMutation, variables })).resolves.toMatchObject({
|
||||
data: { fileReport: null },
|
||||
errors: undefined,
|
||||
})
|
||||
@ -139,47 +198,112 @@ describe('file a report on a resource', () => {
|
||||
it('which belongs to resource', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
fileReport: {
|
||||
id: expect.any(String),
|
||||
reportId: expect.any(String),
|
||||
resource: {
|
||||
name: 'abusive-user',
|
||||
},
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('creates only one report for multiple reports on the same resource', async () => {
|
||||
it('only one report for multiple reports on the same resource', async () => {
|
||||
const firstReport = await mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||
})
|
||||
authenticatedUser = await otherReportingUser.toJson()
|
||||
const secondReport = await mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||
})
|
||||
expect(firstReport.data.fileReport.id).toEqual(secondReport.data.fileReport.id)
|
||||
|
||||
expect(firstReport.data.fileReport.reportId).toEqual(
|
||||
secondReport.data.fileReport.reportId,
|
||||
)
|
||||
})
|
||||
|
||||
it('returns the rule for how the report was decided', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
describe('report properties are set correctly', () => {
|
||||
const reportsCypherQuery =
|
||||
'MATCH (resource:User {id: $resourceId})<-[:BELONGS_TO]-(report:Report {closed: false})<-[filed:FILED]-(user:User {id: $currentUserId}) RETURN report'
|
||||
|
||||
it('with the rule for how the report will be decided', async () => {
|
||||
await mutate({
|
||||
mutation: fileReportMutation,
|
||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
fileReport: {
|
||||
rule: 'latestReviewUpdatedAtRules',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
|
||||
const reportsCypherQueryResponse = await instance.cypher(reportsCypherQuery, {
|
||||
resourceId: 'abusive-user-id',
|
||||
currentUserId: authenticatedUser.id,
|
||||
})
|
||||
expect(reportsCypherQueryResponse.records).toHaveLength(1)
|
||||
const [reportProperties] = reportsCypherQueryResponse.records.map(
|
||||
record => record.get('report').properties,
|
||||
)
|
||||
expect(reportProperties).toMatchObject({ rule: 'latestReviewUpdatedAtRules' })
|
||||
})
|
||||
|
||||
describe('with overtaken disabled from resource in disable property', () => {
|
||||
it('disable is false', async () => {
|
||||
await mutate({
|
||||
mutation: fileReportMutation,
|
||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||
})
|
||||
|
||||
const reportsCypherQueryResponse = await instance.cypher(reportsCypherQuery, {
|
||||
resourceId: 'abusive-user-id',
|
||||
currentUserId: authenticatedUser.id,
|
||||
})
|
||||
expect(reportsCypherQueryResponse.records).toHaveLength(1)
|
||||
const [reportProperties] = reportsCypherQueryResponse.records.map(
|
||||
record => record.get('report').properties,
|
||||
)
|
||||
expect(reportProperties).toMatchObject({ disable: false })
|
||||
})
|
||||
|
||||
it('disable is true', async () => {
|
||||
// first time filling a report to enable a moderator the disable the resource
|
||||
await mutate({
|
||||
mutation: fileReportMutation,
|
||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||
})
|
||||
authenticatedUser = await moderator.toJson()
|
||||
await mutate({
|
||||
mutation: reviewMutation,
|
||||
variables: {
|
||||
resourceId: 'abusive-user-id',
|
||||
disable: true,
|
||||
closed: true,
|
||||
},
|
||||
})
|
||||
authenticatedUser = await currentUser.toJson()
|
||||
// second time filling a report to see if the "disable is true" of the resource is overtaken
|
||||
await mutate({
|
||||
mutation: fileReportMutation,
|
||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||
})
|
||||
|
||||
const reportsCypherQueryResponse = await instance.cypher(reportsCypherQuery, {
|
||||
resourceId: 'abusive-user-id',
|
||||
currentUserId: authenticatedUser.id,
|
||||
})
|
||||
expect(reportsCypherQueryResponse.records).toHaveLength(1)
|
||||
const [reportProperties] = reportsCypherQueryResponse.records.map(
|
||||
record => record.get('report').properties,
|
||||
)
|
||||
expect(reportProperties).toMatchObject({ disable: true })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it.todo('creates multiple filed reports')
|
||||
})
|
||||
|
||||
@ -187,7 +311,7 @@ describe('file a report on a resource', () => {
|
||||
it('returns __typename "User"', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
@ -205,7 +329,7 @@ describe('file a report on a resource', () => {
|
||||
it('returns user attribute info', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
@ -221,32 +345,10 @@ describe('file a report on a resource', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the submitter', async () => {
|
||||
it('returns a createdAt', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
fileReport: {
|
||||
filed: [
|
||||
{
|
||||
submitter: {
|
||||
id: 'current-user-id',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('returns a date', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: { ...variables, resourceId: 'abusive-user-id' },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
@ -262,7 +364,7 @@ describe('file a report on a resource', () => {
|
||||
it('returns the reason category', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
resourceId: 'abusive-user-id',
|
||||
@ -272,11 +374,7 @@ describe('file a report on a resource', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
fileReport: {
|
||||
filed: [
|
||||
{
|
||||
reasonCategory: 'criminal_behavior_violation_german_law',
|
||||
},
|
||||
],
|
||||
reasonCategory: 'criminal_behavior_violation_german_law',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -286,7 +384,7 @@ describe('file a report on a resource', () => {
|
||||
it('gives an error if the reason category is not in enum "ReasonCategory"', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
resourceId: 'abusive-user-id',
|
||||
@ -307,7 +405,7 @@ describe('file a report on a resource', () => {
|
||||
it('returns the reason description', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
resourceId: 'abusive-user-id',
|
||||
@ -317,11 +415,7 @@ describe('file a report on a resource', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
fileReport: {
|
||||
filed: [
|
||||
{
|
||||
reasonDescription: 'My reason!',
|
||||
},
|
||||
],
|
||||
reasonDescription: 'My reason!',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -331,7 +425,7 @@ describe('file a report on a resource', () => {
|
||||
it('sanitizes the reason description', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
resourceId: 'abusive-user-id',
|
||||
@ -341,11 +435,7 @@ describe('file a report on a resource', () => {
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
fileReport: {
|
||||
filed: [
|
||||
{
|
||||
reasonDescription: 'My reason !',
|
||||
},
|
||||
],
|
||||
reasonDescription: 'My reason !',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
@ -371,7 +461,7 @@ describe('file a report on a resource', () => {
|
||||
it('returns type "Post"', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
resourceId: 'post-to-report-id',
|
||||
@ -392,7 +482,7 @@ describe('file a report on a resource', () => {
|
||||
it('returns resource in post attribute', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
resourceId: 'post-to-report-id',
|
||||
@ -442,7 +532,7 @@ describe('file a report on a resource', () => {
|
||||
it('returns type "Comment"', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
resourceId: 'comment-to-report-id',
|
||||
@ -463,7 +553,7 @@ describe('file a report on a resource', () => {
|
||||
it('returns resource in comment attribute', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
resourceId: 'comment-to-report-id',
|
||||
@ -493,7 +583,7 @@ describe('file a report on a resource', () => {
|
||||
it('returns null', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
resourceId: 'tag-to-report-id',
|
||||
@ -510,37 +600,6 @@ describe('file a report on a resource', () => {
|
||||
})
|
||||
|
||||
describe('query for reported resource', () => {
|
||||
const reportsQuery = gql`
|
||||
query {
|
||||
reports(orderBy: createdAt_desc) {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
closed
|
||||
resource {
|
||||
__typename
|
||||
... on User {
|
||||
id
|
||||
}
|
||||
... on Post {
|
||||
id
|
||||
}
|
||||
... on Comment {
|
||||
id
|
||||
}
|
||||
}
|
||||
filed {
|
||||
submitter {
|
||||
id
|
||||
}
|
||||
createdAt
|
||||
reasonCategory
|
||||
reasonDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = null
|
||||
moderator = await Factory.build(
|
||||
@ -632,7 +691,7 @@ describe('file a report on a resource', () => {
|
||||
authenticatedUser = await currentUser.toJson()
|
||||
await Promise.all([
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: {
|
||||
resourceId: 'abusive-post-1',
|
||||
reasonCategory: 'other',
|
||||
@ -640,7 +699,7 @@ describe('file a report on a resource', () => {
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: {
|
||||
resourceId: 'abusive-comment-1',
|
||||
reasonCategory: 'discrimination_etc',
|
||||
@ -648,7 +707,7 @@ describe('file a report on a resource', () => {
|
||||
},
|
||||
}),
|
||||
mutate({
|
||||
mutation: reportMutation,
|
||||
mutation: fileReportMutation,
|
||||
variables: {
|
||||
resourceId: 'abusive-user-1',
|
||||
reasonCategory: 'doxing',
|
||||
|
||||
@ -251,12 +251,12 @@ export default {
|
||||
boolean: {
|
||||
followedByCurrentUser:
|
||||
'MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||
isBlocked:
|
||||
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||
blocked:
|
||||
'MATCH (this)-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||
isMuted:
|
||||
'MATCH (this)<-[:MUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||
isBlocked:
|
||||
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||
},
|
||||
count: {
|
||||
contributionsCount:
|
||||
|
||||
@ -16,3 +16,15 @@ enum ReasonCategory {
|
||||
advert_products_services_commercial
|
||||
criminal_behavior_violation_german_law
|
||||
}
|
||||
|
||||
type FiledReport {
|
||||
createdAt: String!
|
||||
reasonCategory: ReasonCategory!
|
||||
reasonDescription: String!
|
||||
reportId: ID!
|
||||
resource: ReportedResource!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
fileReport(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): FiledReport
|
||||
}
|
||||
@ -4,7 +4,6 @@ type REVIEWED {
|
||||
disable: Boolean!
|
||||
closed: Boolean!
|
||||
report: Report
|
||||
# @cypher(statement: "MATCH (report:Report)<-[this:REVIEWED]-(:User) RETURN report")
|
||||
moderator: User
|
||||
resource: ReviewedResource
|
||||
}
|
||||
|
||||
@ -5,9 +5,9 @@ type Report {
|
||||
rule: ReportRule!
|
||||
disable: Boolean!
|
||||
closed: Boolean!
|
||||
filed: [FILED]
|
||||
filed: [FILED]!
|
||||
reviewed: [REVIEWED]!
|
||||
resource: ReportedResource
|
||||
resource: ReportedResource!
|
||||
}
|
||||
|
||||
union ReportedResource = User | Post | Comment
|
||||
@ -16,10 +16,6 @@ enum ReportRule {
|
||||
latestReviewUpdatedAtRules
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
fileReport(resourceId: ID!, reasonCategory: ReasonCategory!, reasonDescription: String!): Report
|
||||
}
|
||||
|
||||
type Query {
|
||||
reports(orderBy: ReportOrdering, first: Int, offset: Int, reviewed: Boolean, closed: Boolean): [Report]
|
||||
}
|
||||
|
||||
@ -64,10 +64,11 @@ type User {
|
||||
# Is the currently logged in user following that user?
|
||||
followedByCurrentUser: Boolean! @cypher(
|
||||
statement: """
|
||||
MATCH (this)<-[: FOLLOWS]-(u: User { id: $cypherParams.currentUserId})
|
||||
MATCH (this)<-[:FOLLOWS]-(u:User { id: $cypherParams.currentUserId})
|
||||
RETURN COUNT(u) >= 1
|
||||
"""
|
||||
)
|
||||
|
||||
isBlocked: Boolean! @cypher(
|
||||
statement: """
|
||||
MATCH (this)<-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,8 @@
|
||||
{
|
||||
"projectId": "qa7fe2",
|
||||
"ignoreTestFiles": "*.js",
|
||||
"baseUrl": "http://localhost:3000"
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"env": {
|
||||
"RETRIES": 2
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
// please change also version in file "webapp/constants/terms-and-conditions-version.js"
|
||||
export const VERSION = '0.0.3'
|
||||
export const VERSION = '0.0.4'
|
||||
@ -30,7 +30,7 @@ Then("my comment should be successfully created", () => {
|
||||
});
|
||||
|
||||
Then("I should see my comment", () => {
|
||||
cy.get("div.comment p")
|
||||
cy.get("article.comment-card p")
|
||||
.should("contain", "Human Connection rocks")
|
||||
.get(".user-avatar img")
|
||||
.should("have.attr", "src")
|
||||
@ -40,12 +40,12 @@ Then("I should see my comment", () => {
|
||||
});
|
||||
|
||||
Then("I should see the entirety of my comment", () => {
|
||||
cy.get("div.comment")
|
||||
cy.get("article.comment-card")
|
||||
.should("not.contain", "show more")
|
||||
});
|
||||
|
||||
Then("I should see an abreviated version of my comment", () => {
|
||||
cy.get("div.comment")
|
||||
cy.get("article.comment-card")
|
||||
.should("contain", "show more")
|
||||
});
|
||||
|
||||
@ -60,7 +60,7 @@ Then("it should create a mention in the CommentForm", () => {
|
||||
})
|
||||
|
||||
When("I open the content menu of post {string}", (title)=> {
|
||||
cy.contains('.post-card', title)
|
||||
cy.contains('.post-teaser', title)
|
||||
.find('.content-menu .base-button')
|
||||
.click()
|
||||
})
|
||||
@ -77,9 +77,10 @@ Then("there is no button to pin a post", () => {
|
||||
})
|
||||
|
||||
And("the post with title {string} has a ribbon for pinned posts", (title) => {
|
||||
cy.get("article.post-card").contains(title)
|
||||
cy.get(".post-teaser").contains(title)
|
||||
.parent()
|
||||
.find("div.ribbon.ribbon--pinned")
|
||||
.parent()
|
||||
.find(".ribbon.--pinned")
|
||||
.should("contain", "Announcement")
|
||||
})
|
||||
|
||||
@ -111,7 +112,7 @@ Then("I add all required fields", () => {
|
||||
.get(".categories-select .base-button")
|
||||
.first()
|
||||
.click()
|
||||
.get('.ds-flex-item > .ds-form-item .ds-select ')
|
||||
.get('.base-card > .select-field input')
|
||||
.click()
|
||||
.get('.ds-select-option')
|
||||
.eq(languages.findIndex(l => l.code === 'en'))
|
||||
@ -119,7 +120,7 @@ Then("I add all required fields", () => {
|
||||
})
|
||||
|
||||
Then("the post was saved successfully with the {string} teaser image", condition => {
|
||||
cy.get(".ds-card-content > .ds-heading")
|
||||
cy.get(".base-card > .title")
|
||||
.should("contain", condition === 'updated' ? 'to be updated' : 'new post')
|
||||
.get(".content")
|
||||
.should("contain", condition === 'updated' ? 'successfully updated' : 'new post content')
|
||||
@ -128,25 +129,22 @@ Then("the post was saved successfully with the {string} teaser image", condition
|
||||
.and("contains", condition === 'updated' ? 'humanconnection' : 'onourjourney')
|
||||
})
|
||||
|
||||
Then("the first image should be removed from the preview", () => {
|
||||
cy.fixture("humanconnection.png").as('postTeaserImage').then(function() {
|
||||
cy.get("#postdropzone")
|
||||
.children()
|
||||
.get('img.thumbnail-preview')
|
||||
.should('have.length', 1)
|
||||
.and('have.attr', 'src')
|
||||
.and('contain', this.postTeaserImage)
|
||||
})
|
||||
Then("the first image should not be displayed anymore", () => {
|
||||
cy.get(".hero-image")
|
||||
.children()
|
||||
.get('.hero-image > .image')
|
||||
.should('have.length', 1)
|
||||
.and('have.attr', 'src')
|
||||
})
|
||||
|
||||
Then('the {string} post was saved successfully without a teaser image', condition => {
|
||||
cy.get(".ds-card-content > .ds-heading")
|
||||
cy.get(".base-card > .title")
|
||||
.should("contain", condition === 'updated' ? 'to be updated' : 'new post')
|
||||
.get(".content")
|
||||
.should("contain", condition === 'updated' ? 'successfully updated' : 'new post content')
|
||||
.get('.post-page')
|
||||
.should('exist')
|
||||
.get('.post-page img.ds-card-image')
|
||||
.get('.hero-image > .image')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
@ -156,12 +154,12 @@ Then('I should be able to remove it', () => {
|
||||
})
|
||||
|
||||
When('my post has a teaser image', () => {
|
||||
cy.get('.contribution-image')
|
||||
cy.get('.contribution-form .image')
|
||||
.should('exist')
|
||||
.and('have.attr', 'src')
|
||||
})
|
||||
|
||||
Then('I should be able to remove the image', () => {
|
||||
cy.get('.delete-image')
|
||||
cy.get('.dz-message > .base-button')
|
||||
.click()
|
||||
})
|
||||
})
|
||||
|
||||
@ -29,7 +29,7 @@ When("I visit another user's profile page", () => {
|
||||
});
|
||||
|
||||
Then("I cannot upload a picture", () => {
|
||||
cy.get(".ds-card-content")
|
||||
cy.get(".base-card")
|
||||
.children()
|
||||
.should("not.have.id", "customdropzone")
|
||||
.should("have.class", "user-avatar");
|
||||
|
||||
@ -12,7 +12,7 @@ let annoyingUserWhoMutedModeratorTitle = 'Fake news'
|
||||
const savePostTitle = $post => {
|
||||
return $post
|
||||
.first()
|
||||
.find('.ds-heading')
|
||||
.find('.title')
|
||||
.first()
|
||||
.invoke('text')
|
||||
.then(title => {
|
||||
@ -51,7 +51,7 @@ Given('I am logged in with a {string} role', role => {
|
||||
})
|
||||
|
||||
When('I click on "Report Post" from the content menu of the post', () => {
|
||||
cy.contains('.ds-card', davidIrvingPostTitle)
|
||||
cy.contains('.base-card', davidIrvingPostTitle)
|
||||
.find('.content-menu .base-button')
|
||||
.click({force: true})
|
||||
|
||||
@ -61,7 +61,7 @@ When('I click on "Report Post" from the content menu of the post', () => {
|
||||
})
|
||||
|
||||
When('I click on "Report User" from the content menu in the user info box', () => {
|
||||
cy.contains('.ds-card', davidIrvingPostTitle)
|
||||
cy.contains('.base-card', davidIrvingPostTitle)
|
||||
.get('.user-content-menu .base-button')
|
||||
.click({ force: true })
|
||||
|
||||
@ -78,7 +78,7 @@ When('I click on the author', () => {
|
||||
|
||||
When('I report the author', () => {
|
||||
cy.get('.page-name-profile-id-slug').then(() => {
|
||||
invokeReportOnElement('.ds-card').then(() => {
|
||||
invokeReportOnElement('.base-card').then(() => {
|
||||
cy.get('button')
|
||||
.contains('Send')
|
||||
.click()
|
||||
@ -139,7 +139,7 @@ Given('somebody reported the following posts:', table => {
|
||||
.authenticateAs(submitter)
|
||||
.mutate(gql`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||
fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
|
||||
id
|
||||
reportId
|
||||
}
|
||||
}`, {
|
||||
resourceId,
|
||||
@ -169,7 +169,7 @@ Then('each list item links to the post page', () => {
|
||||
Then('I can visit the post page', () => {
|
||||
cy.contains(annoyingUserWhoMutedModeratorTitle).click()
|
||||
cy.location('pathname').should('contain', '/post')
|
||||
.get('h3').should('contain', annoyingUserWhoMutedModeratorTitle)
|
||||
.get('.base-card .title').should('contain', annoyingUserWhoMutedModeratorTitle)
|
||||
})
|
||||
|
||||
When("they have a post someone has reported", () => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { When, Then } from "cypress-cucumber-preprocessor/steps";
|
||||
When("I search for {string}", value => {
|
||||
cy.get(".searchable-input .ds-select-search")
|
||||
cy.get(".searchable-input .ds-select input")
|
||||
.focus()
|
||||
.type(value);
|
||||
});
|
||||
@ -25,7 +25,7 @@ Then("the search should contain the annoying user", () => {
|
||||
expect($li).to.have.length(1);
|
||||
})
|
||||
cy.get(".ds-select-dropdown .user-teaser .slug").should("contain", '@spammy-spammer');
|
||||
cy.get(".searchable-input .ds-select-search")
|
||||
cy.get(".searchable-input .ds-select input")
|
||||
.focus()
|
||||
.type("{esc}");
|
||||
})
|
||||
@ -44,21 +44,21 @@ Then("I should see the following users in the select dropdown:", table => {
|
||||
});
|
||||
|
||||
When("I type {string} and press Enter", value => {
|
||||
cy.get(".searchable-input .ds-select-search")
|
||||
cy.get(".searchable-input .ds-select input")
|
||||
.focus()
|
||||
.type(value)
|
||||
.type("{enter}", { force: true });
|
||||
});
|
||||
|
||||
When("I type {string} and press escape", value => {
|
||||
cy.get(".searchable-input .ds-select-search")
|
||||
cy.get(".searchable-input .ds-select input")
|
||||
.focus()
|
||||
.type(value)
|
||||
.type("{esc}");
|
||||
});
|
||||
|
||||
Then("the search field should clear", () => {
|
||||
cy.get(".searchable-input .ds-select-search").should("have.text", "");
|
||||
cy.get(".searchable-input .ds-select input").should("have.text", "");
|
||||
});
|
||||
|
||||
When("I select a post entry", () => {
|
||||
|
||||
@ -80,7 +80,7 @@ Then('I should be on the {string} page', page => {
|
||||
.should(loc => {
|
||||
expect(loc.pathname).to.eq(page)
|
||||
})
|
||||
.get('h3')
|
||||
.get('h2')
|
||||
.should('contain', 'Social media')
|
||||
})
|
||||
|
||||
@ -112,7 +112,7 @@ Given('I have added a social media link', () => {
|
||||
})
|
||||
|
||||
Then('they should be able to see my social media links', () => {
|
||||
cy.get('.ds-card-content')
|
||||
cy.get('.base-card')
|
||||
.contains('Where else can I find Peter Pan?')
|
||||
.get('a[href="https://freeradical.zone/peter-pan"]')
|
||||
.should('have.length', 1)
|
||||
|
||||
@ -73,7 +73,7 @@ Given("the {string} user searches for {string}", (_, postTitle) => {
|
||||
})
|
||||
})
|
||||
.then(user => cy.login(user))
|
||||
cy.get(".searchable-input .ds-select-search")
|
||||
cy.get(".searchable-input .ds-select input")
|
||||
.focus()
|
||||
.type(postTitle);
|
||||
});
|
||||
@ -167,7 +167,8 @@ When("I fill in my email and password combination and click submit", () => {
|
||||
});
|
||||
|
||||
When(/(?:when )?I refresh the page/, () => {
|
||||
cy.reload();
|
||||
cy.visit('/')
|
||||
.reload();
|
||||
});
|
||||
|
||||
When("I log out through the menu in the top right corner", () => {
|
||||
@ -238,16 +239,16 @@ Given("we have the following comments in our database:", table => {
|
||||
});
|
||||
|
||||
Given("we have the following posts in our database:", table => {
|
||||
table.hashes().forEach((attributesOrOptions, i) => {
|
||||
cy.factory().build("post", {
|
||||
...attributesOrOptions,
|
||||
deleted: Boolean(attributesOrOptions.deleted),
|
||||
disabled: Boolean(attributesOrOptions.disabled),
|
||||
pinned: Boolean(attributesOrOptions.pinned),
|
||||
}, {
|
||||
...attributesOrOptions,
|
||||
});
|
||||
})
|
||||
table.hashes().forEach((attributesOrOptions, i) => {
|
||||
cy.factory().build("post", {
|
||||
...attributesOrOptions,
|
||||
deleted: Boolean(attributesOrOptions.deleted),
|
||||
disabled: Boolean(attributesOrOptions.disabled),
|
||||
pinned: Boolean(attributesOrOptions.pinned),
|
||||
}, {
|
||||
...attributesOrOptions,
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
Then("I see a success message:", message => {
|
||||
@ -295,14 +296,14 @@ Then("I select a category", () => {
|
||||
});
|
||||
|
||||
When("I choose {string} as the language for the post", (languageCode) => {
|
||||
cy.get('.ds-flex-item > .ds-form-item .ds-select ')
|
||||
cy.get('.contribution-form .ds-select')
|
||||
.click().get('.ds-select-option')
|
||||
.eq(languages.findIndex(l => l.code === languageCode)).click()
|
||||
})
|
||||
|
||||
Then("the post shows up on the landing page at position {int}", index => {
|
||||
cy.openPage("landing");
|
||||
const selector = `.post-card:nth-child(${index}) > .ds-card-content`;
|
||||
const selector = `.post-teaser:nth-child(${index}) > .base-card`;
|
||||
cy.get(selector).should("contain", lastPost.title);
|
||||
cy.get(selector).should("contain", lastPost.content);
|
||||
});
|
||||
@ -312,16 +313,16 @@ Then("I get redirected to {string}", route => {
|
||||
});
|
||||
|
||||
Then("the post was saved successfully", () => {
|
||||
cy.get(".ds-card-content > .ds-heading").should("contain", lastPost.title);
|
||||
cy.get(".base-card > .title").should("contain", lastPost.title);
|
||||
cy.get(".content").should("contain", lastPost.content);
|
||||
});
|
||||
|
||||
Then(/^I should see only ([0-9]+) posts? on the landing page/, postCount => {
|
||||
cy.get(".post-card").should("have.length", postCount);
|
||||
cy.get(".post-teaser").should("have.length", postCount);
|
||||
});
|
||||
|
||||
Then("the first post on the landing page has the title:", title => {
|
||||
cy.get(".post-card:first").should("contain", title);
|
||||
cy.get(".post-teaser:first").should("contain", title);
|
||||
});
|
||||
|
||||
Then(
|
||||
@ -388,7 +389,7 @@ Then("I can login successfully with password {string}", password => {
|
||||
|
||||
When("open the notification menu and click on the first item", () => {
|
||||
cy.get(".notifications-menu").invoke('show').click(); // "invoke('show')" because of the delay for show the menu
|
||||
cy.get(".notification-mention-post")
|
||||
cy.get(".notification .link")
|
||||
.first()
|
||||
.click({
|
||||
force: true
|
||||
@ -424,7 +425,7 @@ When("mention {string} in the text", mention => {
|
||||
Then("the notification gets marked as read", () => {
|
||||
cy.get(".notifications-menu-popover .notification")
|
||||
.first()
|
||||
.should("have.class", "read");
|
||||
.should("have.class", "--read");
|
||||
});
|
||||
|
||||
Then("there are no notifications in the top menu", () => {
|
||||
@ -510,14 +511,14 @@ Given('{string} wrote a post {string}', (_, title) => {
|
||||
});
|
||||
|
||||
Then("the list of posts of this user is empty", () => {
|
||||
cy.get(".ds-card-content").not(".post-link");
|
||||
cy.get(".base-card").not(".post-link");
|
||||
cy.get(".main-container").find(".ds-space.hc-empty");
|
||||
});
|
||||
|
||||
Then("I get removed from his follower collection", () => {
|
||||
cy.get(".ds-card-content").not(".post-link");
|
||||
cy.get(".base-card").not(".post-link");
|
||||
cy.get(".main-container").contains(
|
||||
".ds-card-content",
|
||||
".base-card",
|
||||
"is not followed by anyone"
|
||||
);
|
||||
});
|
||||
@ -581,7 +582,7 @@ Then("I see only one post with the title {string}", title => {
|
||||
});
|
||||
|
||||
Then("they should not see the comment form", () => {
|
||||
cy.get(".ds-card-footer").children().should('not.have.class', 'comment-form')
|
||||
cy.get(".base-card").children().should('not.have.class', 'comment-form')
|
||||
})
|
||||
|
||||
Then("they should see a text explaining why commenting is not possible", () => {
|
||||
@ -600,11 +601,11 @@ Then("I {string} see {string} from the content menu in the user info box", (cond
|
||||
})
|
||||
|
||||
Then('I should not see {string} button', button => {
|
||||
cy.get('.ds-card-content .action-buttons')
|
||||
cy.get('.base-card .action-buttons')
|
||||
.should('have.length', 1)
|
||||
})
|
||||
|
||||
Then('I should see the {string} button', button => {
|
||||
cy.get('.ds-card-content .action-buttons .base-button')
|
||||
cy.get('.base-card .action-buttons .base-button')
|
||||
.should('contain', button)
|
||||
})
|
||||
|
||||
@ -20,7 +20,6 @@ Feature: Notification for a mention
|
||||
And I select a category
|
||||
And I choose "en" as the language for the post
|
||||
And I click on "Save"
|
||||
When I log out
|
||||
And I log in as "Matt Rider"
|
||||
And see 1 unread notifications in the top menu
|
||||
And open the notification menu and click on the first item
|
||||
|
||||
@ -35,7 +35,7 @@ Feature: Upload Teaser Image
|
||||
And confirm crop
|
||||
And I should be able to "change" a teaser image
|
||||
And confirm crop
|
||||
And the first image should be removed from the preview
|
||||
And the first image should not be displayed anymore
|
||||
|
||||
Scenario: Add image, then delete it
|
||||
When I click on the big plus icon in the bottom right corner to create post
|
||||
@ -44,4 +44,4 @@ Feature: Upload Teaser Image
|
||||
And I add all required fields
|
||||
And I click on "Save"
|
||||
Then I get redirected to ".../new-post"
|
||||
And the "new" post was saved successfully without a teaser image
|
||||
And the "new" post was saved successfully without a teaser image
|
||||
|
||||
@ -55,6 +55,6 @@ Feature: Block a User
|
||||
Scenario: Blocked users should not see link or button to unblock, only blocking users
|
||||
Given a user has blocked me
|
||||
When I visit the profile page of the annoying user
|
||||
And I "should not" see "Unblock user" from the content menu in the user info box
|
||||
And I should see the "Follow" button
|
||||
And I should not see "Unblock user" button
|
||||
And I should not see "Unblock user" button
|
||||
And I "should not" see "Unblock user" from the content menu in the user info box
|
||||
|
||||
@ -58,14 +58,14 @@ Cypress.Commands.add("login", user => {
|
||||
});
|
||||
|
||||
Cypress.Commands.add("manualLogin", ({ email, password }) => {
|
||||
cy.visit(`/login`);
|
||||
cy.get("input[name=email]")
|
||||
cy.visit(`/login`)
|
||||
.get("input[name=email]")
|
||||
.trigger("focus")
|
||||
.type(email);
|
||||
cy.get("input[name=password]")
|
||||
.type(email)
|
||||
.get("input[name=password]")
|
||||
.trigger("focus")
|
||||
.type(password);
|
||||
cy.get("button[name=submit]")
|
||||
.type(password)
|
||||
.get("button[name=submit]")
|
||||
.as("submitButton")
|
||||
.click();
|
||||
});
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import Factory, { cleanDatabase } from '../../backend/src/db/factories'
|
||||
import { getDriver, getNeode } from '../../backend/src/db/neo4j'
|
||||
import Factory from '../../backend/src/db/factories'
|
||||
import { getNeode } from '../../backend/src/db/neo4j'
|
||||
|
||||
|
||||
const neo4jConfigs = {
|
||||
uri: Cypress.env('NEO4J_URI'),
|
||||
@ -8,7 +9,7 @@ const neo4jConfigs = {
|
||||
}
|
||||
const neodeInstance = getNeode(neo4jConfigs)
|
||||
|
||||
beforeEach(() => cleanDatabase())
|
||||
beforeEach(() => cy.then(() => neodeInstance.cypher('MATCH (everything) DETACH DELETE everything;')))
|
||||
|
||||
Cypress.Commands.add('neode', () => {
|
||||
return neodeInstance
|
||||
|
||||
@ -23,4 +23,11 @@ import 'cypress-plugin-retries'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
import { WebSocket } from 'mock-socket'
|
||||
before(() => {
|
||||
cy.visit('/', {
|
||||
onBeforeLoad(win) {
|
||||
cy.stub(win, "WebSocket", url => new WebSocket(url))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
16
package.json
16
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "human-connection",
|
||||
"version": "0.3.1",
|
||||
"version": "0.4.0",
|
||||
"description": "Fullstack and API tests with cypress and cucumber for Human Connection",
|
||||
"author": "Human Connection gGmbh",
|
||||
"license": "MIT",
|
||||
@ -18,32 +18,34 @@
|
||||
"cypress:backend": "cd backend && yarn run dev",
|
||||
"cypress:webapp": "cd webapp && yarn run dev",
|
||||
"cypress:setup": "run-p cypress:backend cypress:webapp",
|
||||
"cypress:run": "cross-env cypress run",
|
||||
"cypress:open": "cross-env cypress open",
|
||||
"cypress:run": "cross-env cypress run --browser firefox",
|
||||
"cypress:open": "cross-env cypress open --browser firefox",
|
||||
"cucumber:setup": "cd backend && yarn run dev",
|
||||
"cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit",
|
||||
"release": "standard-version",
|
||||
"generate:changelog": "yarn version && auto-changelog"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.8.4",
|
||||
"@babel/core": "^7.8.6",
|
||||
"@babel/preset-env": "^7.8.4",
|
||||
"@babel/register": "^7.8.3",
|
||||
"@babel/register": "^7.8.6",
|
||||
"auto-changelog": "^1.16.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"codecov": "^3.6.5",
|
||||
"cross-env": "^6.0.3",
|
||||
"cucumber": "^6.0.5",
|
||||
"cypress": "^3.8.3",
|
||||
"cypress": "^4.0.0",
|
||||
"cypress-cucumber-preprocessor": "^2.0.1",
|
||||
"cypress-file-upload": "^3.5.3",
|
||||
"cypress-plugin-retries": "^1.5.2",
|
||||
"date-fns": "^2.9.0",
|
||||
"date-fns": "^2.10.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"expect": "^25.1.0",
|
||||
"faker": "Marak/faker.js#master",
|
||||
"graphql-request": "^1.8.2",
|
||||
"import": "^0.0.6",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mock-socket": "^9.0.3",
|
||||
"neo4j-driver": "^4.0.1",
|
||||
"neode": "^0.3.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
|
||||
@ -9,3 +9,13 @@ button {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@ -132,6 +132,7 @@ $border-size-x-large: 6px;
|
||||
$border-radius-x-large: 5px;
|
||||
$border-radius-large: 4px;
|
||||
$border-radius-base: 4px;
|
||||
$border-radius-small: 2px;
|
||||
$border-radius-rounded: 2em;
|
||||
$border-radius-circle: 50%;
|
||||
|
||||
@ -211,7 +212,8 @@ $letter-spacing-x-small: -0.015em;
|
||||
* @presenter Opacity
|
||||
*/
|
||||
|
||||
$opacity-soft: 0.65;
|
||||
$opacity-base: 1;
|
||||
$opacity-soft: 0.7;
|
||||
$opacity-disabled: 0.5;
|
||||
|
||||
/**
|
||||
@ -239,6 +241,7 @@ $size-height-large: 50px;
|
||||
$size-height-xlarge: 60px;
|
||||
$size-height-footer: 64px;
|
||||
$size-tappable-square: 44px;
|
||||
$size-ribbon: 6px;
|
||||
|
||||
/**
|
||||
* @tokens Size Width
|
||||
@ -264,12 +267,23 @@ $size-avatar-large: 114px;
|
||||
$size-button-base: 36px;
|
||||
$size-button-small: 26px;
|
||||
|
||||
/**
|
||||
* @tokens Size Images
|
||||
* @presenter Spacing
|
||||
*/
|
||||
|
||||
$size-image-max-height: 2000px;
|
||||
$size-image-cropper-max-height: 600px;
|
||||
$size-image-cropper-min-height: 400px;
|
||||
$size-image-uploader-min-height: 200px;
|
||||
|
||||
/**
|
||||
* @tokens Size Icons
|
||||
* @presenter Spacing
|
||||
*/
|
||||
|
||||
$size-icon-base: 16px;
|
||||
$size-icon-large: 60px;
|
||||
|
||||
/**
|
||||
* @tokens Shadow
|
||||
@ -285,6 +299,12 @@ $box-shadow-active: 0 0 6px 1px rgba(20, 100, 160, 0.5);
|
||||
$box-shadow-inset: inset 0 0 20px 1px rgba(0,0,0,.15);
|
||||
$box-shadow-small-inset: inset 0 0 0 1px rgba(0,0,0,.05);
|
||||
|
||||
/**
|
||||
* @tokens Effects
|
||||
*/
|
||||
|
||||
$blur-radius: 22px;
|
||||
|
||||
/**
|
||||
* @tokens Animation Duration
|
||||
*/
|
||||
@ -316,7 +336,8 @@ $z-index-page-submenu: 2500;
|
||||
$z-index-page-header: 2000;
|
||||
$z-index-page-sidebar: 1500;
|
||||
$z-index-sticky: 100;
|
||||
$z-index-post-card-link: 5;
|
||||
$z-index-post-teaser-link: 5;
|
||||
$z-index-surface: 1;
|
||||
|
||||
/**
|
||||
* @tokens Media Query
|
||||
|
||||
@ -13,6 +13,8 @@ $easeOut: cubic-bezier(0.19, 1, 0.22, 1);
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
@ -141,10 +143,9 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
.ds-card .ds-section {
|
||||
.base-card > .ds-section {
|
||||
padding: 0;
|
||||
margin-left: -$space-base;
|
||||
margin-right: -$space-base;
|
||||
margin: -$space-base;
|
||||
|
||||
.ds-container {
|
||||
padding: $space-base;
|
||||
|
||||
@ -1,30 +1,18 @@
|
||||
<template>
|
||||
<div class="categories-select">
|
||||
<ds-flex :gutter="{ base: 'xx-small', md: 'small', lg: 'xx-small' }">
|
||||
<div v-for="category in categories" :key="category.id">
|
||||
<ds-flex-item>
|
||||
<base-button
|
||||
:data-test="categoryButtonsId(category.id)"
|
||||
@click="toggleCategory(category.id)"
|
||||
:filled="isActive(category.id)"
|
||||
:disabled="isDisabled(category.id)"
|
||||
:icon="category.icon"
|
||||
size="small"
|
||||
>
|
||||
{{ $t(`contribution.category.name.${category.slug}`) }}
|
||||
</base-button>
|
||||
</ds-flex-item>
|
||||
</div>
|
||||
</ds-flex>
|
||||
<p class="small-info">
|
||||
{{
|
||||
$t('contribution.categories.infoSelectedNoOfMaxCategories', {
|
||||
chosen: selectedCount,
|
||||
max: selectedMax,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<section class="categories-select">
|
||||
<base-button
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:data-test="categoryButtonsId(category.id)"
|
||||
@click="toggleCategory(category.id)"
|
||||
:filled="isActive(category.id)"
|
||||
:disabled="isDisabled(category.id)"
|
||||
:icon="category.icon"
|
||||
size="small"
|
||||
>
|
||||
{{ $t(`contribution.category.name.${category.slug}`) }}
|
||||
</base-button>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -85,3 +73,15 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.categories-select {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> .base-button {
|
||||
margin-right: $space-xx-small;
|
||||
margin-bottom: $space-xx-small;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,230 +0,0 @@
|
||||
<template>
|
||||
<div v-if="(comment.deleted || comment.disabled) && !isModerator" :class="{ comment: true }">
|
||||
<ds-card>
|
||||
<ds-space margin-bottom="base" />
|
||||
<ds-text style="padding-left: 40px; font-weight: bold;" color="soft">
|
||||
<base-icon name="ban" />
|
||||
{{ this.$t('comment.content.unavailable-placeholder') }}
|
||||
</ds-text>
|
||||
<ds-space margin-bottom="base" />
|
||||
</ds-card>
|
||||
</div>
|
||||
<div v-else :class="{ comment: true, 'disabled-content': comment.deleted || comment.disabled }">
|
||||
<ds-card :id="anchor" :class="{ 'comment--target': isTarget }">
|
||||
<ds-space margin-bottom="small" margin-top="small">
|
||||
<user-teaser :user="author" :date-time="comment.createdAt">
|
||||
<template v-slot:dateTime>
|
||||
<ds-text v-if="comment.createdAt !== comment.updatedAt">
|
||||
({{ $t('comment.edited') }})
|
||||
</ds-text>
|
||||
</template>
|
||||
</user-teaser>
|
||||
<client-only>
|
||||
<content-menu
|
||||
v-show="!openEditCommentMenu"
|
||||
placement="bottom-end"
|
||||
resource-type="comment"
|
||||
:resource="comment"
|
||||
:modalsData="menuModalsData"
|
||||
class="float-right"
|
||||
:is-owner="isAuthor(author.id)"
|
||||
@showEditCommentMenu="editCommentMenu"
|
||||
/>
|
||||
</client-only>
|
||||
</ds-space>
|
||||
<div v-if="openEditCommentMenu">
|
||||
<comment-form
|
||||
:update="true"
|
||||
:post="post"
|
||||
:comment="comment"
|
||||
@showEditCommentMenu="editCommentMenu"
|
||||
@updateComment="updateComment"
|
||||
@collapse="isCollapsed = true"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<content-viewer :content="commentContent" class="comment-content" />
|
||||
<button
|
||||
v-if="isLongComment"
|
||||
type="button"
|
||||
class="collapse-button"
|
||||
@click="isCollapsed = !isCollapsed"
|
||||
>
|
||||
{{ isCollapsed ? $t('comment.show.more') : $t('comment.show.less') }}
|
||||
</button>
|
||||
</div>
|
||||
<ds-space margin-bottom="small" />
|
||||
<base-button
|
||||
:title="this.$t('post.comment.reply')"
|
||||
icon="level-down"
|
||||
class="reply-button"
|
||||
circle
|
||||
size="small"
|
||||
v-scroll-to="'.editor'"
|
||||
@click.prevent="reply"
|
||||
></base-button>
|
||||
</ds-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { COMMENT_MAX_UNTRUNCATED_LENGTH, COMMENT_TRUNCATE_TO_LENGTH } from '~/constants/comment'
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
||||
import ContentViewer from '~/components/Editor/ContentViewer'
|
||||
import CommentForm from '~/components/CommentForm/CommentForm'
|
||||
import CommentMutations from '~/graphql/CommentMutations'
|
||||
import scrollToAnchor from '~/mixins/scrollToAnchor.js'
|
||||
import BaseButton from '~/components/_new/generic/BaseButton/BaseButton'
|
||||
|
||||
export default {
|
||||
mixins: [scrollToAnchor],
|
||||
data() {
|
||||
const anchor = `commentId-${this.comment.id}`
|
||||
const isTarget = this.routeHash === `#${anchor}`
|
||||
|
||||
return {
|
||||
anchor,
|
||||
isTarget,
|
||||
isCollapsed: !isTarget,
|
||||
openEditCommentMenu: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
UserTeaser,
|
||||
ContentMenu,
|
||||
ContentViewer,
|
||||
CommentForm,
|
||||
BaseButton,
|
||||
},
|
||||
props: {
|
||||
routeHash: { type: String, default: () => '' },
|
||||
post: { type: Object, default: () => ({}) },
|
||||
comment: { type: Object, default: () => ({}) },
|
||||
dateTime: { type: [Date, String], default: null },
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'auth/user',
|
||||
isModerator: 'auth/isModerator',
|
||||
}),
|
||||
isLongComment() {
|
||||
return this.$filters.removeHtml(this.comment.content).length > COMMENT_MAX_UNTRUNCATED_LENGTH
|
||||
},
|
||||
commentContent() {
|
||||
if (this.isLongComment && this.isCollapsed) {
|
||||
return this.$filters.truncate(this.comment.content, COMMENT_TRUNCATE_TO_LENGTH)
|
||||
}
|
||||
return this.comment.content
|
||||
},
|
||||
displaysComment() {
|
||||
return !this.unavailable || this.isModerator
|
||||
},
|
||||
author() {
|
||||
if (this.deleted) return {}
|
||||
return this.comment.author || {}
|
||||
},
|
||||
menuModalsData() {
|
||||
return {
|
||||
delete: {
|
||||
titleIdent: 'delete.comment.title',
|
||||
messageIdent: 'delete.comment.message',
|
||||
messageParams: {
|
||||
name: this.$filters.truncate(this.comment.contentExcerpt, 30),
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: 'trash',
|
||||
textIdent: 'delete.submit',
|
||||
callback: this.deleteCommentCallback,
|
||||
},
|
||||
cancel: {
|
||||
icon: 'close',
|
||||
textIdent: 'delete.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reply() {
|
||||
const message = { slug: this.comment.author.slug, id: this.comment.author.id }
|
||||
this.$emit('reply', message)
|
||||
},
|
||||
checkAnchor(anchor) {
|
||||
return `#${this.anchor}` === anchor
|
||||
},
|
||||
isAuthor(id) {
|
||||
return this.user.id === id
|
||||
},
|
||||
editCommentMenu(showMenu) {
|
||||
this.openEditCommentMenu = showMenu
|
||||
this.$emit('toggleNewCommentForm', !showMenu)
|
||||
},
|
||||
updateComment(comment) {
|
||||
this.$emit('updateComment', comment)
|
||||
},
|
||||
async deleteCommentCallback() {
|
||||
try {
|
||||
const {
|
||||
data: { DeleteComment },
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: CommentMutations(this.$i18n).DeleteComment,
|
||||
variables: { id: this.comment.id },
|
||||
})
|
||||
this.$toast.success(this.$t(`delete.comment.success`))
|
||||
this.$emit('deleteComment', DeleteComment)
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.collapse-button {
|
||||
// TODO: move this to css resets
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
|
||||
float: right;
|
||||
padding: 0 16px 16px 16px;
|
||||
color: $color-primary;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.reply-button {
|
||||
float: right;
|
||||
top: 0px;
|
||||
}
|
||||
.reply-button:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@keyframes highlight {
|
||||
0% {
|
||||
border: 1px solid $color-primary;
|
||||
}
|
||||
100% {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.comment--target {
|
||||
animation: highlight 4s ease;
|
||||
}
|
||||
</style>
|
||||
@ -1,5 +1,5 @@
|
||||
import { config, mount } from '@vue/test-utils'
|
||||
import Comment from './Comment.vue'
|
||||
import CommentCard from './CommentCard.vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
const localVue = global.localVue
|
||||
@ -8,11 +8,17 @@ localVue.directive('scrollTo', jest.fn())
|
||||
config.stubs['client-only'] = '<span><slot /></span>'
|
||||
config.stubs['nuxt-link'] = '<span><slot /></span>'
|
||||
|
||||
describe('Comment.vue', () => {
|
||||
describe('CommentCard.vue', () => {
|
||||
let propsData, mocks, stubs, getters, wrapper, Wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
propsData = {}
|
||||
propsData = {
|
||||
comment: {
|
||||
id: 'comment007',
|
||||
author: { id: 'some-user' },
|
||||
},
|
||||
postId: 'post42',
|
||||
}
|
||||
mocks = {
|
||||
$t: jest.fn(),
|
||||
$toast: {
|
||||
@ -26,6 +32,7 @@ describe('Comment.vue', () => {
|
||||
truncate: a => a,
|
||||
removeHtml: a => a,
|
||||
},
|
||||
$route: { hash: '' },
|
||||
$scrollTo: jest.fn(),
|
||||
$apollo: {
|
||||
mutate: jest.fn().mockResolvedValue({
|
||||
@ -55,7 +62,7 @@ describe('Comment.vue', () => {
|
||||
const store = new Vuex.Store({
|
||||
getters,
|
||||
})
|
||||
return mount(Comment, {
|
||||
return mount(CommentCard, {
|
||||
store,
|
||||
propsData,
|
||||
mocks,
|
||||
@ -1,6 +1,6 @@
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
import { withA11y } from '@storybook/addon-a11y'
|
||||
import Comment from './Comment'
|
||||
import CommentCard from './CommentCard'
|
||||
import helpers from '~/storybook/helpers'
|
||||
|
||||
helpers.init()
|
||||
@ -41,14 +41,14 @@ const comment = {
|
||||
__typename: 'Comment',
|
||||
}
|
||||
|
||||
storiesOf('Comment', module)
|
||||
storiesOf('CommentCard', module)
|
||||
.addDecorator(withA11y)
|
||||
.addDecorator(helpers.layout)
|
||||
.add('Basic comment', () => ({
|
||||
components: { Comment },
|
||||
components: { CommentCard },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
comment,
|
||||
}),
|
||||
template: `<comment :key="comment.id" :comment="comment" />`,
|
||||
template: `<comment-card :key="comment.id" :comment="comment" />`,
|
||||
}))
|
||||
216
webapp/components/CommentCard/CommentCard.vue
Normal file
216
webapp/components/CommentCard/CommentCard.vue
Normal file
@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<base-card v-if="isUnavailable" class="comment-card">
|
||||
<p>
|
||||
<base-icon name="ban" />
|
||||
{{ this.$t('comment.content.unavailable-placeholder') }}
|
||||
</p>
|
||||
</base-card>
|
||||
<base-card v-else :class="commentClass" :id="anchor">
|
||||
<header class="header">
|
||||
<user-teaser :user="comment.author" :date-time="comment.createdAt">
|
||||
<template v-if="wasEdited" #dateTime>
|
||||
<span>({{ $t('comment.edited') }})</span>
|
||||
</template>
|
||||
</user-teaser>
|
||||
<client-only>
|
||||
<content-menu
|
||||
v-show="!editingComment"
|
||||
placement="bottom-end"
|
||||
resource-type="comment"
|
||||
:resource="comment"
|
||||
:modalsData="menuModalsData"
|
||||
:is-owner="user.id === comment.author.id"
|
||||
@editComment="editComment(true)"
|
||||
/>
|
||||
</client-only>
|
||||
</header>
|
||||
<comment-form
|
||||
v-if="editingComment"
|
||||
:update="true"
|
||||
:postId="postId"
|
||||
:comment="comment"
|
||||
@finishEditing="editComment(false)"
|
||||
@updateComment="updateComment"
|
||||
@collapse="isCollapsed = true"
|
||||
/>
|
||||
<template v-else>
|
||||
<content-viewer :content="commentContent" class="content" />
|
||||
<base-button v-if="hasLongContent" size="small" ghost @click="isCollapsed = !isCollapsed">
|
||||
{{ isCollapsed ? $t('comment.show.more') : $t('comment.show.less') }}
|
||||
</base-button>
|
||||
</template>
|
||||
<base-button
|
||||
:title="this.$t('post.comment.reply')"
|
||||
icon="level-down"
|
||||
class="reply-button"
|
||||
circle
|
||||
size="small"
|
||||
v-scroll-to="'.editor'"
|
||||
@click="reply"
|
||||
/>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { COMMENT_MAX_UNTRUNCATED_LENGTH, COMMENT_TRUNCATE_TO_LENGTH } from '~/constants/comment'
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
||||
import ContentViewer from '~/components/Editor/ContentViewer'
|
||||
import CommentForm from '~/components/CommentForm/CommentForm'
|
||||
import CommentMutations from '~/graphql/CommentMutations'
|
||||
import scrollToAnchor from '~/mixins/scrollToAnchor.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserTeaser,
|
||||
ContentMenu,
|
||||
ContentViewer,
|
||||
CommentForm,
|
||||
},
|
||||
mixins: [scrollToAnchor],
|
||||
data() {
|
||||
const anchor = `commentId-${this.comment.id}`
|
||||
const isTarget = this.$route.hash === `#${anchor}`
|
||||
|
||||
return {
|
||||
anchor,
|
||||
isTarget,
|
||||
isCollapsed: !isTarget,
|
||||
editingComment: false,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
comment: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
postId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'auth/user',
|
||||
isModerator: 'auth/isModerator',
|
||||
}),
|
||||
hasLongContent() {
|
||||
return this.$filters.removeHtml(this.comment.content).length > COMMENT_MAX_UNTRUNCATED_LENGTH
|
||||
},
|
||||
isUnavailable() {
|
||||
return (this.comment.deleted || this.comment.disabled) && !this.isModerator
|
||||
},
|
||||
wasEdited() {
|
||||
return this.comment.createdAt !== this.comment.updatedAt
|
||||
},
|
||||
commentClass() {
|
||||
let commentClass = 'comment-card'
|
||||
|
||||
if (this.comment.deleted || this.comment.disabled) commentClass += ' disabled-content'
|
||||
if (this.isTarget) commentClass += ' --target'
|
||||
|
||||
return commentClass
|
||||
},
|
||||
commentContent() {
|
||||
if (this.hasLongContent && this.isCollapsed) {
|
||||
return this.$filters.truncate(this.comment.content, COMMENT_TRUNCATE_TO_LENGTH)
|
||||
}
|
||||
|
||||
return this.comment.content
|
||||
},
|
||||
menuModalsData() {
|
||||
return {
|
||||
delete: {
|
||||
titleIdent: 'delete.comment.title',
|
||||
messageIdent: 'delete.comment.message',
|
||||
messageParams: {
|
||||
name: this.$filters.truncate(this.comment.contentExcerpt, 30),
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: 'trash',
|
||||
textIdent: 'delete.submit',
|
||||
callback: this.deleteCommentCallback,
|
||||
},
|
||||
cancel: {
|
||||
icon: 'close',
|
||||
textIdent: 'delete.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
checkAnchor(anchor) {
|
||||
return `#${this.anchor}` === anchor
|
||||
},
|
||||
reply() {
|
||||
const message = { slug: this.comment.author.slug, id: this.comment.author.id }
|
||||
this.$emit('reply', message)
|
||||
},
|
||||
editComment(editing) {
|
||||
this.editingComment = editing
|
||||
this.$emit('toggleNewCommentForm', !editing)
|
||||
},
|
||||
updateComment(comment) {
|
||||
this.$emit('updateComment', comment)
|
||||
},
|
||||
async deleteCommentCallback() {
|
||||
try {
|
||||
const {
|
||||
data: { DeleteComment },
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: CommentMutations(this.$i18n).DeleteComment,
|
||||
variables: { id: this.comment.id },
|
||||
})
|
||||
this.$toast.success(this.$t(`delete.comment.success`))
|
||||
this.$emit('deleteComment', DeleteComment)
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.comment-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: $space-small;
|
||||
|
||||
&.--target {
|
||||
animation: highlight 4s ease;
|
||||
}
|
||||
|
||||
> .header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
|
||||
> .base-button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-button {
|
||||
float: right;
|
||||
top: 0px;
|
||||
}
|
||||
.reply-button:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@keyframes highlight {
|
||||
0% {
|
||||
border: $border-size-base solid $color-primary;
|
||||
}
|
||||
100% {
|
||||
border: $border-size-base solid transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -153,10 +153,10 @@ describe('CommentForm.vue', () => {
|
||||
expect(closeMethodSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('emits `showEditCommentMenu` event', async () => {
|
||||
it('emits `finishEditing` event', async () => {
|
||||
wrapper.vm.updateEditorContent('ok')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
expect(wrapper.emitted('showEditCommentMenu')).toEqual([[false]])
|
||||
expect(wrapper.emitted('finishEditing')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@ -167,10 +167,10 @@ describe('CommentForm.vue', () => {
|
||||
expect(closeMethodSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('emits `showEditCommentMenu` event', async () => {
|
||||
it('emits `finishEditing` event', async () => {
|
||||
wrapper.vm.updateEditorContent('ok')
|
||||
await wrapper.find('[data-test="cancel-button"]').trigger('submit')
|
||||
expect(wrapper.emitted('showEditCommentMenu')).toEqual([[false]])
|
||||
expect(wrapper.emitted('finishEditing')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<ds-form v-model="form" @submit="handleSubmit" class="comment-form">
|
||||
<template slot-scope="{ errors }">
|
||||
<ds-card>
|
||||
<!-- with client-only the content is not shown -->
|
||||
<base-card>
|
||||
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
|
||||
<div class="buttons">
|
||||
<base-button
|
||||
@ -17,7 +16,7 @@
|
||||
{{ $t('post.comment.submit') }}
|
||||
</base-button>
|
||||
</div>
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
</ds-form>
|
||||
</template>
|
||||
@ -72,7 +71,7 @@ export default {
|
||||
this.$refs.editor.clear()
|
||||
},
|
||||
closeEditWindow() {
|
||||
this.$emit('showEditCommentMenu', false)
|
||||
this.$emit('finishEditing')
|
||||
},
|
||||
handleCancel() {
|
||||
if (!this.update) {
|
||||
@ -146,10 +145,13 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
.comment-form {
|
||||
.editor {
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: $space-small 0;
|
||||
|
||||
> .base-button {
|
||||
margin-left: $space-x-small;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { config, mount } from '@vue/test-utils'
|
||||
import CommentList from './CommentList'
|
||||
import Comment from '~/components/Comment/Comment'
|
||||
import CommentCard from '~/components/CommentCard/CommentCard'
|
||||
import Vuex from 'vuex'
|
||||
import Vue from 'vue'
|
||||
|
||||
@ -20,9 +20,14 @@ describe('CommentList.vue', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
post: {
|
||||
id: 1,
|
||||
id: 'post42',
|
||||
comments: [
|
||||
{ id: 'comment134', contentExcerpt: 'this is a comment', content: 'this is a comment' },
|
||||
{
|
||||
id: 'comment134',
|
||||
contentExcerpt: 'this is a comment',
|
||||
content: 'this is a comment',
|
||||
author: { id: 'some-user' },
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@ -41,6 +46,7 @@ describe('CommentList.vue', () => {
|
||||
removeHtml: a => a,
|
||||
},
|
||||
$scrollTo: jest.fn(),
|
||||
$route: { hash: '' },
|
||||
$apollo: {
|
||||
queries: {
|
||||
Post: {
|
||||
@ -73,12 +79,6 @@ describe('CommentList.vue', () => {
|
||||
beforeEach(jest.useFakeTimers)
|
||||
|
||||
describe('$route.hash !== `#comments`', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$route = {
|
||||
hash: '',
|
||||
}
|
||||
})
|
||||
|
||||
it('skips $scrollTo', () => {
|
||||
wrapper = Wrapper()
|
||||
jest.runAllTimers()
|
||||
@ -107,7 +107,7 @@ describe('CommentList.vue', () => {
|
||||
})
|
||||
|
||||
it('Comment emitted reply()', () => {
|
||||
wrapper.find(Comment).vm.$emit('reply', {
|
||||
wrapper.find(CommentCard).vm.$emit('reply', {
|
||||
id: 'commentAuthorId',
|
||||
slug: 'ogerly',
|
||||
})
|
||||
|
||||
@ -1,18 +1,15 @@
|
||||
<template>
|
||||
<div id="comments" class="comment-list">
|
||||
<h3 class="title">
|
||||
<counter-icon icon="comments" :count="post.comments.length" />
|
||||
<counter-icon icon="comments" :count="postComments.length" />
|
||||
{{ $t('common.comment', null, 0) }}
|
||||
</h3>
|
||||
<ds-space margin-bottom="large" />
|
||||
<div v-if="post.comments && post.comments.length" id="comments" class="comments">
|
||||
<comment
|
||||
v-for="comment in post.comments"
|
||||
<div v-if="postComments" id="comments" class="comments">
|
||||
<comment-card
|
||||
v-for="comment in postComments"
|
||||
:key="comment.id"
|
||||
:comment="comment"
|
||||
:post="post"
|
||||
:routeHash="routeHash"
|
||||
class="comment-tag"
|
||||
:postId="post.id"
|
||||
@deleteComment="updateCommentList"
|
||||
@updateComment="updateCommentList"
|
||||
@toggleNewCommentForm="toggleNewCommentForm"
|
||||
@ -23,18 +20,25 @@
|
||||
</template>
|
||||
<script>
|
||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||
import Comment from '~/components/Comment/Comment'
|
||||
import CommentCard from '~/components/CommentCard/CommentCard'
|
||||
import scrollToAnchor from '~/mixins/scrollToAnchor'
|
||||
|
||||
export default {
|
||||
mixins: [scrollToAnchor],
|
||||
components: {
|
||||
CounterIcon,
|
||||
Comment,
|
||||
CommentCard,
|
||||
},
|
||||
props: {
|
||||
routeHash: { type: String, default: () => '' },
|
||||
post: { type: Object, default: () => {} },
|
||||
post: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
postComments() {
|
||||
return (this.post && this.post.comments) || []
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reply(message) {
|
||||
@ -44,7 +48,7 @@ export default {
|
||||
return anchor === '#comments'
|
||||
},
|
||||
updateCommentList(updatedComment) {
|
||||
this.post.comments = this.post.comments.map(comment => {
|
||||
this.postComments = this.postComments.map(comment => {
|
||||
return comment.id === updatedComment.id ? updatedComment : comment
|
||||
})
|
||||
},
|
||||
|
||||
@ -154,7 +154,7 @@ describe('ContentMenu.vue', () => {
|
||||
.filter(item => item.text() === 'comment.menu.edit')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(wrapper.emitted('showEditCommentMenu')).toEqual([[true]])
|
||||
expect(wrapper.emitted('editComment')).toBeTruthy()
|
||||
})
|
||||
it('delete the comment', () => {
|
||||
wrapper
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
size="small"
|
||||
circle
|
||||
ghost
|
||||
@click="toggleMenu"
|
||||
@click.prevent="toggleMenu()"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
@ -104,7 +104,7 @@ export default {
|
||||
routes.push({
|
||||
label: this.$t(`comment.menu.edit`),
|
||||
callback: () => {
|
||||
this.$emit('showEditCommentMenu', true)
|
||||
this.$emit('editComment')
|
||||
},
|
||||
icon: 'edit',
|
||||
})
|
||||
|
||||
@ -6,7 +6,7 @@ import Vuex from 'vuex'
|
||||
import PostMutations from '~/graphql/PostMutations.js'
|
||||
import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
|
||||
|
||||
import TeaserImage from '~/components/TeaserImage/TeaserImage'
|
||||
import ImageUploader from '~/components/ImageUploader/ImageUploader'
|
||||
import MutationObserver from 'mutation-observer'
|
||||
|
||||
global.MutationObserver = MutationObserver
|
||||
@ -182,7 +182,7 @@ describe('ContributionForm.vue', () => {
|
||||
})
|
||||
|
||||
it('has no more than three categories', async () => {
|
||||
wrapper.vm.form.categoryIds = ['cat4', 'cat9', 'cat15', 'cat27']
|
||||
wrapper.vm.formData.categoryIds = ['cat4', 'cat9', 'cat15', 'cat27']
|
||||
await Vue.nextTick()
|
||||
wrapper.find('form').trigger('submit')
|
||||
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||
@ -233,10 +233,13 @@ describe('ContributionForm.vue', () => {
|
||||
})
|
||||
|
||||
it('supports adding a teaser image', async () => {
|
||||
const spy = jest.spyOn(FileReader.prototype, 'readAsDataURL').mockImplementation(() => {})
|
||||
expectedParams.variables.imageUpload = imageUpload
|
||||
wrapper.find(TeaserImage).vm.$emit('addTeaserImage', imageUpload)
|
||||
wrapper.find(ImageUploader).vm.$emit('addHeroImage', imageUpload)
|
||||
await wrapper.find('form').trigger('submit')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||
expect(spy).toHaveBeenCalledWith(imageUpload)
|
||||
spy.mockReset()
|
||||
})
|
||||
|
||||
it('content is valid with just a link', async () => {
|
||||
@ -320,20 +323,12 @@ describe('ContributionForm.vue', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('sets id equal to contribution id', () => {
|
||||
expect(wrapper.vm.id).toEqual(propsData.contribution.id)
|
||||
})
|
||||
|
||||
it('sets slug equal to contribution slug', () => {
|
||||
expect(wrapper.vm.slug).toEqual(propsData.contribution.slug)
|
||||
})
|
||||
|
||||
it('sets title equal to contribution title', () => {
|
||||
expect(wrapper.vm.form.title).toEqual(propsData.contribution.title)
|
||||
expect(wrapper.vm.formData.title).toEqual(propsData.contribution.title)
|
||||
})
|
||||
|
||||
it('sets content equal to contribution content', () => {
|
||||
expect(wrapper.vm.form.content).toEqual(propsData.contribution.content)
|
||||
expect(wrapper.vm.formData.content).toEqual(propsData.contribution.content)
|
||||
})
|
||||
|
||||
describe('valid update', () => {
|
||||
@ -362,6 +357,7 @@ describe('ContributionForm.vue', () => {
|
||||
image,
|
||||
imageUpload: null,
|
||||
imageAspectRatio: 1,
|
||||
imageBlurred: false,
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -387,9 +383,10 @@ describe('ContributionForm.vue', () => {
|
||||
|
||||
it('supports deleting a teaser image', async () => {
|
||||
expectedParams.variables.image = null
|
||||
expectedParams.variables.imageAspectRatio = null
|
||||
propsData.contribution.image = '/uploads/someimage.png'
|
||||
wrapper = Wrapper()
|
||||
wrapper.find('.contribution-form .delete-image').trigger('click')
|
||||
wrapper.find('[data-test="delete-button"]').trigger('click')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||
})
|
||||
|
||||
@ -2,115 +2,75 @@
|
||||
<ds-form
|
||||
class="contribution-form"
|
||||
ref="contributionForm"
|
||||
v-model="form"
|
||||
v-model="formData"
|
||||
:schema="formSchema"
|
||||
@submit="submit"
|
||||
>
|
||||
<template slot-scope="{ errors }">
|
||||
<base-button
|
||||
v-if="showDeleteImageButton"
|
||||
class="delete-image"
|
||||
icon="close"
|
||||
size="small"
|
||||
circle
|
||||
danger
|
||||
filled
|
||||
@click.prevent="deleteImage"
|
||||
/>
|
||||
<hc-teaser-image
|
||||
:contribution="contribution"
|
||||
:class="{ '--blur-image': form.blurImage }"
|
||||
@addTeaserImage="addTeaserImage"
|
||||
@addImageAspectRatio="addImageAspectRatio"
|
||||
@cropInProgress="cropInProgress"
|
||||
>
|
||||
<img
|
||||
v-if="contribution"
|
||||
class="contribution-image"
|
||||
:src="contribution.image | proxyApiUrl"
|
||||
/>
|
||||
</hc-teaser-image>
|
||||
|
||||
<ds-card>
|
||||
<div class="blur-toggle">
|
||||
<base-card>
|
||||
<template #heroImage>
|
||||
<img
|
||||
v-if="formData.image"
|
||||
:src="formData.image | proxyApiUrl"
|
||||
:class="['image', formData.imageBlurred && '--blur-image']"
|
||||
/>
|
||||
<image-uploader
|
||||
:hasImage="!!formData.image"
|
||||
:class="[formData.imageBlurred && '--blur-image']"
|
||||
@addHeroImage="addHeroImage"
|
||||
@addImageAspectRatio="addImageAspectRatio"
|
||||
/>
|
||||
</template>
|
||||
<div v-if="formData.image" class="blur-toggle">
|
||||
<label for="blur-img">{{ $t('contribution.inappropriatePicture') }}</label>
|
||||
<input type="checkbox" id="blur-img" v-model="form.blurImage" />
|
||||
<p>
|
||||
<a
|
||||
href="https://support.human-connection.org/kb/faq.php?id=113"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>
|
||||
{{ $t('contribution.inappropriatePictureText') }}
|
||||
<ds-icon name="question-circle" />
|
||||
</a>
|
||||
</p>
|
||||
<input type="checkbox" id="blur-img" v-model="formData.imageBlurred" />
|
||||
<a
|
||||
href="https://support.human-connection.org/kb/faq.php?id=113"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>
|
||||
{{ $t('contribution.inappropriatePictureText') }}
|
||||
<base-icon name="question-circle" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ds-space />
|
||||
<client-only>
|
||||
<user-teaser :user="currentUser" />
|
||||
</client-only>
|
||||
<ds-space />
|
||||
<ds-input
|
||||
model="title"
|
||||
class="post-title"
|
||||
:placeholder="$t('contribution.title')"
|
||||
name="title"
|
||||
autofocus
|
||||
size="large"
|
||||
/>
|
||||
<ds-text align="right">
|
||||
<ds-chip v-if="errors && errors.title" color="danger" size="base">
|
||||
{{ form.title.length }}/{{ formSchema.title.max }}
|
||||
<ds-icon name="warning"></ds-icon>
|
||||
</ds-chip>
|
||||
<ds-chip v-else size="base">{{ form.title.length }}/{{ formSchema.title.max }}</ds-chip>
|
||||
</ds-text>
|
||||
<ds-chip size="base" :color="errors && errors.title && 'danger'">
|
||||
{{ formData.title.length }}/{{ formSchema.title.max }}
|
||||
<base-icon v-if="errors && errors.title" name="warning" />
|
||||
</ds-chip>
|
||||
<hc-editor
|
||||
:users="users"
|
||||
:value="form.content"
|
||||
:value="formData.content"
|
||||
:hashtags="hashtags"
|
||||
@input="updateEditorContent"
|
||||
/>
|
||||
<ds-text align="right">
|
||||
<ds-chip v-if="errors && errors.content" color="danger" size="base">
|
||||
{{ contentLength }}
|
||||
<ds-icon name="warning"></ds-icon>
|
||||
</ds-chip>
|
||||
<ds-chip v-else size="base">
|
||||
{{ contentLength }}
|
||||
</ds-chip>
|
||||
</ds-text>
|
||||
<ds-space margin-bottom="small" />
|
||||
<hc-categories-select model="categoryIds" :existingCategoryIds="form.categoryIds" />
|
||||
<ds-text align="right">
|
||||
<ds-chip v-if="errors && errors.categoryIds" color="danger" size="base">
|
||||
{{ form.categoryIds.length }} / 3
|
||||
<ds-icon name="warning"></ds-icon>
|
||||
</ds-chip>
|
||||
<ds-chip v-else size="base">{{ form.categoryIds.length }} / 3</ds-chip>
|
||||
</ds-text>
|
||||
<ds-flex class="contribution-form-footer">
|
||||
<ds-flex-item :width="{ lg: '50%', md: '50%', sm: '100%' }" />
|
||||
<ds-flex-item>
|
||||
<ds-space margin-bottom="small" />
|
||||
<ds-select
|
||||
model="language"
|
||||
:options="languageOptions"
|
||||
icon="globe"
|
||||
:placeholder="$t('contribution.languageSelectText')"
|
||||
:label="$t('contribution.languageSelectLabel')"
|
||||
/>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
<ds-text align="right">
|
||||
<ds-chip v-if="errors && errors.language" size="base" color="danger">
|
||||
<ds-icon name="warning"></ds-icon>
|
||||
</ds-chip>
|
||||
</ds-text>
|
||||
|
||||
<ds-space />
|
||||
<div slot="footer" style="text-align: right">
|
||||
<ds-chip size="base" :color="errors && errors.content && 'danger'">
|
||||
{{ contentLength }}
|
||||
<base-icon v-if="errors && errors.content" name="warning" />
|
||||
</ds-chip>
|
||||
<categories-select model="categoryIds" :existingCategoryIds="formData.categoryIds" />
|
||||
<ds-chip size="base" :color="errors && errors.categoryIds && 'danger'">
|
||||
{{ formData.categoryIds.length }} / 3
|
||||
<base-icon v-if="errors && errors.categoryIds" name="warning" />
|
||||
</ds-chip>
|
||||
<ds-select
|
||||
model="language"
|
||||
icon="globe"
|
||||
class="select-field"
|
||||
:options="languageOptions"
|
||||
:placeholder="$t('contribution.languageSelectText')"
|
||||
:label="$t('contribution.languageSelectLabel')"
|
||||
/>
|
||||
<ds-chip v-if="errors && errors.language" size="base" color="danger">
|
||||
<base-icon name="warning" />
|
||||
</ds-chip>
|
||||
<div class="buttons">
|
||||
<base-button data-test="cancel-button" :disabled="loading" @click="$router.back()" danger>
|
||||
{{ $t('actions.cancel') }}
|
||||
</base-button>
|
||||
@ -118,8 +78,7 @@
|
||||
{{ $t('actions.save') }}
|
||||
</base-button>
|
||||
</div>
|
||||
<ds-space margin-bottom="large" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
</ds-form>
|
||||
</template>
|
||||
@ -131,127 +90,95 @@ import { mapGetters } from 'vuex'
|
||||
import HcEditor from '~/components/Editor/Editor'
|
||||
import locales from '~/locales'
|
||||
import PostMutations from '~/graphql/PostMutations.js'
|
||||
import HcCategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
|
||||
import HcTeaserImage from '~/components/TeaserImage/TeaserImage'
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
|
||||
import ImageUploader from '~/components/ImageUploader/ImageUploader'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HcEditor,
|
||||
HcCategoriesSelect,
|
||||
HcTeaserImage,
|
||||
UserTeaser,
|
||||
CategoriesSelect,
|
||||
ImageUploader,
|
||||
},
|
||||
props: {
|
||||
contribution: { type: Object, default: () => {} },
|
||||
contribution: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const {
|
||||
title,
|
||||
content,
|
||||
image,
|
||||
imageAspectRatio,
|
||||
imageBlurred,
|
||||
language,
|
||||
categories,
|
||||
} = this.contribution
|
||||
|
||||
const languageOptions = orderBy(locales, 'name').map(locale => {
|
||||
return { label: locale.name, value: locale.code }
|
||||
})
|
||||
|
||||
const formDefaults = {
|
||||
title: '',
|
||||
content: '',
|
||||
teaserImage: null,
|
||||
imageAspectRatio: null,
|
||||
image: null,
|
||||
language: null,
|
||||
categoryIds: [],
|
||||
blurImage: false,
|
||||
}
|
||||
|
||||
let id = null
|
||||
let slug = null
|
||||
const form = { ...formDefaults }
|
||||
if (this.contribution && this.contribution.id) {
|
||||
id = this.contribution.id
|
||||
slug = this.contribution.slug
|
||||
form.title = this.contribution.title
|
||||
form.content = this.contribution.content
|
||||
form.image = this.contribution.image
|
||||
form.language =
|
||||
this.contribution && this.contribution.language
|
||||
? languageOptions.find(o => this.contribution.language === o.value)
|
||||
: null
|
||||
form.categoryIds = this.categoryIds(this.contribution.categories)
|
||||
form.imageAspectRatio = this.contribution.imageAspectRatio
|
||||
form.blurImage = this.contribution.imageBlurred
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
formData: {
|
||||
title: title || '',
|
||||
content: content || '',
|
||||
image: image || null,
|
||||
imageAspectRatio: imageAspectRatio || null,
|
||||
imageBlurred: imageBlurred || false,
|
||||
language: languageOptions.find(option => option.value === language) || null,
|
||||
categoryIds: categories ? categories.map(category => category.id) : [],
|
||||
},
|
||||
formSchema: {
|
||||
title: { required: true, min: 3, max: 100 },
|
||||
content: { required: true },
|
||||
categoryIds: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
validator: (rule, value) => {
|
||||
const errors = []
|
||||
if (!(value && value.length >= 1 && value.length <= 3)) {
|
||||
errors.push(new Error(this.$t('common.validations.categories')))
|
||||
validator: (_, value = []) => {
|
||||
if (value.length === 0 || value.length > 3) {
|
||||
return [new Error(this.$t('common.validations.categories'))]
|
||||
}
|
||||
return errors
|
||||
return []
|
||||
},
|
||||
},
|
||||
language: { required: true },
|
||||
blurImage: { required: false },
|
||||
imageBlurred: { required: false },
|
||||
},
|
||||
languageOptions,
|
||||
id,
|
||||
slug,
|
||||
loading: false,
|
||||
users: [],
|
||||
contentMin: 3,
|
||||
hashtags: [],
|
||||
elem: null,
|
||||
isCropInProgress: null,
|
||||
imageUpload: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
contentLength() {
|
||||
return this.$filters.removeHtml(this.form.content).length
|
||||
},
|
||||
...mapGetters({
|
||||
currentUser: 'auth/user',
|
||||
}),
|
||||
showDeleteImageButton() {
|
||||
return this.contribution && this.contribution.image && !this.isCropInProgress
|
||||
contentLength() {
|
||||
return this.$filters.removeHtml(this.formData.content).length
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
const {
|
||||
language: { value: language },
|
||||
title,
|
||||
content,
|
||||
image,
|
||||
teaserImage,
|
||||
imageAspectRatio,
|
||||
categoryIds,
|
||||
blurImage,
|
||||
} = this.form
|
||||
this.loading = true
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: this.id ? PostMutations().UpdatePost : PostMutations().CreatePost,
|
||||
mutation: this.contribution.id ? PostMutations().UpdatePost : PostMutations().CreatePost,
|
||||
variables: {
|
||||
id: this.id,
|
||||
title,
|
||||
content,
|
||||
categoryIds,
|
||||
language,
|
||||
image,
|
||||
imageUpload: teaserImage,
|
||||
imageBlurred: blurImage,
|
||||
imageAspectRatio,
|
||||
...this.formData,
|
||||
id: this.contribution.id || null,
|
||||
language: this.formData.language.value,
|
||||
image: this.imageUpload ? null : this.formData.image,
|
||||
imageUpload: this.imageUpload,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
this.loading = false
|
||||
this.$toast.success(this.$t('contribution.success'))
|
||||
const result = data[this.id ? 'UpdatePost' : 'CreatePost']
|
||||
const result = data[this.contribution.id ? 'UpdatePost' : 'CreatePost']
|
||||
|
||||
this.$router.push({
|
||||
name: 'post-id-slug',
|
||||
@ -266,22 +193,19 @@ export default {
|
||||
updateEditorContent(value) {
|
||||
this.$refs.contributionForm.update('content', value)
|
||||
},
|
||||
addTeaserImage(file) {
|
||||
this.form.teaserImage = file
|
||||
addHeroImage(file) {
|
||||
this.formData.image = null
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({ target }) => {
|
||||
this.formData.image = target.result
|
||||
}
|
||||
this.imageUpload = file
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
},
|
||||
addImageAspectRatio(aspectRatio) {
|
||||
this.form.imageAspectRatio = aspectRatio
|
||||
},
|
||||
categoryIds(categories) {
|
||||
return categories.map(c => c.id)
|
||||
},
|
||||
deleteImage() {
|
||||
this.contribution.image = null
|
||||
this.form.image = null
|
||||
this.form.teaserImage = null
|
||||
},
|
||||
cropInProgress(boolean) {
|
||||
this.isCropInProgress = boolean
|
||||
this.formData.imageAspectRatio = aspectRatio
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
@ -319,41 +243,48 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.contribution-form {
|
||||
.ds-card-image.--blur-image img {
|
||||
filter: blur(32px);
|
||||
.contribution-form > .base-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .hero-image {
|
||||
position: relative;
|
||||
|
||||
> .image {
|
||||
max-height: $size-image-max-height;
|
||||
}
|
||||
}
|
||||
|
||||
.image.--blur-image {
|
||||
filter: blur($blur-radius);
|
||||
}
|
||||
|
||||
> .ds-form-item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> .ds-chip {
|
||||
align-self: flex-end;
|
||||
margin: $space-xx-small 0 $space-base;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
> .select-field {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
align-self: flex-end;
|
||||
margin-top: $space-base;
|
||||
}
|
||||
|
||||
.blur-toggle {
|
||||
text-align: right;
|
||||
margin-bottom: $space-base;
|
||||
|
||||
> .link {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.ds-chip {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
margin-top: $space-x-small;
|
||||
margin-bottom: $space-xx-small;
|
||||
|
||||
input {
|
||||
border: 0;
|
||||
font-size: $font-size-x-large;
|
||||
font-weight: bold;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.delete-image {
|
||||
right: 10px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
float: right;
|
||||
top: $space-large;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -88,7 +88,7 @@ describe('DeleteData.vue', () => {
|
||||
|
||||
describe('calls the delete user mutation', () => {
|
||||
beforeEach(() => {
|
||||
enableDeletionInput = wrapper.find('.enable-deletion-input input')
|
||||
enableDeletionInput = wrapper.find('.ds-input')
|
||||
enableDeletionInput.setValue(deleteAccountName)
|
||||
deleteAccountBtn = wrapper.find('[data-test="delete-button"]')
|
||||
})
|
||||
@ -107,7 +107,7 @@ describe('DeleteData.vue', () => {
|
||||
|
||||
it("deletes a user's posts if requested", () => {
|
||||
mocks.$t.mockImplementation(() => deleteContributionsMessage)
|
||||
enableContributionDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(0)
|
||||
enableContributionDeletionCheckbox = wrapper.findAll('input[type="checkbox"]').at(0)
|
||||
enableContributionDeletionCheckbox.trigger('click')
|
||||
deleteAccountBtn.trigger('click')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||
@ -122,7 +122,7 @@ describe('DeleteData.vue', () => {
|
||||
|
||||
it("deletes a user's comments if requested", () => {
|
||||
mocks.$t.mockImplementation(() => deleteCommentsMessage)
|
||||
enableCommentDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(1)
|
||||
enableCommentDeletionCheckbox = wrapper.findAll('input[type="checkbox"]').at(1)
|
||||
enableCommentDeletionCheckbox.trigger('click')
|
||||
deleteAccountBtn.trigger('click')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||
@ -137,10 +137,10 @@ describe('DeleteData.vue', () => {
|
||||
|
||||
it("deletes a user's posts and comments if requested", () => {
|
||||
mocks.$t.mockImplementation(() => deleteContributionsMessage)
|
||||
enableContributionDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(0)
|
||||
enableContributionDeletionCheckbox = wrapper.findAll('input[type="checkbox"]').at(0)
|
||||
enableContributionDeletionCheckbox.trigger('click')
|
||||
mocks.$t.mockImplementation(() => deleteCommentsMessage)
|
||||
enableCommentDeletionCheckbox = wrapper.findAll('.checkbox-container input').at(1)
|
||||
enableCommentDeletionCheckbox = wrapper.findAll('input[type="checkbox"]').at(1)
|
||||
enableCommentDeletionCheckbox.trigger('click')
|
||||
deleteAccountBtn.trigger('click')
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||
@ -166,7 +166,7 @@ describe('DeleteData.vue', () => {
|
||||
|
||||
describe('error handling', () => {
|
||||
it('shows an error toaster when the mutation rejects', async () => {
|
||||
enableDeletionInput = wrapper.find('.enable-deletion-input input')
|
||||
enableDeletionInput = wrapper.find('.ds-input')
|
||||
enableDeletionInput.setValue(deleteAccountName)
|
||||
await Vue.nextTick()
|
||||
deleteAccountBtn = wrapper.find('[data-test="delete-button"]')
|
||||
|
||||
@ -1,80 +1,46 @@
|
||||
<template>
|
||||
<div>
|
||||
<ds-card hover>
|
||||
<ds-space />
|
||||
<ds-container>
|
||||
<ds-flex>
|
||||
<ds-flex-item :width="{ base: '22%', sm: '12%', md: '12%', lg: '8%' }">
|
||||
<base-icon name="warning" class="delete-warning-icon" />
|
||||
</ds-flex-item>
|
||||
<ds-flex-item :width="{ base: '78%', sm: '88%', md: '88%', lg: '92%' }">
|
||||
<ds-heading>{{ $t('settings.deleteUserAccount.name') }}</ds-heading>
|
||||
</ds-flex-item>
|
||||
<ds-space />
|
||||
<ds-heading tag="h4">
|
||||
{{ $t('settings.deleteUserAccount.accountDescription') }}
|
||||
</ds-heading>
|
||||
</ds-flex>
|
||||
</ds-container>
|
||||
<ds-space />
|
||||
<ds-container>
|
||||
<transition name="slide-up">
|
||||
<div v-if="deleteEnabled">
|
||||
<label v-if="currentUser.contributionsCount" class="checkbox-container">
|
||||
<input type="checkbox" v-model="deleteContributions" />
|
||||
<span class="checkmark"></span>
|
||||
{{
|
||||
$t('settings.deleteUserAccount.contributionsCount', {
|
||||
count: currentUser.contributionsCount,
|
||||
})
|
||||
}}
|
||||
</label>
|
||||
<ds-space margin-bottom="small" />
|
||||
<label v-if="currentUser.commentedCount" class="checkbox-container">
|
||||
<input type="checkbox" v-model="deleteComments" />
|
||||
<span class="checkmark"></span>
|
||||
{{
|
||||
$t('settings.deleteUserAccount.commentedCount', {
|
||||
count: currentUser.commentedCount,
|
||||
})
|
||||
}}
|
||||
</label>
|
||||
<ds-space margin-bottom="small" />
|
||||
<ds-section id="delete-user-account-warning">
|
||||
<div v-html="$t('settings.deleteUserAccount.accountWarning')"></div>
|
||||
</ds-section>
|
||||
</div>
|
||||
</transition>
|
||||
</ds-container>
|
||||
<template slot="footer" class="delete-data-footer">
|
||||
<ds-container>
|
||||
<div
|
||||
class="delete-input-label"
|
||||
v-html="$t('settings.deleteUserAccount.pleaseConfirm', { confirm: currentUser.name })"
|
||||
></div>
|
||||
<ds-space margin-bottom="xx-small" />
|
||||
<ds-flex :gutter="{ base: 'xx-small', md: 'small', lg: 'large' }">
|
||||
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '100%', lg: 1.75 }">
|
||||
<ds-input v-model="enableDeletionValue" class="enable-deletion-input" />
|
||||
</ds-flex-item>
|
||||
<ds-flex-item :width="{ base: '100%', sm: '100%', md: '100%', lg: 1 }">
|
||||
<base-button
|
||||
icon="trash"
|
||||
danger
|
||||
filled
|
||||
:disabled="!deleteEnabled"
|
||||
data-test="delete-button"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ $t('settings.deleteUserAccount.name') }}
|
||||
</base-button>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-container>
|
||||
</template>
|
||||
</ds-card>
|
||||
</div>
|
||||
<base-card class="delete-data">
|
||||
<h2 class="title">
|
||||
<base-icon name="warning" />
|
||||
{{ $t('settings.deleteUserAccount.name') }}
|
||||
</h2>
|
||||
<label>
|
||||
{{ $t('settings.deleteUserAccount.pleaseConfirm', { confirm: currentUser.name }) }}
|
||||
</label>
|
||||
<ds-input v-model="enableDeletionValue" />
|
||||
<p class="notice">{{ $t('settings.deleteUserAccount.accountDescription') }}</p>
|
||||
<label v-if="currentUser.contributionsCount" class="checkbox">
|
||||
<input type="checkbox" v-model="deleteContributions" />
|
||||
{{
|
||||
$t('settings.deleteUserAccount.contributionsCount', {
|
||||
count: currentUser.contributionsCount,
|
||||
})
|
||||
}}
|
||||
</label>
|
||||
<label v-if="currentUser.commentedCount" class="checkbox">
|
||||
<input type="checkbox" v-model="deleteComments" />
|
||||
{{
|
||||
$t('settings.deleteUserAccount.commentedCount', {
|
||||
count: currentUser.commentedCount,
|
||||
})
|
||||
}}
|
||||
</label>
|
||||
<section v-if="deleteEnabled" class="warning">
|
||||
<p>{{ $t('settings.deleteUserAccount.accountWarning') }}</p>
|
||||
</section>
|
||||
<base-button
|
||||
icon="trash"
|
||||
danger
|
||||
filled
|
||||
:disabled="!deleteEnabled"
|
||||
data-test="delete-button"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ $t('settings.deleteUserAccount.name') }}
|
||||
</base-button>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import gql from 'graphql-tag'
|
||||
@ -131,96 +97,47 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.delete-warning-icon {
|
||||
color: $color-danger;
|
||||
font-size: $font-size-xxx-large;
|
||||
}
|
||||
.delete-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.checkbox-container {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 35px;
|
||||
cursor: pointer;
|
||||
font-size: $font-size-large;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
> .title > .base-icon {
|
||||
color: $color-danger;
|
||||
}
|
||||
|
||||
.checkbox-container input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
> .ds-form-item {
|
||||
align-self: flex-start;
|
||||
margin-top: $space-xxx-small;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border: 2px solid $background-color-inverse-softer;
|
||||
background-color: $background-color-base;
|
||||
border-radius: $border-radius-x-large;
|
||||
}
|
||||
> .notice {
|
||||
font-weight: $font-weight-bold;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
|
||||
.checkbox-container:hover input ~ .checkmark {
|
||||
background-color: $background-color-softest;
|
||||
}
|
||||
> .checkbox {
|
||||
margin-left: $space-base;
|
||||
margin-bottom: $space-x-small;
|
||||
|
||||
.checkbox-container input:checked ~ .checkmark {
|
||||
background-color: $background-color-danger-active;
|
||||
}
|
||||
&:last-of-type {
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
}
|
||||
|
||||
.checkmark:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
> .warning {
|
||||
padding: $space-large;
|
||||
margin-bottom: $space-small;
|
||||
border-radius: $border-radius-base;
|
||||
|
||||
.checkbox-container input:checked ~ .checkmark:after {
|
||||
display: block;
|
||||
}
|
||||
color: $color-danger;
|
||||
background-color: $color-danger-inverse;
|
||||
border-left: 4px solid $color-danger;
|
||||
}
|
||||
|
||||
.checkbox-container .checkmark:after {
|
||||
left: 6px;
|
||||
top: 3px;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border: solid $background-color-base;
|
||||
border-width: 0 $border-size-large $border-size-large 0;
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.enable-deletion-input input:focus {
|
||||
border-color: $border-color-danger;
|
||||
}
|
||||
|
||||
.delete-input-label {
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
|
||||
b.is-danger {
|
||||
color: $text-color-danger;
|
||||
}
|
||||
|
||||
.delete-data-footer {
|
||||
border-top: $border-size-base solid $border-color-softest;
|
||||
background-color: $background-color-danger-inverse;
|
||||
}
|
||||
|
||||
#delete-user-account-warning {
|
||||
background-color: $background-color-danger-inverse;
|
||||
border-left: $border-size-x-large solid $background-color-danger-active;
|
||||
color: $text-color-danger;
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
border-radius: $border-radius-x-large;
|
||||
> .base-button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -40,9 +40,9 @@ storiesOf('Editor', module)
|
||||
return {
|
||||
components: { ctx },
|
||||
template: `
|
||||
<ds-card style="width: 50%; min-width: 500px; margin: 0 auto;">
|
||||
<base-card style="width: 50%; min-width: 500px; margin: 0 auto;">
|
||||
<ctx />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
`,
|
||||
}
|
||||
})
|
||||
|
||||
@ -89,7 +89,7 @@ export default {
|
||||
}
|
||||
|
||||
&.hint {
|
||||
opacity: 0.7;
|
||||
opacity: $opacity-soft;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,10 +12,10 @@ describe('FilterMenu.vue', () => {
|
||||
mocks = { $t: () => {} }
|
||||
})
|
||||
|
||||
describe('given a user', () => {
|
||||
describe('given a hashtag', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
hashtag: null,
|
||||
hashtag: 'Frieden',
|
||||
}
|
||||
})
|
||||
|
||||
@ -27,19 +27,14 @@ describe('FilterMenu.vue', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('does not render a card if there are no hashtags', () => {
|
||||
expect(wrapper.is('.ds-card')).toBe(true)
|
||||
})
|
||||
|
||||
it('renders a card if there are hashtags', () => {
|
||||
propsData.hashtag = 'Frieden'
|
||||
it('renders a card', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.is('.ds-card')).toBe(true)
|
||||
expect(wrapper.is('.base-card')).toBe(true)
|
||||
})
|
||||
|
||||
describe('click "clear-search-button" button', () => {
|
||||
describe('click clear search button', () => {
|
||||
it('emits clearSearch', () => {
|
||||
wrapper.find('[name="clear-search-button"]').trigger('click')
|
||||
wrapper.find('.base-button').trigger('click')
|
||||
expect(wrapper.emitted().clearSearch).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,32 +1,22 @@
|
||||
<template>
|
||||
<ds-card class="filter-menu-card">
|
||||
<ds-flex class="filter-menu-content">
|
||||
<ds-flex-item>
|
||||
<ds-heading size="h3">{{ $t('filter-menu.hashtag-search', { hashtag }) }}</ds-heading>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item>
|
||||
<div class="filter-menu-buttons">
|
||||
<base-button
|
||||
name="clear-search-button"
|
||||
icon="close"
|
||||
circle
|
||||
@click="clearSearch"
|
||||
v-tooltip="{
|
||||
content: this.$t('filter-menu.clearSearch'),
|
||||
placement: 'left',
|
||||
delay: { show: 500 },
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-card>
|
||||
<base-card class="filter-menu">
|
||||
<h2>{{ $t('filter-menu.hashtag-search', { hashtag }) }}</h2>
|
||||
<base-button
|
||||
icon="close"
|
||||
circle
|
||||
:title="this.$t('filter-menu.clearSearch')"
|
||||
@click="clearSearch"
|
||||
/>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
hashtag: { type: String, default: null },
|
||||
hashtag: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clearSearch() {
|
||||
@ -37,21 +27,10 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.filter-menu-card {
|
||||
background-color: $background-color-soft;
|
||||
}
|
||||
|
||||
.filter-menu-content {
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-menu-title {
|
||||
.filter-menu.base-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-menu-buttons {
|
||||
float: right;
|
||||
padding: $space-x-small $space-base;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import TeaserImage from './TeaserImage.vue'
|
||||
import ImageUploader from './ImageUploader.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('TeaserImage.vue', () => {
|
||||
describe('ImageUploader.vue', () => {
|
||||
let wrapper
|
||||
let mocks
|
||||
|
||||
@ -17,7 +17,7 @@ describe('TeaserImage.vue', () => {
|
||||
})
|
||||
describe('mount', () => {
|
||||
const Wrapper = () => {
|
||||
return mount(TeaserImage, { mocks, localVue })
|
||||
return mount(ImageUploader, { mocks, localVue })
|
||||
}
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
@ -28,21 +28,10 @@ describe('TeaserImage.vue', () => {
|
||||
const message = 'File upload failed'
|
||||
const fileError = { status: 'error' }
|
||||
|
||||
it('defaults to error false', () => {
|
||||
expect(wrapper.vm.error).toEqual(false)
|
||||
})
|
||||
|
||||
it('shows an error toaster when verror is called', () => {
|
||||
wrapper.vm.verror(fileError, message)
|
||||
wrapper.vm.onDropzoneError(fileError, message)
|
||||
expect(mocks.$toast.error).toHaveBeenCalledWith(fileError.status, message)
|
||||
})
|
||||
|
||||
it('changes error status from false to true to false', () => {
|
||||
wrapper.vm.verror(fileError, message)
|
||||
expect(wrapper.vm.error).toEqual(true)
|
||||
jest.runAllTimers()
|
||||
expect(wrapper.vm.error).toEqual(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
200
webapp/components/ImageUploader/ImageUploader.vue
Normal file
200
webapp/components/ImageUploader/ImageUploader.vue
Normal file
@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div class="image-uploader">
|
||||
<vue-dropzone
|
||||
v-show="!showCropper"
|
||||
id="postdropzone"
|
||||
:options="dropzoneOptions"
|
||||
:use-custom-slot="true"
|
||||
@vdropzone-error="onDropzoneError"
|
||||
@vdropzone-file-added="initCropper"
|
||||
>
|
||||
<loading-spinner v-if="isLoadingImage" />
|
||||
<base-icon v-else name="image" />
|
||||
<base-button
|
||||
v-if="hasImage"
|
||||
icon="trash"
|
||||
circle
|
||||
danger
|
||||
filled
|
||||
data-test="delete-button"
|
||||
:title="$t('actions.delete')"
|
||||
@click.stop="deleteImage"
|
||||
/>
|
||||
</vue-dropzone>
|
||||
<div v-show="showCropper" class="crop-overlay">
|
||||
<img id="cropping-image" />
|
||||
<base-button class="crop-confirm" filled @click="cropImage">
|
||||
{{ $t('contribution.teaserImage.cropperConfirm') }}
|
||||
</base-button>
|
||||
<base-button
|
||||
class="crop-cancel"
|
||||
icon="close"
|
||||
size="small"
|
||||
circle
|
||||
danger
|
||||
filled
|
||||
@click="closeCropper"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueDropzone from 'nuxt-dropzone'
|
||||
import Cropper from 'cropperjs'
|
||||
import LoadingSpinner from '~/components/_new/generic/LoadingSpinner/LoadingSpinner'
|
||||
import 'cropperjs/dist/cropper.css'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
VueDropzone,
|
||||
},
|
||||
props: {
|
||||
hasImage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dropzoneOptions: {
|
||||
url: () => '',
|
||||
maxFilesize: 5.0,
|
||||
previewTemplate: '<span class="no-preview" />',
|
||||
},
|
||||
cropper: null,
|
||||
file: null,
|
||||
showCropper: false,
|
||||
isLoadingImage: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onDropzoneError(file, message) {
|
||||
this.$toast.error(file.status, message)
|
||||
},
|
||||
initCropper(file) {
|
||||
this.showCropper = true
|
||||
this.file = file
|
||||
|
||||
const imageElement = document.querySelector('#cropping-image')
|
||||
imageElement.src = URL.createObjectURL(file)
|
||||
this.cropper = new Cropper(imageElement, { zoomable: false, autoCropArea: 0.9 })
|
||||
},
|
||||
cropImage() {
|
||||
this.isLoadingImage = true
|
||||
|
||||
const onCropComplete = (aspectRatio, imageFile) => {
|
||||
this.$emit('addImageAspectRatio', aspectRatio)
|
||||
this.$emit('addHeroImage', imageFile)
|
||||
this.$nextTick((this.isLoadingImage = false))
|
||||
this.closeCropper()
|
||||
}
|
||||
|
||||
if (this.file.type === 'image/jpeg') {
|
||||
const canvas = this.cropper.getCroppedCanvas()
|
||||
canvas.toBlob(blob => {
|
||||
const imageAspectRatio = canvas.width / canvas.height
|
||||
const croppedImageFile = new File([blob], this.file.name, { type: this.file.type })
|
||||
onCropComplete(imageAspectRatio, croppedImageFile)
|
||||
}, 'image/jpeg')
|
||||
} else {
|
||||
// TODO: use cropped file instead of original file
|
||||
const imageAspectRatio = this.file.width / this.file.height || 1.0
|
||||
onCropComplete(imageAspectRatio, this.file)
|
||||
}
|
||||
},
|
||||
closeCropper() {
|
||||
this.showCropper = false
|
||||
this.cropper.destroy()
|
||||
},
|
||||
deleteImage() {
|
||||
this.$emit('addHeroImage', null)
|
||||
this.$emit('addImageAspectRatio', null)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.image-uploader {
|
||||
position: relative;
|
||||
min-height: $size-image-uploader-min-height;
|
||||
cursor: pointer;
|
||||
|
||||
.image + & {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&:only-child {
|
||||
background-color: $color-neutral-85;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .crop-overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: $size-image-cropper-min-height;
|
||||
max-height: $size-image-cropper-max-height;
|
||||
font-size: $font-size-base;
|
||||
|
||||
> .img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
> .crop-confirm {
|
||||
position: absolute;
|
||||
left: $space-x-small;
|
||||
top: $space-x-small;
|
||||
z-index: $z-index-surface;
|
||||
}
|
||||
|
||||
> .crop-cancel {
|
||||
position: absolute;
|
||||
right: $space-x-small;
|
||||
top: $space-x-small;
|
||||
z-index: $z-index-surface;
|
||||
}
|
||||
}
|
||||
|
||||
.dz-message {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: $z-index-surface;
|
||||
|
||||
&:hover {
|
||||
> .base-icon {
|
||||
opacity: $opacity-base;
|
||||
}
|
||||
}
|
||||
|
||||
> .base-icon {
|
||||
position: absolute;
|
||||
padding: $space-small;
|
||||
border-radius: 100%;
|
||||
border: $border-size-base dashed $color-neutral-20;
|
||||
background-color: $color-neutral-95;
|
||||
font-size: $size-icon-large;
|
||||
opacity: $opacity-soft;
|
||||
}
|
||||
|
||||
> .base-button {
|
||||
position: absolute;
|
||||
top: $space-small;
|
||||
right: $space-small;
|
||||
z-index: $z-index-surface;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,10 +1,12 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { config, mount } from '@vue/test-utils'
|
||||
|
||||
import LocaleSwitch from './LocaleSwitch.vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
config.stubs['client-only'] = '<span><slot /></span>'
|
||||
|
||||
describe('LocaleSwitch.vue', () => {
|
||||
let wrapper, mocks, computed, deutschLanguageItem, getters
|
||||
|
||||
|
||||
@ -1,35 +1,37 @@
|
||||
<template>
|
||||
<dropdown ref="menu" :placement="placement" :offset="offset">
|
||||
<a
|
||||
slot="default"
|
||||
slot-scope="{ toggleMenu }"
|
||||
class="locale-menu"
|
||||
href="#"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
<base-icon name="globe" />
|
||||
<span class="label">{{ current.code.toUpperCase() }}</span>
|
||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||
</a>
|
||||
<ds-menu
|
||||
slot="popover"
|
||||
slot-scope="{ toggleMenu }"
|
||||
class="locale-menu-popover"
|
||||
:matcher="matcher"
|
||||
:routes="routes"
|
||||
>
|
||||
<ds-menu-item
|
||||
slot="menuitem"
|
||||
slot-scope="item"
|
||||
class="locale-menu-item"
|
||||
:route="item.route"
|
||||
:parents="item.parents"
|
||||
@click.stop.prevent="changeLanguage(item.route.path, toggleMenu)"
|
||||
<client-only>
|
||||
<dropdown ref="menu" :placement="placement" :offset="offset">
|
||||
<a
|
||||
slot="default"
|
||||
slot-scope="{ toggleMenu }"
|
||||
class="locale-menu"
|
||||
href="#"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
{{ item.route.name }}
|
||||
</ds-menu-item>
|
||||
</ds-menu>
|
||||
</dropdown>
|
||||
<base-icon name="globe" />
|
||||
<span class="label">{{ current.code.toUpperCase() }}</span>
|
||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||
</a>
|
||||
<ds-menu
|
||||
slot="popover"
|
||||
slot-scope="{ toggleMenu }"
|
||||
class="locale-menu-popover"
|
||||
:matcher="matcher"
|
||||
:routes="routes"
|
||||
>
|
||||
<ds-menu-item
|
||||
slot="menuitem"
|
||||
slot-scope="item"
|
||||
class="locale-menu-item"
|
||||
:route="item.route"
|
||||
:parents="item.parents"
|
||||
@click.stop.prevent="changeLanguage(item.route.path, toggleMenu)"
|
||||
>
|
||||
{{ item.route.name }}
|
||||
</ds-menu-item>
|
||||
</ds-menu>
|
||||
</dropdown>
|
||||
</client-only>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -1,71 +1,54 @@
|
||||
<template>
|
||||
<ds-container width="medium">
|
||||
<ds-space margin="small">
|
||||
<blockquote>
|
||||
<p>{{ $t('quotes.african.quote') }}</p>
|
||||
<b>- {{ $t('quotes.african.author') }}</b>
|
||||
</blockquote>
|
||||
</ds-space>
|
||||
<ds-card class="login-card">
|
||||
<ds-flex gutter="small">
|
||||
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
|
||||
<client-only>
|
||||
<locale-switch class="login-locale-switch" offset="5" />
|
||||
</client-only>
|
||||
<ds-space margin-top="small" margin-bottom="xxx-small" centered>
|
||||
<img
|
||||
class="login-image"
|
||||
alt="Human Connection"
|
||||
src="/img/sign-up/humanconnection.svg"
|
||||
/>
|
||||
</ds-space>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item :width="{ base: '100%', sm: '50%' }" centered>
|
||||
<ds-space margin="small">
|
||||
<a :href="$t('login.moreInfoURL')" :title="$t('login.moreInfoHint')" target="_blank">
|
||||
{{ $t('login.moreInfo') }}
|
||||
</a>
|
||||
</ds-space>
|
||||
<ds-space margin="small">
|
||||
<ds-text size="small">{{ $t('login.copy') }}</ds-text>
|
||||
</ds-space>
|
||||
<form :disabled="pending" @submit.prevent="onSubmit">
|
||||
<ds-input
|
||||
v-model="form.email"
|
||||
:disabled="pending"
|
||||
:placeholder="$t('login.email')"
|
||||
type="email"
|
||||
name="email"
|
||||
icon="envelope"
|
||||
/>
|
||||
<ds-input
|
||||
v-model="form.password"
|
||||
:disabled="pending"
|
||||
:placeholder="$t('login.password')"
|
||||
icon="lock"
|
||||
icon-right="question-circle"
|
||||
name="password"
|
||||
type="password"
|
||||
/>
|
||||
<ds-space margin-bottom="large">
|
||||
<nuxt-link to="/password-reset/request">{{ $t('login.forgotPassword') }}</nuxt-link>
|
||||
</ds-space>
|
||||
<base-button :loading="pending" filled name="submit" type="submit" icon="sign-in">
|
||||
{{ $t('login.login') }}
|
||||
</base-button>
|
||||
<ds-space margin-top="large" margin-bottom="x-small">
|
||||
{{ $t('login.no-account') }}
|
||||
<nuxt-link to="/registration/signup">{{ $t('login.register') }}</nuxt-link>
|
||||
</ds-space>
|
||||
</form>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-card>
|
||||
</ds-container>
|
||||
<section class="login-form">
|
||||
<blockquote>
|
||||
<p>{{ $t('quotes.african.quote') }}</p>
|
||||
<b>- {{ $t('quotes.african.author') }}</b>
|
||||
</blockquote>
|
||||
<base-card>
|
||||
<template #imageColumn>
|
||||
<a :href="$t('login.moreInfoURL')" :title="$t('login.moreInfo')" target="_blank">
|
||||
<img class="image" alt="Human Connection" src="/img/sign-up/humanconnection.svg" />
|
||||
</a>
|
||||
</template>
|
||||
<h2 class="title">{{ $t('login.login') }}</h2>
|
||||
<form :disabled="pending" @submit.prevent="onSubmit">
|
||||
<ds-input
|
||||
v-model="form.email"
|
||||
:disabled="pending"
|
||||
:placeholder="$t('login.email')"
|
||||
type="email"
|
||||
name="email"
|
||||
icon="envelope"
|
||||
/>
|
||||
<ds-input
|
||||
v-model="form.password"
|
||||
:disabled="pending"
|
||||
:placeholder="$t('login.password')"
|
||||
icon="lock"
|
||||
icon-right="question-circle"
|
||||
name="password"
|
||||
type="password"
|
||||
/>
|
||||
<nuxt-link to="/password-reset/request">
|
||||
{{ $t('login.forgotPassword') }}
|
||||
</nuxt-link>
|
||||
<base-button :loading="pending" filled name="submit" type="submit" icon="sign-in">
|
||||
{{ $t('login.login') }}
|
||||
</base-button>
|
||||
<p>
|
||||
{{ $t('login.no-account') }}
|
||||
<nuxt-link to="/registration/signup">{{ $t('login.register') }}</nuxt-link>
|
||||
</p>
|
||||
</form>
|
||||
<template #topMenu>
|
||||
<locale-switch offset="5" />
|
||||
</template>
|
||||
</base-card>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch.vue'
|
||||
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -100,21 +83,16 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.login-image {
|
||||
width: 90%;
|
||||
max-width: 200px;
|
||||
}
|
||||
.login-card {
|
||||
position: relative;
|
||||
.login-form {
|
||||
width: 80vw;
|
||||
max-width: 620px;
|
||||
margin: auto;
|
||||
|
||||
.base-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: $space-large;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
}
|
||||
.login-locale-switch {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
left: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -63,7 +63,7 @@ describe('Notification', () => {
|
||||
|
||||
it('renders reason', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
||||
expect(wrapper.find('.notification > .description').text()).toEqual(
|
||||
'notifications.reason.commented_on_post',
|
||||
)
|
||||
})
|
||||
@ -79,9 +79,9 @@ describe('Notification', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.')
|
||||
})
|
||||
it('has no class "read"', () => {
|
||||
it('has no class "--read"', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.classes()).not.toContain('read')
|
||||
expect(wrapper.classes()).not.toContain('--read')
|
||||
})
|
||||
|
||||
describe('that is read', () => {
|
||||
@ -90,8 +90,8 @@ describe('Notification', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has class "read"', () => {
|
||||
expect(wrapper.classes()).toContain('read')
|
||||
it('has class "--read"', () => {
|
||||
expect(wrapper.classes()).toContain('--read')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -113,7 +113,7 @@ describe('Notification', () => {
|
||||
|
||||
it('renders reason', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
||||
expect(wrapper.find('.notification > .description').text()).toEqual(
|
||||
'notifications.reason.mentioned_in_post',
|
||||
)
|
||||
})
|
||||
@ -125,9 +125,9 @@ describe('Notification', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.text()).toContain('@jenny-rostock is the best on this post.')
|
||||
})
|
||||
it('has no class "read"', () => {
|
||||
it('has no class "--read"', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.classes()).not.toContain('read')
|
||||
expect(wrapper.classes()).not.toContain('--read')
|
||||
})
|
||||
|
||||
describe('that is read', () => {
|
||||
@ -136,8 +136,8 @@ describe('Notification', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has class "read"', () => {
|
||||
expect(wrapper.classes()).toContain('read')
|
||||
it('has class "--read"', () => {
|
||||
expect(wrapper.classes()).toContain('--read')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -163,7 +163,7 @@ describe('Notification', () => {
|
||||
|
||||
it('renders reason', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
||||
expect(wrapper.find('.notification > .description').text()).toEqual(
|
||||
'notifications.reason.mentioned_in_comment',
|
||||
)
|
||||
})
|
||||
@ -182,9 +182,9 @@ describe('Notification', () => {
|
||||
expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.')
|
||||
})
|
||||
|
||||
it('has no class "read"', () => {
|
||||
it('has no class "--read"', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.classes()).not.toContain('read')
|
||||
expect(wrapper.classes()).not.toContain('--read')
|
||||
})
|
||||
|
||||
describe('that is read', () => {
|
||||
@ -193,8 +193,8 @@ describe('Notification', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has class "read"', () => {
|
||||
expect(wrapper.classes()).toContain('read')
|
||||
it('has class "--read"', () => {
|
||||
expect(wrapper.classes()).toContain('--read')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,37 +1,23 @@
|
||||
<template>
|
||||
<ds-space :class="{ read: notification.read, notification: true }" margin-bottom="x-small">
|
||||
<article :class="{ '--read': notification.read, notification: true }">
|
||||
<client-only>
|
||||
<ds-space margin-bottom="x-small">
|
||||
<user-teaser :user="from.author" :date-time="from.createdAt" />
|
||||
</ds-space>
|
||||
<ds-text class="reason-text-for-test" color="soft">
|
||||
{{ $t(`notifications.reason.${notification.reason}`) }}
|
||||
</ds-text>
|
||||
<user-teaser :user="from.author" :date-time="from.createdAt" />
|
||||
</client-only>
|
||||
<ds-space margin-bottom="x-small" />
|
||||
<p class="description">{{ $t(`notifications.reason.${notification.reason}`) }}</p>
|
||||
<nuxt-link
|
||||
class="notification-mention-post"
|
||||
class="link"
|
||||
:to="{ name: 'post-id-slug', params, ...hashParam }"
|
||||
@click.native="$emit('read')"
|
||||
>
|
||||
<ds-space margin-bottom="x-small">
|
||||
<ds-card
|
||||
:header="from.title || from.post.title"
|
||||
hover
|
||||
space="x-small"
|
||||
class="notifications-card"
|
||||
>
|
||||
<ds-space margin-bottom="x-small" />
|
||||
<div>
|
||||
<span v-if="isComment" class="comment-notification-header">
|
||||
{{ $t(`notifications.comment`) }}:
|
||||
</span>
|
||||
{{ from.contentExcerpt | removeHtml }}
|
||||
</div>
|
||||
</ds-card>
|
||||
</ds-space>
|
||||
<base-card wideContent>
|
||||
<h2 class="title">{{ from.title || from.post.title }}</h2>
|
||||
<p>
|
||||
<strong v-if="isComment" class="comment">{{ $t(`notifications.comment`) }}:</strong>
|
||||
{{ from.contentExcerpt | removeHtml }}
|
||||
</p>
|
||||
</base-card>
|
||||
</nuxt-link>
|
||||
</ds-space>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -70,14 +56,36 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.notification.read {
|
||||
opacity: $opacity-soft;
|
||||
}
|
||||
.notifications-card {
|
||||
min-width: 500px;
|
||||
}
|
||||
.comment-notification-header {
|
||||
font-weight: 700;
|
||||
margin-right: 0.1rem;
|
||||
.notification {
|
||||
margin-bottom: $space-base;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: $space-x-small;
|
||||
}
|
||||
|
||||
&.--read {
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
|
||||
> .description {
|
||||
margin-bottom: $space-x-small;
|
||||
}
|
||||
|
||||
> .link {
|
||||
display: block;
|
||||
color: $text-color-base;
|
||||
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.user-teaser {
|
||||
margin-bottom: $space-x-small;
|
||||
}
|
||||
|
||||
.comment {
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -73,7 +73,7 @@ describe('NotificationList.vue', () => {
|
||||
|
||||
describe('click on a notification', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.find('.notification-mention-post').trigger('click')
|
||||
wrapper.find('.notification > .link').trigger('click')
|
||||
})
|
||||
|
||||
it("emits 'markAsRead' with the id of the notification source", () => {
|
||||
|
||||
@ -3,7 +3,7 @@ import { withA11y } from '@storybook/addon-a11y'
|
||||
import { action } from '@storybook/addon-actions'
|
||||
import NotificationsTable from '~/components/NotificationsTable/NotificationsTable'
|
||||
import helpers from '~/storybook/helpers'
|
||||
import { post } from '~/components/PostCard/PostCard.story.js'
|
||||
import { post } from '~/components/PostTeaser/PostTeaser.story.js'
|
||||
import { user } from '~/components/UserTeaser/UserTeaser.story.js'
|
||||
|
||||
helpers.init()
|
||||
|
||||
@ -1,219 +0,0 @@
|
||||
<template>
|
||||
<ds-card
|
||||
:lang="post.language"
|
||||
:image="post.image | proxyApiUrl"
|
||||
:class="{
|
||||
'post-card': true,
|
||||
'disabled-content': post.disabled,
|
||||
'--pinned': isPinned,
|
||||
'--blur-image': post.imageBlurred,
|
||||
}"
|
||||
>
|
||||
<!-- Post Link Target -->
|
||||
<nuxt-link
|
||||
class="post-link"
|
||||
:to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }"
|
||||
>
|
||||
{{ post.title }}
|
||||
</nuxt-link>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!-- Username, Image & Date of Post -->
|
||||
<div class="user-wrapper">
|
||||
<client-only>
|
||||
<user-teaser :user="post.author" :date-time="post.createdAt" />
|
||||
</client-only>
|
||||
<hc-ribbon v-if="isPinned" class="ribbon--pinned" :text="$t('post.pinned')" />
|
||||
<hc-ribbon v-else :text="$t('post.name')" />
|
||||
</div>
|
||||
<ds-space margin-bottom="small" />
|
||||
<!-- Post Title -->
|
||||
<ds-heading tag="h3" class="hyphenate-text post-title">{{ 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 hyphenate-text" 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: $t(`contribution.category.name.${category.slug}`),
|
||||
placement: 'bottom-start',
|
||||
delay: { show: 500 },
|
||||
}"
|
||||
:icon="category.icon"
|
||||
/>
|
||||
</div>
|
||||
<client-only>
|
||||
<div style="display: inline-block; float: right">
|
||||
<!-- Shouts Count -->
|
||||
<span :style="{ opacity: post.shoutedCount ? 1 : 0.5 }">
|
||||
<base-icon name="bullhorn" />
|
||||
<small>{{ post.shoutedCount }}</small>
|
||||
</span>
|
||||
|
||||
<!-- Comments Count -->
|
||||
<span :style="{ opacity: post.commentsCount ? 1 : 0.5 }">
|
||||
<base-icon name="comments" />
|
||||
<small>{{ post.commentsCount }}</small>
|
||||
</span>
|
||||
<!-- Menu -->
|
||||
<content-menu
|
||||
resource-type="contribution"
|
||||
:resource="post"
|
||||
:modalsData="menuModalsData"
|
||||
:is-owner="isAuthor"
|
||||
@pinPost="pinPost"
|
||||
@unpinPost="unpinPost"
|
||||
/>
|
||||
</div>
|
||||
</client-only>
|
||||
</template>
|
||||
</ds-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
||||
import HcCategory from '~/components/Category'
|
||||
import HcRibbon from '~/components/Ribbon'
|
||||
// import { randomBytes } from 'crypto'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers'
|
||||
|
||||
export default {
|
||||
name: 'HcPostCard',
|
||||
components: {
|
||||
UserTeaser,
|
||||
HcCategory,
|
||||
HcRibbon,
|
||||
ContentMenu,
|
||||
},
|
||||
props: {
|
||||
post: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const width = this.$el.offsetWidth
|
||||
const height = Math.min(width / this.post.imageAspectRatio, 2000)
|
||||
const imageElement = this.$el.querySelector('.ds-card-image')
|
||||
if (imageElement) {
|
||||
imageElement.style.height = `${height}px`
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'auth/user',
|
||||
}),
|
||||
excerpt() {
|
||||
return this.$filters.removeLinks(this.post.contentExcerpt)
|
||||
},
|
||||
isAuthor() {
|
||||
const { author } = this.post
|
||||
if (!author) return false
|
||||
return this.user.id === this.post.author.id
|
||||
},
|
||||
menuModalsData() {
|
||||
return postMenuModalsData(
|
||||
// "this.post" may not always be defined at the beginning …
|
||||
this.post ? this.$filters.truncate(this.post.title, 30) : '',
|
||||
this.deletePostCallback,
|
||||
)
|
||||
},
|
||||
isPinned() {
|
||||
return this.post && this.post.pinned
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async deletePostCallback() {
|
||||
try {
|
||||
const {
|
||||
data: { DeletePost },
|
||||
} = await this.$apollo.mutate(deletePostMutation(this.post.id))
|
||||
this.$toast.success(this.$t('delete.contribution.success'))
|
||||
this.$emit('removePostFromList', DeletePost)
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
}
|
||||
},
|
||||
pinPost(post) {
|
||||
this.$emit('pinPost', post)
|
||||
},
|
||||
unpinPost(post) {
|
||||
this.$emit('unpinPost', post)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.post-card {
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
|
||||
&.--pinned {
|
||||
border: 1px solid $color-warning;
|
||||
}
|
||||
|
||||
&.--blur-image > .ds-card-image img {
|
||||
filter: blur(22px);
|
||||
}
|
||||
|
||||
> .ds-card-image img {
|
||||
width: 100%;
|
||||
max-height: 2000px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
> .ds-card-content {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
/* workaround to avoid jumping layout when footer is rendered */
|
||||
> .ds-card-footer {
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
margin-top: $space-large;
|
||||
}
|
||||
|
||||
/* workaround to avoid jumping layout when user-teaser is rendered */
|
||||
.user-wrapper {
|
||||
height: 36px;
|
||||
position: relative;
|
||||
z-index: $z-index-post-card-link;
|
||||
}
|
||||
|
||||
.content-menu {
|
||||
position: relative;
|
||||
z-index: $z-index-post-card-link;
|
||||
display: inline-block;
|
||||
margin-left: $space-xx-small;
|
||||
margin-right: -$space-x-small;
|
||||
}
|
||||
|
||||
.post-link {
|
||||
margin: 15px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-indent: -999999px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -2,14 +2,14 @@ import { config, shallowMount, mount, RouterLinkStub } from '@vue/test-utils'
|
||||
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import PostCard from './PostCard.vue'
|
||||
import PostTeaser from './PostTeaser.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
config.stubs['client-only'] = '<span><slot /></span>'
|
||||
config.stubs['v-popover'] = '<span><slot /></span>'
|
||||
|
||||
describe('PostCard', () => {
|
||||
describe('PostTeaser', () => {
|
||||
let store
|
||||
let stubs
|
||||
let mocks
|
||||
@ -22,11 +22,13 @@ describe('PostCard', () => {
|
||||
propsData = {
|
||||
post: {
|
||||
id: 'p23',
|
||||
disabled: false,
|
||||
shoutedCount: 0,
|
||||
commentsCount: 0,
|
||||
name: 'It is a post',
|
||||
author: {
|
||||
id: 'u1',
|
||||
},
|
||||
disabled: false,
|
||||
},
|
||||
}
|
||||
stubs = {
|
||||
@ -55,7 +57,7 @@ describe('PostCard', () => {
|
||||
describe('shallowMount', () => {
|
||||
Wrapper = () => {
|
||||
store = new Vuex.Store({ getters })
|
||||
return shallowMount(PostCard, {
|
||||
return shallowMount(PostTeaser, {
|
||||
store,
|
||||
propsData,
|
||||
mocks,
|
||||
@ -63,6 +65,13 @@ describe('PostCard', () => {
|
||||
})
|
||||
}
|
||||
|
||||
it('has no validation errors', () => {
|
||||
const spy = jest.spyOn(global.console, 'error')
|
||||
Wrapper()
|
||||
expect(spy).not.toBeCalled()
|
||||
spy.mockReset()
|
||||
})
|
||||
|
||||
beforeEach(jest.useFakeTimers)
|
||||
|
||||
describe('test Post callbacks', () => {
|
||||
@ -99,7 +108,7 @@ describe('PostCard', () => {
|
||||
const store = new Vuex.Store({
|
||||
getters,
|
||||
})
|
||||
return mount(PostCard, {
|
||||
return mount(PostTeaser, {
|
||||
stubs,
|
||||
mocks,
|
||||
propsData,
|
||||
@ -111,6 +120,7 @@ describe('PostCard', () => {
|
||||
describe('given a post', () => {
|
||||
beforeEach(() => {
|
||||
propsData.post = {
|
||||
...propsData.post,
|
||||
title: "It's a title",
|
||||
}
|
||||
})
|
||||
@ -1,6 +1,6 @@
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
import { withA11y } from '@storybook/addon-a11y'
|
||||
import HcPostCard from './PostCard.vue'
|
||||
import PostTeaser from './PostTeaser.vue'
|
||||
import helpers from '~/storybook/helpers'
|
||||
|
||||
helpers.init()
|
||||
@ -44,24 +44,24 @@ export const post = {
|
||||
__typename: 'Post',
|
||||
}
|
||||
|
||||
storiesOf('Post Card', module)
|
||||
storiesOf('PostTeaser', module)
|
||||
.addDecorator(withA11y)
|
||||
.addDecorator(helpers.layout)
|
||||
.add('without image', () => ({
|
||||
components: { HcPostCard },
|
||||
components: { PostTeaser },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
post,
|
||||
}),
|
||||
template: `
|
||||
<hc-post-card
|
||||
<post-teaser
|
||||
:post="post"
|
||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||
/>
|
||||
`,
|
||||
}))
|
||||
.add('with image', () => ({
|
||||
components: { HcPostCard },
|
||||
components: { PostTeaser },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
post: {
|
||||
@ -70,27 +70,23 @@ storiesOf('Post Card', module)
|
||||
},
|
||||
}),
|
||||
template: `
|
||||
<hc-post-card
|
||||
<post-teaser
|
||||
:post="post"
|
||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||
/>
|
||||
`,
|
||||
}))
|
||||
.add('pinned by admin', () => ({
|
||||
components: { HcPostCard },
|
||||
components: { PostTeaser },
|
||||
store: helpers.store,
|
||||
data: () => ({
|
||||
post: {
|
||||
...post,
|
||||
pinnedBy: {
|
||||
id: '4711',
|
||||
name: 'Ad Min',
|
||||
role: 'admin',
|
||||
},
|
||||
pinned: true,
|
||||
},
|
||||
}),
|
||||
template: `
|
||||
<hc-post-card
|
||||
<post-teaser
|
||||
:post="post"
|
||||
:width="{ base: '100%', xs: '100%', md: '50%', xl: '33%' }"
|
||||
/>
|
||||
207
webapp/components/PostTeaser/PostTeaser.vue
Normal file
207
webapp/components/PostTeaser/PostTeaser.vue
Normal file
@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<nuxt-link
|
||||
class="post-teaser"
|
||||
:to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }"
|
||||
>
|
||||
<base-card
|
||||
:lang="post.language"
|
||||
:class="{
|
||||
'disabled-content': post.disabled,
|
||||
'--blur-image': post.imageBlurred,
|
||||
}"
|
||||
:highlight="isPinned"
|
||||
>
|
||||
<template v-if="post.image" #heroImage>
|
||||
<img :src="post.image | proxyApiUrl" class="image" />
|
||||
</template>
|
||||
<client-only>
|
||||
<user-teaser :user="post.author" :date-time="post.createdAt" />
|
||||
</client-only>
|
||||
<h2 class="title hyphenate-text">{{ post.title }}</h2>
|
||||
<!-- TODO: replace editor content with tiptap render view -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div class="content hyphenate-text" v-html="excerpt" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<footer class="footer">
|
||||
<div class="categories">
|
||||
<hc-category
|
||||
v-for="category in post.categories"
|
||||
:key="category.id"
|
||||
v-tooltip="{
|
||||
content: $t(`contribution.category.name.${category.slug}`),
|
||||
placement: 'bottom-start',
|
||||
delay: { show: 500 },
|
||||
}"
|
||||
:icon="category.icon"
|
||||
/>
|
||||
</div>
|
||||
<counter-icon
|
||||
icon="bullhorn"
|
||||
:count="post.shoutedCount"
|
||||
:title="$t('contribution.amount-shouts', { amount: post.shoutedCount })"
|
||||
/>
|
||||
<counter-icon
|
||||
icon="comments"
|
||||
:count="post.commentsCount"
|
||||
:title="$t('contribution.amount-comments', { amount: post.commentsCount })"
|
||||
/>
|
||||
<client-only>
|
||||
<content-menu
|
||||
resource-type="contribution"
|
||||
:resource="post"
|
||||
:modalsData="menuModalsData"
|
||||
:is-owner="isAuthor"
|
||||
@pinPost="pinPost"
|
||||
@unpinPost="unpinPost"
|
||||
/>
|
||||
</client-only>
|
||||
</footer>
|
||||
</base-card>
|
||||
<hc-ribbon
|
||||
:class="{ '--pinned': isPinned }"
|
||||
:text="isPinned ? $t('post.pinned') : $t('post.name')"
|
||||
/>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
||||
import HcCategory from '~/components/Category'
|
||||
import HcRibbon from '~/components/Ribbon'
|
||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers'
|
||||
|
||||
export default {
|
||||
name: 'PostTeaser',
|
||||
components: {
|
||||
UserTeaser,
|
||||
HcCategory,
|
||||
HcRibbon,
|
||||
ContentMenu,
|
||||
CounterIcon,
|
||||
},
|
||||
props: {
|
||||
post: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const width = this.$el.offsetWidth
|
||||
const height = Math.min(width / this.post.imageAspectRatio, 2000)
|
||||
const imageElement = this.$el.querySelector('.hero-image')
|
||||
if (imageElement) {
|
||||
imageElement.style.height = `${height}px`
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'auth/user',
|
||||
}),
|
||||
excerpt() {
|
||||
return this.$filters.removeLinks(this.post.contentExcerpt)
|
||||
},
|
||||
isAuthor() {
|
||||
const { author } = this.post
|
||||
if (!author) return false
|
||||
return this.user.id === this.post.author.id
|
||||
},
|
||||
menuModalsData() {
|
||||
return postMenuModalsData(
|
||||
// "this.post" may not always be defined at the beginning …
|
||||
this.post ? this.$filters.truncate(this.post.title, 30) : '',
|
||||
this.deletePostCallback,
|
||||
)
|
||||
},
|
||||
isPinned() {
|
||||
return this.post && this.post.pinned
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async deletePostCallback() {
|
||||
try {
|
||||
const {
|
||||
data: { DeletePost },
|
||||
} = await this.$apollo.mutate(deletePostMutation(this.post.id))
|
||||
this.$toast.success(this.$t('delete.contribution.success'))
|
||||
this.$emit('removePostFromList', DeletePost)
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
}
|
||||
},
|
||||
pinPost(post) {
|
||||
this.$emit('pinPost', post)
|
||||
},
|
||||
unpinPost(post) {
|
||||
this.$emit('unpinPost', post)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.post-teaser,
|
||||
.post-teaser:hover,
|
||||
.post-teaser:active {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
color: $text-color-base;
|
||||
|
||||
> .ribbon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -7px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-teaser > .base-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
&.--blur-image > .hero-image > .image {
|
||||
filter: blur($blur-radius);
|
||||
}
|
||||
|
||||
> .content {
|
||||
flex-grow: 1;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
|
||||
> .footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
> .categories {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
> .counter-icon {
|
||||
display: block;
|
||||
margin-right: $space-small;
|
||||
opacity: $opacity-disabled;
|
||||
}
|
||||
|
||||
> .content-menu {
|
||||
position: relative;
|
||||
z-index: $z-index-post-teaser-link;
|
||||
}
|
||||
|
||||
.ds-tag {
|
||||
margin: 0;
|
||||
margin-right: $space-xx-small;
|
||||
}
|
||||
}
|
||||
|
||||
.user-teaser {
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="ribbon">
|
||||
<small>{{ text }}</small>
|
||||
</div>
|
||||
<aside class="ribbon">
|
||||
<p>{{ text }}</p>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -18,40 +18,29 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ribbon {
|
||||
$card-shadow: 0px 12px 26px -4px rgba(0, 0, 0, 0.1);
|
||||
//position: absolute;
|
||||
position: relative;
|
||||
right: -31px;
|
||||
top: -31px;
|
||||
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
padding: 6px 6px;
|
||||
color: #fff;
|
||||
padding: $size-ribbon $size-ribbon;
|
||||
border-radius: $border-radius-small 0 0 $border-radius-small;
|
||||
color: $color-neutral-100;
|
||||
background-color: $background-color-secondary-active;
|
||||
float: right;
|
||||
border-radius: 2px 0 0 2px;
|
||||
box-shadow: $card-shadow;
|
||||
z-index: 11;
|
||||
// border: 1px solid #ccc;
|
||||
&:before {
|
||||
font-size: $font-size-x-small;
|
||||
font-weight: $font-weight-bold;
|
||||
|
||||
&::before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
right: 0;
|
||||
bottom: -6px;
|
||||
border-width: 3px 4px 3px 3px;
|
||||
bottom: -$size-ribbon;
|
||||
border-width: $border-size-large 4px $border-size-large $border-size-large;
|
||||
border-style: solid;
|
||||
border-color: $background-color-secondary transparent transparent $background-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.ribbon--pinned {
|
||||
background-color: $color-warning-active;
|
||||
&.--pinned {
|
||||
background-color: $color-warning;
|
||||
|
||||
&::before {
|
||||
border-color: $color-warning transparent transparent $color-warning;
|
||||
&::before {
|
||||
border-color: $color-warning transparent transparent $color-warning;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,257 +0,0 @@
|
||||
<template>
|
||||
<vue-dropzone
|
||||
:options="dropzoneOptions"
|
||||
ref="el"
|
||||
id="postdropzone"
|
||||
class="ds-card-image"
|
||||
:use-custom-slot="true"
|
||||
@vdropzone-error="verror"
|
||||
@vdropzone-thumbnail="transformImage"
|
||||
>
|
||||
<div class="crop-overlay" ref="cropperOverlay" v-show="showCropper">
|
||||
<base-button @click="cropImage" class="crop-confirm" filled>
|
||||
{{ $t('contribution.teaserImage.cropperConfirm') }}
|
||||
</base-button>
|
||||
<base-button
|
||||
class="crop-cancel"
|
||||
icon="close"
|
||||
size="small"
|
||||
circle
|
||||
danger
|
||||
filled
|
||||
@click="cancelCrop"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
'hc-attachments-upload-area-post': true,
|
||||
'hc-attachments-upload-area-update-post': contribution,
|
||||
}"
|
||||
>
|
||||
<slot></slot>
|
||||
<div
|
||||
:class="{
|
||||
'hc-drag-marker-post': true,
|
||||
'hc-drag-marker-update-post': contribution,
|
||||
}"
|
||||
>
|
||||
<base-icon name="image" />
|
||||
</div>
|
||||
</div>
|
||||
</vue-dropzone>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import vueDropzone from 'nuxt-dropzone'
|
||||
import Cropper from 'cropperjs'
|
||||
import 'cropperjs/dist/cropper.css'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
vueDropzone,
|
||||
},
|
||||
props: {
|
||||
contribution: { type: Object, default: () => {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dropzoneOptions: {
|
||||
url: () => '',
|
||||
maxFilesize: 5.0,
|
||||
previewTemplate: this.template(),
|
||||
},
|
||||
image: null,
|
||||
file: null,
|
||||
editor: null,
|
||||
cropper: null,
|
||||
thumbnailElement: null,
|
||||
oldImage: null,
|
||||
error: false,
|
||||
showCropper: false,
|
||||
imageAspectRatio: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
template() {
|
||||
return `<div class="dz-preview dz-file-preview">
|
||||
<div class="dz-image">
|
||||
<div data-dz-thumbnail-bg></div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
verror(file, message) {
|
||||
this.error = true
|
||||
this.$toast.error(file.status, message)
|
||||
setTimeout(() => {
|
||||
this.error = false
|
||||
}, 2000)
|
||||
},
|
||||
transformImage(file) {
|
||||
this.file = file
|
||||
this.$emit('cropInProgress', true)
|
||||
this.showCropper = true
|
||||
this.initEditor()
|
||||
this.initCropper()
|
||||
},
|
||||
initEditor() {
|
||||
this.editor = this.$refs.cropperOverlay
|
||||
this.clearImages()
|
||||
this.thumbnailElement.appendChild(this.editor)
|
||||
},
|
||||
clearImages() {
|
||||
this.thumbnailElement = document.querySelectorAll('#postdropzone')[0]
|
||||
const thumbnailPreview = document.querySelectorAll('.thumbnail-preview')[0]
|
||||
if (thumbnailPreview) thumbnailPreview.remove()
|
||||
const contributionImage = document.querySelectorAll('.contribution-image')[0]
|
||||
this.oldImage = contributionImage
|
||||
if (contributionImage) contributionImage.remove()
|
||||
},
|
||||
deleteImage() {
|
||||
this.clearImages()
|
||||
},
|
||||
initCropper() {
|
||||
this.image = new Image()
|
||||
this.image.src = URL.createObjectURL(this.file)
|
||||
this.editor.appendChild(this.image)
|
||||
this.cropper = new Cropper(this.image, { zoomable: false, autoCropArea: 0.9 })
|
||||
},
|
||||
cropImage() {
|
||||
this.showCropper = false
|
||||
if (this.file.type === 'image/jpeg') {
|
||||
this.uploadJpeg()
|
||||
} else {
|
||||
this.uploadOtherImageType()
|
||||
}
|
||||
},
|
||||
uploadOtherImageType() {
|
||||
this.imageAspectRatio = this.file.width / this.file.height || 1.0
|
||||
this.image = new Image()
|
||||
this.image.src = this.file.dataURL
|
||||
this.setupPreview()
|
||||
this.emitImageData(this.file)
|
||||
},
|
||||
uploadJpeg() {
|
||||
const canvas = this.cropper.getCroppedCanvas()
|
||||
canvas.toBlob(blob => {
|
||||
this.imageAspectRatio = canvas.width / canvas.height
|
||||
this.image = new Image()
|
||||
this.image.src = canvas.toDataURL()
|
||||
this.setupPreview()
|
||||
const croppedImageFile = new File([blob], this.file.name, { type: this.file.type })
|
||||
this.emitImageData(croppedImageFile)
|
||||
}, 'image/jpeg')
|
||||
},
|
||||
setupPreview() {
|
||||
this.image.classList.add('thumbnail-preview')
|
||||
this.thumbnailElement.appendChild(this.image)
|
||||
},
|
||||
cancelCrop() {
|
||||
if (this.oldImage) this.thumbnailElement.appendChild(this.oldImage)
|
||||
this.showCropper = false
|
||||
this.$emit('cropInProgress', false)
|
||||
},
|
||||
emitImageData(imageFile) {
|
||||
this.$emit('addTeaserImage', imageFile)
|
||||
this.$emit('addImageAspectRatio', this.imageAspectRatio)
|
||||
this.$emit('cropInProgress', false)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#postdropzone {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
background-color: $background-color-softest;
|
||||
}
|
||||
|
||||
.hc-attachments-upload-area-post {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hc-attachments-upload-area-update-post img {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hc-attachments-upload-area-update-post:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.hc-drag-marker-post {
|
||||
position: absolute;
|
||||
width: 122px;
|
||||
height: 122px;
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 180px 5px;
|
||||
color: hsl(0, 0%, 25%);
|
||||
transition: all 0.2s ease-out;
|
||||
font-size: 60px;
|
||||
background-color: $background-color-softest;
|
||||
opacity: 0.65;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 100%;
|
||||
border: 20px solid $text-color-base;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
border-radius: 100%;
|
||||
border: $border-size-base dashed $text-color-base;
|
||||
}
|
||||
|
||||
.hc-attachments-upload-area-post:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.hc-drag-marker-update-post {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.contribution-form-footer {
|
||||
border-top: $border-size-base solid $border-color-softest;
|
||||
}
|
||||
|
||||
.crop-overlay {
|
||||
max-height: 2000px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.crop-confirm {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
.crop-cancel {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@ -80,7 +80,7 @@ storiesOf('UserTeaser', module)
|
||||
}),
|
||||
template: `
|
||||
<user-teaser :user="user" :date-time="new Date()">
|
||||
<template v-slot:dateTime>
|
||||
<template #dateTime>
|
||||
- HEY! I'm edited
|
||||
</template>
|
||||
</user>
|
||||
|
||||
@ -3,107 +3,36 @@
|
||||
<user-avatar v-if="showAvatar" size="small" />
|
||||
<span class="info anonymous">{{ $t('profile.userAnonym') }}</span>
|
||||
</div>
|
||||
<dropdown
|
||||
v-else
|
||||
:class="[{ 'disabled-content': user.disabled }]"
|
||||
placement="top-start"
|
||||
offset="0"
|
||||
>
|
||||
<template #default="{ openMenu, closeMenu, isOpen }">
|
||||
<nuxt-link
|
||||
:to="userLink"
|
||||
:class="['user-teaser', isOpen && 'active']"
|
||||
@mouseover.native="showPopover ? openMenu(true) : () => {}"
|
||||
@mouseleave.native="closeMenu(true)"
|
||||
>
|
||||
<user-avatar v-if="showAvatar" :user="user" size="small" />
|
||||
<div class="info">
|
||||
<span class="text">
|
||||
<span class="slug">{{ userSlug }}</span>
|
||||
<span v-if="dateTime">{{ userName }}</span>
|
||||
</span>
|
||||
<span v-if="dateTime" class="text">
|
||||
<base-icon name="clock" />
|
||||
<hc-relative-date-time :date-time="dateTime" />
|
||||
<slot name="dateTime"></slot>
|
||||
</span>
|
||||
<span v-else class="text">{{ userName }}</span>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<template #popover v-if="showPopover">
|
||||
<div style="min-width: 250px">
|
||||
<hc-badges v-if="user.badges && user.badges.length" :badges="user.badges" />
|
||||
<ds-text
|
||||
v-if="user.location"
|
||||
align="center"
|
||||
color="soft"
|
||||
size="small"
|
||||
style="margin-top: 5px"
|
||||
bold
|
||||
>
|
||||
<base-icon name="map-marker" />
|
||||
{{ user.location.name }}
|
||||
</ds-text>
|
||||
<ds-flex style="margin-top: -10px">
|
||||
<ds-flex-item class="ds-tab-nav-item">
|
||||
<ds-space margin="small">
|
||||
<ds-number
|
||||
:count="user.followedByCount"
|
||||
:label="$t('profile.followers')"
|
||||
size="x-large"
|
||||
/>
|
||||
</ds-space>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item class="ds-tab-nav-item ds-tab-nav-item-active">
|
||||
<ds-space margin="small">
|
||||
<ds-number
|
||||
:count="user.contributionsCount"
|
||||
:label="$t('common.post', null, user.contributionsCount)"
|
||||
/>
|
||||
</ds-space>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item class="ds-tab-nav-item">
|
||||
<ds-space margin="small">
|
||||
<ds-number
|
||||
:count="user.commentedCount"
|
||||
:label="$t('common.comment', null, user.commentedCount)"
|
||||
/>
|
||||
</ds-space>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
<ds-flex v-if="!itsMe" gutter="x-small" style="margin-bottom: 0;">
|
||||
<ds-flex-item>
|
||||
<hc-follow-button
|
||||
:follow-id="user.id"
|
||||
:is-followed="user.followedByCurrentUser"
|
||||
@optimistic="optimisticFollow"
|
||||
@update="updateFollow"
|
||||
/>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
<div v-else :class="[{ 'disabled-content': user.disabled }]" placement="top-start">
|
||||
<nuxt-link :to="userLink" :class="['user-teaser']">
|
||||
<user-avatar v-if="showAvatar" :user="user" size="small" />
|
||||
<div class="info">
|
||||
<span class="text">
|
||||
<span class="slug">{{ userSlug }}</span>
|
||||
<span v-if="dateTime">{{ userName }}</span>
|
||||
</span>
|
||||
<span v-if="dateTime" class="text">
|
||||
<base-icon name="clock" />
|
||||
<hc-relative-date-time :date-time="dateTime" />
|
||||
<slot name="dateTime"></slot>
|
||||
</span>
|
||||
<span v-else class="text">{{ userName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</dropdown>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import HcRelativeDateTime from '~/components/RelativeDateTime'
|
||||
import HcFollowButton from '~/components/FollowButton'
|
||||
import HcBadges from '~/components/Badges'
|
||||
import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
|
||||
export default {
|
||||
name: 'UserTeaser',
|
||||
components: {
|
||||
HcRelativeDateTime,
|
||||
HcFollowButton,
|
||||
UserAvatar,
|
||||
HcBadges,
|
||||
Dropdown,
|
||||
},
|
||||
props: {
|
||||
user: { type: Object, default: null },
|
||||
|
||||
78
webapp/components/_new/generic/BaseCard/BaseCard.story.js
Normal file
78
webapp/components/_new/generic/BaseCard/BaseCard.story.js
Normal file
@ -0,0 +1,78 @@
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
import helpers from '~/storybook/helpers'
|
||||
import BaseCard from './BaseCard.vue'
|
||||
|
||||
storiesOf('Generic/BaseCard', module)
|
||||
.addDecorator(helpers.layout)
|
||||
|
||||
.add('default', () => ({
|
||||
components: { BaseCard },
|
||||
template: `
|
||||
<base-card>
|
||||
<h2 class="title">I am a card heading</h2>
|
||||
<p>And I am a paragraph.</p>
|
||||
</base-card>
|
||||
`,
|
||||
}))
|
||||
|
||||
.add('with slot: hero image', () => ({
|
||||
components: { BaseCard },
|
||||
template: `
|
||||
<base-card style="width: 400px;">
|
||||
<template #heroImage>
|
||||
<img class="image" src="https://unsplash.com/photos/R4y_E5ZQDPg/download" />
|
||||
</template>
|
||||
<h2 class="title">I am a card heading</h2>
|
||||
<p>And I am a paragraph.</p>
|
||||
</base-card>
|
||||
`,
|
||||
}))
|
||||
|
||||
.add('with slot: image column', () => ({
|
||||
components: { BaseCard },
|
||||
template: `
|
||||
<base-card style="width: 600px;">
|
||||
<template #imageColumn>
|
||||
<img class="image" src="/img/sign-up/humanconnection.svg" />
|
||||
</template>
|
||||
<h2 class="title">I am a card heading</h2>
|
||||
<p>And I am a paragraph.</p>
|
||||
</base-card>
|
||||
`,
|
||||
}))
|
||||
|
||||
.add('with slot: topMenu', () => ({
|
||||
components: { BaseCard },
|
||||
template: `
|
||||
<base-card style="width: 600px;">
|
||||
<template #imageColumn>
|
||||
<img class="image" src="/img/sign-up/humanconnection.svg" />
|
||||
</template>
|
||||
<h2 class="title">I am a card heading</h2>
|
||||
<p>And I am a paragraph.</p>
|
||||
<template #topMenu>
|
||||
<base-button size="small">Menu</base-button>
|
||||
</template>
|
||||
</base-card>
|
||||
`,
|
||||
}))
|
||||
|
||||
.add('with highlight prop', () => ({
|
||||
components: { BaseCard },
|
||||
template: `
|
||||
<base-card highlight style="width: 400px;">
|
||||
<h2 class="title">I am a card heading</h2>
|
||||
<p>And I am a paragraph.</p>
|
||||
</base-card>
|
||||
`,
|
||||
}))
|
||||
|
||||
.add('with wideContent prop', () => ({
|
||||
components: { BaseCard },
|
||||
template: `
|
||||
<base-card wideContent style="width: 400px;">
|
||||
<h2 class="title">I am a card heading</h2>
|
||||
<p>And I am a paragraph.</p>
|
||||
</base-card>
|
||||
`,
|
||||
}))
|
||||
132
webapp/components/_new/generic/BaseCard/BaseCard.vue
Normal file
132
webapp/components/_new/generic/BaseCard/BaseCard.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<article :class="classNames">
|
||||
<template v-if="$slots.imageColumn">
|
||||
<aside class="image-column">
|
||||
<slot name="imageColumn" />
|
||||
</aside>
|
||||
<section class="content-column">
|
||||
<slot />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="$slots.heroImage">
|
||||
<section class="hero-image">
|
||||
<slot name="heroImage" />
|
||||
</section>
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
<slot v-else />
|
||||
<aside v-if="$slots.topMenu" class="top-menu">
|
||||
<slot name="topMenu" />
|
||||
</aside>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
highlight: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
wideContent: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
classNames() {
|
||||
let classNames = 'base-card'
|
||||
|
||||
if (this.$slots.imageColumn) classNames += ' --columns'
|
||||
if (this.highlight) classNames += ' --highlight'
|
||||
if (this.wideContent) classNames += ' --wide-content'
|
||||
|
||||
return classNames
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.base-card {
|
||||
position: relative;
|
||||
padding: $space-base;
|
||||
border-radius: $border-radius-x-large;
|
||||
overflow: hidden;
|
||||
background-color: $color-neutral-100;
|
||||
box-shadow: $box-shadow-base;
|
||||
|
||||
&.--columns {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.--highlight {
|
||||
border: $border-size-base solid $color-warning;
|
||||
}
|
||||
|
||||
&.--wide-content {
|
||||
padding: $space-small;
|
||||
|
||||
> .hero-image {
|
||||
width: calc(100% + (2 * #{$space-small}));
|
||||
margin: -$space-small;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
}
|
||||
|
||||
> .title,
|
||||
> .content-column > .title {
|
||||
font-size: $font-size-large;
|
||||
margin-bottom: $space-x-small;
|
||||
}
|
||||
|
||||
> .hero-image {
|
||||
width: calc(100% + (2 * #{$space-base}));
|
||||
max-height: $size-image-max-height;
|
||||
margin: -$space-base;
|
||||
margin-bottom: $space-base;
|
||||
overflow: hidden;
|
||||
|
||||
> .image {
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
> .image-column {
|
||||
flex-basis: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-right: $space-base;
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
> .content-column {
|
||||
flex-basis: 50%;
|
||||
}
|
||||
|
||||
> .top-menu {
|
||||
position: absolute;
|
||||
top: $space-small;
|
||||
left: $space-small;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 565px) {
|
||||
.base-card.--columns {
|
||||
flex-direction: column;
|
||||
|
||||
> .image-column {
|
||||
padding-right: 0;
|
||||
margin-bottom: $space-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,7 +1,7 @@
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
import { withA11y } from '@storybook/addon-a11y'
|
||||
import { action } from '@storybook/addon-actions'
|
||||
import { post } from '~/components/PostCard/PostCard.story.js'
|
||||
import { post } from '~/components/PostTeaser/PostTeaser.story.js'
|
||||
import { user } from '~/components/UserTeaser/UserTeaser.story.js'
|
||||
import helpers from '~/storybook/helpers'
|
||||
import ReportList from './ReportList'
|
||||
@ -183,11 +183,11 @@ storiesOf('ReportList', module)
|
||||
openModal: action('openModal'),
|
||||
filter: action('filter'),
|
||||
},
|
||||
template: `<ds-card>
|
||||
template: `<base-card>
|
||||
<div class="reports-header">
|
||||
<h3 class="title">Reports</h3>
|
||||
<dropdown-filter @filter="filter" :filterOptions="filterOptions" :selected="selected" />
|
||||
</div>
|
||||
<reports-table :reports="reports" @confirm="openModal" />
|
||||
</ds-card>`,
|
||||
</base-card>`,
|
||||
}))
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ds-card>
|
||||
<base-card>
|
||||
<div class="reports-header">
|
||||
<h3 class="title">{{ $t('moderation.reports.name') }}</h3>
|
||||
<client-only>
|
||||
@ -8,8 +8,9 @@
|
||||
</div>
|
||||
<reports-table :reports="reports" @confirm="openModal" />
|
||||
<pagination-buttons :hasNext="hasNext" :hasPrevious="hasPrevious" @back="back" @next="next" />
|
||||
</ds-card>
|
||||
</base-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex'
|
||||
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
|
||||
@ -43,13 +44,22 @@ export default {
|
||||
computed: {
|
||||
filterOptions() {
|
||||
return [
|
||||
{ label: this.$t('moderation.reports.filterLabel.all'), value: { reviewed: null } },
|
||||
{
|
||||
label: this.$t('moderation.reports.filterLabel.all'),
|
||||
value: { reviewed: null, closed: null },
|
||||
},
|
||||
{
|
||||
label: this.$t('moderation.reports.filterLabel.unreviewed'),
|
||||
value: { reviewed: false },
|
||||
value: { reviewed: false, closed: false },
|
||||
},
|
||||
{
|
||||
label: this.$t('moderation.reports.filterLabel.reviewed'),
|
||||
value: { reviewed: true, closed: false },
|
||||
},
|
||||
{
|
||||
label: this.$t('moderation.reports.filterLabel.closed'),
|
||||
value: { reviewed: null, closed: true },
|
||||
},
|
||||
{ label: this.$t('moderation.reports.filterLabel.reviewed'), value: { reviewed: true } },
|
||||
{ label: this.$t('moderation.reports.filterLabel.closed'), value: { closed: true } },
|
||||
]
|
||||
},
|
||||
modalData() {
|
||||
@ -107,13 +117,8 @@ export default {
|
||||
filter(option) {
|
||||
this.selected = option.label
|
||||
this.offset = 0
|
||||
if (option.value.closed) {
|
||||
this.closed = option.value.closed
|
||||
this.reviewed = null
|
||||
return
|
||||
}
|
||||
this.closed = null
|
||||
this.reviewed = option.value.reviewed
|
||||
this.closed = option.value.closed
|
||||
},
|
||||
async confirmCallback(resource) {
|
||||
const { disabled: disable, id: resourceId } = resource
|
||||
@ -166,7 +171,7 @@ export default {
|
||||
.reports-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: $space-small 0;
|
||||
margin-bottom: $space-small;
|
||||
|
||||
> .title {
|
||||
margin: 0;
|
||||
|
||||
@ -24,11 +24,8 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<template v-for="report in reports">
|
||||
<report-row
|
||||
:key="report.resource.id"
|
||||
:report="report"
|
||||
@confirm-report="$emit('confirm', report)"
|
||||
/>
|
||||
<!-- should be ':key="report.resource.id"' for having one element for every resource, but this crashes at the moment, because the 'reports' query returns multiple reports on the same resource! I will create an issue -->
|
||||
<report-row :key="report.id" :report="report" @confirm-report="$emit('confirm', report)" />
|
||||
</template>
|
||||
</table>
|
||||
<hc-empty v-else icon="alert" :message="$t('moderation.reports.empty')" />
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
// please change also version in file "cypress/constants/terms-and-conditions-version.js"
|
||||
export const VERSION = '0.0.3'
|
||||
export const VERSION = '0.0.4'
|
||||
|
||||
@ -100,7 +100,7 @@ export const reportMutation = () => {
|
||||
reasonCategory: $reasonCategory
|
||||
reasonDescription: $reasonDescription
|
||||
) {
|
||||
id
|
||||
reportId
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -10,9 +10,7 @@
|
||||
</a>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item width="20%" style="flex-grow:0;">
|
||||
<client-only>
|
||||
<locale-switch class="topbar-locale-switch" placement="top" offset="16" />
|
||||
</client-only>
|
||||
<locale-switch class="topbar-locale-switch" placement="top" offset="16" />
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-container>
|
||||
|
||||
@ -50,9 +50,7 @@
|
||||
}"
|
||||
style="flex-basis: auto;"
|
||||
>
|
||||
<client-only>
|
||||
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
|
||||
</client-only>
|
||||
<locale-switch class="topbar-locale-switch" placement="top" offset="8" />
|
||||
<template v-if="isLoggedIn">
|
||||
<client-only>
|
||||
<notification-menu placement="top" />
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
"tagCountUnique": "Benutzer"
|
||||
},
|
||||
"invites": {
|
||||
"description": "Einladungen sind ein wunderbarer Weg, deine Freund in deinem Netzwerk zu haben …",
|
||||
"description": "Einladungen sind eine wunderbare Möglichkeit, Deine Freunde in Deinem Netzwerk zu haben …",
|
||||
"name": "Benutzer einladen",
|
||||
"title": "Leute einladen"
|
||||
},
|
||||
@ -66,7 +66,7 @@
|
||||
"table": {
|
||||
"columns": {
|
||||
"createdAt": "Erstellt am",
|
||||
"email": "E-mail",
|
||||
"email": "E-Mail",
|
||||
"name": "Name",
|
||||
"number": "Nr.",
|
||||
"role": "Rolle",
|
||||
@ -81,9 +81,9 @@
|
||||
"list": {
|
||||
"0": "Aufforderung zum sofortigen Abstellen des inakzeptablen Verhaltens",
|
||||
"1": "Sperren oder Löschen von Kommentaren",
|
||||
"2": "Temporärer Ausschluss aus dem jeweiligen Beitrag",
|
||||
"2": "Vorübergehender Ausschluss aus dem jeweiligen Beitrag",
|
||||
"3": "Sperren bzw. Löschen von Inhalten",
|
||||
"4": "Temporärer Entzug von Schreibrechten",
|
||||
"4": "Vorübergehender Entzug von Schreibrechten",
|
||||
"5": "Vorübergehender Ausschluss aus dem Netzwerk",
|
||||
"6": "Endgültiger Ausschluss aus dem Netzwerk",
|
||||
"7": "Verstöße gegen deutsches Recht können zur Anzeige gebracht werden."
|
||||
@ -93,10 +93,10 @@
|
||||
"expected-behaviour": {
|
||||
"description": "Die folgenden Verhaltensweisen werden von allen Community-Mitgliedern erwartet und gefordert:",
|
||||
"list": {
|
||||
"0": "Sei rücksichtsvoll und respektvoll bei dem was Du schreibst und tust.",
|
||||
"0": "Sei rücksichtsvoll und respektvoll, bei dem, was Du schreibst und tust.",
|
||||
"1": "Versuche auf andere zuzugehen, bevor ein Konflikt entsteht.",
|
||||
"2": "Vermeide erniedrigende, diskriminierende oder belästigende Verhaltensweisen und Ausdrücke.",
|
||||
"3": "Gehe achtsam mit Deiner Umgebung um. Informiere den Support bei gefährlichen Situationen, wenn eine Person in Not ist oder bei Verstößen gegen diesen Verhaltenskodex, auch wenn sie unbedeutend erscheinen."
|
||||
"3": "Achte Dein Umfeld und Deine Mitmenschen. Warne die Verantwortlichen der Community, falls Du eine gefährliche Situation, jemanden in Not oder Verstöße gegen diesen Verhaltenskodex bemerkst, auch wenn diese unbedeutend erscheinen."
|
||||
},
|
||||
"title": "Erwartetes Verhalten"
|
||||
},
|
||||
@ -114,9 +114,9 @@
|
||||
"description": "Die folgenden Verhaltensweisen sind in unserer Community inakzeptabel:",
|
||||
"list": {
|
||||
"0": "Diskriminierende Beiträge, Kommentare, Äußerungen oder Beleidigungen, insbesondere solche, die sich auf Geschlecht, sexuelle Orientierung, Rasse, Religion, politische oder weltanschauliche Ausrichtung oder Behinderung beziehen.",
|
||||
"1": "Das Posten oder Verlinken eindeutig pornografischen Materials.",
|
||||
"1": "Das Senden oder Verlinken eindeutig pornografischen Materials.",
|
||||
"2": "Verherrlichung oder Verharmlosung grausamer oder unmenschlicher Gewalttätigkeiten.",
|
||||
"3": "Das Veröffentlichen von personenbezogenen Daten anderer ohne deren Einverständnis oder das Androhen dessen (\"Doxing\").",
|
||||
"3": "Das Veröffentlichen von personenbezogenen Daten anderer ohne deren Einverständnis oder das Androhen dessen („Doxing“).",
|
||||
"4": "Absichtliche Einschüchterung, Stalking oder Verfolgung.",
|
||||
"5": "Bewerben von Produkten und Dienstleistungen mit kommerzieller Absicht.",
|
||||
"6": "Strafbares Verhalten bzw. Verstoß gegen deutsches Recht.",
|
||||
@ -157,7 +157,7 @@
|
||||
"user": "Benutzer ::: Benutzer",
|
||||
"validations": {
|
||||
"categories": "es müssen eine bis drei Kategorien ausgewählt werden",
|
||||
"email": "muss eine gültige E-Mail Adresse sein",
|
||||
"email": "muss eine gültige E-Mail-Adresse sein",
|
||||
"url": "muss eine gültige URL sein"
|
||||
},
|
||||
"versus": "Versus"
|
||||
@ -165,7 +165,7 @@
|
||||
"components": {
|
||||
"enter-nonce": {
|
||||
"form": {
|
||||
"description": "Öffne dein E-Mail Postfach und gib den Code ein, den wir geschickt haben.",
|
||||
"description": "Öffne Dein E-Mail Postfach und gib den Code ein, den wir geschickt haben.",
|
||||
"next": "Weiter",
|
||||
"nonce": "Code eingeben",
|
||||
"validations": {
|
||||
@ -181,9 +181,9 @@
|
||||
},
|
||||
"request": {
|
||||
"form": {
|
||||
"description": "Eine Mail zum Zurücksetzen des Passworts wird an die angegebene E-Mail Adresse geschickt.",
|
||||
"description": "Eine E-Mail zum Zurücksetzen des Passworts wird an die angegebene Adresse geschickt.",
|
||||
"submit": "E-Mail anfordern",
|
||||
"submitted": "Eine E-Mail mit weiteren Instruktionen wurde verschickt an <b>{email}</b>"
|
||||
"submitted": "Eine E-Mail mit weiteren Hinweisen wurde verschickt an <b>{email}</b>"
|
||||
},
|
||||
"title": "Passwort zurücksetzen"
|
||||
}
|
||||
@ -191,25 +191,25 @@
|
||||
"registration": {
|
||||
"create-user-account": {
|
||||
"error": "Es konnte kein Benutzerkonto erstellt werden!",
|
||||
"help": "Vielleicht war der Bestätigungscode falsch oder abgelaufen? Wenn das Problem weiterhin besteht, schick uns gerne eine E-Mail an:",
|
||||
"help": "Vielleicht war der Bestätigungscode falsch oder abgelaufen? Wenn das Problem weiterhin besteht, schicke uns gerne eine E-Mail an:",
|
||||
"success": "Dein Benutzerkonto wurde erstellt!",
|
||||
"title": "Benutzerkonto anlegen"
|
||||
},
|
||||
"signup": {
|
||||
"form": {
|
||||
"data-privacy": "Ich habe die <a href=\"https://human-connection.org/datenschutz/\" target=\"_blank\"><ds-text bold color=\"primary\" >Datenschutzerklärung</ds-text></a> gelesen und verstanden",
|
||||
"description": "Um loszulegen, kannst du dich hier kostenfrei registrieren:",
|
||||
"data-privacy": "Ich habe die <a href=\"https://human-connection.org/datenschutz/\" target=\"_blank\"><ds-text bold color=\"primary\" >Datenschutzerklärung</ds-text></a> gelesen und verstanden",
|
||||
"description": "Um loszulegen, kannst Du Dich hier kostenfrei registrieren:",
|
||||
"errors": {
|
||||
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail Adresse!",
|
||||
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail-Adresse!",
|
||||
"invalid-invitation-token": "Es sieht so aus, als ob der Einladungscode schon eingelöst wurde. Jeder Code kann nur einmalig benutzt werden."
|
||||
},
|
||||
"invitation-code": "Dein Einladungscode lautet: <b>{code}</b>",
|
||||
"minimum-age": "Ich bin 18 Jahre oder älter.",
|
||||
"no-commercial": "Ich habe keine kommerziellen Absichten und ich repräsentiere kein kommerzielles Unternehmen oder Organisation.",
|
||||
"no-political": "Ich bin nicht im Auftrag einer Partei oder politischen Organisation im Netzwerk. ",
|
||||
"no-political": "Ich bin nicht im Auftrag einer Partei oder politischen Organisation im Netzwerk.",
|
||||
"submit": "Konto erstellen",
|
||||
"success": "Eine Mail mit einem Bestätigungslink für die Registrierung wurde an <b>{email}</b> geschickt",
|
||||
"terms-and-condition": "Ich stimme den <a href=\"/terms-and-conditions\"><ds-text bold color=\"primary\" > Nutzungsbedingungen</ds-text></a>zu."
|
||||
"success": "Eine E-Mail mit einem Link zum Abschließen Deiner Registrierung wurde an <b>{email}</b> geschickt",
|
||||
"terms-and-condition": "Ich stimme den <a href=\"/terms-and-conditions\"><ds-text bold color=\"primary\" >Nutzungsbedingungen</ds-text></a> zu."
|
||||
},
|
||||
"title": "Mach mit bei Human Connection!",
|
||||
"unavailable": "Leider ist die öffentliche Registrierung von Benutzerkonten auf diesem Server derzeit nicht möglich."
|
||||
@ -217,6 +217,8 @@
|
||||
}
|
||||
},
|
||||
"contribution": {
|
||||
"amount-comments": "{amount} comments",
|
||||
"amount-shouts": "{amount} recommendations",
|
||||
"categories": {
|
||||
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
|
||||
},
|
||||
@ -248,10 +250,10 @@
|
||||
"surprised": "Erstaunt"
|
||||
},
|
||||
"filterALL": "Alle Beiträge anzeigen",
|
||||
"filterFollow": "Beiträge filtern von Usern denen ich folge",
|
||||
"filterFollow": "Beiträge von Benutzern filtern, denen ich folge",
|
||||
"inappropriatePicture": "Dieses Bild kann für einige Menschen unangemessen sein.",
|
||||
"inappropriatePictureText": "Wann soll ein Foto versteckt werden",
|
||||
"languageSelectLabel": "Sprache deines Beitrags",
|
||||
"inappropriatePictureText": "Wann sollte mein Beitragsbild verschwommen sein?",
|
||||
"languageSelectLabel": "Sprache Deines Beitrags",
|
||||
"languageSelectText": "Sprache wählen",
|
||||
"newPost": "Erstelle einen neuen Beitrag",
|
||||
"success": "Gespeichert!",
|
||||
@ -263,16 +265,16 @@
|
||||
"delete": {
|
||||
"cancel": "Abbrechen",
|
||||
"comment": {
|
||||
"message": "Bist du sicher, dass du den Kommentar \"<b>{name}</b>\" löschen möchtest?",
|
||||
"message": "Bist Du sicher, dass Du den Kommentar „<b>{name}</b>“ löschen möchtest?",
|
||||
"success": "Kommentar erfolgreich gelöscht!",
|
||||
"title": "Lösche Kommentar",
|
||||
"type": "Comment"
|
||||
"type": "Kommentar"
|
||||
},
|
||||
"contribution": {
|
||||
"message": "Bist du sicher, dass du den Beitrag \"<b>{name}</b>\" löschen möchtest?",
|
||||
"message": "Bist Du sicher, dass Du den Beitrag „<b>{name}</b>“ löschen möchtest?",
|
||||
"success": "Beitrag erfolgreich gelöscht!",
|
||||
"title": "Lösche Beitrag",
|
||||
"type": "Contribution"
|
||||
"type": "Beitrag"
|
||||
},
|
||||
"submit": "Löschen"
|
||||
},
|
||||
@ -297,15 +299,15 @@
|
||||
}
|
||||
},
|
||||
"donations": {
|
||||
"amount-of-total": "{amount} von {total} € erreicht",
|
||||
"amount-of-total": "{amount} von {total} € erreicht",
|
||||
"donate-now": "Jetzt spenden",
|
||||
"donations-for": "Spenden für"
|
||||
},
|
||||
"editor": {
|
||||
"embed": {
|
||||
"always_allow": "Inhalte von Drittanbietern immer anzeigen (diese Einstellung kannst du jederzeit ändern)",
|
||||
"data_privacy_info": "Deine Daten wurden noch nicht an Drittanbieter weitergegeben. Wenn du dieses Video jetzt abspielst, registriert der folgende Anbieter wahrscheinlich deine Nutzerdaten:",
|
||||
"data_privacy_warning": "Achte auf deine Daten!",
|
||||
"always_allow": "Einzubettende Inhalte von Drittanbietern immer erlauben (diese Einstellung ist jederzeit änderbar)",
|
||||
"data_privacy_info": "Deine Daten wurden noch nicht an Drittanbieter weitergegeben. Wenn Du dieses Video jetzt abspielst, registriert der folgende Anbieter wahrscheinlich Deine Nutzerdaten:",
|
||||
"data_privacy_warning": "Achte auf Deine Daten!",
|
||||
"play_now": "Jetzt ansehen"
|
||||
},
|
||||
"hashtag": {
|
||||
@ -355,7 +357,7 @@
|
||||
"following": "Folge Ich"
|
||||
},
|
||||
"index": {
|
||||
"change-filter-settings": "Verändere die Filter-Einstellungen um mehr Ergebnisse zu erhalten.",
|
||||
"change-filter-settings": "Verändere die Filter-Einstellungen, um mehr Ergebnisse zu erhalten.",
|
||||
"no-results": "Keine Beiträge gefunden."
|
||||
},
|
||||
"login": {
|
||||
@ -375,8 +377,8 @@
|
||||
"success": "Du bist eingeloggt!"
|
||||
},
|
||||
"maintenance": {
|
||||
"explanation": "Zurzeit führen wir einige geplante Wartungsarbeiten durch, bitte versuch es später erneut.",
|
||||
"questions": "Bei Fragen oder Problemen erreichst du uns per E-Mail an",
|
||||
"explanation": "Derzeit führen wir einige geplante Wartungsarbeiten durch, bitte versuche es später erneut.",
|
||||
"questions": "Bei Fragen oder Problemen erreichst Du uns per E-Mail an",
|
||||
"title": "Human Connection befindet sich in der Wartung"
|
||||
},
|
||||
"moderation": {
|
||||
@ -390,32 +392,32 @@
|
||||
"cancel": "Abbruch",
|
||||
"Comment": {
|
||||
"disable": {
|
||||
"message": "Möchtest du den Kommentar \"<b>{name}</b>\" wirklich <b>gesperrt</b> lassen?",
|
||||
"message": "Möchtest Du den Kommentar „<b>{name}</b>“ wirklich <b>gesperrt</b> lassen?",
|
||||
"title": "Sperre den Kommentar abschließend"
|
||||
},
|
||||
"enable": {
|
||||
"message": "Möchtest du den Kommentar \"<b>{name}</b>\" wirklich <b>entsperrt</b> lassen?",
|
||||
"message": "Möchtest Du den Kommentar „<b>{name}</b>“ wirklich <b>entsperrt</b> lassen?",
|
||||
"title": "Entsperre den Kommentar abschließend"
|
||||
}
|
||||
},
|
||||
"Post": {
|
||||
"disable": {
|
||||
"message": "Möchtest du den Beitrag \"<b>{name}</b>\" wirklich <b>gesperrt</b> lassen?",
|
||||
"message": "Möchtest Du den Beitrag „<b>{name}</b>“ wirklich <b>gesperrt</b> lassen?",
|
||||
"title": "Sperre den Beitrag abschließend"
|
||||
},
|
||||
"enable": {
|
||||
"message": "Möchtest du den Beitrag \"<b>{name}</b>\" wirklich <b>entsperrt</b> lassen?",
|
||||
"message": "Möchtest Du den Beitrag „<b>{name}</b>“ wirklich <b>entsperrt</b> lassen?",
|
||||
"title": "Entsperre den Beitrag abschließend"
|
||||
}
|
||||
},
|
||||
"submit": "Bestätige Entscheidung",
|
||||
"User": {
|
||||
"disable": {
|
||||
"message": "Möchtest du den Benutzer \"<b>{name}</b>\" wirklich <b>gesperrt</b> lassen?",
|
||||
"message": "Möchtest Du den Benutzer „<b>{name}</b>“ wirklich <b>gesperrt</b> lassen?",
|
||||
"title": "Sperre den Benutzer abschließend"
|
||||
},
|
||||
"enable": {
|
||||
"message": "Möchtest du den Benutzer \"<b>{name}</b>\" wirklich <b>entsperrt</b> lassen?",
|
||||
"message": "Möchtest Du den Benutzer „<b>{name}</b>“ wirklich <b>entsperrt</b> lassen?",
|
||||
"title": "Entsperre den Benutzer abschließend"
|
||||
}
|
||||
}
|
||||
@ -459,9 +461,9 @@
|
||||
"pageLink": "Alle Benachrichtigungen",
|
||||
"post": "Beitrag",
|
||||
"reason": {
|
||||
"commented_on_post": "Hat deinen Beitrag kommentiert …",
|
||||
"mentioned_in_comment": "Hat dich in einem Kommentar erwähnt …",
|
||||
"mentioned_in_post": "Hat dich in einem Beitrag erwähnt …"
|
||||
"commented_on_post": "Hat Deinen Beitrag kommentiert …",
|
||||
"mentioned_in_comment": "Hat Dich in einem Kommentar erwähnt …",
|
||||
"mentioned_in_post": "Hat Dich in einem Beitrag erwähnt …"
|
||||
},
|
||||
"title": "Benachrichtigungen",
|
||||
"user": "Benutzer"
|
||||
@ -470,20 +472,20 @@
|
||||
"comment": {
|
||||
"reply": "Antworten",
|
||||
"submit": "Kommentiere",
|
||||
"submitted": "Kommentar gesendet!",
|
||||
"submitted": "Kommentar gesendet",
|
||||
"updated": "Änderungen gespeichert"
|
||||
},
|
||||
"edited": "bearbeitet",
|
||||
"menu": {
|
||||
"delete": "Beitrag löschen",
|
||||
"edit": "Beitrag bearbeiten",
|
||||
"pin": "Post festpinnen",
|
||||
"pinnedSuccessfully": "Post erfolgreich festgepinnt!",
|
||||
"unpin": "Post nicht mehr festpinnen",
|
||||
"unpinnedSuccessfully": "Post erfolgreich nicht mehr festgepinnt!"
|
||||
"pin": "Beitrag anheften",
|
||||
"pinnedSuccessfully": "Beitrag erfolgreich angeheftet!",
|
||||
"unpin": "Beitrag loslösen",
|
||||
"unpinnedSuccessfully": "Angehefteten Beitrag erfolgreich losgelöst!"
|
||||
},
|
||||
"moreInfo": {
|
||||
"description": "Hier findest du weitere Infos zum Thema.",
|
||||
"description": "Hier findest Du weitere Infos zum Thema.",
|
||||
"name": "Mehr Info",
|
||||
"title": "Mehr Informationen",
|
||||
"titleOfCategoriesSection": "Kategorien",
|
||||
@ -529,22 +531,22 @@
|
||||
"release": {
|
||||
"cancel": "Abbrechen",
|
||||
"comment": {
|
||||
"error": "Den Kommentar hast du schon gemeldet!",
|
||||
"message": "Bist du sicher, dass du den Kommentar \"<b>{name}</b>\" freigeben möchtest?",
|
||||
"error": "Den Kommentar hast Du schon gemeldet!",
|
||||
"message": "Bist Du sicher, dass Du den Kommentar „<b>{name}</b>“ freigeben möchtest?",
|
||||
"title": "Kommentar freigeben",
|
||||
"type": "Kommentar"
|
||||
},
|
||||
"contribution": {
|
||||
"error": "Den Beitrag hast du schon gemeldet!",
|
||||
"message": "Bist du sicher, dass du den Beitrag \"<b>{name}</b>\" freigeben möchtest?",
|
||||
"error": "Den Beitrag hast Du schon gemeldet!",
|
||||
"message": "Bist Du sicher, dass Du den Beitrag „<b>{name}</b>“ freigeben möchtest?",
|
||||
"title": "Beitrag freigeben",
|
||||
"type": "Beitrag"
|
||||
},
|
||||
"submit": "freigeben",
|
||||
"success": "Erfolgreich freigegeben!",
|
||||
"user": {
|
||||
"error": "Den User hast du schon gemeldet!",
|
||||
"message": "Bist du sicher, dass du den Nutzer \"<b>{name}</b>\" freigeben möchtest?",
|
||||
"error": "Den Benutzer hast Du schon gemeldet!",
|
||||
"message": "Bist Du sicher, dass Du den Nutzer „<b>{name}</b>“ freigeben möchtest?",
|
||||
"title": "Nutzer freigeben",
|
||||
"type": "Nutzer"
|
||||
}
|
||||
@ -571,16 +573,16 @@
|
||||
"advert_products_services_commercial": "Bewerben von Produkten und Dienstleistungen mit kommerzieller Absicht.",
|
||||
"criminal_behavior_violation_german_law": "Strafbares Verhalten bzw. Verstoß gegen deutsches Recht.",
|
||||
"discrimination_etc": "Diskriminierende Beiträge, Kommentare, Äußerungen oder Beleidigungen.",
|
||||
"doxing": "Das Veröffentlichen von personenbezogenen Daten anderer ohne deren Einverständnis oder das Androhen dessen (\"Doxing\").",
|
||||
"doxing": "Das Veröffentlichen von personenbezogenen Daten anderer ohne deren Einverständnis oder das Androhen dessen („Doxing“).",
|
||||
"glorific_trivia_of_cruel_inhuman_acts": "Verherrlichung oder Verharmlosung grausamer oder unmenschlicher Gewalttätigkeiten.",
|
||||
"intentional_intimidation_stalking_persecution": "Absichtliche Einschüchterung, Stalking oder Verfolgung.",
|
||||
"other": "Andere …",
|
||||
"pornographic_content_links": "Das Posten oder Verlinken eindeutig pornografischen Materials."
|
||||
"pornographic_content_links": "Das Senden oder Verlinken eindeutig pornografischen Materials."
|
||||
},
|
||||
"placeholder": "Kategorie …"
|
||||
},
|
||||
"description": {
|
||||
"label": "Bitte erkläre: Warum möchtest du dies melden?",
|
||||
"label": "Bitte erkläre: Warum möchtest Du dies melden?",
|
||||
"placeholder": "Zusätzliche Information …"
|
||||
}
|
||||
},
|
||||
@ -610,19 +612,19 @@
|
||||
"slug": "Alias",
|
||||
"unblock": "Entsperren"
|
||||
},
|
||||
"empty": "Bislang hast du niemanden blockiert.",
|
||||
"empty": "Bislang hast Du niemanden blockiert.",
|
||||
"explanation": {
|
||||
"closing": "Das sollte fürs Erste genügen, damit blockierte Benutzer dich nicht mehr länger belästigen können.",
|
||||
"closing": "Das sollte fürs Erste genügen, damit blockierte Benutzer Dich nicht mehr länger belästigen können.",
|
||||
"commenting-disabled": "Du kannst den Beitrag derzeit nicht kommentieren.",
|
||||
"commenting-explanation": "Dafür kann es mehrere Gründe geben, bitte schau in unsere ",
|
||||
"intro": "Wenn ein anderer Benutzer von dir blockiert wurde, dann passiert folgendes:",
|
||||
"notifications": "Von dir blockierte Personen erhalten keine Benachrichtigungen mehr, wenn sie in deinen Beiträgen erwähnt werden.",
|
||||
"their-perspective": "Die blockierte Person kann deine Beiträge nicht mehr kommentieren",
|
||||
"your-perspective": "Du kannst keine Beiträge der blockierten Person mehr kommentieren."
|
||||
"intro": "Wenn ein anderer Benutzer durch Dich blockiert wurde, dann passiert Folgendes:",
|
||||
"notifications": "Von Dir blockierte Benutzer werden keine Benachrichtigungen mehr erhalten, falls sie in Deinen Beiträgen erwähnt werden.",
|
||||
"their-perspective": "Umgekehrt das gleiche: Die blockierte Person bekommt auch in ihren Benachrichtigungen Deine Beiträge nicht mehr zu sehen.",
|
||||
"your-perspective": "In Deinen Benachrichtigungen tauchen keine Beiträge der blockierten Person mehr auf."
|
||||
},
|
||||
"how-to": "Du kannst andere Benutzer auf deren Profilseite über das Inhaltsmenü blockieren.",
|
||||
"name": "Blocked users",
|
||||
"unblock": "Nutzer entsperren",
|
||||
"name": "Blockierte Benutzer",
|
||||
"unblock": "Blockierten Nutzer freigeben",
|
||||
"unblocked": "{name} ist wieder entsperrt"
|
||||
},
|
||||
"data": {
|
||||
@ -636,47 +638,47 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Sei dir bewusst, dass deine Beiträge und Kommentare für unsere Community wichtig sind. Wenn du sie trotzdem löschen möchtest, musst du sie unten markieren.",
|
||||
"accountWarning": "Dein Konto, deine Beiträge oder Kommentare kannst du nach dem Löschen <b>WEDER VERWALTEN NOCH WIEDERHERSTELLEN!</b>",
|
||||
"accountWarning": "Dein Konto, deine Beiträge oder Kommentare kannst du nach dem Löschen WEDER VERWALTEN NOCH WIEDERHERSTELLEN!",
|
||||
"commentedCount": "Meine {count} Kommentare löschen",
|
||||
"contributionsCount": "Meine {count} Beiträge löschen",
|
||||
"name": "Benutzerkonto löschen",
|
||||
"pleaseConfirm": "<b class='is-danger'>Zerstörerische Aktion!</b> Gib <b>{confirm}</b> ein, um zu bestätigen.",
|
||||
"pleaseConfirm": "Zerstörerische Aktion! Gib „{confirm}“ ein, um zu bestätigen.",
|
||||
"success": "Konto erfolgreich gelöscht!"
|
||||
},
|
||||
"download": {
|
||||
"name": "Daten herunterladen"
|
||||
},
|
||||
"email": {
|
||||
"change-successful": "Deine E-Mail Adresse wurde erfolgreich geändert.",
|
||||
"labelEmail": "E-Mail Adresse ändern",
|
||||
"labelNewEmail": "Neue E-Mail Adresse",
|
||||
"change-successful": "Deine E-Mail-Adresse wurde erfolgreich geändert.",
|
||||
"labelEmail": "E-Mail-Adresse ändern",
|
||||
"labelNewEmail": "Neue E-Mail-Adresse",
|
||||
"labelNonce": "Bestätigungscode eingeben",
|
||||
"name": "Deine E-Mail",
|
||||
"submitted": "Eine E-Mail zur Bestätigung deiner Adresse wurde an <b>{email}</b> gesendet.",
|
||||
"success": "Eine neue E-Mail Addresse wurde registriert.",
|
||||
"submitted": "Eine E-Mail zur Bestätigung Deiner Adresse wurde an <b>{email}</b> gesendet.",
|
||||
"success": "Eine neue E-Mail-Adresse wurde registriert.",
|
||||
"validation": {
|
||||
"same-email": "Das ist deine aktuelle E-Mail Addresse"
|
||||
"same-email": "Das ist Deine aktuelle E-Mail-Adresse"
|
||||
},
|
||||
"verification-error": {
|
||||
"explanation": "Das kann verschiedene Ursachen haben:",
|
||||
"message": "Deine E-Mail Adresse konnte nicht verifiziert werden.",
|
||||
"message": "Deine E-Mail-Adresse konnte nicht verifiziert werden.",
|
||||
"reason": {
|
||||
"invalid-nonce": "Ist der Bestätigungscode falsch?",
|
||||
"no-email-request": "Bist du dir sicher, dass du eine Änderung deiner E-Mail Adresse angefragt hattest?"
|
||||
"no-email-request": "Bist Du Dir sicher, dass Du eine Änderung Deiner E-Mail-Adresse angefragt hattest?"
|
||||
},
|
||||
"support": "Wenn das Problem weiterhin besteht, kontaktiere uns gerne per E-Mail an"
|
||||
}
|
||||
},
|
||||
"embeds": {
|
||||
"info-description": "Hier ist die Liste an Drittanbietern, deren Inhalte als Fremdcode z.B. in Form von eingebetteten Videos angezeigt werden kann:",
|
||||
"info-description": "Hier ist die Liste der Drittanbieter, deren Inhalte, z.B. in Form eingebetteter Videos, mittels Drittanbieter-Programmcode angezeigt werden kann:",
|
||||
"name": "Drittanbieter",
|
||||
"status": {
|
||||
"change": {
|
||||
"allow": "Na klar",
|
||||
"deny": "Lieber nicht",
|
||||
"question": "Soll eingebetter Fremdcode von Dritten für dich immer angezeigt werden?"
|
||||
"question": "Soll einzubettender Programmcode der Drittanbieter Dir immer zur Anzeige gebracht werden?"
|
||||
},
|
||||
"description": "Als Grundeinstellung für dich wird eingebetter Fremdcode von Drittanbietern",
|
||||
"description": "Als Grundeinstellung wird Dir der einzubettende Code der Drittanbieter",
|
||||
"disabled": {
|
||||
"off": "zunächst nicht angezeigt",
|
||||
"on": "sofort angezeigt"
|
||||
@ -722,10 +724,10 @@
|
||||
"label-new-password": "Dein neues Passwort",
|
||||
"label-new-password-confirm": "Bestätige Dein neues Passwort",
|
||||
"label-old-password": "Dein altes Passwort",
|
||||
"message-new-password-confirm-required": "Bestätige dein neues Passwort",
|
||||
"message-new-password-missmatch": "Gebe das gleiche Passwort nochmals ein",
|
||||
"message-new-password-required": "Gebe ein neues Passwort ein",
|
||||
"message-old-password-required": "Gebe dein altes Passwort ein",
|
||||
"message-new-password-confirm-required": "Bestätige Dein neues Passwort",
|
||||
"message-new-password-missmatch": "Gib dasselbe Passwort nochmals ein",
|
||||
"message-new-password-required": "Gib ein neues Passwort ein",
|
||||
"message-old-password-required": "Gib Dein altes Passwort ein",
|
||||
"passwordSecurity": "Passwortsicherheit",
|
||||
"passwordStrength0": "Sehr unsicheres Passwort",
|
||||
"passwordStrength1": "Unsicheres Passwort",
|
||||
@ -737,12 +739,12 @@
|
||||
"name": "Sicherheit"
|
||||
},
|
||||
"social-media": {
|
||||
"name": "Soziale Medien",
|
||||
"placeholder": "Deine Social-Media URL",
|
||||
"name": "Soziale Netzwerke",
|
||||
"placeholder": "Deine Webadresse des Sozialen Netzwerkes",
|
||||
"requireUnique": "Dieser Link existiert bereits",
|
||||
"submit": "Link hinzufügen",
|
||||
"successAdd": "Social-Media hinzugefügt. Profil aktualisiert!",
|
||||
"successDelete": "Social-Media gelöscht. Profil aktualisiert!"
|
||||
"successAdd": "Soziales Netzwerk hinzugefügt. Profil wurde aktualisiert!",
|
||||
"successDelete": "Soziales Netzwerk entfernt. Profil wurde aktualisiert!"
|
||||
},
|
||||
"validation": {
|
||||
"slug": {
|
||||
@ -767,7 +769,7 @@
|
||||
"imprint": "Impressum",
|
||||
"made": "Mit ❤ gemacht",
|
||||
"register": "Registernummer",
|
||||
"responsible": "Verantwortlicher gemäß § 55 Abs. 2 RStV ",
|
||||
"responsible": "Verantwortlich für Inhalte dieser Seite (§ 55 Abs. 2 RStV)",
|
||||
"taxident": "Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz (Deutschland)",
|
||||
"termsAndConditions": "Nutzungsbedingungen",
|
||||
"thanks": "Danke!",
|
||||
@ -788,11 +790,11 @@
|
||||
"termsAndConditions": {
|
||||
"addition": {
|
||||
"description": "<a href=\"https://human-connection.org/veranstaltungen/\" target=\"_blank\" > https://human-connection.org/veranstaltungen/ </a>",
|
||||
"title": "Zusätzlich machen wir regelmäßig Veranstaltungen, wo Du auch Eindrücke wiedergeben und Fragen stellen kannst. Du findest eine aktuelle Übersicht hier:"
|
||||
"title": "Zusätzlich veranstalten wir regelmäßig Ereignisse, wo Du auch Eindrücke teilen und Fragen stellen kannst. Du findest eine aktuelle Übersicht hier:"
|
||||
},
|
||||
"agree": "Ich stimme zu!",
|
||||
"code-of-conduct": {
|
||||
"description": "Unser Verhaltenskodex dient als Leitfaden für das persönliche Auftreten und den Umgang miteinander. Wer als Nutzer im Human Connection-Netzwerk aktiv ist, Beiträge verfasst, kommentiert oder mit anderen Nutzern, auch außerhalb des Netzwerkes, Kontakt aufnimmt, erkennt diese Verhaltensregeln als verbindlich an. <a href=\"https://alpha.human-connection.org/code-of-conduct\" target=\"_blank\"> https://alpha.human-connection.org/code-of-conduct</a>",
|
||||
"description": "Unser Verhaltenskodex dient als Leitfaden für das persönliche Auftreten und den Umgang miteinander. Wer als Nutzer im Human Connection-Netzwerk aktiv ist, Beiträge verfasst, kommentiert oder mit anderen Nutzern, auch außerhalb des Netzwerkes, Kontakt aufnimmt, erkennt diese Verhaltensregeln als verbindlich an. <a href=\"https://alpha.human-connection.org/code-of-conduct\" target=\"_blank\">https://alpha.human-connection.org/code-of-conduct</a>",
|
||||
"title": "Verhaltenscodex"
|
||||
},
|
||||
"errors-and-feedback": {
|
||||
@ -800,7 +802,7 @@
|
||||
"title": "Fehler und Rückmeldungen"
|
||||
},
|
||||
"help-and-questions": {
|
||||
"description": "Für Hilfe und Fragen haben wir Dir eine umfassende Sammlung an häufig gestellten Fragen und Antworten (FAQ) zusammengestellt. Du findest diese hier: <a href=\"https://support.human-connection.org/kb/\" target=\"_blank\" > https://support.human-connection.org/kb/ </a>",
|
||||
"description": "Für Hilfe und Fragen haben wir Dir eine umfassende Sammlung an häufig gestellten Fragen und Antworten (FAQ) zusammengestellt; Du findest diese auf <a href=\"https://support.human-connection.org/kb/\" target=\"_blank\" > support.human-connection.org/kb/ </a>",
|
||||
"title": "Hilfe und Fragen"
|
||||
},
|
||||
"moderation": {
|
||||
@ -809,11 +811,15 @@
|
||||
},
|
||||
"newTermsAndConditions": "Neue Nutzungsbedingungen",
|
||||
"no-commercial-use": {
|
||||
"description": "Die Nutzung des Human Connection Netzwerkes ist nicht gestattet für kommerzielle Nutzung. Darunter fällt unter anderem das Bewerben von Produkten mit kommerzieller Absicht, das Einstellen von Affiliate-Links, direkter Aufruf zu Spenden oder finanzieller Unterstützung für Zwecke, die steuerlich nicht als gemeinnützig anerkannt sind.",
|
||||
"description": "Die Nutzung des Human Connection Netzwerkes ist nicht gestattet für kommerzielle Nutzung. Darunter fällt unter anderem das Bewerben von Produkten mit kommerzieller Absicht, das Einstellen von Affiliate-Links (Geschäftspartner-Links), direkter Aufruf zu Spenden oder finanzieller Unterstützung für Zwecke, die steuerlich nicht als gemeinnützig anerkannt sind.",
|
||||
"title": "Keine kommerzielle Nutzung"
|
||||
},
|
||||
"no-parties": {
|
||||
"description": "Nutzerkonten von politischen Parteien oder offizielle Nutzerkonten eines politischen Vertreters sind unzulässig.",
|
||||
"title": "Keine politische Nutzung"
|
||||
},
|
||||
"privacy-statement": {
|
||||
"description": "Unser Netzwerk ist ein soziales Wissens- und Aktionsnetzwerk. Daher ist es uns besonders wichtig, dass möglichst viele Inhalte öffentlich zugänglich sind. Im Laufe der Entwicklung unseres Netzwerkes wird es mehr und mehr die Möglichkeit geben, über die Sichtbarkeit der selbst angegebenen bzw. persönlichen Daten zu entscheiden. Über diese neuen Funktionen werden wir Euch informieren. Ansonsten gilt, dass Du immer darüber nachdenken solltest, welche persönlichen Daten Du über Dich (oder andere) preisgibst. Dies gilt insbesondere für Inhalte von Beiträgen und Kommentaren, da diese einen weitgehend öffentlichen Charakter haben. Später wird es Möglichkeiten geben, die Sichtbarkeit Deines Profils einzuschränken. Teil der Nutzungsbedingungen ist unsere Datenschutzerklärung, die Dich über die einzelnen Datenverarbeitungen in unserem Netzwerk informiert: <a href=\"https://human-connection.org/datenschutz/#netzwerk\" target=\"_blank\"> https://human-connection.org/datenschutz/#netzwerk</a> bzw. <a href=\"https://human-connection.org/datenschutz/\" target=\"_blank\">https://human-connection.org/datenschutz/</a> Unsere Datenschutzerklärung ist an die Gesetzeslage und die Charakteristika unseres Netzwerks angepasst und gilt immer in der aktuellsten Version.",
|
||||
"description": "Unser Netzwerk ist ein soziales Wissens- und Aktionsnetzwerk. Daher ist es uns besonders wichtig, dass möglichst viele Inhalte öffentlich zugänglich sind. Im Laufe der Entwicklung unseres Netzwerkes wird es mehr und mehr die Möglichkeit geben, über die Sichtbarkeit der selbst angegebenen bzw. persönlichen Daten zu entscheiden. Über diese neuen Funktionen werden wir Euch informieren. Ansonsten gilt, dass Du immer darüber nachdenken solltest, welche persönlichen Daten Du über Dich (oder andere) preisgibst. Dies gilt insbesondere für Inhalte von Beiträgen und Kommentaren, da diese einen weitgehend öffentlichen Charakter haben. Später wird es Möglichkeiten geben, die Sichtbarkeit Deines Profils einzuschränken. Teil der Nutzungsbedingungen ist unsere Datenschutzerklärung, die Dich über die einzelnen Datenverarbeitungen in unserem Netzwerk informiert: <a href=\"https://human-connection.org/datenschutz/#netzwerk\" target=\"_blank\">https://human-connection.org/datenschutz/#netzwerk</a> bzw. <a href=\"https://human-connection.org/datenschutz/\" target=\"_blank\">https://human-connection.org/datenschutz/</a>. Unsere Datenschutzerklärung ist an die Gesetzeslage und die Charakteristika unseres Netzwerks angepasst und gilt immer in der aktuellsten Version.",
|
||||
"title": "Datenschutz"
|
||||
},
|
||||
"terms-of-service": {
|
||||
@ -822,7 +828,7 @@
|
||||
},
|
||||
"termsAndConditionsConfirmed": "Ich habe die <a href=\"/terms-and-conditions\" target=\"_blank\">Nutzungsbedingungen</a> durchgelesen und stimme ihnen zu.",
|
||||
"termsAndConditionsNewConfirm": "Ich habe die neuen Nutzungsbedingungen durchgelesen und stimme zu.",
|
||||
"termsAndConditionsNewConfirmText": "Bitte lies Dir die neue Nutzungsbedingungen jetzt durch!",
|
||||
"termsAndConditionsNewConfirmText": "Bitte lies Dir die neuen Nutzungsbedingungen jetzt durch!",
|
||||
"use-and-license": {
|
||||
"description": "Sind Inhalte, die Du bei uns einstellst, durch Rechte am geistigen Eigentum geschützt, erteilst Du uns eine nicht-exklusive, übertragbare, unterlizenzierbare und weltweite Lizenz für die Nutzung dieser Inhalte für die Bereitstellung in unserem Netzwerk. Diese Lizenz endet, sobald Du Deine Inhalte oder Deinen ganzen Account löscht. Bedenke, dass andere Deine Inhalte weiter teilen können und wir diese nicht löschen können.",
|
||||
"title": "Nutzung und Lizenz"
|
||||
@ -830,7 +836,7 @@
|
||||
},
|
||||
"user": {
|
||||
"avatar": {
|
||||
"submitted": "Upload erfolgreich"
|
||||
"submitted": "Erfolgreich hochgeladen!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -217,6 +217,8 @@
|
||||
}
|
||||
},
|
||||
"contribution": {
|
||||
"amount-comments": "{amount} comments",
|
||||
"amount-shouts": "{amount} recommendations",
|
||||
"categories": {
|
||||
"infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected"
|
||||
},
|
||||
@ -636,11 +638,11 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Be aware that your Posts and Comments are important to our community. If you still choose to delete them, you have to mark them below.",
|
||||
"accountWarning": "You <b>CAN'T MANAGE</b> and <b>CAN'T RECOVER</b> your Account, Posts, or Comments after deleting your account!",
|
||||
"accountWarning": "You CAN'T MANAGE and CAN'T RECOVER your Account, Posts, or Comments after deleting your account!",
|
||||
"commentedCount": "Delete my {count} comments",
|
||||
"contributionsCount": "Delete my {count} posts",
|
||||
"name": "Delete user account",
|
||||
"pleaseConfirm": "<b class='is-danger'>Destructive action!</b> Type <b>{confirm}</b> to confirm",
|
||||
"pleaseConfirm": "Destructive action! Type “{confirm}” to confirm.",
|
||||
"success": "Account successfully deleted!"
|
||||
},
|
||||
"download": {
|
||||
@ -812,6 +814,10 @@
|
||||
"description": "The use of the Human Connection Network is not permitted for commercial purposes. This includes, but is not limited to, advertising products with commercial intent, posting affiliate links, directly soliciting donations, or providing financial support for purposes that are not recognized as charitable for tax purposes.",
|
||||
"title": "No Commercial Use"
|
||||
},
|
||||
"no-parties": {
|
||||
"description": "User accounts of political parties or official user accounts of a political representative are not permitted.",
|
||||
"title": "No Political Use"
|
||||
},
|
||||
"privacy-statement": {
|
||||
"description": "Our network is a social knowledge and action network. It is therefore particularly important to us that as much content as possible is publicly accessible. In the course of the development of our network there will be more and more the possibility to decide about the visibility of the personal data. We will inform you about these new features. Otherwise, you should always think about which personal data you disclose about yourself (or others). This applies in particular to the content of posts and comments, as these have a largely public character. Later there will be possibilities to limit the visibility of your profile. Part of the terms of service is our privacy statement, which informs you about the individual data processing operations in our network: <a href=\"https://human-connection.org/datenschutz/#netzwerk\" target=\"_blank\"> https://human-connection.org/datenschutz/#netzwerk</a> bzw. <a href=\"https://human-connection.org/datenschutz/\" target=\"_blank\">https://human-connection.org/datenschutz/</a> Our privacy statement is adapted to the legal situation and characteristics of our network and is always valid in the most current version.",
|
||||
"title": "Privacy Statement"
|
||||
|
||||
@ -636,11 +636,11 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Tenga en cuenta que su contribución y sus comentarios son importantes para nuestra comunidad. Si aún decide borrarlos, debe marcarlos a continuación.",
|
||||
"accountWarning": "¡<b> NO PUEDE GESTIONAR </b> y <b> NO PUEDE RECUPERAR </b> su cuenta, contribuciones o comentarios después de eliminar su cuenta!",
|
||||
"accountWarning": "¡NO PUEDE GESTIONAR y NO PUEDE RECUPERAR su cuenta, contribuciones o comentarios después de eliminar su cuenta!",
|
||||
"commentedCount": "Eliminar mis {count} comentarios",
|
||||
"contributionsCount": "Eliminar mis {count} contribuciones",
|
||||
"name": "Borrar datos",
|
||||
"pleaseConfirm": "<b class='is-danger'> ¡Acción destructiva! </b> Escriba <b> {confirm} </b> para confirmar",
|
||||
"name": "Eliminar cuenta de usuario",
|
||||
"pleaseConfirm": "¡Acción destructiva! Escriba “{confirm}” para confirmar.",
|
||||
"success": "¡Cuenta eliminada con éxito!"
|
||||
},
|
||||
"download": {
|
||||
@ -812,6 +812,10 @@
|
||||
"description": "El uso de la red Human Connection no está permitido para fines comerciales. Esto incluye, pero no se limita a, publicitar productos con intención comercial, publicar enlaces de afiliados, solicitar donaciones directamente o brindar apoyo financiero para fines que no se reconocen como caritativos para fines fiscales.",
|
||||
"title": "Sin uso comercial"
|
||||
},
|
||||
"no-parties": {
|
||||
"description": "No se permiten las cuentas de usuario de los partidos políticos ni las cuentas de usuario oficiales de un representante político.",
|
||||
"title": "No tiene uso político"
|
||||
},
|
||||
"privacy-statement": {
|
||||
"description": "Nuestra red es una red de conocimiento y acción social. Por lo tanto, es especialmente importante para nosotros que el mayor número posible de contenidos sea accesible al público. En el curso del desarrollo de nuestra red habrá cada vez más la posibilidad de decidir sobre la visibilidad de los datos personales. Le informaremos sobre estas nuevas características. De lo contrario, siempre debe pensar en los datos personales que revela sobre usted (u otros). Esto se aplica en particular al contenido de los mensajes y comentarios, ya que éstos tienen un carácter ampliamente público. Más tarde habrá posibilidades de limitar la visibilidad de su perfil. Parte de los términos del servicio es nuestra declaración de privacidad, que le informa sobre las operaciones individuales de procesamiento de datos en nuestra red: <a href=\"https://human-connection.org/datenschutz/#netzwerk\" target=\"_blank\">https://human-connection.org/datenschutz/#netzwerk</a> o <a href=\"https://human-connection.org/datenschutz/\" target=\"_blank\">https://human-connection.org/datenschutz/</a> Nuestra declaración de privacidad está adaptada a la situación legal y a las características de nuestra red y es siempre válida en la versión más actualizada.",
|
||||
"title": "Protección de datos"
|
||||
|
||||
@ -152,7 +152,7 @@
|
||||
"project": "Projet ::: Projets",
|
||||
"reportContent": "Signaler",
|
||||
"shout": "Partage ::: Partages",
|
||||
"tag": "Tag ::: Tags",
|
||||
"tag": "Tag ::: Tags",
|
||||
"takeAction": "Passer à l'action",
|
||||
"user": "Utilisateur ::: Utilisateurs",
|
||||
"validations": {
|
||||
@ -622,11 +622,11 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Sachez que vos postes et commentaires sont importants pour notre communauté. Si vous voulez quand même les supprimer, vous devez les marquer ci-dessous.",
|
||||
"accountWarning": "Vous <b>NE POUVEZ PAS GÉRER</b> et <b>NE POUVEZ PAS RECOUVRIR</b> votre compte, vos messages ou vos commentaires après avoir supprimé votre compte !",
|
||||
"accountWarning": "Vous NE POUVEZ PAS GÉRER et NE POUVEZ PAS RECOUVRIR votre compte, vos messages ou vos commentaires après avoir supprimé votre compte!",
|
||||
"commentedCount": "Supprimer mes {count} commentaires",
|
||||
"contributionsCount": "Supprimer mes {count} postes",
|
||||
"name": "Effacer les données",
|
||||
"pleaseConfirm": "<b class='is-danger'> Action destructive! </b> Saisissez <b> {confirm} </b> pour confirmer",
|
||||
"name": "Supprimer un compte utilisateur",
|
||||
"pleaseConfirm": "Action destructive! Saisissez “{confirm}” pour confirmer.",
|
||||
"success": "Compte supprimer avec succès!"
|
||||
},
|
||||
"download": {
|
||||
|
||||
@ -551,11 +551,11 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Essere consapevoli che i tuoi post e commenti sono importanti per la nostra comunità. Se cancelli il tuo account utente, tutto scomparirà per sempre - e sarebbe un vero peccato!",
|
||||
"accountWarning": "Attenzione!Tu <b>Non puoi gestire</b> e <b>Non puoi recuperare il tuo account, i tuoi messaggi o commenti dopo aver cancellato il tuo account!",
|
||||
"accountWarning": "Attenzione! Tu NON PUOI GESTIRE e NON PUOI RECUPERARE il tuo account, i tuoi messaggi o commenti dopo aver cancellato il tuo account!",
|
||||
"commentedCount": "Cancella i miei {count} commenti",
|
||||
"contributionsCount": "Cancellare i miei {count} messaggi",
|
||||
"name": "Cancellare l'account utente",
|
||||
"pleaseConfirm": "<b class='is-danger'>Azione distruttiva! </b> Digita <b>{conferma}</b> per confermare",
|
||||
"pleaseConfirm": "Azione distruttiva! Digita “{confirm}” per confermare.",
|
||||
"success": "Account eliminato con successo!"
|
||||
},
|
||||
"download": {
|
||||
|
||||
@ -295,11 +295,11 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Be aware that your Post and Comments are important to our community. If you still choose to delete them, you have to mark them below.",
|
||||
"accountWarning": "Po usunięcie Twojego konta, nie możesz <b>ZARZĄDZAĆ</b> ani <b>ODZYSKAĆ</b> danych, wpisów oraz komentarzy!",
|
||||
"accountWarning": "Po usunięcie Twojego konta, nie możesz ZARZĄDZAĆ ani ODZYSKAĆ danych, wpisów oraz komentarzy!",
|
||||
"commentedCount": "Usuń {count} moich komentarzy",
|
||||
"contributionsCount": "Usuń {count} moich postów",
|
||||
"name": "Usuń dane",
|
||||
"pleaseConfirm": "<b class='is-danger'>Uwaga, niebezpieczeństwo!</b> Wpisz <b>{confirm}</b>, aby potwierdzić",
|
||||
"pleaseConfirm": "Uwaga, niebezpieczeństwo! Wpisz „{confirm}”, aby potwierdzić.",
|
||||
"success": "Konto zostało usunięte"
|
||||
},
|
||||
"download": {
|
||||
|
||||
@ -557,11 +557,11 @@
|
||||
},
|
||||
"deleteUserAccount": {
|
||||
"accountDescription": "Esteja ciente de que o suas Publicações e Comentários são importantes para a nossa comunidade. Se você ainda optar por excluí-los, você tem que marcá-los abaixo.",
|
||||
"accountWarning": "Você <b>NÃO PODE GERENCIAR</b> e <b>NÃO PODE RECUPERAR</b> sua conta, Publicações, ou Comentários após excluir sua conta!",
|
||||
"accountWarning": "Você NÃO PODE GERENCIAR e NÃO PODE RECUPERAR sua conta, Publicações, ou Comentários após excluir sua conta!",
|
||||
"commentedCount": "Deletar meus {count} comentários",
|
||||
"contributionsCount": "Deletar minhas {count} publicações",
|
||||
"name": "Deletar dados",
|
||||
"pleaseConfirm": "<b class='is-danger'>Ação destrutiva!</b> Digitar <b>{confirm}</b> para confirmar",
|
||||
"pleaseConfirm": "Ação destrutiva! Digitar “{confirm}” para confirmar.",
|
||||
"success": "Conta eliminada com sucesso!"
|
||||
},
|
||||
"download": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user